@gajae-code/coding-agent 0.13.2 → 0.14.0
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/CHANGELOG.md +215 -8
- package/README.md +2 -0
- package/bin/gajaessi.js +4 -0
- package/dist/types/async/job-manager.d.ts +55 -2
- package/dist/types/async/support.d.ts +14 -0
- package/dist/types/capability/mcp.d.ts +6 -0
- package/dist/types/capability/types.d.ts +6 -0
- package/dist/types/cli/accounts-cli.d.ts +19 -0
- package/dist/types/cli/args.d.ts +3 -0
- package/dist/types/cli/config-cli.d.ts +6 -0
- package/dist/types/cli/crash-cli.d.ts +10 -0
- package/dist/types/cli/customize-doctor.d.ts +145 -0
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/mcp-cli.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/root-flags.d.ts +12 -0
- package/dist/types/cli/setup-cli.d.ts +3 -1
- package/dist/types/cli/skills-cli.d.ts +2 -1
- package/dist/types/cli/update-cli.d.ts +19 -1
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/accounts.d.ts +40 -0
- package/dist/types/commands/crash.d.ts +24 -0
- package/dist/types/commands/customize.d.ts +22 -0
- package/dist/types/commands/daemon.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/commands/skills.d.ts +6 -1
- package/dist/types/commands/update.d.ts +7 -0
- package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
- package/dist/types/config/config-schema-version.d.ts +10 -0
- package/dist/types/config/file-lock.d.ts +18 -0
- package/dist/types/config/model-profile-activation.d.ts +3 -1
- package/dist/types/config/model-registry.d.ts +10 -1
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +73 -9
- package/dist/types/config/settings.d.ts +63 -0
- package/dist/types/config/skill-settings-defaults.d.ts +26 -0
- package/dist/types/config/task-delegation.d.ts +39 -0
- package/dist/types/config/update-channel.d.ts +10 -0
- package/dist/types/coordinator-mcp/server.d.ts +5 -1
- package/dist/types/crash/index-store.d.ts +87 -0
- package/dist/types/crash/nudge.d.ts +50 -0
- package/dist/types/crash/record-loader.d.ts +17 -0
- package/dist/types/crash/report.d.ts +134 -0
- package/dist/types/crash/sanitize.d.ts +25 -0
- package/dist/types/cursor.d.ts +13 -2
- package/dist/types/customization/import.d.ts +37 -0
- package/dist/types/customization/index.d.ts +4 -0
- package/dist/types/customization/inventory.d.ts +25 -0
- package/dist/types/customization/mutations.d.ts +32 -0
- package/dist/types/customization/types.d.ts +115 -0
- package/dist/types/daemon/control-types.d.ts +8 -1
- package/dist/types/daemon/operator-contract.d.ts +4 -4
- package/dist/types/discovery/claude.d.ts +39 -1
- package/dist/types/discovery/codex.d.ts +39 -1
- package/dist/types/discovery/index.d.ts +2 -0
- package/dist/types/discovery/mcp-compat.d.ts +30 -0
- package/dist/types/edit/renderer.d.ts +23 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -4
- package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
- package/dist/types/extensibility/hooks/loader.d.ts +19 -5
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
- package/dist/types/extensibility/skill-management.d.ts +107 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
- package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
- package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
- package/dist/types/hooks/events.d.ts +60 -0
- package/dist/types/hooks/normalize.d.ts +83 -0
- package/dist/types/hooks/skill-state.d.ts +9 -2
- package/dist/types/main.d.ts +2 -1
- package/dist/types/modes/acp/abort-scope.d.ts +13 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -4
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
- package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
- package/dist/types/modes/components/customization/index.d.ts +1 -0
- package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
- package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
- package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
- package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
- package/dist/types/modes/components/oauth-selector.d.ts +10 -4
- package/dist/types/modes/components/pet-capability.d.ts +7 -21
- package/dist/types/modes/components/settings-selector.d.ts +5 -0
- package/dist/types/modes/components/status-line/gh.d.ts +6 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
- package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/defaults/index.d.ts +118 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
- package/dist/types/runtime-mcp/config.d.ts +9 -0
- package/dist/types/runtime-mcp/manager.d.ts +26 -1
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
- package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
- package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
- package/dist/types/runtime-mcp/protocol.d.ts +278 -0
- package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
- package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
- package/dist/types/runtime-mcp/types.d.ts +102 -10
- package/dist/types/sdk/acp/adapter.d.ts +26 -5
- package/dist/types/sdk/broker/broker.d.ts +3 -55
- package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
- package/dist/types/sdk/broker/session-index.d.ts +14 -5
- package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
- package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
- package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
- package/dist/types/sdk/bus/index.d.ts +85 -99
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
- package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
- package/dist/types/sdk/bus/notification-service.d.ts +52 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
- package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
- package/dist/types/sdk/bus/session-control.d.ts +25 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
- package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
- package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
- package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
- package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
- package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
- package/dist/types/sdk/cli/rows.d.ts +2 -11
- package/dist/types/sdk/cli/session-cli.d.ts +15 -25
- package/dist/types/sdk/client/client.d.ts +97 -1
- package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +18 -5
- package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
- package/dist/types/sdk/host/host.d.ts +15 -1
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/host/session-runtime.d.ts +127 -7
- package/dist/types/sdk/host/types.d.ts +1 -1
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +2 -1
- package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
- package/dist/types/sdk/lifecycle/client.d.ts +59 -0
- package/dist/types/sdk/lifecycle/index.d.ts +3 -0
- package/dist/types/sdk/lifecycle/service.d.ts +232 -0
- package/dist/types/sdk/mcp/server.d.ts +9 -6
- package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
- package/dist/types/sdk/prompt-status.d.ts +10 -3
- package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
- package/dist/types/sdk/receipt-state.d.ts +17 -0
- package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
- package/dist/types/sdk/router/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +152 -0
- package/dist/types/sdk/session-list.d.ts +22 -0
- package/dist/types/sdk/session-reconnect.d.ts +43 -1
- package/dist/types/sdk/session.d.ts +34 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
- package/dist/types/session/account-inventory.d.ts +86 -0
- package/dist/types/session/agent-session.d.ts +145 -6
- package/dist/types/session/agent-storage.d.ts +10 -0
- package/dist/types/session/coordinator-tool-label.d.ts +28 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
- package/dist/types/session/messages.d.ts +1 -1
- package/dist/types/session/secure-token-file.d.ts +3 -0
- package/dist/types/session/session-manager.d.ts +47 -1
- package/dist/types/session/startup-auth-config.d.ts +44 -0
- package/dist/types/session/terminal-abort.d.ts +378 -0
- package/dist/types/session/yield-queue.d.ts +7 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +88 -0
- package/dist/types/session-import/command.d.ts +35 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/provider-codex.d.ts +22 -0
- package/dist/types/session-import/provider-service.d.ts +38 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +47 -0
- package/dist/types/session-import/types.d.ts +120 -0
- package/dist/types/setup/paseo/check.d.ts +16 -0
- package/dist/types/setup/paseo/install-saga.d.ts +115 -0
- package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
- package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
- package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
- package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
- package/dist/types/setup/paseo/provider-config.d.ts +35 -0
- package/dist/types/setup/paseo/remove.d.ts +14 -0
- package/dist/types/setup/paseo/result-types.d.ts +59 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
- package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
- package/dist/types/skill-state/active-state.d.ts +5 -1
- package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
- package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
- package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
- package/dist/types/slash-commands/types.d.ts +7 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/tools/ask-answer-registry.d.ts +6 -3
- package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
- package/dist/types/tools/bash.d.ts +2 -6
- package/dist/types/tools/browser/attach.d.ts +11 -0
- package/dist/types/tools/browser/launch.d.ts +20 -0
- package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
- package/dist/types/tools/browser/render.d.ts +1 -0
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +10 -2
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/monitor.d.ts +1 -1
- package/dist/types/tools/skill-discovery.d.ts +8 -1
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
- package/dist/types/utils/herdr-pane.d.ts +76 -0
- package/package.json +19 -9
- package/scripts/build-binary.ts +2 -1
- package/scripts/build-sdk-package-smoke.ts +2 -13
- package/scripts/capture-sticky-viewport-showcase.ts +0 -1
- package/scripts/compile-args.ts +2 -0
- package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
- package/scripts/generate-sdk-operation-inventory.ts +28 -0
- package/scripts/generate-telegram-baseline-manifest.ts +1 -0
- package/scripts/generate-tool-catalog.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
- package/src/async/job-manager.ts +352 -17
- package/src/async/support.ts +25 -0
- package/src/autoresearch/dashboard.ts +28 -13
- package/src/capability/index.ts +3 -1
- package/src/capability/mcp.ts +6 -0
- package/src/capability/types.ts +6 -0
- package/src/cli/accounts-cli.ts +395 -0
- package/src/cli/args.ts +23 -0
- package/src/cli/auth-broker-cli.ts +136 -66
- package/src/cli/auth-gateway-cli.ts +94 -70
- package/src/cli/config-cli.ts +27 -2
- package/src/cli/crash-cli.ts +78 -0
- package/src/cli/customize-doctor.ts +1605 -0
- package/src/cli/daemon-cli.ts +12 -3
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/mcp-cli.ts +128 -60
- package/src/cli/notify-cli.ts +8 -14
- package/src/cli/root-flags.ts +8 -0
- package/src/cli/setup-cli.ts +145 -0
- package/src/cli/skills-cli.ts +38 -2
- package/src/cli/update-cli.ts +167 -12
- package/src/cli.ts +4 -0
- package/src/commands/accounts.ts +71 -0
- package/src/commands/crash.ts +35 -0
- package/src/commands/customize.ts +84 -0
- package/src/commands/daemon.ts +2 -2
- package/src/commands/deep-interview.ts +2 -0
- package/src/commands/launch.ts +1 -0
- package/src/commands/mcp.ts +12 -7
- package/src/commands/ralplan.ts +2 -0
- package/src/commands/sdk.ts +65 -32
- package/src/commands/setup.ts +5 -0
- package/src/commands/skills.ts +14 -4
- package/src/commands/ultragoal.ts +10 -1
- package/src/commands/update.ts +26 -15
- package/src/config/atomic-yaml-patch-worker.ts +57 -0
- package/src/config/atomic-yaml-patch.ts +769 -29
- package/src/config/config-schema-version.ts +10 -0
- package/src/config/file-lock.ts +70 -1
- package/src/config/mcp-schema.json +52 -14
- package/src/config/model-profile-activation.ts +23 -2
- package/src/config/model-profiles.ts +88 -21
- package/src/config/model-registry.ts +147 -21
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +77 -7
- package/src/config/settings.ts +4038 -199
- package/src/config/skill-settings-defaults.ts +41 -3
- package/src/config/task-delegation.ts +87 -0
- package/src/config/update-channel.ts +23 -0
- package/src/coordinator-mcp/server.ts +377 -308
- package/src/crash/index-store.ts +723 -0
- package/src/crash/nudge.ts +89 -0
- package/src/crash/record-loader.ts +149 -0
- package/src/crash/report.ts +551 -0
- package/src/crash/sanitize.ts +129 -0
- package/src/cursor.ts +36 -10
- package/src/customization/import.ts +946 -0
- package/src/customization/index.ts +4 -0
- package/src/customization/inventory.ts +335 -0
- package/src/customization/mutations.ts +141 -0
- package/src/customization/types.ts +223 -0
- package/src/daemon/control-types.ts +8 -1
- package/src/daemon/operator-contract.ts +6 -6
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
- package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
- package/src/discovery/claude-hooks.ts +38 -0
- package/src/discovery/claude.ts +142 -209
- package/src/discovery/codex-hooks.ts +38 -0
- package/src/discovery/codex.ts +132 -198
- package/src/discovery/helpers.ts +8 -1
- package/src/discovery/index.ts +2 -0
- package/src/discovery/mcp-compat.ts +234 -0
- package/src/discovery/mcp-json.ts +6 -0
- package/src/edit/renderer.ts +40 -0
- package/src/edit/streaming.ts +4 -2
- package/src/exec/bash-executor.ts +24 -20
- package/src/export/html/index.ts +51 -2
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +17 -4
- package/src/extensibility/extensions/types.ts +31 -6
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
- package/src/extensibility/gjc-plugins/installer.ts +9 -5
- package/src/extensibility/gjc-plugins/observability.ts +8 -2
- package/src/extensibility/gjc-plugins/registry.ts +13 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
- package/src/extensibility/gjc-plugins/schema.ts +222 -39
- package/src/extensibility/hooks/loader.ts +154 -10
- package/src/extensibility/runtime-skill-discovery.ts +257 -60
- package/src/extensibility/skill-management.ts +368 -0
- package/src/extensibility/skills.ts +52 -16
- package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
- package/src/gjc-runtime/launch-worktree.ts +56 -2
- package/src/gjc-runtime/linux-proc.ts +7 -1
- package/src/gjc-runtime/ralplan-runtime.ts +100 -164
- package/src/gjc-runtime/session-state-lock.ts +992 -0
- package/src/gjc-runtime/session-state-sidecar.ts +644 -142
- package/src/gjc-runtime/state-writer.ts +39 -14
- package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
- package/src/gjc-runtime/ultragoal-guard.ts +11 -4
- package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
- package/src/gjc-runtime/workflow-settings.ts +596 -0
- package/src/goals/tools/goal-tool.ts +6 -1
- package/src/hooks/events.ts +291 -0
- package/src/hooks/native-skill-hook.ts +4 -0
- package/src/hooks/normalize.ts +369 -0
- package/src/hooks/skill-state.ts +165 -46
- package/src/internal-urls/docs-index.generated.ts +41 -32
- package/src/main.ts +36 -5
- package/src/migrate/mcp-mapper.ts +25 -2
- package/src/modes/acp/abort-scope.ts +29 -0
- package/src/modes/acp/acp-agent.ts +423 -98
- package/src/modes/components/assistant-message.ts +6 -1
- package/src/modes/components/chrome.ts +4 -3
- package/src/modes/components/customization/customization-dashboard.ts +443 -0
- package/src/modes/components/customization/import-wizard.ts +372 -0
- package/src/modes/components/customization/index.ts +1 -0
- package/src/modes/components/dynamic-theme-text.ts +22 -0
- package/src/modes/components/gajae-pet-widget.ts +607 -193
- package/src/modes/components/irc-sidebar.ts +11 -0
- package/src/modes/components/iterm-pet-transport.ts +585 -0
- package/src/modes/components/model-selector.ts +1 -6
- package/src/modes/components/oauth-selector.ts +277 -86
- package/src/modes/components/pet-capability.ts +35 -31
- package/src/modes/components/settings-selector.ts +29 -5
- package/src/modes/components/status-line/gh.ts +45 -1
- package/src/modes/components/status-line/segments.ts +19 -4
- package/src/modes/components/tool-status-header.ts +13 -3
- package/src/modes/controllers/command-controller.ts +20 -8
- package/src/modes/controllers/extension-ui-controller.ts +56 -257
- package/src/modes/controllers/input-controller.ts +10 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
- package/src/modes/controllers/selector-controller.ts +238 -77
- package/src/modes/controllers/stt-controller.ts +3 -3
- package/src/modes/interactive-mode.ts +88 -16
- package/src/modes/print-mode.ts +35 -6
- package/src/modes/runtime-init.ts +21 -68
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/defaults/ouroboros.json +118 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/injected-user-submission.ts +25 -0
- package/src/modes/utils/terminal-bell.ts +15 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/critic.md +1 -1
- package/src/prompts/system/rlm-research.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -2
- package/src/prompts/tools/bash.md +7 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +6 -6
- package/src/prompts/tools/read.md +0 -2
- package/src/prompts/tools/skill-discovery.md +4 -2
- package/src/runtime-mcp/client.ts +427 -23
- package/src/runtime-mcp/config.ts +37 -5
- package/src/runtime-mcp/manager-pool.test.ts +56 -0
- package/src/runtime-mcp/manager.ts +52 -2
- package/src/runtime-mcp/oauth-discovery.ts +13 -0
- package/src/runtime-mcp/oauth-flow.ts +46 -1
- package/src/runtime-mcp/pool-key.ts +5 -1
- package/src/runtime-mcp/pool.test.ts +9 -0
- package/src/runtime-mcp/protocol.test.ts +353 -0
- package/src/runtime-mcp/protocol.ts +624 -0
- package/src/runtime-mcp/scope-config.ts +78 -0
- package/src/runtime-mcp/tool-bridge.ts +44 -10
- package/src/runtime-mcp/transports/http.ts +84 -13
- package/src/runtime-mcp/types.ts +123 -12
- package/src/sdk/acp/adapter.ts +328 -97
- package/src/sdk/broker/broker.ts +160 -741
- package/src/sdk/broker/ensure.ts +9 -1
- package/src/sdk/broker/lifecycle.ts +510 -116
- package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
- package/src/sdk/broker/process-incarnation.ts +19 -0
- package/src/sdk/broker/session-index.ts +516 -117
- package/src/sdk/broker/startup-failure.ts +28 -5
- package/src/sdk/broker/transport.ts +1 -13
- package/src/sdk/bus/chat-daemon-cli.ts +4 -2
- package/src/sdk/bus/chat-daemon-control.ts +67 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
- package/src/sdk/bus/config.ts +13 -0
- package/src/sdk/bus/conversation-store.ts +59 -171
- package/src/sdk/bus/daemon-paths.ts +20 -0
- package/src/sdk/bus/discord-conversation.ts +2 -0
- package/src/sdk/bus/discord-daemon.ts +524 -126
- package/src/sdk/bus/discord-live-provider.ts +206 -79
- package/src/sdk/bus/existing-thread-readiness.ts +4 -97
- package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
- package/src/sdk/bus/index.ts +2480 -456
- package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
- package/src/sdk/bus/lifecycle-commands.ts +2 -2
- package/src/sdk/bus/managed-daemon.ts +2 -145
- package/src/sdk/bus/notification-service.ts +196 -0
- package/src/sdk/bus/operator-runtime.ts +1 -1
- package/src/sdk/bus/prompt-reconciliation.ts +24 -15
- package/src/sdk/bus/reconciliation-store.ts +553 -24
- package/src/sdk/bus/session-control.ts +49 -3
- package/src/sdk/bus/slack-conversation.ts +4 -0
- package/src/sdk/bus/slack-daemon.ts +733 -134
- package/src/sdk/bus/slack-live-provider.ts +65 -15
- package/src/sdk/bus/slack-provider.ts +4 -0
- package/src/sdk/bus/slack-thread-binding.ts +9 -104
- package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
- package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
- package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
- package/src/sdk/bus/telegram-daemon-control.ts +3 -13
- package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
- package/src/sdk/bus/telegram-daemon.ts +2552 -2280
- package/src/sdk/bus/telegram-reference.ts +1 -226
- package/src/sdk/bus/telegram-setup.ts +2 -2
- package/src/sdk/bus/threaded-render.ts +1 -0
- package/src/sdk/bus/topic-registry.ts +279 -146
- package/src/sdk/cli/rows.ts +2 -35
- package/src/sdk/cli/session-cli.ts +776 -700
- package/src/sdk/client/client.ts +892 -1
- package/src/sdk/host/control/dispatch.ts +93 -58
- package/src/sdk/host/control/index.ts +1 -1
- package/src/sdk/host/control/operations.ts +18 -10
- package/src/sdk/host/control/runtime-gate.ts +40 -0
- package/src/sdk/host/host.ts +63 -12
- package/src/sdk/host/query/handlers.ts +66 -7
- package/src/sdk/host/reverse-leases.ts +2 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +2787 -34
- package/src/sdk/host/session-runtime.ts +1489 -101
- package/src/sdk/host/surface-policy.ts +12 -8
- package/src/sdk/host/types.ts +1 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
- package/src/sdk/host/websocket-transport.ts +85 -50
- package/src/sdk/index.ts +2 -1
- package/src/sdk/lifecycle/broker-client.ts +53 -0
- package/src/sdk/lifecycle/client.ts +234 -0
- package/src/sdk/lifecycle/index.ts +3 -0
- package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
- package/src/sdk/lifecycle/service.ts +720 -0
- package/src/sdk/mcp/server.ts +226 -188
- package/src/sdk/prompt-deadline-lease.ts +55 -0
- package/src/sdk/prompt-deadline-manager.ts +148 -0
- package/src/sdk/prompt-status.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +272 -129
- package/src/sdk/protocol/operation-registry.ts +71 -8
- package/src/sdk/receipt-state.ts +43 -0
- package/src/sdk/reconciliation-extensions.ts +14 -0
- package/src/sdk/router/index.ts +1 -0
- package/src/sdk/router/session-router.ts +1658 -0
- package/src/sdk/session-list.ts +93 -0
- package/src/sdk/session-reconnect.ts +45 -1
- package/src/sdk/session.ts +716 -120
- package/src/sdk/transport/serve-cli.ts +18 -34
- package/src/session/account-inventory.ts +603 -0
- package/src/session/agent-session.ts +2873 -559
- package/src/session/agent-storage.ts +35 -0
- package/src/session/blob-store.ts +32 -1
- package/src/session/coordinator-tool-label.ts +40 -0
- package/src/session/internal/managed-session-scope.ts +4 -0
- package/src/session/internal/managed-session-storage.ts +199 -31
- package/src/session/messages.ts +1 -1
- package/src/session/secure-token-file.ts +80 -0
- package/src/session/session-manager.ts +757 -57
- package/src/session/session-storage.ts +25 -16
- package/src/session/startup-auth-config.ts +297 -0
- package/src/session/terminal-abort.ts +1253 -0
- package/src/session/yield-queue.ts +57 -15
- package/src/session-import/claude.ts +334 -0
- package/src/session-import/codex.ts +1033 -0
- package/src/session-import/command.ts +77 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +7 -0
- package/src/session-import/provider-codex.ts +327 -0
- package/src/session-import/provider-service.ts +457 -0
- package/src/session-import/redact.ts +128 -0
- package/src/session-import/service.ts +1264 -0
- package/src/session-import/types.ts +148 -0
- package/src/setup/paseo/check.ts +213 -0
- package/src/setup/paseo/install-saga.ts +293 -0
- package/src/setup/paseo/json-publisher.ts +263 -0
- package/src/setup/paseo/orchestration-preferences.ts +86 -0
- package/src/setup/paseo/paseo-ownership.ts +224 -0
- package/src/setup/paseo/paseo-setup.ts +253 -0
- package/src/setup/paseo/provider-config.ts +139 -0
- package/src/setup/paseo/remove.ts +183 -0
- package/src/setup/paseo/result-types.ts +79 -0
- package/src/setup/paseo/setup-deps.ts +175 -0
- package/src/setup/paseo/skills-bridge.ts +260 -0
- package/src/skill-state/active-state.ts +21 -11
- package/src/slash-commands/acp-builtins.ts +9 -1
- package/src/slash-commands/builtin-registry.ts +117 -19
- package/src/slash-commands/helpers/credential-switch.ts +143 -0
- package/src/slash-commands/helpers/mcp.ts +9 -3
- package/src/slash-commands/helpers/usage-report.ts +61 -56
- package/src/slash-commands/types.ts +7 -2
- package/src/task/executor.ts +30 -11
- package/src/task/index.ts +66 -7
- package/src/task/render.ts +23 -9
- package/src/tools/ask-answer-registry.ts +23 -7
- package/src/tools/ask.ts +8 -4
- package/src/tools/bash-sleep-advisory.ts +115 -0
- package/src/tools/bash.ts +98 -18
- package/src/tools/browser/attach.ts +71 -3
- package/src/tools/browser/launch.ts +86 -13
- package/src/tools/browser/profile-discovery.ts +57 -7
- package/src/tools/browser/render.ts +8 -1
- package/src/tools/browser.ts +130 -21
- package/src/tools/computer.enforcement.test.ts +1 -1
- package/src/tools/computer.ts +3 -2
- package/src/tools/cron.ts +12 -1
- package/src/tools/image-gen.ts +349 -142
- package/src/tools/index.ts +8 -0
- package/src/tools/job.ts +33 -6
- package/src/tools/monitor.ts +41 -2
- package/src/tools/skill-discovery.ts +21 -29
- package/src/tools/subagent.ts +142 -24
- package/src/tools/todo-contract.ts +55 -3
- package/src/tools/todo-write.ts +4 -3
- package/src/tools/tool-catalog.generated.ts +6 -6
- package/src/tools/ultragoal-ask-guard.ts +10 -3
- package/src/utils/herdr-pane.ts +345 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/scrapers/index.ts +6 -4
- package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
- package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
- package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
- package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
- package/dist/types/runtime/lazy-service.test.d.ts +0 -1
- package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
- package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
- package/dist/types/runtime-mcp/config.test.d.ts +0 -1
- package/dist/types/runtime-mcp/http.test.d.ts +0 -1
- package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
- package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
- package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
- package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
- package/dist/types/sdk/elevation/audit.d.ts +0 -93
- package/dist/types/sdk/elevation/capability.d.ts +0 -3
- package/dist/types/sdk/elevation/digest.d.ts +0 -72
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
- package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
- package/dist/types/sdk/elevation/owner.d.ts +0 -48
- package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
- package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
- package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
- package/dist/types/session/auth-broker-config.d.ts +0 -13
- package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
- package/dist/types/skills/skills.test.d.ts +0 -1
- package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
- package/dist/types/task/token-log.test.d.ts +0 -1
- package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
- package/dist/types/tools/descriptors.test.d.ts +0 -1
- package/scripts/g005-daemon-smoke.ts +0 -180
- package/scripts/g011-daemon-path-smoke.ts +0 -153
- package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
- package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
- package/src/sdk/bus/telegram-cli.ts +0 -212
- package/src/sdk/elevation/allowlist.ts +0 -54
- package/src/sdk/elevation/audit.ts +0 -208
- package/src/sdk/elevation/capability.ts +0 -41
- package/src/sdk/elevation/digest.ts +0 -129
- package/src/sdk/elevation/dispatch-receipt.ts +0 -184
- package/src/sdk/elevation/grant-ledger.ts +0 -1142
- package/src/sdk/elevation/owner.ts +0 -109
- package/src/session/auth-broker-config.ts +0 -109
- /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
- /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
- /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
|
@@ -1,1733 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wires the authenticated Rust control endpoint (NotificationControlServer) to
|
|
3
|
-
* the lifecycle orchestrator with REAL daemon-side effects: a daemon-safe tmux
|
|
4
|
-
* launcher (create / cold-restart), force-close, and reattach-or-cold-restart
|
|
5
|
-
* resume. Kept separate from telegram-daemon.ts so the effects + wiring are
|
|
6
|
-
* unit-testable; the daemon calls {@link attachLifecycleControl} once it owns
|
|
7
|
-
* the control server.
|
|
8
|
-
*/
|
|
9
|
-
import * as crypto from "node:crypto";
|
|
10
|
-
import * as fs from "node:fs";
|
|
11
|
-
import * as fsPromises from "node:fs/promises";
|
|
12
|
-
import * as path from "node:path";
|
|
13
|
-
import type { NotificationControlServer as NativeNotificationControlServer } from "@gajae-code/natives";
|
|
14
|
-
import { logger } from "@gajae-code/utils";
|
|
15
|
-
import { readLinuxProcStartTime } from "../../gjc-runtime/linux-proc";
|
|
16
|
-
import {
|
|
17
|
-
MANAGED_OWNER_PREDECESSOR_GENERATION_ENV,
|
|
18
|
-
MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV,
|
|
19
|
-
MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
|
|
20
|
-
MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
|
|
21
|
-
MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
|
|
22
|
-
} from "../../gjc-runtime/managed-owner-admission";
|
|
23
|
-
import {
|
|
24
|
-
MANAGED_OWNER_CHILD_TOKEN_ENV,
|
|
25
|
-
MANAGED_OWNER_COMMAND_ENV,
|
|
26
|
-
MANAGED_OWNER_INCARNATION_ENV,
|
|
27
|
-
MANAGED_OWNER_RUN_ID_ENV,
|
|
28
|
-
MANAGED_OWNER_SUPERVISOR_ARG,
|
|
29
|
-
} from "../../gjc-runtime/managed-owner-supervisor";
|
|
30
|
-
import { tmuxRuntimeSessionPath } from "../../gjc-runtime/session-layout";
|
|
31
|
-
import {
|
|
32
|
-
GJC_COORDINATOR_SESSION_ID_ENV,
|
|
33
|
-
GJC_COORDINATOR_SESSION_STATE_FILE_ENV,
|
|
34
|
-
GJC_TMUX_OWNER_GENERATION_ENV,
|
|
35
|
-
GJC_TMUX_OWNER_SERVER_KEY_ENV,
|
|
36
|
-
GJC_TMUX_OWNER_STATE_DIR_ENV,
|
|
37
|
-
} from "../../gjc-runtime/session-state-sidecar";
|
|
38
|
-
import {
|
|
39
|
-
buildGjcTmuxProfileCommands,
|
|
40
|
-
buildGjcTmuxSessionSlug,
|
|
41
|
-
resolveGjcTmuxBinary,
|
|
42
|
-
resolveGjcTmuxCommand,
|
|
43
|
-
} from "../../gjc-runtime/tmux-common";
|
|
44
|
-
import {
|
|
45
|
-
captureOwnerGenerationBaseline,
|
|
46
|
-
classifyCgroup,
|
|
47
|
-
isExactScopedBootstrapSuccessReceipt,
|
|
48
|
-
type OwnerGenerationBaseline,
|
|
49
|
-
type OwnerIsolationProbe,
|
|
50
|
-
planTmuxOwnerIsolation,
|
|
51
|
-
replaceOwnerGeneration,
|
|
52
|
-
resolveManagedOwnerPredecessorSync,
|
|
53
|
-
type TmuxServerProof,
|
|
54
|
-
} from "../../gjc-runtime/tmux-owner-isolation";
|
|
55
|
-
import {
|
|
56
|
-
findGjcTmuxSessionByName,
|
|
57
|
-
forceCloseGjcTmuxSession,
|
|
58
|
-
type GjcTmuxSessionStatus,
|
|
59
|
-
listGjcTmuxSessions,
|
|
60
|
-
} from "../../gjc-runtime/tmux-sessions";
|
|
61
|
-
import { processIncarnation } from "../broker/process-incarnation";
|
|
62
|
-
import type {
|
|
63
|
-
LifecycleErrorReason,
|
|
64
|
-
ResumeCandidate,
|
|
65
|
-
SessionCreateFrame,
|
|
66
|
-
SessionLifecycleRequest,
|
|
67
|
-
SessionLifecycleResponse,
|
|
68
|
-
} from "./index";
|
|
69
|
-
import { normalizeLifecyclePath } from "./lifecycle-commands";
|
|
70
|
-
import {
|
|
71
|
-
type AuditEvent,
|
|
72
|
-
type CreateEffectResult,
|
|
73
|
-
handleLifecycleRequest,
|
|
74
|
-
type LedgerDoc,
|
|
75
|
-
type LedgerEntry,
|
|
76
|
-
type LedgerStore,
|
|
77
|
-
type LifecycleOutcome,
|
|
78
|
-
type OrchestratorDeps,
|
|
79
|
-
type ResumeEffectResult,
|
|
80
|
-
} from "./lifecycle-orchestrator";
|
|
81
|
-
import { listRecentSessions } from "./recent-activity";
|
|
82
|
-
|
|
83
|
-
const directLifecycleManagedOwnerEnvUnsets = [
|
|
84
|
-
GJC_TMUX_OWNER_GENERATION_ENV,
|
|
85
|
-
GJC_TMUX_OWNER_STATE_DIR_ENV,
|
|
86
|
-
GJC_TMUX_OWNER_SERVER_KEY_ENV,
|
|
87
|
-
MANAGED_OWNER_COMMAND_ENV,
|
|
88
|
-
MANAGED_OWNER_RUN_ID_ENV,
|
|
89
|
-
MANAGED_OWNER_INCARNATION_ENV,
|
|
90
|
-
MANAGED_OWNER_CHILD_TOKEN_ENV,
|
|
91
|
-
MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
|
|
92
|
-
MANAGED_OWNER_PREDECESSOR_GENERATION_ENV,
|
|
93
|
-
MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
|
|
94
|
-
MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV,
|
|
95
|
-
MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
|
|
96
|
-
] as const;
|
|
97
|
-
const directLifecycleStabilizationMs = 250;
|
|
98
|
-
|
|
99
|
-
function directLifecycleEnvArguments(env: Record<string, string>): string {
|
|
100
|
-
return [
|
|
101
|
-
...directLifecycleManagedOwnerEnvUnsets.flatMap(name => ["-u", shellQuote(name)]),
|
|
102
|
-
...Object.entries(env).map(([key, value]) => `${key}=${shellQuote(value)}`),
|
|
103
|
-
].join(" ");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
type NativeControlServerConstructor = new (
|
|
107
|
-
token: string,
|
|
108
|
-
ownerId: string,
|
|
109
|
-
agentDir?: string,
|
|
110
|
-
) => NativeNotificationControlServer;
|
|
111
|
-
|
|
112
|
-
interface NativeControlServerModule {
|
|
113
|
-
NotificationControlServer: NativeControlServerConstructor;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Minimal view of the native control server this runtime depends on. */
|
|
117
|
-
export interface ControlServerLike {
|
|
118
|
-
onLifecycleRequest(
|
|
119
|
-
cb: (err: Error | null, req: { kind: string; requestId: string; payloadJson: string }) => void,
|
|
120
|
-
): void;
|
|
121
|
-
respond(responseJson: string): void;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* A startable control server (the native NotificationControlServer, or a fake in
|
|
126
|
-
* tests). Extends {@link ControlServerLike} with the start/stop lifecycle the
|
|
127
|
-
* daemon owns.
|
|
128
|
-
*/
|
|
129
|
-
export interface LifecycleControlServer extends ControlServerLike {
|
|
130
|
-
start(): Promise<unknown>;
|
|
131
|
-
stop(): void;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Factory the daemon uses to construct a control server bound to its ownership. */
|
|
135
|
-
export type LifecycleControlServerFactory = (input: {
|
|
136
|
-
token: string;
|
|
137
|
-
ownerId: string;
|
|
138
|
-
agentDir: string;
|
|
139
|
-
}) => LifecycleControlServer;
|
|
140
|
-
const lifecycleErrorReasons = new Set<LifecycleErrorReason>([
|
|
141
|
-
"unauthorized",
|
|
142
|
-
"rate_limited",
|
|
143
|
-
"duplicate_conflict",
|
|
144
|
-
"invalid_target",
|
|
145
|
-
"ambiguous_target",
|
|
146
|
-
"spawn_failed",
|
|
147
|
-
"discovery_timeout",
|
|
148
|
-
"readiness_timeout",
|
|
149
|
-
"close_refused",
|
|
150
|
-
"not_found",
|
|
151
|
-
"terminal_uncertain",
|
|
152
|
-
"unsupported_platform",
|
|
153
|
-
]);
|
|
154
|
-
|
|
155
|
-
function isLifecycleErrorReason(value: unknown): value is LifecycleErrorReason {
|
|
156
|
-
return typeof value === "string" && lifecycleErrorReasons.has(value as LifecycleErrorReason);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Atomic + fsynced file-backed idempotency ledger store. */
|
|
160
|
-
function hasNonDirectoryLedgerParent(idempotencyFile: string): boolean {
|
|
161
|
-
let parent = path.dirname(idempotencyFile);
|
|
162
|
-
while (true) {
|
|
163
|
-
try {
|
|
164
|
-
return !fs.statSync(parent).isDirectory();
|
|
165
|
-
} catch (error) {
|
|
166
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
167
|
-
const nextParent = path.dirname(parent);
|
|
168
|
-
if (nextParent === parent) return false;
|
|
169
|
-
parent = nextParent;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function ledgerReadError(error: unknown): Error {
|
|
175
|
-
const errorCode = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
176
|
-
const code = typeof errorCode === "string" ? errorCode : "invalid";
|
|
177
|
-
return new Error(`gjc_lifecycle_ledger_read_failed:${code.slice(0, 32)}`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function isLedgerDoc(value: unknown): value is LedgerDoc {
|
|
181
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
182
|
-
const doc = value as { version?: unknown; entries?: unknown };
|
|
183
|
-
if (doc.version !== 1 || !doc.entries || typeof doc.entries !== "object" || Array.isArray(doc.entries)) return false;
|
|
184
|
-
return Object.values(doc.entries).every(entry => {
|
|
185
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
186
|
-
const candidate = entry as Partial<LedgerEntry>;
|
|
187
|
-
const isBaseEntry =
|
|
188
|
-
typeof candidate.requestHash === "string" &&
|
|
189
|
-
(candidate.state === "in_progress" ||
|
|
190
|
-
candidate.state === "success" ||
|
|
191
|
-
candidate.state === "failure" ||
|
|
192
|
-
candidate.state === "terminal_uncertain") &&
|
|
193
|
-
typeof candidate.requestId === "string" &&
|
|
194
|
-
(candidate.verb === "session_create" ||
|
|
195
|
-
candidate.verb === "session_close" ||
|
|
196
|
-
candidate.verb === "session_resume") &&
|
|
197
|
-
typeof candidate.createdAt === "number" &&
|
|
198
|
-
typeof candidate.updatedAt === "number" &&
|
|
199
|
-
!!candidate.targetSummary &&
|
|
200
|
-
typeof candidate.targetSummary === "object" &&
|
|
201
|
-
!Array.isArray(candidate.targetSummary);
|
|
202
|
-
if (!isBaseEntry) return false;
|
|
203
|
-
if (candidate.state === "failure" || candidate.state === "terminal_uncertain")
|
|
204
|
-
return isLifecycleErrorReason(candidate.reason);
|
|
205
|
-
if (candidate.state !== "success") return true;
|
|
206
|
-
switch (candidate.verb) {
|
|
207
|
-
case "session_create":
|
|
208
|
-
return (
|
|
209
|
-
typeof candidate.intendedSessionId === "string" &&
|
|
210
|
-
typeof candidate.sessionId === "string" &&
|
|
211
|
-
typeof candidate.tmuxSession === "string" &&
|
|
212
|
-
typeof candidate.endpointUrl === "string"
|
|
213
|
-
);
|
|
214
|
-
case "session_close":
|
|
215
|
-
return typeof candidate.sessionId === "string" && typeof candidate.processGone === "boolean";
|
|
216
|
-
case "session_resume":
|
|
217
|
-
return (
|
|
218
|
-
typeof candidate.sessionId === "string" &&
|
|
219
|
-
typeof candidate.tmuxSession === "string" &&
|
|
220
|
-
typeof candidate.endpointUrl === "string" &&
|
|
221
|
-
(candidate.resumeMode === undefined ||
|
|
222
|
-
candidate.resumeMode === "reattached" ||
|
|
223
|
-
candidate.resumeMode === "cold_restarted")
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
return false;
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function migrateLegacyLedger(doc: LedgerDoc): LedgerDoc {
|
|
231
|
-
for (const entry of Object.values(doc.entries)) {
|
|
232
|
-
if (entry.state === "success" && entry.verb === "session_resume" && entry.resumeMode === undefined) {
|
|
233
|
-
entry.resumeMode = "reattached";
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return doc;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const LIFECYCLE_COMPATIBILITY_DIAGNOSTIC_LIMIT = 1;
|
|
240
|
-
const LIFECYCLE_CONTROL_DIAGNOSTIC_LIMIT = 10;
|
|
241
|
-
let lifecycleCompatibilityDiagnosticCount = 0;
|
|
242
|
-
let lifecycleControlDiagnosticCount = 0;
|
|
243
|
-
|
|
244
|
-
function isUnsupportedDirectorySyncError(error: unknown): boolean {
|
|
245
|
-
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
246
|
-
return (
|
|
247
|
-
code === "EINVAL" ||
|
|
248
|
-
code === "ENOTSUP" ||
|
|
249
|
-
code === "EOPNOTSUPP" ||
|
|
250
|
-
(process.platform === "win32" && code === "EPERM")
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function recordBoundedLifecycleDiagnostic(message: string, compatibility = false): void {
|
|
255
|
-
if (compatibility) {
|
|
256
|
-
if (lifecycleCompatibilityDiagnosticCount >= LIFECYCLE_COMPATIBILITY_DIAGNOSTIC_LIMIT) return;
|
|
257
|
-
lifecycleCompatibilityDiagnosticCount++;
|
|
258
|
-
} else {
|
|
259
|
-
if (lifecycleControlDiagnosticCount >= LIFECYCLE_CONTROL_DIAGNOSTIC_LIMIT) return;
|
|
260
|
-
lifecycleControlDiagnosticCount++;
|
|
261
|
-
}
|
|
262
|
-
try {
|
|
263
|
-
logger.warn(message);
|
|
264
|
-
} catch {
|
|
265
|
-
// Diagnostics are best-effort and must not poison the lifecycle queue.
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function fsyncLedgerParentDirectory(directory: string): void {
|
|
270
|
-
let dirFd: number;
|
|
271
|
-
try {
|
|
272
|
-
dirFd = fs.openSync(directory, "r");
|
|
273
|
-
} catch (error) {
|
|
274
|
-
if (process.platform === "win32" && isUnsupportedDirectorySyncError(error)) {
|
|
275
|
-
recordBoundedLifecycleDiagnostic("GJC lifecycle ledger directory sync unsupported", true);
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
throw error;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
let syncFailure: unknown;
|
|
282
|
-
let closeFailure: unknown;
|
|
283
|
-
try {
|
|
284
|
-
fs.fsyncSync(dirFd);
|
|
285
|
-
} catch (error) {
|
|
286
|
-
if (process.platform === "win32" && isUnsupportedDirectorySyncError(error))
|
|
287
|
-
recordBoundedLifecycleDiagnostic("GJC lifecycle ledger directory sync unsupported", true);
|
|
288
|
-
else syncFailure = error;
|
|
289
|
-
} finally {
|
|
290
|
-
try {
|
|
291
|
-
fs.closeSync(dirFd);
|
|
292
|
-
} catch (error) {
|
|
293
|
-
closeFailure = error;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (syncFailure !== undefined && closeFailure !== undefined)
|
|
297
|
-
throw new AggregateError([syncFailure, closeFailure], "ledger directory sync failed");
|
|
298
|
-
if (syncFailure !== undefined) throw syncFailure;
|
|
299
|
-
if (closeFailure !== undefined) throw closeFailure;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function fileLedgerStore(idempotencyFile: string): LedgerStore {
|
|
303
|
-
return {
|
|
304
|
-
async read(): Promise<LedgerDoc> {
|
|
305
|
-
let contents: string;
|
|
306
|
-
try {
|
|
307
|
-
contents = fs.readFileSync(idempotencyFile, "utf8");
|
|
308
|
-
} catch (error) {
|
|
309
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
310
|
-
if (code === "ENOENT") {
|
|
311
|
-
let hasNonDirectoryParent: boolean;
|
|
312
|
-
try {
|
|
313
|
-
hasNonDirectoryParent = hasNonDirectoryLedgerParent(idempotencyFile);
|
|
314
|
-
} catch (parentError) {
|
|
315
|
-
throw ledgerReadError(parentError);
|
|
316
|
-
}
|
|
317
|
-
if (hasNonDirectoryParent) throw ledgerReadError({ code: "ENOTDIR" });
|
|
318
|
-
return { version: 1, entries: {} };
|
|
319
|
-
}
|
|
320
|
-
throw ledgerReadError(error);
|
|
321
|
-
}
|
|
322
|
-
try {
|
|
323
|
-
const doc = JSON.parse(contents) as unknown;
|
|
324
|
-
if (!isLedgerDoc(doc)) throw new Error("invalid");
|
|
325
|
-
return migrateLegacyLedger(doc);
|
|
326
|
-
} catch (error) {
|
|
327
|
-
throw ledgerReadError(error);
|
|
328
|
-
}
|
|
329
|
-
},
|
|
330
|
-
async write(doc: LedgerDoc): Promise<void> {
|
|
331
|
-
fs.mkdirSync(path.dirname(idempotencyFile), { recursive: true });
|
|
332
|
-
const tmp = `${idempotencyFile}.${process.pid}.${Date.now()}.tmp`;
|
|
333
|
-
let fd: number | undefined;
|
|
334
|
-
let writeFailure: unknown;
|
|
335
|
-
let closeFailure: unknown;
|
|
336
|
-
try {
|
|
337
|
-
fd = fs.openSync(tmp, "w", 0o600);
|
|
338
|
-
const encodedDoc = Buffer.from(JSON.stringify(doc), "utf8");
|
|
339
|
-
let offset = 0;
|
|
340
|
-
while (offset < encodedDoc.length) {
|
|
341
|
-
const written = fs.writeSync(fd, encodedDoc, offset, encodedDoc.length - offset);
|
|
342
|
-
if (written === 0) throw new Error("Short write");
|
|
343
|
-
offset += written;
|
|
344
|
-
}
|
|
345
|
-
fs.fsyncSync(fd);
|
|
346
|
-
} catch (error) {
|
|
347
|
-
writeFailure = error;
|
|
348
|
-
} finally {
|
|
349
|
-
if (fd !== undefined) {
|
|
350
|
-
try {
|
|
351
|
-
fs.closeSync(fd);
|
|
352
|
-
} catch (error) {
|
|
353
|
-
closeFailure = error;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
if (writeFailure !== undefined || closeFailure !== undefined) {
|
|
358
|
-
try {
|
|
359
|
-
fs.unlinkSync(tmp);
|
|
360
|
-
} catch {
|
|
361
|
-
// The failed temporary file is never published.
|
|
362
|
-
}
|
|
363
|
-
if (writeFailure !== undefined && closeFailure !== undefined)
|
|
364
|
-
throw new AggregateError([writeFailure, closeFailure], "ledger temporary write failed");
|
|
365
|
-
throw writeFailure ?? closeFailure;
|
|
366
|
-
}
|
|
367
|
-
fs.renameSync(tmp, idempotencyFile);
|
|
368
|
-
// The temp-file fsync does not persist the rename's directory entry.
|
|
369
|
-
fsyncLedgerParentDirectory(path.dirname(idempotencyFile));
|
|
370
|
-
},
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/** Append-only JSONL audit sink (0600). Never receives tokens or raw prompts. */
|
|
375
|
-
export function fileAudit(auditPath: string): (e: AuditEvent) => void {
|
|
376
|
-
return (e: AuditEvent) => {
|
|
377
|
-
fs.mkdirSync(path.dirname(auditPath), { recursive: true });
|
|
378
|
-
fs.appendFileSync(auditPath, `${JSON.stringify(e)}\n`, { mode: 0o600 });
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** Simple per-chat sliding-window create rate limiter. */
|
|
383
|
-
export function createRateLimiter(maxPerWindow: number, windowMs: number): (chatId: string, nowMs: number) => boolean {
|
|
384
|
-
const hits = new Map<string, number[]>();
|
|
385
|
-
return (chatId: string, nowMs: number) => {
|
|
386
|
-
const arr = (hits.get(chatId) ?? []).filter(t => nowMs - t < windowMs);
|
|
387
|
-
if (arr.length >= maxPerWindow) {
|
|
388
|
-
hits.set(chatId, arr);
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
|
-
arr.push(nowMs);
|
|
392
|
-
hits.set(chatId, arr);
|
|
393
|
-
return true;
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function tmuxSessionNameFor(sessionId: string): string {
|
|
398
|
-
return `gjc_lc_${sessionId}`;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/** Build the `gjc` argv for a create target (existing path / worktree / dir).
|
|
402
|
-
*
|
|
403
|
-
* The launched session id is carried via `GJC_SESSION_ID` in the child env (see
|
|
404
|
-
* {@link daemonSpawnCreate}); the root `gjc` launcher has no `--session-id`
|
|
405
|
-
* flag, so it must never appear in argv. Only flags the launch parser actually
|
|
406
|
-
* supports are emitted (`--worktree <branch>` for worktree targets,
|
|
407
|
-
* `--mpreset <profile>` for model presets). */
|
|
408
|
-
export function buildCreateArgv(
|
|
409
|
-
frame: SessionCreateFrame,
|
|
410
|
-
_ids: { intendedSessionId: string; startupPromptRef?: string },
|
|
411
|
-
): { cwd: string; args: string[] } {
|
|
412
|
-
const extraArgs: string[] = [];
|
|
413
|
-
if (frame.modelPreset) {
|
|
414
|
-
extraArgs.push("--mpreset", frame.modelPreset);
|
|
415
|
-
}
|
|
416
|
-
if (frame.target.kind === "worktree") {
|
|
417
|
-
const cwd = normalizeLifecyclePath(frame.target.repo);
|
|
418
|
-
if (!cwd) throw new Error("invalid_lifecycle_repo_path");
|
|
419
|
-
// Use the `--worktree=<branch>` form so the branch is a single argv token:
|
|
420
|
-
// a flag-shaped branch (e.g. `-x`) can never be mis-parsed as a separate
|
|
421
|
-
// launcher flag / detached-mode trigger.
|
|
422
|
-
return { cwd, args: [`--worktree=${frame.target.branch}`, ...extraArgs] };
|
|
423
|
-
}
|
|
424
|
-
const cwd = normalizeLifecyclePath(frame.target.path);
|
|
425
|
-
if (!cwd) throw new Error("invalid_lifecycle_path");
|
|
426
|
-
return { cwd, args: extraArgs };
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function isKnownNoServerDiagnostic(stderr: string): boolean {
|
|
430
|
-
const diagnostic = stderr.trim().toLowerCase();
|
|
431
|
-
return (
|
|
432
|
-
diagnostic.length > 0 &&
|
|
433
|
-
diagnostic.length <= 512 &&
|
|
434
|
-
/no server running|failed to connect to server|error connecting to/.test(diagnostic)
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function lifecycleOwnerIsolationProbe(tmux: string, env: NodeJS.ProcessEnv): OwnerIsolationProbe {
|
|
439
|
-
return {
|
|
440
|
-
readCallerCgroup: async () =>
|
|
441
|
-
process.platform === "linux" ? await fsPromises.readFile("/proc/self/cgroup", "utf8").catch(() => null) : null,
|
|
442
|
-
probeServer: async (serverKey: string): Promise<TmuxServerProof> => {
|
|
443
|
-
const result = Bun.spawnSync([tmux, "-L", serverKey, "list-sessions", "-F", "#{pid}\t#{session_name}"], {
|
|
444
|
-
stdout: "pipe",
|
|
445
|
-
stderr: "pipe",
|
|
446
|
-
env,
|
|
447
|
-
});
|
|
448
|
-
const stderr = result.stderr.toString();
|
|
449
|
-
if (result.exitCode !== 0)
|
|
450
|
-
return isKnownNoServerDiagnostic(stderr) ? { state: "absent" } : { state: "unverifiable" };
|
|
451
|
-
const lines = result.stdout
|
|
452
|
-
.toString()
|
|
453
|
-
.split("\n")
|
|
454
|
-
.map(line => line.trim())
|
|
455
|
-
.filter(Boolean);
|
|
456
|
-
const [pidText] = lines[0]?.split("\t") ?? [];
|
|
457
|
-
const pid = Number(pidText);
|
|
458
|
-
const sessionNames = lines.map(line => line.split("\t")[1]).filter((name): name is string => Boolean(name));
|
|
459
|
-
if (!Number.isSafeInteger(pid) || pid <= 0) return { state: "unverifiable" };
|
|
460
|
-
if (process.platform !== "linux")
|
|
461
|
-
return {
|
|
462
|
-
state: "safe",
|
|
463
|
-
pid,
|
|
464
|
-
startTime: "not_applicable",
|
|
465
|
-
cgroup: { classification: "not_applicable" },
|
|
466
|
-
sessionNames,
|
|
467
|
-
};
|
|
468
|
-
const [cgroupText, startTime] = await Promise.all([
|
|
469
|
-
fsPromises.readFile(`/proc/${pid}/cgroup`, "utf8").catch(() => null),
|
|
470
|
-
readLinuxProcStartTime(pid),
|
|
471
|
-
]);
|
|
472
|
-
const cgroup = classifyCgroup({ platform: process.platform, cgroupText });
|
|
473
|
-
if (!startTime) return { state: "unverifiable", pid, cgroup, sessionNames };
|
|
474
|
-
return {
|
|
475
|
-
state:
|
|
476
|
-
cgroup.classification === "safe"
|
|
477
|
-
? "safe"
|
|
478
|
-
: cgroup.classification === "unsafe_service"
|
|
479
|
-
? "unsafe"
|
|
480
|
-
: "unverifiable",
|
|
481
|
-
pid,
|
|
482
|
-
startTime,
|
|
483
|
-
cgroup,
|
|
484
|
-
sessionNames,
|
|
485
|
-
};
|
|
486
|
-
},
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function lifecycleRuntimeStateFile(cwd: string, sessionId: string, tmuxSession: string): string {
|
|
491
|
-
return tmuxRuntimeSessionPath(cwd, sessionId, buildGjcTmuxSessionSlug(tmuxSession));
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
async function preflightLifecycleTmuxOwner(input: {
|
|
495
|
-
tmux: string;
|
|
496
|
-
env: NodeJS.ProcessEnv;
|
|
497
|
-
ownerIsolationProbe?: OwnerIsolationProbe;
|
|
498
|
-
}): Promise<void> {
|
|
499
|
-
const probe = input.ownerIsolationProbe ?? lifecycleOwnerIsolationProbe(input.tmux, input.env);
|
|
500
|
-
let server: TmuxServerProof;
|
|
501
|
-
try {
|
|
502
|
-
server = await probe.probeServer("default");
|
|
503
|
-
} catch {
|
|
504
|
-
throw new Error("gjc_lifecycle_owner_server_unverifiable");
|
|
505
|
-
}
|
|
506
|
-
if (server.state === "unsafe") throw new Error("gjc_lifecycle_owner_server_unsafe");
|
|
507
|
-
if (server.state === "unverifiable") throw new Error("gjc_lifecycle_owner_server_unverifiable");
|
|
508
|
-
if (
|
|
509
|
-
server.state === "safe" &&
|
|
510
|
-
(!server.pid || !server.startTime || (process.platform === "linux" && server.cgroup?.classification !== "safe"))
|
|
511
|
-
)
|
|
512
|
-
throw new Error("gjc_lifecycle_owner_server_unverifiable");
|
|
513
|
-
if (server.state === "absent") {
|
|
514
|
-
try {
|
|
515
|
-
if (
|
|
516
|
-
classifyCgroup({ platform: process.platform, cgroupText: await probe.readCallerCgroup() })
|
|
517
|
-
.classification === "unverifiable"
|
|
518
|
-
)
|
|
519
|
-
throw new Error("gjc_lifecycle_owner_server_unverifiable");
|
|
520
|
-
} catch (error) {
|
|
521
|
-
if (error instanceof Error && error.message === "gjc_lifecycle_owner_server_unverifiable") throw error;
|
|
522
|
-
throw new Error("gjc_lifecycle_owner_server_unverifiable");
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
interface LifecycleAttemptExecution {
|
|
528
|
-
attemptCreated: true;
|
|
529
|
-
serverKey: string;
|
|
530
|
-
serverPid?: number;
|
|
531
|
-
serverStartTime?: string;
|
|
532
|
-
nativeSessionId?: string;
|
|
533
|
-
attemptSession: string;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function nativeTmuxSessionIdFromSpawn(stdout: string): string | undefined {
|
|
537
|
-
const value = stdout.endsWith("\n") ? stdout.slice(0, -1) : stdout;
|
|
538
|
-
return !value.includes("\n") && !value.includes("\r") && /^\$\d+$/.test(value) ? value : undefined;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
async function isLifecycleGenerationUnchanged(
|
|
542
|
-
stateDir: string,
|
|
543
|
-
sessionId: string,
|
|
544
|
-
previousBaseline: OwnerGenerationBaseline,
|
|
545
|
-
): Promise<boolean> {
|
|
546
|
-
try {
|
|
547
|
-
return (
|
|
548
|
-
JSON.stringify(await captureOwnerGenerationBaseline(stateDir, sessionId)) === JSON.stringify(previousBaseline)
|
|
549
|
-
);
|
|
550
|
-
} catch {
|
|
551
|
-
return false;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
async function executeLifecycleTmuxOwnerPlan(input: {
|
|
556
|
-
tmux: string;
|
|
557
|
-
env: NodeJS.ProcessEnv;
|
|
558
|
-
sessionId: string;
|
|
559
|
-
generation: string;
|
|
560
|
-
stateDir: string;
|
|
561
|
-
cwd: string;
|
|
562
|
-
argv: string[];
|
|
563
|
-
sessionName: string;
|
|
564
|
-
ownerIsolationProbe?: OwnerIsolationProbe;
|
|
565
|
-
prepareSpawn?: () => void;
|
|
566
|
-
onAttemptCreated?: (attempt: LifecycleAttemptExecution) => void;
|
|
567
|
-
previousBaseline: OwnerGenerationBaseline;
|
|
568
|
-
}): Promise<LifecycleAttemptExecution> {
|
|
569
|
-
const probe = input.ownerIsolationProbe ?? lifecycleOwnerIsolationProbe(input.tmux, input.env);
|
|
570
|
-
await preflightLifecycleTmuxOwner(input);
|
|
571
|
-
if (!(await isLifecycleGenerationUnchanged(input.stateDir, input.sessionId, input.previousBaseline)))
|
|
572
|
-
throw new Error("gjc_lifecycle_owner_generation_changed");
|
|
573
|
-
const plan = await planTmuxOwnerIsolation(
|
|
574
|
-
{
|
|
575
|
-
schema_version: 1,
|
|
576
|
-
op: "plan",
|
|
577
|
-
platform: process.platform,
|
|
578
|
-
session_id: input.sessionId,
|
|
579
|
-
owner_generation: input.generation,
|
|
580
|
-
baseline: input.previousBaseline,
|
|
581
|
-
cwd: input.cwd,
|
|
582
|
-
state_dir: input.stateDir,
|
|
583
|
-
socket_key: "default",
|
|
584
|
-
tmux_argv: input.argv,
|
|
585
|
-
},
|
|
586
|
-
probe,
|
|
587
|
-
);
|
|
588
|
-
if (!plan.ok) throw new Error(`gjc_lifecycle_owner_${plan.code}`);
|
|
589
|
-
const preSpawnProof =
|
|
590
|
-
plan.execution.mode === "direct" && plan.server_state === "safe"
|
|
591
|
-
? await probe.probeServer(plan.execution.server_key)
|
|
592
|
-
: undefined;
|
|
593
|
-
if (
|
|
594
|
-
preSpawnProof &&
|
|
595
|
-
(preSpawnProof.state !== "safe" ||
|
|
596
|
-
!preSpawnProof.pid ||
|
|
597
|
-
!preSpawnProof.startTime ||
|
|
598
|
-
(process.platform === "linux" && preSpawnProof.cgroup?.classification !== "safe"))
|
|
599
|
-
)
|
|
600
|
-
throw new Error("gjc_lifecycle_owner_server_unverifiable");
|
|
601
|
-
input.prepareSpawn?.();
|
|
602
|
-
if (!(await isLifecycleGenerationUnchanged(input.stateDir, input.sessionId, input.previousBaseline)))
|
|
603
|
-
throw new Error("gjc_lifecycle_owner_generation_changed");
|
|
604
|
-
const created = Bun.spawnSync(plan.execution.argv, {
|
|
605
|
-
stdout: "pipe",
|
|
606
|
-
stderr: "pipe",
|
|
607
|
-
env: input.env,
|
|
608
|
-
...(plan.execution.mode === "scoped" ? { stdin: new TextEncoder().encode(plan.execution.stdin_line) } : {}),
|
|
609
|
-
});
|
|
610
|
-
const rawStdout = created.stdout.toString();
|
|
611
|
-
const rawScopedReceipt =
|
|
612
|
-
plan.execution.mode === "scoped" && isExactScopedBootstrapSuccessReceipt(rawStdout)
|
|
613
|
-
? (JSON.parse(rawStdout.endsWith("\n") ? rawStdout.slice(0, -1) : rawStdout) as {
|
|
614
|
-
native_session_id: string;
|
|
615
|
-
server_pid: number;
|
|
616
|
-
server_start_time: string;
|
|
617
|
-
session_name: string;
|
|
618
|
-
})
|
|
619
|
-
: undefined;
|
|
620
|
-
const scopedReceipt =
|
|
621
|
-
rawScopedReceipt?.session_name === plan.execution.attempt_session ? rawScopedReceipt : undefined;
|
|
622
|
-
const attempt: LifecycleAttemptExecution = {
|
|
623
|
-
attemptCreated: true,
|
|
624
|
-
serverKey: plan.execution.server_key,
|
|
625
|
-
nativeSessionId:
|
|
626
|
-
plan.execution.mode === "scoped" ? scopedReceipt?.native_session_id : nativeTmuxSessionIdFromSpawn(rawStdout),
|
|
627
|
-
attemptSession: plan.execution.attempt_session,
|
|
628
|
-
...(scopedReceipt
|
|
629
|
-
? { serverPid: scopedReceipt.server_pid, serverStartTime: scopedReceipt.server_start_time }
|
|
630
|
-
: preSpawnProof
|
|
631
|
-
? { serverPid: preSpawnProof.pid, serverStartTime: preSpawnProof.startTime }
|
|
632
|
-
: {}),
|
|
633
|
-
};
|
|
634
|
-
input.onAttemptCreated?.(attempt);
|
|
635
|
-
if (created.exitCode !== 0) {
|
|
636
|
-
if (!attempt.nativeSessionId || attempt.serverPid === undefined || attempt.serverStartTime === undefined)
|
|
637
|
-
throw new Error("gjc_lifecycle_spawn_failed_cleanup_uncertain");
|
|
638
|
-
throw new Error("gjc_lifecycle_spawn_failed");
|
|
639
|
-
}
|
|
640
|
-
if (!attempt.nativeSessionId) throw new Error("gjc_lifecycle_spawn_failed_cleanup_uncertain");
|
|
641
|
-
const proof = await probe.probeServer(plan.execution.server_key);
|
|
642
|
-
if (
|
|
643
|
-
proof.state !== "safe" ||
|
|
644
|
-
!proof.pid ||
|
|
645
|
-
!proof.startTime ||
|
|
646
|
-
!proof.sessionNames?.includes(plan.execution.attempt_session) ||
|
|
647
|
-
(process.platform === "linux" && proof.cgroup?.classification !== "safe") ||
|
|
648
|
-
(preSpawnProof && (proof.pid !== preSpawnProof.pid || proof.startTime !== preSpawnProof.startTime)) ||
|
|
649
|
-
(scopedReceipt && (proof.pid !== scopedReceipt.server_pid || proof.startTime !== scopedReceipt.server_start_time))
|
|
650
|
-
)
|
|
651
|
-
throw new Error("gjc_lifecycle_owner_server_unverifiable");
|
|
652
|
-
attempt.serverPid = proof.pid;
|
|
653
|
-
attempt.serverStartTime = proof.startTime;
|
|
654
|
-
if (
|
|
655
|
-
!(await reproveLifecycleAttempt({
|
|
656
|
-
tmux: input.tmux,
|
|
657
|
-
env: input.env,
|
|
658
|
-
serverKey: attempt.serverKey,
|
|
659
|
-
nativeSessionId: attempt.nativeSessionId,
|
|
660
|
-
attemptSession: attempt.attemptSession,
|
|
661
|
-
expectedServerPid: attempt.serverPid,
|
|
662
|
-
expectedServerStartTime: attempt.serverStartTime,
|
|
663
|
-
ownerIsolationProbe: input.ownerIsolationProbe,
|
|
664
|
-
}))
|
|
665
|
-
)
|
|
666
|
-
throw new Error("gjc_lifecycle_owner_server_unverifiable");
|
|
667
|
-
|
|
668
|
-
if (!(await isLifecycleGenerationUnchanged(input.stateDir, input.sessionId, input.previousBaseline)))
|
|
669
|
-
throw new Error("gjc_lifecycle_owner_generation_changed");
|
|
670
|
-
return attempt;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function hasExactNativeSessionBinding(input: {
|
|
674
|
-
tmux: string;
|
|
675
|
-
env: NodeJS.ProcessEnv;
|
|
676
|
-
serverKey: string;
|
|
677
|
-
nativeSessionId: string;
|
|
678
|
-
attemptSession: string;
|
|
679
|
-
}): boolean {
|
|
680
|
-
const result = Bun.spawnSync(
|
|
681
|
-
[
|
|
682
|
-
input.tmux,
|
|
683
|
-
"-L",
|
|
684
|
-
input.serverKey,
|
|
685
|
-
"display-message",
|
|
686
|
-
"-p",
|
|
687
|
-
"-t",
|
|
688
|
-
input.nativeSessionId,
|
|
689
|
-
"#{session_id}\t#{session_name}",
|
|
690
|
-
],
|
|
691
|
-
{ stdout: "pipe", stderr: "pipe", env: input.env },
|
|
692
|
-
);
|
|
693
|
-
if (result.exitCode !== 0) return false;
|
|
694
|
-
const output = result.stdout.toString();
|
|
695
|
-
const line = output.endsWith("\n") ? output.slice(0, -1) : output;
|
|
696
|
-
return line === `${input.nativeSessionId}\t${input.attemptSession}`;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function cleanupUncertain(): Error {
|
|
700
|
-
return new Error("gjc_lifecycle_cleanup_uncertain");
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
async function reproveLifecycleAttempt(input: {
|
|
704
|
-
tmux: string;
|
|
705
|
-
env: NodeJS.ProcessEnv;
|
|
706
|
-
serverKey: string;
|
|
707
|
-
nativeSessionId: string;
|
|
708
|
-
attemptSession: string;
|
|
709
|
-
expectedServerPid: number;
|
|
710
|
-
expectedServerStartTime: string;
|
|
711
|
-
ownerIsolationProbe?: OwnerIsolationProbe;
|
|
712
|
-
}): Promise<boolean> {
|
|
713
|
-
const probe = input.ownerIsolationProbe ?? lifecycleOwnerIsolationProbe(input.tmux, input.env);
|
|
714
|
-
const isExpectedServer = (proof: TmuxServerProof): boolean =>
|
|
715
|
-
proof.state === "safe" &&
|
|
716
|
-
proof.pid === input.expectedServerPid &&
|
|
717
|
-
proof.startTime === input.expectedServerStartTime &&
|
|
718
|
-
proof.sessionNames?.includes(input.attemptSession) === true &&
|
|
719
|
-
(process.platform !== "linux" || proof.cgroup?.classification === "safe");
|
|
720
|
-
let proof: TmuxServerProof;
|
|
721
|
-
try {
|
|
722
|
-
proof = await probe.probeServer(input.serverKey);
|
|
723
|
-
} catch {
|
|
724
|
-
return false;
|
|
725
|
-
}
|
|
726
|
-
if (!isExpectedServer(proof) || !hasExactNativeSessionBinding(input)) return false;
|
|
727
|
-
try {
|
|
728
|
-
proof = await probe.probeServer(input.serverKey);
|
|
729
|
-
} catch {
|
|
730
|
-
return false;
|
|
731
|
-
}
|
|
732
|
-
return isExpectedServer(proof);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
async function cleanupLifecycleAttempt(input: {
|
|
736
|
-
tmux: string;
|
|
737
|
-
env: NodeJS.ProcessEnv;
|
|
738
|
-
serverKey: string;
|
|
739
|
-
nativeSessionId?: string;
|
|
740
|
-
attemptSession: string;
|
|
741
|
-
expectedServerPid?: number;
|
|
742
|
-
expectedServerStartTime?: string;
|
|
743
|
-
ownerIsolationProbe?: OwnerIsolationProbe;
|
|
744
|
-
}): Promise<void> {
|
|
745
|
-
if (!input.nativeSessionId || input.expectedServerPid === undefined || input.expectedServerStartTime === undefined)
|
|
746
|
-
throw cleanupUncertain();
|
|
747
|
-
// External reproof narrows the window and detects obvious replacement before
|
|
748
|
-
// mutation, but the server evaluates the identity predicate atomically.
|
|
749
|
-
if (
|
|
750
|
-
!(await reproveLifecycleAttempt({
|
|
751
|
-
tmux: input.tmux,
|
|
752
|
-
env: input.env,
|
|
753
|
-
serverKey: input.serverKey,
|
|
754
|
-
nativeSessionId: input.nativeSessionId,
|
|
755
|
-
attemptSession: input.attemptSession,
|
|
756
|
-
expectedServerPid: input.expectedServerPid,
|
|
757
|
-
expectedServerStartTime: input.expectedServerStartTime,
|
|
758
|
-
ownerIsolationProbe: input.ownerIsolationProbe,
|
|
759
|
-
}))
|
|
760
|
-
)
|
|
761
|
-
throw cleanupUncertain();
|
|
762
|
-
const guarded = Bun.spawnSync(
|
|
763
|
-
[
|
|
764
|
-
input.tmux,
|
|
765
|
-
"-L",
|
|
766
|
-
input.serverKey,
|
|
767
|
-
"if-shell",
|
|
768
|
-
"-t",
|
|
769
|
-
input.nativeSessionId,
|
|
770
|
-
"-F",
|
|
771
|
-
lifecycleMetadataPredicate(input.expectedServerPid, input.nativeSessionId, input.attemptSession),
|
|
772
|
-
`kill-session -t '${input.nativeSessionId}' ; display-message -p __gjc_lifecycle_cleanup_ok__`,
|
|
773
|
-
"display-message -p __gjc_lifecycle_cleanup_refused__",
|
|
774
|
-
],
|
|
775
|
-
{ stdout: "pipe", stderr: "pipe", env: input.env },
|
|
776
|
-
);
|
|
777
|
-
if (guarded.exitCode !== 0 || guarded.stdout.toString().trim() !== "__gjc_lifecycle_cleanup_ok__")
|
|
778
|
-
throw cleanupUncertain();
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
async function assertLifecycleTmuxServerSafe(input: {
|
|
782
|
-
tmux: string;
|
|
783
|
-
env: NodeJS.ProcessEnv;
|
|
784
|
-
ownerIsolationProbe?: OwnerIsolationProbe;
|
|
785
|
-
}): Promise<void> {
|
|
786
|
-
const proof = await (input.ownerIsolationProbe ?? lifecycleOwnerIsolationProbe(input.tmux, input.env)).probeServer(
|
|
787
|
-
"default",
|
|
788
|
-
);
|
|
789
|
-
if (
|
|
790
|
-
proof.state !== "safe" ||
|
|
791
|
-
!proof.pid ||
|
|
792
|
-
!proof.startTime ||
|
|
793
|
-
(process.platform === "linux" && proof.cgroup?.classification !== "safe")
|
|
794
|
-
)
|
|
795
|
-
throw new Error(`gjc_lifecycle_owner_${proof.state === "unsafe" ? "server_unsafe" : "server_unverifiable"}`);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
async function completeLifecycleSpawnTransaction(input: {
|
|
799
|
-
tmux: string;
|
|
800
|
-
env: NodeJS.ProcessEnv;
|
|
801
|
-
sessionId: string;
|
|
802
|
-
generation: string;
|
|
803
|
-
stateDir: string;
|
|
804
|
-
cwd: string;
|
|
805
|
-
sessionName: string;
|
|
806
|
-
sessionStateFile: string;
|
|
807
|
-
argv: string[];
|
|
808
|
-
ownerIsolationProbe?: OwnerIsolationProbe;
|
|
809
|
-
prepareSpawn?: () => void;
|
|
810
|
-
previousBaseline?: OwnerGenerationBaseline;
|
|
811
|
-
}): Promise<void> {
|
|
812
|
-
const previousBaseline =
|
|
813
|
-
input.previousBaseline ?? (await captureOwnerGenerationBaseline(input.stateDir, input.sessionId));
|
|
814
|
-
let ownerExecution: LifecycleAttemptExecution | undefined;
|
|
815
|
-
try {
|
|
816
|
-
ownerExecution = await executeLifecycleTmuxOwnerPlan({
|
|
817
|
-
...input,
|
|
818
|
-
previousBaseline,
|
|
819
|
-
onAttemptCreated: attempt => {
|
|
820
|
-
ownerExecution = attempt;
|
|
821
|
-
},
|
|
822
|
-
});
|
|
823
|
-
if (
|
|
824
|
-
!ownerExecution.nativeSessionId ||
|
|
825
|
-
ownerExecution.serverPid === undefined ||
|
|
826
|
-
ownerExecution.serverStartTime === undefined
|
|
827
|
-
)
|
|
828
|
-
throw new Error("gjc_lifecycle_owner_server_unverifiable");
|
|
829
|
-
await applyRequiredLifecycleTmuxMetadata(
|
|
830
|
-
input.tmux,
|
|
831
|
-
`${ownerExecution.nativeSessionId}:`,
|
|
832
|
-
input.env,
|
|
833
|
-
{
|
|
834
|
-
sessionId: input.sessionId,
|
|
835
|
-
sessionStateFile: input.sessionStateFile,
|
|
836
|
-
project: input.cwd,
|
|
837
|
-
ownerGeneration: input.generation,
|
|
838
|
-
ownerServerKey: ownerExecution.serverKey,
|
|
839
|
-
},
|
|
840
|
-
{
|
|
841
|
-
nativeSessionId: ownerExecution.nativeSessionId,
|
|
842
|
-
attemptSession: ownerExecution.attemptSession,
|
|
843
|
-
serverPid: ownerExecution.serverPid,
|
|
844
|
-
},
|
|
845
|
-
);
|
|
846
|
-
if (
|
|
847
|
-
!(await reproveLifecycleAttempt({
|
|
848
|
-
tmux: input.tmux,
|
|
849
|
-
env: input.env,
|
|
850
|
-
serverKey: ownerExecution.serverKey,
|
|
851
|
-
nativeSessionId: ownerExecution.nativeSessionId,
|
|
852
|
-
attemptSession: ownerExecution.attemptSession,
|
|
853
|
-
expectedServerPid: ownerExecution.serverPid,
|
|
854
|
-
expectedServerStartTime: ownerExecution.serverStartTime,
|
|
855
|
-
ownerIsolationProbe: input.ownerIsolationProbe,
|
|
856
|
-
}))
|
|
857
|
-
)
|
|
858
|
-
throw new Error("gjc_lifecycle_owner_server_unverifiable");
|
|
859
|
-
if (!(await isLifecycleGenerationUnchanged(input.stateDir, input.sessionId, previousBaseline)))
|
|
860
|
-
throw new Error("gjc_lifecycle_owner_generation_changed");
|
|
861
|
-
resolveManagedOwnerPredecessorSync(input.stateDir, input.sessionId, previousBaseline);
|
|
862
|
-
await replaceOwnerGeneration(input.stateDir, input.sessionId, input.generation, previousBaseline);
|
|
863
|
-
} catch (error) {
|
|
864
|
-
let cleanupFailure: unknown;
|
|
865
|
-
if (ownerExecution?.attemptCreated) {
|
|
866
|
-
try {
|
|
867
|
-
await cleanupLifecycleAttempt({
|
|
868
|
-
tmux: input.tmux,
|
|
869
|
-
env: input.env,
|
|
870
|
-
serverKey: ownerExecution.serverKey,
|
|
871
|
-
nativeSessionId: ownerExecution.nativeSessionId,
|
|
872
|
-
attemptSession: ownerExecution.attemptSession,
|
|
873
|
-
expectedServerPid: ownerExecution.serverPid,
|
|
874
|
-
expectedServerStartTime: ownerExecution.serverStartTime,
|
|
875
|
-
ownerIsolationProbe: input.ownerIsolationProbe,
|
|
876
|
-
});
|
|
877
|
-
} catch (cleanupError) {
|
|
878
|
-
cleanupFailure = cleanupError;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
if (cleanupFailure !== undefined)
|
|
882
|
-
throw new AggregateError([error, cleanupFailure], "gjc_lifecycle_cleanup_uncertain");
|
|
883
|
-
throw error;
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
function directTmuxReceipt(
|
|
887
|
-
stdout: string,
|
|
888
|
-
sessionName: string,
|
|
889
|
-
): { nativeSessionId: string; sessionName: string } | undefined {
|
|
890
|
-
const line = stdout.endsWith("\n") ? stdout.slice(0, -1) : stdout;
|
|
891
|
-
const [nativeSessionId, receivedName, ...rest] = line.split("\t");
|
|
892
|
-
return rest.length === 0 && /^\$\d+$/.test(nativeSessionId) && receivedName === sessionName
|
|
893
|
-
? { nativeSessionId, sessionName: receivedName }
|
|
894
|
-
: undefined;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
type DirectTmuxBinding =
|
|
898
|
-
| { state: "bound"; serverPid: number; panePid: number; paneDead: boolean; paneDeadStatus: string }
|
|
899
|
-
| { state: "absent" }
|
|
900
|
-
| { state: "unverifiable" };
|
|
901
|
-
|
|
902
|
-
function isKnownNoSessionDiagnostic(stderr: string): boolean {
|
|
903
|
-
const diagnostic = stderr.trim().toLowerCase();
|
|
904
|
-
return (
|
|
905
|
-
diagnostic.length > 0 &&
|
|
906
|
-
diagnostic.length <= 512 &&
|
|
907
|
-
/no server running|can't find session|no such session|unknown session/.test(diagnostic)
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
function directTmuxBinding(
|
|
912
|
-
tmux: string,
|
|
913
|
-
env: NodeJS.ProcessEnv,
|
|
914
|
-
cwd: string,
|
|
915
|
-
nativeSessionId: string,
|
|
916
|
-
sessionName: string,
|
|
917
|
-
): DirectTmuxBinding {
|
|
918
|
-
const result = Bun.spawnSync(
|
|
919
|
-
[
|
|
920
|
-
tmux,
|
|
921
|
-
"display-message",
|
|
922
|
-
"-p",
|
|
923
|
-
"-t",
|
|
924
|
-
nativeSessionId,
|
|
925
|
-
"#{pid}\t#{session_id}\t#{session_name}\t#{pane_pid}\t#{pane_dead}\t#{pane_dead_status}",
|
|
926
|
-
],
|
|
927
|
-
{ stdout: "pipe", stderr: "pipe", env, cwd },
|
|
928
|
-
);
|
|
929
|
-
if (result.exitCode !== 0)
|
|
930
|
-
return isKnownNoSessionDiagnostic(result.stderr.toString()) ? { state: "absent" } : { state: "unverifiable" };
|
|
931
|
-
const output = result.stdout.toString();
|
|
932
|
-
const line = output.endsWith("\n") ? output.slice(0, -1) : output;
|
|
933
|
-
const [pid, receivedId, receivedName, panePid, paneDead, paneDeadStatus, ...rest] = line.split("\t");
|
|
934
|
-
return rest.length === 0 &&
|
|
935
|
-
/^\d+$/.test(pid) &&
|
|
936
|
-
Number(pid) > 0 &&
|
|
937
|
-
receivedId === nativeSessionId &&
|
|
938
|
-
receivedName === sessionName &&
|
|
939
|
-
/^\d+$/.test(panePid) &&
|
|
940
|
-
Number(panePid) > 0 &&
|
|
941
|
-
(paneDead === "0" || paneDead === "1")
|
|
942
|
-
? {
|
|
943
|
-
state: "bound",
|
|
944
|
-
serverPid: Number(pid),
|
|
945
|
-
panePid: Number(panePid),
|
|
946
|
-
paneDead: paneDead === "1",
|
|
947
|
-
paneDeadStatus,
|
|
948
|
-
}
|
|
949
|
-
: { state: "unverifiable" };
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function directTmuxIdentityIsCurrent(input: {
|
|
953
|
-
tmux: string;
|
|
954
|
-
env: NodeJS.ProcessEnv;
|
|
955
|
-
cwd: string;
|
|
956
|
-
nativeSessionId: string;
|
|
957
|
-
sessionName: string;
|
|
958
|
-
serverPid: number;
|
|
959
|
-
serverIncarnation: string;
|
|
960
|
-
readProcessIncarnation: (pid: number) => string | undefined;
|
|
961
|
-
}): boolean {
|
|
962
|
-
const binding = directTmuxBinding(input.tmux, input.env, input.cwd, input.nativeSessionId, input.sessionName);
|
|
963
|
-
return (
|
|
964
|
-
binding.state === "bound" &&
|
|
965
|
-
binding.serverPid === input.serverPid &&
|
|
966
|
-
input.readProcessIncarnation(input.serverPid) === input.serverIncarnation
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
function directTmuxPaneLivenessIsCurrent(input: {
|
|
970
|
-
tmux: string;
|
|
971
|
-
env: NodeJS.ProcessEnv;
|
|
972
|
-
cwd: string;
|
|
973
|
-
nativeSessionId: string;
|
|
974
|
-
sessionName: string;
|
|
975
|
-
serverPid: number;
|
|
976
|
-
serverIncarnation: string;
|
|
977
|
-
panePid: number;
|
|
978
|
-
paneIncarnation: string;
|
|
979
|
-
readProcessIncarnation: (pid: number) => string | undefined;
|
|
980
|
-
}): boolean {
|
|
981
|
-
const binding = directTmuxBinding(input.tmux, input.env, input.cwd, input.nativeSessionId, input.sessionName);
|
|
982
|
-
return (
|
|
983
|
-
binding.state === "bound" &&
|
|
984
|
-
binding.serverPid === input.serverPid &&
|
|
985
|
-
input.readProcessIncarnation(input.serverPid) === input.serverIncarnation &&
|
|
986
|
-
!binding.paneDead &&
|
|
987
|
-
binding.panePid === input.panePid &&
|
|
988
|
-
input.readProcessIncarnation(input.panePid) === input.paneIncarnation
|
|
989
|
-
);
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
function directTmuxGuardedMutation(input: {
|
|
993
|
-
tmux: string;
|
|
994
|
-
env: NodeJS.ProcessEnv;
|
|
995
|
-
cwd: string;
|
|
996
|
-
nativeSessionId: string;
|
|
997
|
-
sessionName: string;
|
|
998
|
-
serverPid: number;
|
|
999
|
-
serverIncarnation: string;
|
|
1000
|
-
readProcessIncarnation: (pid: number) => string | undefined;
|
|
1001
|
-
commands: string;
|
|
1002
|
-
success: string;
|
|
1003
|
-
}): boolean {
|
|
1004
|
-
if (!directTmuxIdentityIsCurrent(input)) return false;
|
|
1005
|
-
const result = Bun.spawnSync(
|
|
1006
|
-
[
|
|
1007
|
-
input.tmux,
|
|
1008
|
-
"if-shell",
|
|
1009
|
-
"-t",
|
|
1010
|
-
input.nativeSessionId,
|
|
1011
|
-
"-F",
|
|
1012
|
-
lifecycleMetadataPredicate(input.serverPid, input.nativeSessionId, input.sessionName),
|
|
1013
|
-
`${input.commands} ; display-message -p ${input.success}`,
|
|
1014
|
-
"display-message -p __gjc_lifecycle_metadata_refused__",
|
|
1015
|
-
],
|
|
1016
|
-
{ stdout: "pipe", stderr: "pipe", env: input.env, cwd: input.cwd },
|
|
1017
|
-
);
|
|
1018
|
-
return result.exitCode === 0 && result.stdout.toString().trim() === input.success;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function cleanupDirectLifecycleAttempt(input: {
|
|
1022
|
-
tmux: string;
|
|
1023
|
-
env: NodeJS.ProcessEnv;
|
|
1024
|
-
cwd: string;
|
|
1025
|
-
nativeSessionId: string;
|
|
1026
|
-
sessionName: string;
|
|
1027
|
-
serverPid: number;
|
|
1028
|
-
serverIncarnation: string;
|
|
1029
|
-
readProcessIncarnation: (pid: number) => string | undefined;
|
|
1030
|
-
}): void {
|
|
1031
|
-
if (
|
|
1032
|
-
!directTmuxGuardedMutation({
|
|
1033
|
-
...input,
|
|
1034
|
-
commands: `kill-session -t '${input.nativeSessionId}'`,
|
|
1035
|
-
success: "__gjc_lifecycle_cleanup_ok__",
|
|
1036
|
-
})
|
|
1037
|
-
)
|
|
1038
|
-
throw cleanupUncertain();
|
|
1039
|
-
if (directTmuxBinding(input.tmux, input.env, input.cwd, input.nativeSessionId, input.sessionName).state !== "absent")
|
|
1040
|
-
throw cleanupUncertain();
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
async function completeNonLinuxLifecycleSpawn(input: {
|
|
1044
|
-
tmux: string;
|
|
1045
|
-
env: NodeJS.ProcessEnv;
|
|
1046
|
-
sessionId: string;
|
|
1047
|
-
cwd: string;
|
|
1048
|
-
sessionName: string;
|
|
1049
|
-
sessionStateFile: string;
|
|
1050
|
-
command: string;
|
|
1051
|
-
readProcessIncarnation: (pid: number) => string | undefined;
|
|
1052
|
-
}): Promise<void> {
|
|
1053
|
-
let attempt:
|
|
1054
|
-
| {
|
|
1055
|
-
nativeSessionId: string;
|
|
1056
|
-
sessionName: string;
|
|
1057
|
-
serverPid: number;
|
|
1058
|
-
serverIncarnation: string;
|
|
1059
|
-
panePid?: number;
|
|
1060
|
-
paneIncarnation?: string;
|
|
1061
|
-
}
|
|
1062
|
-
| undefined;
|
|
1063
|
-
let primaryError: unknown;
|
|
1064
|
-
try {
|
|
1065
|
-
const created = Bun.spawnSync(
|
|
1066
|
-
[
|
|
1067
|
-
input.tmux,
|
|
1068
|
-
"new-session",
|
|
1069
|
-
"-d",
|
|
1070
|
-
"-P",
|
|
1071
|
-
"-F",
|
|
1072
|
-
"#{session_id}\t#{session_name}",
|
|
1073
|
-
"-s",
|
|
1074
|
-
input.sessionName,
|
|
1075
|
-
"sh",
|
|
1076
|
-
"-c",
|
|
1077
|
-
input.command,
|
|
1078
|
-
],
|
|
1079
|
-
{ stdout: "pipe", stderr: "pipe", env: input.env, cwd: input.cwd },
|
|
1080
|
-
);
|
|
1081
|
-
const receipt = directTmuxReceipt(created.stdout.toString(), input.sessionName);
|
|
1082
|
-
if (!receipt) throw new Error("gjc_lifecycle_tmux_launch_failed");
|
|
1083
|
-
const binding = directTmuxBinding(input.tmux, input.env, input.cwd, receipt.nativeSessionId, receipt.sessionName);
|
|
1084
|
-
const serverIncarnation = binding.state === "bound" && input.readProcessIncarnation(binding.serverPid);
|
|
1085
|
-
const paneIncarnation =
|
|
1086
|
-
binding.state === "bound" && !binding.paneDead && input.readProcessIncarnation(binding.panePid);
|
|
1087
|
-
if (binding.state !== "bound" || !serverIncarnation) throw new Error("gjc_lifecycle_tmux_launch_failed");
|
|
1088
|
-
attempt = { ...receipt, serverPid: binding.serverPid, serverIncarnation };
|
|
1089
|
-
if (!binding.paneDead && paneIncarnation) {
|
|
1090
|
-
attempt.panePid = binding.panePid;
|
|
1091
|
-
attempt.paneIncarnation = paneIncarnation;
|
|
1092
|
-
}
|
|
1093
|
-
if (created.exitCode !== 0 || !directTmuxIdentityIsCurrent({ ...input, ...attempt }))
|
|
1094
|
-
throw new Error("gjc_lifecycle_tmux_launch_failed");
|
|
1095
|
-
const commands = buildGjcTmuxProfileCommands(attempt.nativeSessionId, input.env, {
|
|
1096
|
-
sessionId: input.sessionId,
|
|
1097
|
-
sessionStateFile: input.sessionStateFile,
|
|
1098
|
-
project: input.cwd,
|
|
1099
|
-
})
|
|
1100
|
-
.map(command => command.args.map(tmuxCommandArgument).join(" "))
|
|
1101
|
-
.join(" ; ");
|
|
1102
|
-
if (
|
|
1103
|
-
!directTmuxGuardedMutation({ ...input, ...attempt, commands, success: "__gjc_lifecycle_metadata_ok__" }) ||
|
|
1104
|
-
!directTmuxIdentityIsCurrent({ ...input, ...attempt })
|
|
1105
|
-
)
|
|
1106
|
-
throw new Error("gjc_lifecycle_metadata_write_failed");
|
|
1107
|
-
await Bun.sleep(directLifecycleStabilizationMs);
|
|
1108
|
-
if (
|
|
1109
|
-
attempt.panePid === undefined ||
|
|
1110
|
-
attempt.paneIncarnation === undefined ||
|
|
1111
|
-
!directTmuxPaneLivenessIsCurrent({
|
|
1112
|
-
...input,
|
|
1113
|
-
...attempt,
|
|
1114
|
-
panePid: attempt.panePid,
|
|
1115
|
-
paneIncarnation: attempt.paneIncarnation,
|
|
1116
|
-
})
|
|
1117
|
-
)
|
|
1118
|
-
throw new Error("gjc_lifecycle_tmux_launch_liveness_failed");
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
primaryError = error;
|
|
1121
|
-
}
|
|
1122
|
-
if (primaryError === undefined) return;
|
|
1123
|
-
let cleanupFailure: unknown;
|
|
1124
|
-
if (attempt) {
|
|
1125
|
-
try {
|
|
1126
|
-
cleanupDirectLifecycleAttempt({ ...input, ...attempt });
|
|
1127
|
-
} catch (error) {
|
|
1128
|
-
cleanupFailure = error;
|
|
1129
|
-
}
|
|
1130
|
-
} else cleanupFailure = cleanupUncertain();
|
|
1131
|
-
if (cleanupFailure !== undefined)
|
|
1132
|
-
throw new AggregateError([primaryError, cleanupFailure], "gjc_lifecycle_cleanup_uncertain");
|
|
1133
|
-
throw primaryError;
|
|
1134
|
-
}
|
|
1135
|
-
/** Real daemon-safe tmux launcher: canonical owner-isolation plan + GJC tags. */
|
|
1136
|
-
export function daemonSpawnCreate(
|
|
1137
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
1138
|
-
opts: {
|
|
1139
|
-
ownerIsolationProbe?: OwnerIsolationProbe;
|
|
1140
|
-
platform?: NodeJS.Platform;
|
|
1141
|
-
processIncarnation?: (pid: number) => string | undefined;
|
|
1142
|
-
} = {},
|
|
1143
|
-
) {
|
|
1144
|
-
return async (
|
|
1145
|
-
frame: SessionCreateFrame,
|
|
1146
|
-
ids: { lifecycleRequestId: string; intendedSessionId: string; startupPromptRef?: string },
|
|
1147
|
-
): Promise<CreateEffectResult> => {
|
|
1148
|
-
const tmuxBinary = resolveGjcTmuxBinary({ env, platform: opts.platform });
|
|
1149
|
-
if (tmuxBinary.isPsmux) throw new Error("gjc_lifecycle_psmux_unsupported");
|
|
1150
|
-
const tmux = tmuxBinary.command;
|
|
1151
|
-
const name = tmuxSessionNameFor(ids.intendedSessionId);
|
|
1152
|
-
const { cwd, args } = buildCreateArgv(frame, ids);
|
|
1153
|
-
const sessionStateFile = lifecycleRuntimeStateFile(cwd, ids.intendedSessionId, name);
|
|
1154
|
-
const commonChildEnv: Record<string, string> = {
|
|
1155
|
-
GJC_TMUX_LAUNCHED: "1",
|
|
1156
|
-
GJC_NOTIFICATIONS: "1",
|
|
1157
|
-
GJC_SESSION_ID: ids.intendedSessionId,
|
|
1158
|
-
GJC_LIFECYCLE_REQUEST_ID: ids.lifecycleRequestId,
|
|
1159
|
-
[GJC_COORDINATOR_SESSION_ID_ENV]: ids.intendedSessionId,
|
|
1160
|
-
[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
|
|
1161
|
-
};
|
|
1162
|
-
if (ids.startupPromptRef) commonChildEnv.GJC_STARTUP_PROMPT_REF = ids.startupPromptRef;
|
|
1163
|
-
if ((opts.platform ?? process.platform) === "linux") {
|
|
1164
|
-
const stateDir = path.dirname(sessionStateFile);
|
|
1165
|
-
const previousBaseline = await captureOwnerGenerationBaseline(stateDir, ids.intendedSessionId);
|
|
1166
|
-
const predecessor = resolveManagedOwnerPredecessorSync(stateDir, ids.intendedSessionId, previousBaseline);
|
|
1167
|
-
const generation = crypto.randomUUID();
|
|
1168
|
-
const runId = crypto.randomUUID();
|
|
1169
|
-
const incarnation = crypto.randomUUID();
|
|
1170
|
-
const managedChildEnv: Record<string, string> = {
|
|
1171
|
-
...commonChildEnv,
|
|
1172
|
-
[GJC_TMUX_OWNER_GENERATION_ENV]: generation,
|
|
1173
|
-
[GJC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
|
|
1174
|
-
[GJC_TMUX_OWNER_SERVER_KEY_ENV]: "default",
|
|
1175
|
-
[MANAGED_OWNER_COMMAND_ENV]: JSON.stringify(["gjc", ...args]),
|
|
1176
|
-
[MANAGED_OWNER_RUN_ID_ENV]: runId,
|
|
1177
|
-
[MANAGED_OWNER_INCARNATION_ENV]: incarnation,
|
|
1178
|
-
...(predecessor
|
|
1179
|
-
? {
|
|
1180
|
-
[MANAGED_OWNER_PREDECESSOR_TOKEN_ENV]: predecessor.predecessorToken,
|
|
1181
|
-
[MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]: predecessor.generation,
|
|
1182
|
-
[MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]: predecessor.runId,
|
|
1183
|
-
[MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]: predecessor.incarnation,
|
|
1184
|
-
[MANAGED_OWNER_TRANSCRIPT_PATH_ENV]: env.GJC_SESSION_FILE ?? "",
|
|
1185
|
-
}
|
|
1186
|
-
: {}),
|
|
1187
|
-
};
|
|
1188
|
-
const managedEnvPairs = Object.entries(managedChildEnv)
|
|
1189
|
-
.map(([key, value]) => `${key}=${shellQuote(value)}`)
|
|
1190
|
-
.join(" ");
|
|
1191
|
-
const command = `cd ${shellQuote(cwd)} && exec env ${managedEnvPairs} gjc ${shellQuote(MANAGED_OWNER_SUPERVISOR_ARG)}`;
|
|
1192
|
-
await completeLifecycleSpawnTransaction({
|
|
1193
|
-
tmux,
|
|
1194
|
-
env,
|
|
1195
|
-
sessionId: ids.intendedSessionId,
|
|
1196
|
-
generation,
|
|
1197
|
-
stateDir,
|
|
1198
|
-
cwd,
|
|
1199
|
-
sessionName: name,
|
|
1200
|
-
sessionStateFile,
|
|
1201
|
-
argv: [tmux, "new-session", "-d", "-P", "-F", "#{session_id}", "-s", name, "sh", "-c", command],
|
|
1202
|
-
ownerIsolationProbe: opts.ownerIsolationProbe,
|
|
1203
|
-
prepareSpawn: () => {
|
|
1204
|
-
if (frame.target.kind === "plain_dir") fs.mkdirSync(cwd, { recursive: true });
|
|
1205
|
-
},
|
|
1206
|
-
previousBaseline,
|
|
1207
|
-
});
|
|
1208
|
-
} else {
|
|
1209
|
-
if (frame.target.kind === "plain_dir") fs.mkdirSync(cwd, { recursive: true });
|
|
1210
|
-
const directEnvPairs = directLifecycleEnvArguments(commonChildEnv);
|
|
1211
|
-
const directCommand = `cd ${shellQuote(cwd)} && exec env ${directEnvPairs} gjc ${args.map(shellQuote).join(" ")}`;
|
|
1212
|
-
await completeNonLinuxLifecycleSpawn({
|
|
1213
|
-
tmux,
|
|
1214
|
-
env,
|
|
1215
|
-
sessionId: ids.intendedSessionId,
|
|
1216
|
-
cwd,
|
|
1217
|
-
sessionName: name,
|
|
1218
|
-
sessionStateFile,
|
|
1219
|
-
command: directCommand,
|
|
1220
|
-
readProcessIncarnation: opts.processIncarnation ?? processIncarnation,
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
return {
|
|
1225
|
-
sessionId: ids.intendedSessionId,
|
|
1226
|
-
tmuxSession: name,
|
|
1227
|
-
sessionStateFile,
|
|
1228
|
-
endpointUrl: "",
|
|
1229
|
-
topicThreadId: "",
|
|
1230
|
-
};
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
function tmuxCommandArgument(value: string): string {
|
|
1235
|
-
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
function lifecycleMetadataPredicate(expectedPid: number, nativeSessionId: string, attemptSession: string): string {
|
|
1239
|
-
return `#{&&:#{==:#{pid},${expectedPid}},#{&&:#{==:#{session_id},${nativeSessionId}},#{==:#{session_name},${attemptSession}}}}`;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
async function applyRequiredLifecycleTmuxMetadata(
|
|
1243
|
-
tmux: string,
|
|
1244
|
-
target: string,
|
|
1245
|
-
env: NodeJS.ProcessEnv,
|
|
1246
|
-
metadata: {
|
|
1247
|
-
sessionId: string;
|
|
1248
|
-
sessionStateFile: string;
|
|
1249
|
-
project: string;
|
|
1250
|
-
ownerGeneration: string;
|
|
1251
|
-
ownerServerKey: string;
|
|
1252
|
-
},
|
|
1253
|
-
attempt: { nativeSessionId: string; attemptSession: string; serverPid: number },
|
|
1254
|
-
): Promise<void> {
|
|
1255
|
-
if (
|
|
1256
|
-
!metadata.sessionId.trim() ||
|
|
1257
|
-
!metadata.sessionStateFile.trim() ||
|
|
1258
|
-
!metadata.project.trim() ||
|
|
1259
|
-
!metadata.ownerGeneration.trim() ||
|
|
1260
|
-
!metadata.ownerServerKey.trim()
|
|
1261
|
-
)
|
|
1262
|
-
throw new Error("gjc_lifecycle_metadata_required_missing");
|
|
1263
|
-
const commands = buildGjcTmuxProfileCommands(target, env, metadata)
|
|
1264
|
-
.map(command => command.args.map(tmuxCommandArgument).join(" "))
|
|
1265
|
-
.join(" ; ");
|
|
1266
|
-
const result = Bun.spawnSync(
|
|
1267
|
-
[
|
|
1268
|
-
tmux,
|
|
1269
|
-
"-L",
|
|
1270
|
-
metadata.ownerServerKey,
|
|
1271
|
-
"if-shell",
|
|
1272
|
-
"-t",
|
|
1273
|
-
attempt.nativeSessionId,
|
|
1274
|
-
"-F",
|
|
1275
|
-
lifecycleMetadataPredicate(attempt.serverPid, attempt.nativeSessionId, attempt.attemptSession),
|
|
1276
|
-
`${commands} ; display-message -p __gjc_lifecycle_metadata_ok__`,
|
|
1277
|
-
"display-message -p __gjc_lifecycle_metadata_refused__",
|
|
1278
|
-
],
|
|
1279
|
-
{ stdout: "pipe", stderr: "pipe", env },
|
|
1280
|
-
);
|
|
1281
|
-
if (result.exitCode !== 0 || result.stdout.toString().trim() !== "__gjc_lifecycle_metadata_ok__")
|
|
1282
|
-
throw new Error("gjc_lifecycle_metadata_write_failed");
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
/** Real force-close effect (GJC-managed only, id-matched). */
|
|
1286
|
-
export function daemonCloseSession(
|
|
1287
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
1288
|
-
deps: {
|
|
1289
|
-
forceClose?: (
|
|
1290
|
-
name: string,
|
|
1291
|
-
env: NodeJS.ProcessEnv,
|
|
1292
|
-
expectedSessionId?: string,
|
|
1293
|
-
sessionStateFile?: string,
|
|
1294
|
-
) => Promise<void>;
|
|
1295
|
-
findSession?: (name: string, env: NodeJS.ProcessEnv) => GjcTmuxSessionStatus | undefined;
|
|
1296
|
-
} = {},
|
|
1297
|
-
) {
|
|
1298
|
-
return async (target: { sessionId: string; tmuxSession?: string; sessionStateFile?: string }) => {
|
|
1299
|
-
const name = target.tmuxSession ?? tmuxSessionNameFor(target.sessionId);
|
|
1300
|
-
await (deps.forceClose ?? forceCloseGjcTmuxSession)(name, env, target.sessionId, target.sessionStateFile);
|
|
1301
|
-
return { processGone: (deps.findSession ?? findGjcTmuxSessionByName)(name, env) === undefined };
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
/** Real resume effect: reattach if a live GJC session matches; else resolve the
|
|
1306
|
-
* prefix against saved history and fail closed (`ambiguous`/`notFound`) before
|
|
1307
|
-
* cold-restarting exactly one resolved session via the daemon-safe launcher. */
|
|
1308
|
-
export function daemonResumeSession(
|
|
1309
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
1310
|
-
opts: {
|
|
1311
|
-
agentDir?: string;
|
|
1312
|
-
/** Explicit managed root for isolated tests. */
|
|
1313
|
-
sessionsRoot?: string;
|
|
1314
|
-
listSessions?: (env: NodeJS.ProcessEnv) => GjcTmuxSessionStatus[];
|
|
1315
|
-
ownerIsolationProbe?: OwnerIsolationProbe;
|
|
1316
|
-
platform?: NodeJS.Platform;
|
|
1317
|
-
processIncarnation?: (pid: number) => string | undefined;
|
|
1318
|
-
} = {},
|
|
1319
|
-
) {
|
|
1320
|
-
return async (target: {
|
|
1321
|
-
sessionIdOrPrefix: string;
|
|
1322
|
-
path?: string;
|
|
1323
|
-
}): Promise<ResumeEffectResult | { ambiguous: ResumeCandidate[] } | { notFound: true }> => {
|
|
1324
|
-
const tmuxBinary = resolveGjcTmuxBinary({ env, platform: opts.platform });
|
|
1325
|
-
if (tmuxBinary.isPsmux) throw new Error("gjc_lifecycle_psmux_unsupported");
|
|
1326
|
-
const live = (opts.listSessions?.(env) ?? listGjcTmuxSessions(env)).filter(
|
|
1327
|
-
s => s.sessionId === target.sessionIdOrPrefix || s.sessionId?.startsWith(target.sessionIdOrPrefix),
|
|
1328
|
-
);
|
|
1329
|
-
if (live.length > 1) {
|
|
1330
|
-
return {
|
|
1331
|
-
ambiguous: live.map(s => ({ sessionId: s.sessionId ?? s.name, path: s.project })),
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
if (live.length === 1) {
|
|
1335
|
-
const s = live[0]!;
|
|
1336
|
-
await assertLifecycleTmuxServerSafe({
|
|
1337
|
-
tmux: resolveGjcTmuxCommand(env),
|
|
1338
|
-
env,
|
|
1339
|
-
ownerIsolationProbe: opts.ownerIsolationProbe,
|
|
1340
|
-
});
|
|
1341
|
-
return {
|
|
1342
|
-
sessionId: s.sessionId ?? s.name,
|
|
1343
|
-
tmuxSession: s.name,
|
|
1344
|
-
sessionStateFile: s.sessionStateFile,
|
|
1345
|
-
endpointUrl: "",
|
|
1346
|
-
topicThreadId: "",
|
|
1347
|
-
mode: "reattached",
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1350
|
-
// Dead: resolve the id/prefix against saved session history BEFORE cold
|
|
1351
|
-
// restart, so an unknown or ambiguous prefix fails closed instead of
|
|
1352
|
-
// blindly spawning `gjc --resume <prefix>` against a non-authoritative id.
|
|
1353
|
-
let resumeId = target.sessionIdOrPrefix;
|
|
1354
|
-
let resumeCwd = target.path;
|
|
1355
|
-
if (!target.path && !opts.agentDir && !opts.sessionsRoot) return { notFound: true };
|
|
1356
|
-
const recent = await listRecentSessions({
|
|
1357
|
-
cwd: target.path ?? opts.agentDir ?? opts.sessionsRoot ?? "",
|
|
1358
|
-
agentDir: opts.agentDir,
|
|
1359
|
-
sessionsRoot: opts.sessionsRoot,
|
|
1360
|
-
allWorkspaces: target.path === undefined,
|
|
1361
|
-
limit: 1000,
|
|
1362
|
-
});
|
|
1363
|
-
if (recent.kind === "error") throw new Error(`gjc_lifecycle_saved_sessions_unavailable: ${recent.message}`);
|
|
1364
|
-
const saved = recent.entries;
|
|
1365
|
-
const prefixed = saved.filter(
|
|
1366
|
-
s => s.sessionId === target.sessionIdOrPrefix || s.sessionId.startsWith(target.sessionIdOrPrefix),
|
|
1367
|
-
);
|
|
1368
|
-
const exact = prefixed.filter(s => s.sessionId === target.sessionIdOrPrefix);
|
|
1369
|
-
const resolved = exact.length > 0 ? exact : prefixed;
|
|
1370
|
-
if (resolved.length === 0) return { notFound: true };
|
|
1371
|
-
if (resolved.length > 1) {
|
|
1372
|
-
return { ambiguous: resolved.map(s => ({ sessionId: s.sessionId, path: s.path })) };
|
|
1373
|
-
}
|
|
1374
|
-
const selected = resolved[0]!;
|
|
1375
|
-
resumeId = selected.sessionId;
|
|
1376
|
-
resumeCwd = selected.path;
|
|
1377
|
-
const resolvedResumeCwd = resumeCwd ? path.resolve(resumeCwd) : undefined;
|
|
1378
|
-
const resumeCwdStat = resolvedResumeCwd ? fs.statSync(resolvedResumeCwd, { throwIfNoEntry: false }) : undefined;
|
|
1379
|
-
if (typeof resolvedResumeCwd !== "string" || !resumeCwdStat?.isDirectory()) {
|
|
1380
|
-
throw new Error(`gjc_lifecycle_resume_cwd_unavailable: ${resolvedResumeCwd ?? "(missing)"}`);
|
|
1381
|
-
}
|
|
1382
|
-
const tmux = tmuxBinary.command;
|
|
1383
|
-
const name = tmuxSessionNameFor(resumeId);
|
|
1384
|
-
const sessionStateFile = lifecycleRuntimeStateFile(resolvedResumeCwd, resumeId, name);
|
|
1385
|
-
const commonChildEnv: Record<string, string> = {
|
|
1386
|
-
GJC_TMUX_LAUNCHED: "1",
|
|
1387
|
-
GJC_NOTIFICATIONS: "1",
|
|
1388
|
-
[GJC_COORDINATOR_SESSION_ID_ENV]: resumeId,
|
|
1389
|
-
[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
|
|
1390
|
-
};
|
|
1391
|
-
if ((opts.platform ?? process.platform) === "linux") {
|
|
1392
|
-
const stateDir = path.dirname(sessionStateFile);
|
|
1393
|
-
const previousBaseline = await captureOwnerGenerationBaseline(stateDir, resumeId);
|
|
1394
|
-
const predecessor = resolveManagedOwnerPredecessorSync(stateDir, resumeId, previousBaseline);
|
|
1395
|
-
const generation = crypto.randomUUID();
|
|
1396
|
-
const runId = crypto.randomUUID();
|
|
1397
|
-
const incarnation = crypto.randomUUID();
|
|
1398
|
-
const managedChildEnv: Record<string, string> = {
|
|
1399
|
-
...commonChildEnv,
|
|
1400
|
-
[GJC_TMUX_OWNER_GENERATION_ENV]: generation,
|
|
1401
|
-
[GJC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
|
|
1402
|
-
[GJC_TMUX_OWNER_SERVER_KEY_ENV]: "default",
|
|
1403
|
-
[MANAGED_OWNER_COMMAND_ENV]: JSON.stringify(["gjc", "--resume", resumeId]),
|
|
1404
|
-
[MANAGED_OWNER_RUN_ID_ENV]: runId,
|
|
1405
|
-
[MANAGED_OWNER_INCARNATION_ENV]: incarnation,
|
|
1406
|
-
...(predecessor
|
|
1407
|
-
? {
|
|
1408
|
-
[MANAGED_OWNER_PREDECESSOR_TOKEN_ENV]: predecessor.predecessorToken,
|
|
1409
|
-
[MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]: predecessor.generation,
|
|
1410
|
-
[MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]: predecessor.runId,
|
|
1411
|
-
[MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]: predecessor.incarnation,
|
|
1412
|
-
[MANAGED_OWNER_TRANSCRIPT_PATH_ENV]: env.GJC_SESSION_FILE ?? "",
|
|
1413
|
-
}
|
|
1414
|
-
: {}),
|
|
1415
|
-
};
|
|
1416
|
-
const managedEnvPairs = Object.entries(managedChildEnv)
|
|
1417
|
-
.map(([key, value]) => `${key}=${shellQuote(value)}`)
|
|
1418
|
-
.join(" ");
|
|
1419
|
-
const command = `cd ${shellQuote(resolvedResumeCwd)} && exec env ${managedEnvPairs} gjc ${shellQuote(MANAGED_OWNER_SUPERVISOR_ARG)}`;
|
|
1420
|
-
await completeLifecycleSpawnTransaction({
|
|
1421
|
-
tmux,
|
|
1422
|
-
env,
|
|
1423
|
-
sessionId: resumeId,
|
|
1424
|
-
generation,
|
|
1425
|
-
stateDir,
|
|
1426
|
-
cwd: resolvedResumeCwd,
|
|
1427
|
-
sessionName: name,
|
|
1428
|
-
sessionStateFile,
|
|
1429
|
-
argv: [tmux, "new-session", "-d", "-P", "-F", "#{session_id}", "-s", name, "sh", "-c", command],
|
|
1430
|
-
ownerIsolationProbe: opts.ownerIsolationProbe,
|
|
1431
|
-
previousBaseline,
|
|
1432
|
-
});
|
|
1433
|
-
} else {
|
|
1434
|
-
const directEnvPairs = directLifecycleEnvArguments(commonChildEnv);
|
|
1435
|
-
const directCommand = `cd ${shellQuote(resolvedResumeCwd)} && exec env ${directEnvPairs} gjc ${shellQuote("--resume")} ${shellQuote(resumeId)}`;
|
|
1436
|
-
await completeNonLinuxLifecycleSpawn({
|
|
1437
|
-
tmux,
|
|
1438
|
-
env,
|
|
1439
|
-
sessionId: resumeId,
|
|
1440
|
-
cwd: resolvedResumeCwd,
|
|
1441
|
-
sessionName: name,
|
|
1442
|
-
sessionStateFile,
|
|
1443
|
-
command: directCommand,
|
|
1444
|
-
readProcessIncarnation: opts.processIncarnation ?? processIncarnation,
|
|
1445
|
-
});
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
return {
|
|
1449
|
-
sessionId: resumeId,
|
|
1450
|
-
tmuxSession: name,
|
|
1451
|
-
sessionStateFile,
|
|
1452
|
-
endpointUrl: "",
|
|
1453
|
-
topicThreadId: "",
|
|
1454
|
-
mode: "cold_restarted",
|
|
1455
|
-
};
|
|
1456
|
-
};
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
function shellQuote(value: string): string {
|
|
1460
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
/** Translate an orchestrator outcome into a wire response frame. */
|
|
1464
|
-
export function outcomeToResponse(frame: SessionLifecycleRequest, outcome: LifecycleOutcome): SessionLifecycleResponse {
|
|
1465
|
-
if (outcome.status === "error" || outcome.status === "pending") {
|
|
1466
|
-
const reason = outcome.status === "pending" ? "terminal_uncertain" : outcome.reason;
|
|
1467
|
-
return {
|
|
1468
|
-
type: "session_lifecycle_error",
|
|
1469
|
-
requestId: frame.requestId,
|
|
1470
|
-
status: "error",
|
|
1471
|
-
reason,
|
|
1472
|
-
message: outcome.status === "pending" ? "request already in progress" : outcome.message,
|
|
1473
|
-
...(outcome.status === "error" && outcome.candidates ? { candidates: outcome.candidates } : {}),
|
|
1474
|
-
};
|
|
1475
|
-
}
|
|
1476
|
-
const e = outcome.entry;
|
|
1477
|
-
if (e.state !== "success") throw new Error("invalid lifecycle success ledger entry");
|
|
1478
|
-
if (frame.type === "session_create") {
|
|
1479
|
-
if (typeof e.sessionId !== "string" || typeof e.endpointUrl !== "string")
|
|
1480
|
-
throw new Error("invalid create success ledger entry");
|
|
1481
|
-
return {
|
|
1482
|
-
type: "session_create_response",
|
|
1483
|
-
requestId: frame.requestId,
|
|
1484
|
-
status: "ok",
|
|
1485
|
-
lifecycleRequestId: frame.lifecycleRequestId,
|
|
1486
|
-
sessionId: e.sessionId,
|
|
1487
|
-
matchedBy: "spawn_marker",
|
|
1488
|
-
endpoint: { url: e.endpointUrl, token: "" },
|
|
1489
|
-
topic: { chatId: frame.chatId, threadId: "" },
|
|
1490
|
-
target: frame.target,
|
|
1491
|
-
};
|
|
1492
|
-
}
|
|
1493
|
-
if (frame.type === "session_close") {
|
|
1494
|
-
if (typeof e.sessionId !== "string" || typeof e.processGone !== "boolean")
|
|
1495
|
-
throw new Error("invalid close success ledger entry");
|
|
1496
|
-
if (!e.processGone) {
|
|
1497
|
-
return {
|
|
1498
|
-
type: "session_lifecycle_error",
|
|
1499
|
-
requestId: frame.requestId,
|
|
1500
|
-
status: "error",
|
|
1501
|
-
reason: "terminal_uncertain",
|
|
1502
|
-
message: "close did not confirm process termination",
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
|
-
return {
|
|
1506
|
-
type: "session_close_response",
|
|
1507
|
-
requestId: frame.requestId,
|
|
1508
|
-
status: "ok",
|
|
1509
|
-
sessionId: e.sessionId,
|
|
1510
|
-
processGone: true,
|
|
1511
|
-
historyPreserved: true,
|
|
1512
|
-
// The killed session's per-session endpoint record is reaped by the
|
|
1513
|
-
// daemon's dead-PID scan (scanRoots), so it is effectively stale.
|
|
1514
|
-
endpointStale: true,
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
if (
|
|
1518
|
-
typeof e.sessionId !== "string" ||
|
|
1519
|
-
typeof e.endpointUrl !== "string" ||
|
|
1520
|
-
(e.resumeMode !== "reattached" && e.resumeMode !== "cold_restarted")
|
|
1521
|
-
)
|
|
1522
|
-
throw new Error("invalid resume success ledger entry");
|
|
1523
|
-
return {
|
|
1524
|
-
type: "session_resume_response",
|
|
1525
|
-
requestId: frame.requestId,
|
|
1526
|
-
status: "ok",
|
|
1527
|
-
sessionId: e.sessionId,
|
|
1528
|
-
mode: outcome.mode ?? e.resumeMode,
|
|
1529
|
-
endpoint: { url: e.endpointUrl, token: "" },
|
|
1530
|
-
topic: { chatId: frame.chatId, threadId: "" },
|
|
1531
|
-
};
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
const MAX_LIFECYCLE_REQUEST_ID_BYTES = 128;
|
|
1535
|
-
|
|
1536
|
-
function hasLoneUtf16Surrogate(value: string): boolean {
|
|
1537
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
1538
|
-
const codeUnit = value.charCodeAt(index);
|
|
1539
|
-
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
1540
|
-
if (index + 1 >= value.length || value.charCodeAt(index + 1) < 0xdc00 || value.charCodeAt(index + 1) > 0xdfff)
|
|
1541
|
-
return true;
|
|
1542
|
-
index += 1;
|
|
1543
|
-
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
1544
|
-
return true;
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
return false;
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
function boundedLifecycleRequestId(requestId: unknown): string {
|
|
1551
|
-
if (typeof requestId !== "string" || hasLoneUtf16Surrogate(requestId)) return "";
|
|
1552
|
-
let bounded = "";
|
|
1553
|
-
let byteLength = 0;
|
|
1554
|
-
for (const codePoint of requestId) {
|
|
1555
|
-
const codePointBytes = Buffer.byteLength(codePoint, "utf8");
|
|
1556
|
-
if (byteLength + codePointBytes > MAX_LIFECYCLE_REQUEST_ID_BYTES) break;
|
|
1557
|
-
bounded += codePoint;
|
|
1558
|
-
byteLength += codePointBytes;
|
|
1559
|
-
}
|
|
1560
|
-
return bounded;
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
function lifecycleFailureResponse(requestId: unknown): SessionLifecycleResponse {
|
|
1564
|
-
return {
|
|
1565
|
-
type: "session_lifecycle_error",
|
|
1566
|
-
requestId: boundedLifecycleRequestId(requestId),
|
|
1567
|
-
status: "error",
|
|
1568
|
-
reason: "terminal_uncertain",
|
|
1569
|
-
message: "request could not be processed",
|
|
1570
|
-
};
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
function recordLifecycleControlDiagnostic(): void {
|
|
1574
|
-
recordBoundedLifecycleDiagnostic("GJC lifecycle control request failed");
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1578
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
function isSafeLifecycleId(value: unknown): value is string {
|
|
1582
|
-
return typeof value === "string" && !hasLoneUtf16Surrogate(value);
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
function isNonEmptySafeLifecycleId(value: unknown): value is string {
|
|
1586
|
-
return isSafeLifecycleId(value) && value.length > 0;
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
/** The native server authenticates callbacks before redacting the control token. */
|
|
1590
|
-
type AuthenticatedNativeLifecycleRequest =
|
|
1591
|
-
| Omit<SessionCreateFrame, "token">
|
|
1592
|
-
| Omit<Extract<SessionLifecycleRequest, { type: "session_close" }>, "token">
|
|
1593
|
-
| Omit<Extract<SessionLifecycleRequest, { type: "session_resume" }>, "token">;
|
|
1594
|
-
|
|
1595
|
-
function isBoundedRequestFrame(frame: unknown): frame is AuthenticatedNativeLifecycleRequest {
|
|
1596
|
-
if (
|
|
1597
|
-
!isRecord(frame) ||
|
|
1598
|
-
!isSafeLifecycleId(frame.requestId) ||
|
|
1599
|
-
Buffer.byteLength(frame.requestId, "utf8") > MAX_LIFECYCLE_REQUEST_ID_BYTES
|
|
1600
|
-
)
|
|
1601
|
-
return false;
|
|
1602
|
-
if (
|
|
1603
|
-
!isSafeLifecycleId(frame.chatId) ||
|
|
1604
|
-
(typeof frame.token !== "undefined" && typeof frame.token !== "string") ||
|
|
1605
|
-
!Number.isSafeInteger(frame.updateId)
|
|
1606
|
-
)
|
|
1607
|
-
return false;
|
|
1608
|
-
if (!isRecord(frame.target)) return false;
|
|
1609
|
-
switch (frame.type) {
|
|
1610
|
-
case "session_create":
|
|
1611
|
-
return (
|
|
1612
|
-
isNonEmptySafeLifecycleId(frame.lifecycleRequestId) &&
|
|
1613
|
-
isNonEmptySafeLifecycleId(frame.intendedSessionId) &&
|
|
1614
|
-
(typeof frame.startupPromptRef === "undefined" || typeof frame.startupPromptRef === "string") &&
|
|
1615
|
-
(typeof frame.modelPreset === "undefined" || typeof frame.modelPreset === "string") &&
|
|
1616
|
-
((frame.target.kind === "worktree" &&
|
|
1617
|
-
typeof frame.target.repo === "string" &&
|
|
1618
|
-
typeof frame.target.branch === "string") ||
|
|
1619
|
-
((frame.target.kind === "existing_path" || frame.target.kind === "plain_dir") &&
|
|
1620
|
-
typeof frame.target.path === "string"))
|
|
1621
|
-
);
|
|
1622
|
-
case "session_close":
|
|
1623
|
-
return (
|
|
1624
|
-
isSafeLifecycleId(frame.target.sessionId) &&
|
|
1625
|
-
(typeof frame.target.tmuxSession === "undefined" || isSafeLifecycleId(frame.target.tmuxSession)) &&
|
|
1626
|
-
(typeof frame.target.sessionStateFile === "undefined" ||
|
|
1627
|
-
typeof frame.target.sessionStateFile === "string") &&
|
|
1628
|
-
(typeof frame.force === "undefined" || typeof frame.force === "boolean")
|
|
1629
|
-
);
|
|
1630
|
-
case "session_resume":
|
|
1631
|
-
return (
|
|
1632
|
-
isSafeLifecycleId(frame.target.sessionIdOrPrefix) &&
|
|
1633
|
-
(typeof frame.target.path === "undefined" || typeof frame.target.path === "string") &&
|
|
1634
|
-
(typeof frame.startupPromptRef === "undefined" || typeof frame.startupPromptRef === "string")
|
|
1635
|
-
);
|
|
1636
|
-
default:
|
|
1637
|
-
return false;
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
/**
|
|
1642
|
-
* Wire a control server's lifecycle requests through the orchestrator.
|
|
1643
|
-
*
|
|
1644
|
-
* Handlers run on a single serial queue (a promise chain): the daemon owns the
|
|
1645
|
-
* one control endpoint, so serializing here makes each request's ledger
|
|
1646
|
-
* read -> classify -> write atomic with respect to every other request. Two
|
|
1647
|
-
* identical updates that arrive nearly simultaneously can no longer both
|
|
1648
|
-
* classify as `new` and both spawn — the second sees the first's persisted
|
|
1649
|
-
* `in_progress`/`success` entry and re-acks instead.
|
|
1650
|
-
*/
|
|
1651
|
-
export function attachLifecycleControl(server: ControlServerLike, deps: OrchestratorDeps): void {
|
|
1652
|
-
let queue: Promise<void> = Promise.resolve();
|
|
1653
|
-
server.onLifecycleRequest((err, req) => {
|
|
1654
|
-
if (err) {
|
|
1655
|
-
recordLifecycleControlDiagnostic();
|
|
1656
|
-
return;
|
|
1657
|
-
}
|
|
1658
|
-
const envelopeRequestId = boundedLifecycleRequestId(req.requestId);
|
|
1659
|
-
const run = async (): Promise<void> => {
|
|
1660
|
-
let frame: AuthenticatedNativeLifecycleRequest | undefined;
|
|
1661
|
-
|
|
1662
|
-
try {
|
|
1663
|
-
const parsed = JSON.parse(req.payloadJson) as unknown;
|
|
1664
|
-
if (!isBoundedRequestFrame(parsed)) throw new Error("invalid lifecycle frame");
|
|
1665
|
-
frame = parsed;
|
|
1666
|
-
const outcome = await handleLifecycleRequest(frame as SessionLifecycleRequest, deps);
|
|
1667
|
-
|
|
1668
|
-
server.respond(
|
|
1669
|
-
JSON.stringify(
|
|
1670
|
-
outcome.status === "error" && outcome.reason === "terminal_uncertain"
|
|
1671
|
-
? lifecycleFailureResponse(frame.requestId)
|
|
1672
|
-
: outcomeToResponse(frame as SessionLifecycleRequest, outcome),
|
|
1673
|
-
),
|
|
1674
|
-
);
|
|
1675
|
-
} catch {
|
|
1676
|
-
recordLifecycleControlDiagnostic();
|
|
1677
|
-
// A valid frame preserves its immutable request id; malformed frames use
|
|
1678
|
-
// only the bounded native envelope id.
|
|
1679
|
-
server.respond(JSON.stringify(lifecycleFailureResponse(frame?.requestId ?? envelopeRequestId)));
|
|
1680
|
-
}
|
|
1681
|
-
};
|
|
1682
|
-
// Recover queue liveness only after this request has attempted its own
|
|
1683
|
-
// failure response. A response transport failure cannot poison later work.
|
|
1684
|
-
queue = queue.then(run, run).catch(() => {
|
|
1685
|
-
recordLifecycleControlDiagnostic();
|
|
1686
|
-
});
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
/** Assemble real orchestrator deps for the daemon (ledger/audit under agentDir). */
|
|
1691
|
-
export function buildOrchestratorDeps(input: {
|
|
1692
|
-
pairedChatId: string;
|
|
1693
|
-
agentNotificationsDir: string;
|
|
1694
|
-
/** Required in-memory, 32-byte audit-v2 redaction key. */
|
|
1695
|
-
auditRedactionKey: Uint8Array;
|
|
1696
|
-
/** Root of saved session histories (`<agentDir>/sessions`), for resume resolution. */
|
|
1697
|
-
sessionsRoot?: string;
|
|
1698
|
-
/** Agent directory used for readonly managed-session resume resolution. */
|
|
1699
|
-
agentDir?: string;
|
|
1700
|
-
env?: NodeJS.ProcessEnv;
|
|
1701
|
-
}): OrchestratorDeps {
|
|
1702
|
-
if (input.auditRedactionKey.byteLength !== 32) throw new Error("invalid_audit_redaction_key");
|
|
1703
|
-
const env = input.env ?? process.env;
|
|
1704
|
-
return {
|
|
1705
|
-
pairedChatId: input.pairedChatId,
|
|
1706
|
-
auditRedactionKey: input.auditRedactionKey,
|
|
1707
|
-
now: () => Date.now(),
|
|
1708
|
-
store: fileLedgerStore(path.join(input.agentNotificationsDir, "telegram-lifecycle-idempotency.json")),
|
|
1709
|
-
audit: fileAudit(path.join(input.agentNotificationsDir, "telegram-lifecycle-audit.jsonl")),
|
|
1710
|
-
isPsmuxProvider: () => resolveGjcTmuxBinary({ env }).isPsmux,
|
|
1711
|
-
allowCreate: createRateLimiter(3, 10 * 60 * 1000),
|
|
1712
|
-
writeStartupPrompt: async (_requestId, prompt, _persistRef) => {
|
|
1713
|
-
if (prompt === undefined) return undefined;
|
|
1714
|
-
throw new Error("startup_prompt_capability_transport_unavailable");
|
|
1715
|
-
},
|
|
1716
|
-
spawnCreate: daemonSpawnCreate(env),
|
|
1717
|
-
closeSession: daemonCloseSession(env),
|
|
1718
|
-
resumeSession: daemonResumeSession(env, {
|
|
1719
|
-
agentDir: input.agentDir ?? path.dirname(input.agentNotificationsDir),
|
|
1720
|
-
}),
|
|
1721
|
-
};
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
/**
|
|
1725
|
-
* Default production factory: a real native NotificationControlServer bound to
|
|
1726
|
-
* the daemon's control token, owner id, and agent dir.
|
|
1727
|
-
*/
|
|
1728
|
-
export const createNativeControlServer: LifecycleControlServerFactory = ({ token, ownerId, agentDir }) => {
|
|
1729
|
-
// Lazy require so loading this module (for the orchestrator / wiring / tests)
|
|
1730
|
-
// never eagerly resolves the native addon — only a real production start does.
|
|
1731
|
-
const { NotificationControlServer } = require("@gajae-code/natives") as NativeControlServerModule;
|
|
1732
|
-
return new NotificationControlServer(token, ownerId, agentDir);
|
|
1733
|
-
};
|