@gajae-code/coding-agent 0.13.3 → 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 +184 -2
- 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 +9 -1
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +70 -6
- 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 +1 -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/gajae-pet-widget.d.ts +8 -30
- 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 +11 -25
- 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/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 +40 -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 +9 -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 +20 -1
- package/src/config/model-profiles.ts +29 -19
- package/src/config/model-registry.ts +132 -21
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +76 -6
- 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/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 +40 -32
- package/src/main.ts +32 -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/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/gajae-pet-widget.ts +539 -213
- package/src/modes/components/irc-sidebar.ts +11 -0
- package/src/modes/components/iterm-pet-transport.ts +585 -0
- package/src/modes/components/oauth-selector.ts +277 -86
- package/src/modes/components/pet-capability.ts +39 -40
- package/src/modes/components/settings-selector.ts +9 -0
- package/src/modes/components/status-line/gh.ts +45 -1
- package/src/modes/components/status-line/segments.ts +16 -1
- 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 +177 -72
- 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/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 +2857 -582
- 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 +173 -12
- package/src/session/messages.ts +1 -1
- package/src/session/secure-token-file.ts +80 -0
- package/src/session/session-manager.ts +730 -58
- 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 +98 -16
- 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 +27 -16
- 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/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
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type GjcLaunchWorktreePlan,
|
|
29
29
|
planLaunchWorktree,
|
|
30
30
|
} from "../../gjc-runtime/launch-worktree";
|
|
31
|
+
import { probeLinuxProcPidSync } from "../../gjc-runtime/linux-proc";
|
|
31
32
|
import {
|
|
32
33
|
GJC_COORDINATOR_SESSION_BRANCH_ENV,
|
|
33
34
|
GJC_COORDINATOR_SESSION_ID_ENV,
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
} from "../../session/session-storage";
|
|
45
46
|
import type { SessionLifecycleMcpServer } from "../acp/mcp";
|
|
46
47
|
import { SdkClient, SdkClientError } from "../client/client";
|
|
48
|
+
import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "../host/control/runtime-gate";
|
|
47
49
|
import { SESSION_PREPARED_EVENT } from "../host/host";
|
|
48
50
|
import {
|
|
49
51
|
type LogicalSessionCandidate,
|
|
@@ -72,6 +74,7 @@ import {
|
|
|
72
74
|
processIncarnation,
|
|
73
75
|
} from "./process-incarnation";
|
|
74
76
|
import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
|
|
77
|
+
import { isSessionAuthorityEligible } from "./session-index";
|
|
75
78
|
import {
|
|
76
79
|
cancellableSleep,
|
|
77
80
|
DEFAULT_READINESS_TIMEOUT_MS,
|
|
@@ -89,8 +92,6 @@ export {
|
|
|
89
92
|
|
|
90
93
|
const POLL_MS = 50;
|
|
91
94
|
const CLOSE_TIMEOUT_MS = 2_000;
|
|
92
|
-
const STARTUP_CLEANUP_PROOF_GRACE_MS = 500;
|
|
93
|
-
|
|
94
95
|
const MAX_RECEIVED_AT_SKEW_MS = 5_000;
|
|
95
96
|
const MAX_LIFECYCLE_METADATA_BYTES = 4096;
|
|
96
97
|
const MAX_EFFECT_MARKER_LENGTH = 128;
|
|
@@ -557,6 +558,7 @@ type LiveResumeRecord = {
|
|
|
557
558
|
pid: number;
|
|
558
559
|
endpointMtimeMs?: number;
|
|
559
560
|
live: boolean;
|
|
561
|
+
ambiguous: boolean;
|
|
560
562
|
};
|
|
561
563
|
type ResumeScope = {
|
|
562
564
|
cwd: string;
|
|
@@ -591,6 +593,7 @@ function sameResumeSessionIdentity(left: ResumeScope, right: ResumeScope): boole
|
|
|
591
593
|
function sameLiveResumeRecord(expected: LiveResumeRecord, current: LiveResumeRecord): boolean {
|
|
592
594
|
return (
|
|
593
595
|
current.live &&
|
|
596
|
+
isSessionAuthorityEligible(current) &&
|
|
594
597
|
current.endpointGeneration === expected.endpointGeneration &&
|
|
595
598
|
current.pid === expected.pid &&
|
|
596
599
|
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
@@ -747,6 +750,8 @@ async function reconcileReadyScope(broker: Broker, id: string, scope: string | u
|
|
|
747
750
|
// published about its own process has to survive it: dropping the incarnation
|
|
748
751
|
// here would silently disarm the teardown fence for every lifecycle session.
|
|
749
752
|
...(record.processIncarnation === undefined ? {} : { processIncarnation: record.processIncarnation }),
|
|
753
|
+
...(record.hostIncarnation === undefined ? {} : { hostIncarnation: record.hostIncarnation }),
|
|
754
|
+
...(record.lifecycleRequestId === undefined ? {} : { lifecycleRequestId: record.lifecycleRequestId }),
|
|
750
755
|
endpointMtimeMs: record.endpointMtimeMs,
|
|
751
756
|
});
|
|
752
757
|
}
|
|
@@ -795,7 +800,8 @@ type ReadinessResult =
|
|
|
795
800
|
| { kind: "startup_failed"; failure: SdkStartupFailure }
|
|
796
801
|
| { kind: "child_exited" }
|
|
797
802
|
| { kind: "timeout" };
|
|
798
|
-
|
|
803
|
+
type BrokerIndex = Pick<Broker, "index">;
|
|
804
|
+
const processIncarnationReadersForTest = new WeakMap<BrokerIndex, (pid: number) => string | undefined>();
|
|
799
805
|
|
|
800
806
|
export function setProcessIncarnationForTest(
|
|
801
807
|
broker: Broker,
|
|
@@ -805,7 +811,7 @@ export function setProcessIncarnationForTest(
|
|
|
805
811
|
else processIncarnationReadersForTest.delete(broker);
|
|
806
812
|
}
|
|
807
813
|
|
|
808
|
-
function processIncarnationForBroker(broker:
|
|
814
|
+
function processIncarnationForBroker(broker: BrokerIndex, pid: number): string | undefined {
|
|
809
815
|
const reader = processIncarnationReadersForTest.get(broker);
|
|
810
816
|
return reader ? reader(pid) : processIncarnation(pid);
|
|
811
817
|
}
|
|
@@ -826,6 +832,11 @@ function observeProcess(
|
|
|
826
832
|
} catch (error) {
|
|
827
833
|
return (error as NodeJS.ErrnoException).code === "ESRCH" ? "exited" : "uncertain";
|
|
828
834
|
}
|
|
835
|
+
if (process.platform === "linux") {
|
|
836
|
+
const probe = probeLinuxProcPidSync(pid);
|
|
837
|
+
if (probe.kind === "live" && probe.state === "Z" && expectedIncarnation === `linux:${probe.startTime}`)
|
|
838
|
+
return "exited";
|
|
839
|
+
}
|
|
829
840
|
if (!expectedIncarnation) return "uncertain";
|
|
830
841
|
const actualIncarnation = readIncarnation(pid);
|
|
831
842
|
if (!actualIncarnation) return "uncertain";
|
|
@@ -841,6 +852,41 @@ function hasObservedProcessExit(pid: number): boolean {
|
|
|
841
852
|
}
|
|
842
853
|
}
|
|
843
854
|
|
|
855
|
+
type StableProcessExitObserver = {
|
|
856
|
+
hasExited(): boolean;
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Pin the indexed process before shutdown so exit remains observable after it
|
|
861
|
+
* becomes a zombie. `kill(pid, 0)` and `/proc/<pid>/stat` continue to describe
|
|
862
|
+
* an unreaped zombie as present, which otherwise makes a timely SIGTERM exit
|
|
863
|
+
* look unverifiable and incorrectly escalates the close to terminal uncertainty.
|
|
864
|
+
*/
|
|
865
|
+
function stableProcessExitObserver(record: CloseRecord): StableProcessExitObserver | undefined {
|
|
866
|
+
if (!record.processIncarnation) return undefined;
|
|
867
|
+
try {
|
|
868
|
+
const reference = nativeLifecycle().Process.fromPid(record.pid);
|
|
869
|
+
if (!reference || reference.incarnation !== record.processIncarnation) return undefined;
|
|
870
|
+
return { hasExited: () => reference.status() === "exited" };
|
|
871
|
+
} catch {
|
|
872
|
+
return undefined;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function observedProcessExited(
|
|
877
|
+
pid: number,
|
|
878
|
+
expectedIncarnation: string | undefined,
|
|
879
|
+
observer: StableProcessExitObserver | undefined,
|
|
880
|
+
): boolean {
|
|
881
|
+
try {
|
|
882
|
+
if (observer?.hasExited()) return true;
|
|
883
|
+
} catch {}
|
|
884
|
+
if (hasObservedProcessExit(pid)) return true;
|
|
885
|
+
if (process.platform !== "linux" || !expectedIncarnation?.startsWith("linux:")) return false;
|
|
886
|
+
const probe = probeLinuxProcPidSync(pid);
|
|
887
|
+
return probe.kind === "live" && probe.state === "Z" && `linux:${probe.startTime}` === expectedIncarnation;
|
|
888
|
+
}
|
|
889
|
+
|
|
844
890
|
function isEffectMarker(value: unknown): value is EffectMarker {
|
|
845
891
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
846
892
|
const marker = value as Record<string, unknown>;
|
|
@@ -1959,7 +2005,6 @@ type SignalTarget = {
|
|
|
1959
2005
|
pid: number;
|
|
1960
2006
|
lifecycleRequestId?: string;
|
|
1961
2007
|
processIncarnation?: string;
|
|
1962
|
-
hostIncarnation?: string;
|
|
1963
2008
|
};
|
|
1964
2009
|
|
|
1965
2010
|
/**
|
|
@@ -1982,11 +2027,7 @@ async function hasDurableProcessIdentity(target: SignalTarget, id: string, expec
|
|
|
1982
2027
|
marker.incarnation === processIncarnation(target.pid)
|
|
1983
2028
|
);
|
|
1984
2029
|
if (expected && (expected.pid !== target.pid || target.lifecycleRequestId !== expected.effectMarker)) return false;
|
|
1985
|
-
|
|
1986
|
-
// the index's append path auto-stamps `hostIncarnation` for everything else.
|
|
1987
|
-
// Either binding is the same pid-to-incarnation proof.
|
|
1988
|
-
const recorded = target.hostIncarnation ?? target.processIncarnation;
|
|
1989
|
-
return recorded !== undefined && recorded === processIncarnation(target.pid);
|
|
2030
|
+
return target.processIncarnation !== undefined && target.processIncarnation === processIncarnation(target.pid);
|
|
1990
2031
|
}
|
|
1991
2032
|
|
|
1992
2033
|
async function hasOwnedReadinessEvidence(
|
|
@@ -2074,7 +2115,13 @@ function captureLifecycleFile(file: string, requireRegular = false, bounded = fa
|
|
|
2074
2115
|
}
|
|
2075
2116
|
}
|
|
2076
2117
|
|
|
2077
|
-
async function removeOwnedLifecycleArtifacts(
|
|
2118
|
+
async function removeOwnedLifecycleArtifacts(
|
|
2119
|
+
root: string,
|
|
2120
|
+
id: string,
|
|
2121
|
+
expected: EffectMarker,
|
|
2122
|
+
onRetainedUnknown?: () => void,
|
|
2123
|
+
onDurablePlaceholder?: (file: string) => void,
|
|
2124
|
+
): Promise<boolean> {
|
|
2078
2125
|
const marker = await readEffectMarker(lifecycleMarkerPath(root, id));
|
|
2079
2126
|
if (!marker || !sameEffectMarker(marker, expected)) return false;
|
|
2080
2127
|
const endpointPath = path.join(root, "sdk", `${id}.json`);
|
|
@@ -2106,7 +2153,7 @@ async function removeOwnedLifecycleArtifacts(root: string, id: string, expected:
|
|
|
2106
2153
|
} catch {
|
|
2107
2154
|
return false;
|
|
2108
2155
|
}
|
|
2109
|
-
if (parsed.pid !== expected.pid ||
|
|
2156
|
+
if (parsed.pid !== expected.pid || observeProcess(expected.pid, expected.incarnation) !== "exited") return false;
|
|
2110
2157
|
if (createHash("sha256").update(endpoint.bytes).digest("hex") !== endpoint.digest) return false;
|
|
2111
2158
|
const endpointRemoval = exactUnlinkLifecycleFile(
|
|
2112
2159
|
endpointSource,
|
|
@@ -2118,7 +2165,49 @@ async function removeOwnedLifecycleArtifacts(root: string, id: string, expected:
|
|
|
2118
2165
|
: finalEndpointPath,
|
|
2119
2166
|
{ dev: BigInt(endpointParent.dev), ino: BigInt(endpointParent.ino) },
|
|
2120
2167
|
);
|
|
2121
|
-
if (!endpointRemoval.ok)
|
|
2168
|
+
if (!endpointRemoval.ok) {
|
|
2169
|
+
if (endpointRemoval.retainedUnknownPath) {
|
|
2170
|
+
onRetainedUnknown?.();
|
|
2171
|
+
return false;
|
|
2172
|
+
}
|
|
2173
|
+
if (endpointRemoval.code !== "cleanup_pending") return false;
|
|
2174
|
+
if (endpointRemoval.retainedPlaceholderPath) {
|
|
2175
|
+
if (endpointRemoval.payloadDurable !== true) return false;
|
|
2176
|
+
onDurablePlaceholder?.(path.resolve(endpointRemoval.retainedPlaceholderPath));
|
|
2177
|
+
for (const candidate of [endpointSource, plannedEndpointPath, retryEndpointPath, finalEndpointPath]) {
|
|
2178
|
+
try {
|
|
2179
|
+
if (fsSync.lstatSync(candidate).size === 0) onDurablePlaceholder?.(path.resolve(candidate));
|
|
2180
|
+
} catch {
|
|
2181
|
+
// Absent aliases carry no retained authority.
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
// A payload-durable POSIX detach has scrubbed its quarantine payload to a
|
|
2186
|
+
// zero-byte placeholder, so the original identity cannot authorize a retry.
|
|
2187
|
+
if (
|
|
2188
|
+
endpointRemoval.detachedPath &&
|
|
2189
|
+
endpointRemoval.payloadDurable !== true &&
|
|
2190
|
+
fsSync.existsSync(endpointRemoval.detachedPath)
|
|
2191
|
+
) {
|
|
2192
|
+
const detachedPath = path.resolve(endpointRemoval.detachedPath);
|
|
2193
|
+
if (path.dirname(detachedPath) !== path.dirname(path.resolve(endpointPath))) return false;
|
|
2194
|
+
let detachedRemoval: NativeExactUnlinkResult;
|
|
2195
|
+
try {
|
|
2196
|
+
detachedRemoval = exactUnlinkLifecycleFile(
|
|
2197
|
+
detachedPath,
|
|
2198
|
+
endpoint.identity,
|
|
2199
|
+
path.join(
|
|
2200
|
+
path.dirname(detachedPath),
|
|
2201
|
+
`.gjc-delete-endpoint-detached-${expected.effectMarker}-${path.basename(endpointPath)}`,
|
|
2202
|
+
),
|
|
2203
|
+
{ dev: BigInt(endpointParent.dev), ino: BigInt(endpointParent.ino) },
|
|
2204
|
+
);
|
|
2205
|
+
} catch {
|
|
2206
|
+
return false;
|
|
2207
|
+
}
|
|
2208
|
+
if (!detachedRemoval.ok && detachedRemoval.code !== "not_found") return false;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2122
2211
|
}
|
|
2123
2212
|
const currentMarker = await readEffectMarker(lifecycleMarkerPath(root, id));
|
|
2124
2213
|
if (!currentMarker || !sameEffectMarker(currentMarker, expected)) return false;
|
|
@@ -2139,6 +2228,8 @@ async function recordTerminalUncertain(broker: Broker, id: string, root: string,
|
|
|
2139
2228
|
locator: registered.locator,
|
|
2140
2229
|
endpointGeneration: registered.endpointGeneration,
|
|
2141
2230
|
pid: registered.pid,
|
|
2231
|
+
...(registered.processIncarnation === undefined ? {} : { processIncarnation: registered.processIncarnation }),
|
|
2232
|
+
...(registered.hostIncarnation === undefined ? {} : { hostIncarnation: registered.hostIncarnation }),
|
|
2142
2233
|
terminalUncertain: true,
|
|
2143
2234
|
});
|
|
2144
2235
|
else
|
|
@@ -2185,6 +2276,12 @@ async function terminateSpawnedChild(
|
|
|
2185
2276
|
};
|
|
2186
2277
|
|
|
2187
2278
|
let observation = observe();
|
|
2279
|
+
const recheckOwnedExitObservation = async (): Promise<void> => {
|
|
2280
|
+
if (observation !== "uncertain") return;
|
|
2281
|
+
// This direct ChildProcess is owned by this broker invocation. A process-incarnation
|
|
2282
|
+
// read can briefly lag its exit event, so recheck only this owned child before failing.
|
|
2283
|
+
observation = await waitForExit(deadline);
|
|
2284
|
+
};
|
|
2188
2285
|
if (observation === "alive") {
|
|
2189
2286
|
await waitUntil(timing, terminationStartDeadlineAt);
|
|
2190
2287
|
observation = observe();
|
|
@@ -2192,6 +2289,7 @@ async function terminateSpawnedChild(
|
|
|
2192
2289
|
if (observation === "alive") {
|
|
2193
2290
|
if (!(await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGTERM", expected))) {
|
|
2194
2291
|
observation = observe();
|
|
2292
|
+
await recheckOwnedExitObservation();
|
|
2195
2293
|
if (observation !== "exited") {
|
|
2196
2294
|
await recordTerminalUncertain(broker, id, root, pid);
|
|
2197
2295
|
return false;
|
|
@@ -2205,6 +2303,7 @@ async function terminateSpawnedChild(
|
|
|
2205
2303
|
if (observation === "alive") {
|
|
2206
2304
|
if (!(await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGKILL", expected))) {
|
|
2207
2305
|
observation = observe();
|
|
2306
|
+
await recheckOwnedExitObservation();
|
|
2208
2307
|
if (observation !== "exited") {
|
|
2209
2308
|
await recordTerminalUncertain(broker, id, root, pid);
|
|
2210
2309
|
return false;
|
|
@@ -2213,6 +2312,7 @@ async function terminateSpawnedChild(
|
|
|
2213
2312
|
observation = await waitForExit(deadline);
|
|
2214
2313
|
}
|
|
2215
2314
|
}
|
|
2315
|
+
await recheckOwnedExitObservation();
|
|
2216
2316
|
if (observation !== "exited") {
|
|
2217
2317
|
await recordTerminalUncertain(broker, id, root, pid);
|
|
2218
2318
|
return false;
|
|
@@ -2284,14 +2384,186 @@ async function endpointRemoved(root: string, id: string): Promise<boolean> {
|
|
|
2284
2384
|
}
|
|
2285
2385
|
}
|
|
2286
2386
|
|
|
2387
|
+
async function removeExactDeadSessionEndpoint(
|
|
2388
|
+
broker: Broker,
|
|
2389
|
+
id: string,
|
|
2390
|
+
record: CloseRecord,
|
|
2391
|
+
attempt = 0,
|
|
2392
|
+
authorizedSource?: string,
|
|
2393
|
+
durablePlaceholders?: Set<string>,
|
|
2394
|
+
): Promise<boolean> {
|
|
2395
|
+
if (record.endpointMtimeMs === undefined) return await endpointRemoved(record.locator.stateRoot, id);
|
|
2396
|
+
await broker.index.refresh();
|
|
2397
|
+
const current = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
2398
|
+
if (
|
|
2399
|
+
!current ||
|
|
2400
|
+
current.endpointGeneration !== record.endpointGeneration ||
|
|
2401
|
+
current.pid !== record.pid ||
|
|
2402
|
+
current.endpointMtimeMs !== record.endpointMtimeMs ||
|
|
2403
|
+
current.lifecycleRequestId !== record.lifecycleRequestId ||
|
|
2404
|
+
current.processIncarnation !== record.processIncarnation ||
|
|
2405
|
+
path.resolve(current.locator.stateRoot) !== path.resolve(record.locator.stateRoot)
|
|
2406
|
+
)
|
|
2407
|
+
return false;
|
|
2408
|
+
const endpointPath = path.join(record.locator.stateRoot, "sdk", `${id}.json`);
|
|
2409
|
+
const suffix = `${id}-${record.endpointGeneration}-${record.pid}-${String(record.endpointMtimeMs).replaceAll(".", "_")}.json`;
|
|
2410
|
+
const plannedEndpointPath = path.join(path.dirname(endpointPath), `.gjc-dead-endpoint-${suffix}`);
|
|
2411
|
+
const retryEndpointPath = path.join(path.dirname(endpointPath), `.gjc-dead-endpoint-retry-${suffix}`);
|
|
2412
|
+
const finalEndpointPath = path.join(path.dirname(endpointPath), `.gjc-dead-endpoint-final-${suffix}`);
|
|
2413
|
+
const ownedEndpointPaths = record.lifecycleRequestId
|
|
2414
|
+
? [
|
|
2415
|
+
`.gjc-delete-endpoint-detached-${record.lifecycleRequestId}-${path.basename(endpointPath)}`,
|
|
2416
|
+
`.gjc-delete-endpoint-final-${record.lifecycleRequestId}-${path.basename(endpointPath)}`,
|
|
2417
|
+
`.gjc-delete-endpoint-retry-${record.lifecycleRequestId}-${path.basename(endpointPath)}`,
|
|
2418
|
+
`.gjc-delete-endpoint-${record.lifecycleRequestId}-${path.basename(endpointPath)}`,
|
|
2419
|
+
].map(name => path.join(path.dirname(endpointPath), name))
|
|
2420
|
+
: [];
|
|
2421
|
+
const ownedPayloadPaths: string[] = [];
|
|
2422
|
+
for (const ownedPath of ownedEndpointPaths) {
|
|
2423
|
+
let size: number;
|
|
2424
|
+
try {
|
|
2425
|
+
size = fsSync.lstatSync(ownedPath).size;
|
|
2426
|
+
} catch (error) {
|
|
2427
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
2428
|
+
return false;
|
|
2429
|
+
}
|
|
2430
|
+
if (size > 0) ownedPayloadPaths.push(ownedPath);
|
|
2431
|
+
}
|
|
2432
|
+
const candidates = [...ownedPayloadPaths, finalEndpointPath, retryEndpointPath, plannedEndpointPath, endpointPath];
|
|
2433
|
+
let authorizedDetachedSource: string | undefined;
|
|
2434
|
+
if (authorizedSource && path.dirname(path.resolve(authorizedSource)) === path.dirname(path.resolve(endpointPath))) {
|
|
2435
|
+
try {
|
|
2436
|
+
if (fsSync.lstatSync(authorizedSource).size > 0) authorizedDetachedSource = authorizedSource;
|
|
2437
|
+
} catch {
|
|
2438
|
+
// The detached source was already retired; deterministic candidates remain authoritative.
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
const endpointSource =
|
|
2442
|
+
authorizedDetachedSource && fsSync.existsSync(authorizedDetachedSource)
|
|
2443
|
+
? authorizedDetachedSource
|
|
2444
|
+
: candidates.find(candidate => {
|
|
2445
|
+
try {
|
|
2446
|
+
return fsSync.lstatSync(candidate).isFile();
|
|
2447
|
+
} catch {
|
|
2448
|
+
return false;
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
2451
|
+
if (!endpointSource) return await endpointRemoved(record.locator.stateRoot, id);
|
|
2452
|
+
const plannedPath =
|
|
2453
|
+
endpointSource === endpointPath
|
|
2454
|
+
? plannedEndpointPath
|
|
2455
|
+
: endpointSource === plannedEndpointPath
|
|
2456
|
+
? retryEndpointPath
|
|
2457
|
+
: finalEndpointPath;
|
|
2458
|
+
let handle: fs.FileHandle | undefined;
|
|
2459
|
+
try {
|
|
2460
|
+
handle = await fs.open(endpointSource, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
|
|
2461
|
+
const metadata = await handle.stat({ bigint: true });
|
|
2462
|
+
if (!metadata.isFile() || metadata.nlink !== 1n || metadata.size > 4096n) return false;
|
|
2463
|
+
const bytes = Buffer.alloc(Number(metadata.size) + 1);
|
|
2464
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
2465
|
+
if (bytesRead !== Number(metadata.size)) return false;
|
|
2466
|
+
const source = bytes.subarray(0, bytesRead);
|
|
2467
|
+
const endpoint = JSON.parse(source.toString("utf8")) as { sessionId?: unknown; pid?: unknown; stale?: unknown };
|
|
2468
|
+
const indexedEndpointMtimeMs = Math.trunc(record.endpointMtimeMs);
|
|
2469
|
+
if (
|
|
2470
|
+
endpoint.sessionId !== id ||
|
|
2471
|
+
endpoint.pid !== record.pid ||
|
|
2472
|
+
endpoint.stale === true ||
|
|
2473
|
+
!Number.isSafeInteger(indexedEndpointMtimeMs) ||
|
|
2474
|
+
metadata.mtimeNs / 1_000_000n !== BigInt(indexedEndpointMtimeMs)
|
|
2475
|
+
)
|
|
2476
|
+
return false;
|
|
2477
|
+
await handle.close();
|
|
2478
|
+
handle = undefined;
|
|
2479
|
+
const removed = exactUnlinkLifecycleFile(
|
|
2480
|
+
endpointSource,
|
|
2481
|
+
{
|
|
2482
|
+
dev: metadata.dev,
|
|
2483
|
+
ino: metadata.ino,
|
|
2484
|
+
nlink: metadata.nlink,
|
|
2485
|
+
size: metadata.size,
|
|
2486
|
+
mtimeNs: metadata.mtimeNs,
|
|
2487
|
+
sha256: createHash("sha256").update(source).digest("hex"),
|
|
2488
|
+
},
|
|
2489
|
+
plannedPath,
|
|
2490
|
+
(() => {
|
|
2491
|
+
const parent = lifecycleParentIdentity(path.dirname(endpointSource));
|
|
2492
|
+
return parent ? { dev: BigInt(parent.dev), ino: BigInt(parent.ino) } : undefined;
|
|
2493
|
+
})(),
|
|
2494
|
+
);
|
|
2495
|
+
if (removed.ok || removed.code === "not_found") return true;
|
|
2496
|
+
if (removed.code === "cleanup_pending" && removed.retainedUnknownPath) return false;
|
|
2497
|
+
if (removed.code === "cleanup_pending" && removed.retainedPlaceholderPath) {
|
|
2498
|
+
if (removed.payloadDurable !== true) return false;
|
|
2499
|
+
durablePlaceholders?.add(path.resolve(removed.retainedPlaceholderPath));
|
|
2500
|
+
for (const candidate of [endpointSource, plannedEndpointPath, retryEndpointPath, finalEndpointPath]) {
|
|
2501
|
+
try {
|
|
2502
|
+
if (fsSync.lstatSync(candidate).size === 0) durablePlaceholders?.add(path.resolve(candidate));
|
|
2503
|
+
} catch {
|
|
2504
|
+
// Absent aliases carry no retained authority.
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
if (removed.code === "cleanup_pending" && attempt < 4)
|
|
2509
|
+
return await removeExactDeadSessionEndpoint(
|
|
2510
|
+
broker,
|
|
2511
|
+
id,
|
|
2512
|
+
record,
|
|
2513
|
+
attempt + 1,
|
|
2514
|
+
removed.detachedPath,
|
|
2515
|
+
durablePlaceholders,
|
|
2516
|
+
);
|
|
2517
|
+
return false;
|
|
2518
|
+
} catch {
|
|
2519
|
+
return false;
|
|
2520
|
+
} finally {
|
|
2521
|
+
await handle?.close();
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
async function hasOwnedEndpointPayload(
|
|
2526
|
+
root: string,
|
|
2527
|
+
id: string,
|
|
2528
|
+
effectMarker: string,
|
|
2529
|
+
durablePlaceholders: ReadonlySet<string>,
|
|
2530
|
+
): Promise<boolean> {
|
|
2531
|
+
const directory = path.join(root, "sdk");
|
|
2532
|
+
const endpointName = `${id}.json`;
|
|
2533
|
+
let names: string[];
|
|
2534
|
+
try {
|
|
2535
|
+
names = await fs.readdir(directory);
|
|
2536
|
+
} catch (error) {
|
|
2537
|
+
return (error as NodeJS.ErrnoException).code !== "ENOENT";
|
|
2538
|
+
}
|
|
2539
|
+
for (const name of names) {
|
|
2540
|
+
if (!name.startsWith(".gjc-delete-endpoint-") || !name.includes(effectMarker) || !name.includes(endpointName))
|
|
2541
|
+
continue;
|
|
2542
|
+
try {
|
|
2543
|
+
const candidate = path.join(directory, name);
|
|
2544
|
+
const metadata = await fs.lstat(candidate);
|
|
2545
|
+
if (
|
|
2546
|
+
!metadata.isFile() ||
|
|
2547
|
+
metadata.size > 0 ||
|
|
2548
|
+
(metadata.size === 0 && !durablePlaceholders.has(path.resolve(candidate)))
|
|
2549
|
+
)
|
|
2550
|
+
return true;
|
|
2551
|
+
} catch (error) {
|
|
2552
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return true;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
return false;
|
|
2556
|
+
}
|
|
2287
2557
|
async function waitForClose(
|
|
2288
2558
|
broker: Broker,
|
|
2289
2559
|
id: string,
|
|
2290
|
-
record:
|
|
2560
|
+
record: CloseRecord,
|
|
2291
2561
|
timeoutMs: number,
|
|
2562
|
+
processExitObserver?: StableProcessExitObserver,
|
|
2292
2563
|
): Promise<boolean> {
|
|
2293
2564
|
const timing = lifecycleTiming(broker);
|
|
2294
2565
|
const deadline = timing.now() + timeoutMs;
|
|
2566
|
+
const durablePlaceholders = new Set<string>();
|
|
2295
2567
|
while (timing.now() < deadline) {
|
|
2296
2568
|
await broker.index.refresh();
|
|
2297
2569
|
const registration = broker.index.findHostRegistration(
|
|
@@ -2304,7 +2576,14 @@ async function waitForClose(
|
|
|
2304
2576
|
registration &&
|
|
2305
2577
|
broker.index.hostUnregisteredAfter(registration) &&
|
|
2306
2578
|
(await endpointRemoved(record.locator.stateRoot, id)) &&
|
|
2307
|
-
|
|
2579
|
+
observedProcessExited(record.pid, record.processIncarnation, processExitObserver) &&
|
|
2580
|
+
(!record.lifecycleRequestId ||
|
|
2581
|
+
!(await hasOwnedEndpointPayload(
|
|
2582
|
+
record.locator.stateRoot,
|
|
2583
|
+
id,
|
|
2584
|
+
record.lifecycleRequestId,
|
|
2585
|
+
durablePlaceholders,
|
|
2586
|
+
)))
|
|
2308
2587
|
)
|
|
2309
2588
|
return true;
|
|
2310
2589
|
// A host that dies before it can withdraw its own registration — killed
|
|
@@ -2313,17 +2592,36 @@ async function waitForClose(
|
|
|
2313
2592
|
// That is exactly the evidence the dead-registration sweep retires records
|
|
2314
2593
|
// on, so retire it here too instead of escalating signals at a pid that no
|
|
2315
2594
|
// longer exists and then reporting the teardown as unfinished.
|
|
2316
|
-
if (registration &&
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2595
|
+
if (registration && observedProcessExited(record.pid, record.processIncarnation, processExitObserver)) {
|
|
2596
|
+
const expected =
|
|
2597
|
+
typeof record.lifecycleRequestId === "string" && typeof record.processIncarnation === "string"
|
|
2598
|
+
? {
|
|
2599
|
+
pid: record.pid,
|
|
2600
|
+
effectMarker: record.lifecycleRequestId,
|
|
2601
|
+
incarnation: record.processIncarnation,
|
|
2602
|
+
}
|
|
2603
|
+
: undefined;
|
|
2604
|
+
let retainedUnknown = false;
|
|
2605
|
+
if (expected)
|
|
2606
|
+
await removeOwnedLifecycleArtifacts(
|
|
2607
|
+
record.locator.stateRoot,
|
|
2608
|
+
id,
|
|
2609
|
+
expected,
|
|
2610
|
+
() => {
|
|
2611
|
+
retainedUnknown = true;
|
|
2612
|
+
},
|
|
2613
|
+
file => durablePlaceholders.add(path.resolve(file)),
|
|
2614
|
+
);
|
|
2615
|
+
if (retainedUnknown) return false;
|
|
2616
|
+
if (!(await removeExactDeadSessionEndpoint(broker, id, record, 0, undefined, durablePlaceholders)))
|
|
2617
|
+
return false;
|
|
2618
|
+
if (
|
|
2619
|
+
expected &&
|
|
2620
|
+
(await hasOwnedEndpointPayload(record.locator.stateRoot, id, expected.effectMarker, durablePlaceholders))
|
|
2621
|
+
)
|
|
2622
|
+
return false;
|
|
2623
|
+
if (!(await broker.index.unregisterIfCurrent(registration))) return false;
|
|
2624
|
+
return await endpointRemoved(record.locator.stateRoot, id);
|
|
2327
2625
|
}
|
|
2328
2626
|
await timing.sleep(POLL_MS);
|
|
2329
2627
|
}
|
|
@@ -2349,6 +2647,10 @@ async function currentReadyAuthority(
|
|
|
2349
2647
|
token?: unknown;
|
|
2350
2648
|
pid?: unknown;
|
|
2351
2649
|
};
|
|
2650
|
+
// A new lifecycle registration has no heartbeat until the broker checkpoints it.
|
|
2651
|
+
// Publish that positive liveness evidence through the broker-owned checkpoint before
|
|
2652
|
+
// enforcing ready authority; endpoint, marker, incarnation, and generation checks below remain unchanged.
|
|
2653
|
+
await broker.heartbeatSessions();
|
|
2352
2654
|
await broker.index.refresh();
|
|
2353
2655
|
const record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
2354
2656
|
if (
|
|
@@ -2984,6 +3286,7 @@ type CloseRecord = {
|
|
|
2984
3286
|
pid: number;
|
|
2985
3287
|
endpointMtimeMs?: number;
|
|
2986
3288
|
lifecycleRequestId?: string;
|
|
3289
|
+
processIncarnation?: string;
|
|
2987
3290
|
};
|
|
2988
3291
|
|
|
2989
3292
|
function endpointIncarnation(record: CloseRecord, sessionId: string): string | undefined {
|
|
@@ -3033,10 +3336,12 @@ function sameCloseAuthority(authority: CloseAuthority, record: CloseRecord, sess
|
|
|
3033
3336
|
);
|
|
3034
3337
|
}
|
|
3035
3338
|
|
|
3036
|
-
function
|
|
3339
|
+
function sameCloseStoredProcessIdentity(expected: CloseRecord, current: CloseRecord): boolean {
|
|
3037
3340
|
return (
|
|
3038
|
-
current.live &&
|
|
3039
3341
|
current.pid === expected.pid &&
|
|
3342
|
+
typeof expected.processIncarnation === "string" &&
|
|
3343
|
+
expected.processIncarnation.length > 0 &&
|
|
3344
|
+
current.processIncarnation === expected.processIncarnation &&
|
|
3040
3345
|
typeof expected.lifecycleRequestId === "string" &&
|
|
3041
3346
|
expected.lifecycleRequestId.length > 0 &&
|
|
3042
3347
|
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
@@ -3045,23 +3350,58 @@ function sameCloseProcessIdentity(expected: CloseRecord, current: CloseRecord &
|
|
|
3045
3350
|
);
|
|
3046
3351
|
}
|
|
3047
3352
|
|
|
3353
|
+
/**
|
|
3354
|
+
* Endpoint control remains safe without a durable signal identity: the caller has
|
|
3355
|
+
* just read and re-read this exact generation before connecting. This narrower
|
|
3356
|
+
* identity is never sufficient for signal fallback, but permits the host's typed
|
|
3357
|
+
* close response to remain observable instead of being masked as stale.
|
|
3358
|
+
*/
|
|
3359
|
+
function sameCloseEndpointIdentity(expected: CloseRecord, current: CloseRecord): boolean {
|
|
3360
|
+
return (
|
|
3361
|
+
current.endpointGeneration === expected.endpointGeneration &&
|
|
3362
|
+
current.pid === expected.pid &&
|
|
3363
|
+
current.endpointMtimeMs !== undefined &&
|
|
3364
|
+
expected.endpointMtimeMs !== undefined &&
|
|
3365
|
+
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
3366
|
+
path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
|
|
3367
|
+
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
function sameCloseProcessIdentity(expected: CloseRecord, current: CloseRecord & { live: boolean }): boolean {
|
|
3372
|
+
return current.live && sameCloseStoredProcessIdentity(expected, current);
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
function sameCloseGeneration(expected: CloseRecord, current: CloseRecord & { live: boolean }): boolean {
|
|
3376
|
+
return (
|
|
3377
|
+
current.live &&
|
|
3378
|
+
current.endpointGeneration === expected.endpointGeneration &&
|
|
3379
|
+
current.pid === expected.pid &&
|
|
3380
|
+
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
3381
|
+
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
3382
|
+
current.processIncarnation === expected.processIncarnation &&
|
|
3383
|
+
path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
|
|
3384
|
+
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
3385
|
+
);
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3048
3388
|
async function revalidateCloseGeneration(
|
|
3049
3389
|
broker: Broker,
|
|
3050
3390
|
id: string,
|
|
3051
3391
|
expected: CloseRecord,
|
|
3052
3392
|
authority: CloseAuthority | undefined,
|
|
3053
|
-
allowUnreachable = false,
|
|
3054
3393
|
): Promise<BrokerResponse | undefined> {
|
|
3055
3394
|
await broker.index.refresh();
|
|
3056
3395
|
const current = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
current
|
|
3060
|
-
current
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3396
|
+
if (
|
|
3397
|
+
!authority &&
|
|
3398
|
+
current &&
|
|
3399
|
+
(sameCloseEndpointIdentity(expected, current) || sameCloseStoredProcessIdentity(expected, current))
|
|
3400
|
+
) {
|
|
3401
|
+
expected.endpointMtimeMs = current.endpointMtimeMs;
|
|
3402
|
+
return undefined;
|
|
3403
|
+
}
|
|
3404
|
+
return authority && current && sameCloseGeneration(expected, current) && sameCloseAuthority(authority, current, id)
|
|
3065
3405
|
? undefined
|
|
3066
3406
|
: fail("endpoint_stale", "session endpoint is stale");
|
|
3067
3407
|
}
|
|
@@ -3069,9 +3409,7 @@ async function revalidateCloseGeneration(
|
|
|
3069
3409
|
function isTransportFailure(error: unknown): error is SdkClientError {
|
|
3070
3410
|
return (
|
|
3071
3411
|
error instanceof SdkClientError &&
|
|
3072
|
-
["unavailable", "timeout", "connection_closed", "
|
|
3073
|
-
error.code,
|
|
3074
|
-
)
|
|
3412
|
+
["unavailable", "timeout", "connection_closed", "reconnect_exhausted"].includes(error.code)
|
|
3075
3413
|
);
|
|
3076
3414
|
}
|
|
3077
3415
|
|
|
@@ -3105,11 +3443,14 @@ async function executeLifecycleResponse(
|
|
|
3105
3443
|
return fail("invalid_input", "sourceSessionId must be a canonical safe identifier.");
|
|
3106
3444
|
if (operation === "session.create" || operation === "session.fork" || operation === "session.resume") {
|
|
3107
3445
|
await broker.index.refresh();
|
|
3446
|
+
await broker.heartbeatSessions();
|
|
3108
3447
|
if (operation === "session.resume") {
|
|
3109
3448
|
const requestedSessionId = sessionId(input);
|
|
3110
3449
|
const existing = requestedSessionId
|
|
3111
3450
|
? broker.index.listSessions().sessions.find(session => session.sessionId === requestedSessionId)
|
|
3112
3451
|
: undefined;
|
|
3452
|
+
if (existing && !isSessionAuthorityEligible(existing))
|
|
3453
|
+
return fail("endpoint_stale", "Session authority is ambiguous and cannot be resumed safely.");
|
|
3113
3454
|
if (existing?.live) {
|
|
3114
3455
|
const initialScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
|
|
3115
3456
|
if ("ok" in initialScope) return initialScope;
|
|
@@ -3136,12 +3477,16 @@ async function executeLifecycleResponse(
|
|
|
3136
3477
|
if ("ok" in finalScope) return finalScope;
|
|
3137
3478
|
if (!sameResumeSessionIdentity(initialScope, finalScope))
|
|
3138
3479
|
return fail("endpoint_stale", "Saved session changed while its resume authority was being verified.");
|
|
3480
|
+
if (current.endpointMtimeMs === undefined)
|
|
3481
|
+
return fail("endpoint_stale", "Live session endpoint authority is incomplete.");
|
|
3139
3482
|
return {
|
|
3140
3483
|
ok: true,
|
|
3141
3484
|
result: {
|
|
3142
3485
|
sessionId: requestedSessionId,
|
|
3143
3486
|
cwd: finalScope.cwd,
|
|
3144
3487
|
endpointGeneration: current.endpointGeneration,
|
|
3488
|
+
pid: current.pid,
|
|
3489
|
+
endpointMtimeMs: current.endpointMtimeMs,
|
|
3145
3490
|
endpoint: endpoint.result,
|
|
3146
3491
|
reused: true,
|
|
3147
3492
|
},
|
|
@@ -3433,6 +3778,9 @@ async function executeLifecycleResponse(
|
|
|
3433
3778
|
result: {
|
|
3434
3779
|
sessionId: launch.id,
|
|
3435
3780
|
cwd: launch.cwd,
|
|
3781
|
+
endpointGeneration: verified.endpointGeneration,
|
|
3782
|
+
pid: verified.endpoint.pid,
|
|
3783
|
+
endpointMtimeMs: verified.endpointMtimeMs,
|
|
3436
3784
|
endpoint: verified.endpoint,
|
|
3437
3785
|
...(launch.readiness === "deferred" ? { readiness: "prepared" as const } : {}),
|
|
3438
3786
|
...(worktreeReceipt ? { worktree: worktreeReceipt } : {}),
|
|
@@ -3447,18 +3795,30 @@ async function executeLifecycleResponse(
|
|
|
3447
3795
|
let record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
3448
3796
|
if (operation === "session.close") {
|
|
3449
3797
|
if (!record) return fail("not_found", "session is not indexed");
|
|
3450
|
-
// DR-1 retains closed/unregistered rows for inspect and offline tail.
|
|
3451
|
-
// Pre-DR-1 they vanished from the listing and close answered not_found;
|
|
3452
|
-
// a terminal row is the same already-gone outcome, never an escalation
|
|
3453
|
-
// target — signalling its recorded pid could hit a reused process.
|
|
3454
|
-
if (record.terminal) return fail("resource_gone", "Session is already closed and unregistered.");
|
|
3455
3798
|
if (record.terminalUncertain)
|
|
3456
3799
|
return fail("terminal_uncertain", "Session ownership is uncertain and cannot be closed safely.");
|
|
3800
|
+
if (!isSessionAuthorityEligible(record))
|
|
3801
|
+
return fail("endpoint_stale", "Session authority is ambiguous and cannot be closed safely.");
|
|
3802
|
+
if (!record.live && !record.terminal) {
|
|
3803
|
+
await broker.heartbeatSessions();
|
|
3804
|
+
await broker.index.refresh();
|
|
3805
|
+
record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
3806
|
+
if (!record) return fail("not_found", "session is not indexed");
|
|
3807
|
+
if (record.terminalUncertain)
|
|
3808
|
+
return fail("terminal_uncertain", "Session ownership is uncertain and cannot be closed safely.");
|
|
3809
|
+
if (!isSessionAuthorityEligible(record))
|
|
3810
|
+
return fail("endpoint_stale", "Session authority is ambiguous and cannot be closed safely.");
|
|
3811
|
+
}
|
|
3457
3812
|
const requestedAuthority = requestedCloseAuthority(input);
|
|
3458
3813
|
if ("error" in requestedAuthority) return requestedAuthority.error;
|
|
3459
3814
|
if (requestedAuthority.authority && !sameCloseAuthority(requestedAuthority.authority, record, id))
|
|
3460
3815
|
return fail("endpoint_stale", "session endpoint is stale");
|
|
3461
3816
|
await broker.ledger.transition(identity, "effect_started", { intendedSessionId: id, effectMarker: randomUUID() });
|
|
3817
|
+
const signalAuthority: EffectMarker | undefined =
|
|
3818
|
+
typeof record.lifecycleRequestId === "string" && typeof record.processIncarnation === "string"
|
|
3819
|
+
? { pid: record.pid, effectMarker: record.lifecycleRequestId, incarnation: record.processIncarnation }
|
|
3820
|
+
: undefined;
|
|
3821
|
+
let processExitObserver = stableProcessExitObserver(record);
|
|
3462
3822
|
|
|
3463
3823
|
let usedSignalFallback = false;
|
|
3464
3824
|
let note: string | undefined;
|
|
@@ -3468,6 +3828,7 @@ async function executeLifecycleResponse(
|
|
|
3468
3828
|
});
|
|
3469
3829
|
if (!endpointResult.ok && endpointResult.error.code === "endpoint_stale" && !requestedAuthority.authority) {
|
|
3470
3830
|
await broker.index.refresh();
|
|
3831
|
+
await broker.heartbeatSessions();
|
|
3471
3832
|
const refreshed = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
3472
3833
|
if (refreshed && sameCloseProcessIdentity(record, refreshed)) {
|
|
3473
3834
|
record = refreshed;
|
|
@@ -3478,13 +3839,9 @@ async function executeLifecycleResponse(
|
|
|
3478
3839
|
}
|
|
3479
3840
|
}
|
|
3480
3841
|
if (!endpointResult.ok) {
|
|
3481
|
-
if (endpointResult.error.code === "endpoint_stale")
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
} else {
|
|
3485
|
-
if (endpointResult.error.code !== "resource_gone") return endpointResult;
|
|
3486
|
-
usedSignalFallback = true;
|
|
3487
|
-
}
|
|
3842
|
+
if (endpointResult.error.code === "endpoint_stale") return endpointResult;
|
|
3843
|
+
if (endpointResult.error.code !== "resource_gone") return endpointResult;
|
|
3844
|
+
usedSignalFallback = true;
|
|
3488
3845
|
} else {
|
|
3489
3846
|
const endpoint = closeEndpoint(endpointResult.result);
|
|
3490
3847
|
if (!endpoint) return fail("close_refused", "Session endpoint is malformed.");
|
|
@@ -3494,11 +3851,35 @@ async function executeLifecycleResponse(
|
|
|
3494
3851
|
timeoutMs: 2_000,
|
|
3495
3852
|
reconnectAttempts: 0,
|
|
3496
3853
|
});
|
|
3497
|
-
|
|
3854
|
+
let refreshedEndpointResult = await broker.handleRequest("session.get_endpoint", {
|
|
3498
3855
|
sessionId: id,
|
|
3499
3856
|
endpointGeneration: record.endpointGeneration,
|
|
3500
3857
|
});
|
|
3858
|
+
if (
|
|
3859
|
+
!refreshedEndpointResult.ok &&
|
|
3860
|
+
refreshedEndpointResult.error.code === "endpoint_stale" &&
|
|
3861
|
+
!requestedAuthority.authority
|
|
3862
|
+
) {
|
|
3863
|
+
await broker.index.refresh();
|
|
3864
|
+
const heartbeatRecord = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
3865
|
+
if (
|
|
3866
|
+
heartbeatRecord &&
|
|
3867
|
+
heartbeatRecord.endpointGeneration === record.endpointGeneration &&
|
|
3868
|
+
sameCloseProcessIdentity(record, heartbeatRecord)
|
|
3869
|
+
) {
|
|
3870
|
+
record = heartbeatRecord;
|
|
3871
|
+
refreshedEndpointResult = await broker.handleRequest("session.get_endpoint", {
|
|
3872
|
+
sessionId: id,
|
|
3873
|
+
endpointGeneration: record.endpointGeneration,
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3501
3877
|
if (!refreshedEndpointResult.ok) return refreshedEndpointResult;
|
|
3878
|
+
await broker.index.refresh();
|
|
3879
|
+
const refreshedRecord = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
3880
|
+
if (!refreshedRecord || !sameCloseEndpointIdentity(record, refreshedRecord))
|
|
3881
|
+
return fail("endpoint_stale", "session endpoint is stale");
|
|
3882
|
+
record = refreshedRecord;
|
|
3502
3883
|
const refreshedEndpoint = closeEndpoint(refreshedEndpointResult.result);
|
|
3503
3884
|
if (
|
|
3504
3885
|
!refreshedEndpoint ||
|
|
@@ -3508,11 +3889,23 @@ async function executeLifecycleResponse(
|
|
|
3508
3889
|
return fail("endpoint_stale", "session endpoint is stale");
|
|
3509
3890
|
const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
|
|
3510
3891
|
if (stale) return stale;
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3892
|
+
if (record.lifecycleRequestId === undefined) {
|
|
3893
|
+
usedSignalFallback = true;
|
|
3894
|
+
} else {
|
|
3895
|
+
processExitObserver ??= stableProcessExitObserver(record);
|
|
3896
|
+
const response = await client.control("session.close", {
|
|
3897
|
+
[BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD]: record.lifecycleRequestId,
|
|
3898
|
+
});
|
|
3899
|
+
if ((response as { ok?: unknown }).ok !== true) {
|
|
3900
|
+
const closeError = (response as { error?: { code?: unknown } }).error;
|
|
3901
|
+
if (closeError?.code === "operation_prohibited") usedSignalFallback = true;
|
|
3902
|
+
else return fail("close_refused", "Session endpoint rejected session.close.");
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3514
3905
|
} catch (error) {
|
|
3515
3906
|
if (isTransportFailure(error)) usedSignalFallback = true;
|
|
3907
|
+
else if (error instanceof SdkClientError && error.code === "operation_prohibited")
|
|
3908
|
+
usedSignalFallback = true;
|
|
3516
3909
|
else if (error instanceof SdkClientError) return fail(error.code, error.message);
|
|
3517
3910
|
else
|
|
3518
3911
|
return fail(
|
|
@@ -3520,26 +3913,40 @@ async function executeLifecycleResponse(
|
|
|
3520
3913
|
`Session endpoint close failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3521
3914
|
);
|
|
3522
3915
|
} finally {
|
|
3523
|
-
await client
|
|
3916
|
+
await client
|
|
3917
|
+
?.close()
|
|
3918
|
+
.catch(error =>
|
|
3919
|
+
logger.warn(
|
|
3920
|
+
`SDK session-close client cleanup failed after control dispatch: ${error instanceof Error ? error.message : String(error)}`,
|
|
3921
|
+
),
|
|
3922
|
+
);
|
|
3524
3923
|
}
|
|
3525
3924
|
}
|
|
3526
3925
|
|
|
3527
3926
|
if (usedSignalFallback) {
|
|
3528
|
-
const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority
|
|
3927
|
+
const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
|
|
3529
3928
|
if (stale) return stale;
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3929
|
+
processExitObserver ??= stableProcessExitObserver(record);
|
|
3930
|
+
const exited =
|
|
3931
|
+
typeof record.processIncarnation === "string" &&
|
|
3932
|
+
observeProcess(record.pid, record.processIncarnation, value =>
|
|
3933
|
+
processIncarnationForBroker(broker, value),
|
|
3934
|
+
) === "exited";
|
|
3935
|
+
if (!exited) {
|
|
3936
|
+
if (!(await signalVerifiedSession(record, id, "SIGTERM", signalAuthority)))
|
|
3937
|
+
return fail(
|
|
3938
|
+
"close_refused",
|
|
3939
|
+
"Session endpoint is unavailable and its durable process identity could not be verified.",
|
|
3940
|
+
);
|
|
3941
|
+
note = "Endpoint close was unreachable; sent SIGTERM to the durably identified session process.";
|
|
3942
|
+
}
|
|
3536
3943
|
}
|
|
3537
3944
|
|
|
3538
|
-
let closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS);
|
|
3945
|
+
let closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS, processExitObserver);
|
|
3539
3946
|
if (!closed && !usedSignalFallback) {
|
|
3540
3947
|
const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
|
|
3541
3948
|
if (stale) return stale;
|
|
3542
|
-
if (!(await signalVerifiedSession(record, id, "SIGTERM"))) {
|
|
3949
|
+
if (!(await signalVerifiedSession(record, id, "SIGTERM", signalAuthority))) {
|
|
3543
3950
|
await recordTerminalUncertain(broker, id, record.locator.stateRoot, record.pid);
|
|
3544
3951
|
return fail(
|
|
3545
3952
|
"terminal_uncertain",
|
|
@@ -3548,12 +3955,12 @@ async function executeLifecycleResponse(
|
|
|
3548
3955
|
}
|
|
3549
3956
|
note =
|
|
3550
3957
|
"Session acknowledged session.close but graceful teardown did not complete within the bounded deadline; sent SIGTERM to the durably identified session process.";
|
|
3551
|
-
closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS);
|
|
3958
|
+
closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS, processExitObserver);
|
|
3552
3959
|
}
|
|
3553
3960
|
if (!closed) {
|
|
3554
3961
|
const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
|
|
3555
3962
|
if (stale) return stale;
|
|
3556
|
-
if (!(await signalVerifiedSession(record, id, "SIGKILL"))) {
|
|
3963
|
+
if (!(await signalVerifiedSession(record, id, "SIGKILL", signalAuthority))) {
|
|
3557
3964
|
await recordTerminalUncertain(broker, id, record.locator.stateRoot, record.pid);
|
|
3558
3965
|
return fail(
|
|
3559
3966
|
"terminal_uncertain",
|
|
@@ -3562,7 +3969,7 @@ async function executeLifecycleResponse(
|
|
|
3562
3969
|
}
|
|
3563
3970
|
note =
|
|
3564
3971
|
"Session teardown did not complete after SIGTERM within the bounded deadline; sent SIGKILL to the durably identified session process.";
|
|
3565
|
-
closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS);
|
|
3972
|
+
closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS, processExitObserver);
|
|
3566
3973
|
}
|
|
3567
3974
|
if (!closed) {
|
|
3568
3975
|
await recordTerminalUncertain(broker, id, record.locator.stateRoot, record.pid);
|
|
@@ -3577,6 +3984,8 @@ async function executeLifecycleResponse(
|
|
|
3577
3984
|
if (operation === "session.delete") {
|
|
3578
3985
|
if (record?.terminalUncertain)
|
|
3579
3986
|
return fail("terminal_uncertain", "Session ownership is uncertain and cannot be deleted safely.");
|
|
3987
|
+
if (record && !isSessionAuthorityEligible(record))
|
|
3988
|
+
return fail("endpoint_stale", "Session authority is ambiguous and cannot be deleted safely.");
|
|
3580
3989
|
if (record?.live) return fail("live_session", "Refusing to delete a live session; close it first.");
|
|
3581
3990
|
if (cleanup === undefined) {
|
|
3582
3991
|
const requestedTranscriptPath = text(input.sessionPath);
|
|
@@ -4191,7 +4600,7 @@ async function executeLifecycleResponse(
|
|
|
4191
4600
|
}
|
|
4192
4601
|
if (record)
|
|
4193
4602
|
await broker.index.append({
|
|
4194
|
-
type: "
|
|
4603
|
+
type: "session_deleted",
|
|
4195
4604
|
sessionId: id,
|
|
4196
4605
|
locator: record.locator,
|
|
4197
4606
|
endpointGeneration: record.endpointGeneration,
|
|
@@ -4262,23 +4671,6 @@ async function exactCleanupProof(
|
|
|
4262
4671
|
: undefined;
|
|
4263
4672
|
}
|
|
4264
4673
|
|
|
4265
|
-
async function waitForExactCleanupProof(
|
|
4266
|
-
broker: Broker,
|
|
4267
|
-
root: string | undefined,
|
|
4268
|
-
id: string | undefined,
|
|
4269
|
-
expected: EffectMarker | undefined,
|
|
4270
|
-
evidence: { artifact: LifecycleFailureArtifact } | undefined,
|
|
4271
|
-
): Promise<LifecycleCleanupProof | undefined> {
|
|
4272
|
-
const timing = lifecycleTiming(broker);
|
|
4273
|
-
const deadline = timing.now() + STARTUP_CLEANUP_PROOF_GRACE_MS;
|
|
4274
|
-
for (;;) {
|
|
4275
|
-
const proof = await exactCleanupProof(broker, root, id, expected, evidence);
|
|
4276
|
-
if (proof) return proof;
|
|
4277
|
-
const remaining = deadline - timing.now();
|
|
4278
|
-
if (remaining <= 0) return undefined;
|
|
4279
|
-
await timing.sleep(Math.min(POLL_MS, remaining));
|
|
4280
|
-
}
|
|
4281
|
-
}
|
|
4282
4674
|
function validateLifecycleDeleteMetadataBinding(
|
|
4283
4675
|
broker: Broker,
|
|
4284
4676
|
operation: string,
|
|
@@ -4392,7 +4784,7 @@ export async function executeLifecycle(
|
|
|
4392
4784
|
expected,
|
|
4393
4785
|
)
|
|
4394
4786
|
: undefined;
|
|
4395
|
-
const cleanupProof = await
|
|
4787
|
+
const cleanupProof = await exactCleanupProof(broker, root, entry?.intendedSessionId, expected, evidence);
|
|
4396
4788
|
const startupFailure: LifecycleStartupFailureReceipt | undefined = evidence
|
|
4397
4789
|
? {
|
|
4398
4790
|
artifactDigest: evidence.digest,
|
|
@@ -4592,15 +4984,23 @@ export function sessionHostWorkInFlight(): boolean {
|
|
|
4592
4984
|
* How often a live broker re-checks its own session registrations against OS
|
|
4593
4985
|
* process liveness.
|
|
4594
4986
|
*
|
|
4595
|
-
* This sweep can never disturb healthy work: a registration is only
|
|
4596
|
-
*
|
|
4597
|
-
*
|
|
4598
|
-
*
|
|
4599
|
-
* a corpse for longer than a single poll while costing one index
|
|
4600
|
-
* minute on an otherwise idle broker.
|
|
4987
|
+
* This sweep can never disturb healthy work: a registration is dropped only when
|
|
4988
|
+
* `observeProcess` proves its exact published process identity exited. A live
|
|
4989
|
+
* replacement at the same pid retires the stale registration without being
|
|
4990
|
+
* signaled. One minute keeps `gjc_sessions`/`session.get_endpoint` from
|
|
4991
|
+
* advertising a corpse for longer than a single poll while costing one index
|
|
4992
|
+
* refresh per minute on an otherwise idle broker.
|
|
4601
4993
|
*/
|
|
4602
4994
|
export const BROKER_DEAD_REGISTRATION_SWEEP_MS = 60_000;
|
|
4603
4995
|
|
|
4996
|
+
/**
|
|
4997
|
+
* Registrations reaped per sweep. Every reap is its own locked index transaction,
|
|
4998
|
+
* so an uncapped sweep over a long-lived index turns one broker into a continuous
|
|
4999
|
+
* holder of the shared session-index lock and starves unrelated `gjc` launches out
|
|
5000
|
+
* of their bounded retry budget. Surplus dead rows are reaped by later sweeps.
|
|
5001
|
+
*/
|
|
5002
|
+
export const BROKER_DEAD_REGISTRATION_SWEEP_LIMIT = 64;
|
|
5003
|
+
|
|
4604
5004
|
/** One reaped registration, as recorded by {@link reapDeadSessionRegistrations}. */
|
|
4605
5005
|
export interface ReapedSessionRegistration {
|
|
4606
5006
|
sessionId: string;
|
|
@@ -4617,35 +5017,29 @@ export interface ReapedSessionRegistration {
|
|
|
4617
5017
|
* missing heartbeat merely makes a session read as not-live — the host may
|
|
4618
5018
|
* still be running ahead of the next heartbeat checkpoint pass, so it is never
|
|
4619
5019
|
* grounds for a reap. EPERM and unreadable incarnations stay "uncertain", so an
|
|
4620
|
-
* alien or unreadable process is never mistaken for a dead one.
|
|
4621
|
-
*
|
|
4622
|
-
*
|
|
4623
|
-
* silently dropping them would turn a fail-closed `session.close`/
|
|
4624
|
-
* `session.delete` refusal into `not_found`.
|
|
5020
|
+
* alien or unreadable process is never mistaken for a dead one. Terminal and
|
|
5021
|
+
* terminal-uncertain identities are retained. Identity-level rows let the sweep
|
|
5022
|
+
* retire a dead losing root without disturbing the surviving authority.
|
|
4625
5023
|
*/
|
|
4626
5024
|
export async function reapDeadSessionRegistrations(
|
|
4627
|
-
broker:
|
|
5025
|
+
broker: BrokerIndex,
|
|
5026
|
+
limit = BROKER_DEAD_REGISTRATION_SWEEP_LIMIT,
|
|
4628
5027
|
): Promise<ReapedSessionRegistration[]> {
|
|
4629
5028
|
await broker.index.refresh();
|
|
4630
5029
|
const dead = broker.index
|
|
4631
|
-
.
|
|
4632
|
-
.
|
|
4633
|
-
session
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
observeProcess(session.pid,
|
|
4637
|
-
|
|
5030
|
+
.listSessionIdentities()
|
|
5031
|
+
.filter(session => {
|
|
5032
|
+
if (session.terminal || session.terminalUncertain) return false;
|
|
5033
|
+
const recordedIncarnation = session.hostIncarnation ?? session.processIncarnation;
|
|
5034
|
+
return (
|
|
5035
|
+
observeProcess(session.pid, recordedIncarnation, pid => processIncarnationForBroker(broker, pid)) ===
|
|
5036
|
+
"exited"
|
|
5037
|
+
);
|
|
5038
|
+
})
|
|
5039
|
+
.slice(0, Math.max(0, limit));
|
|
4638
5040
|
const reaped: ReapedSessionRegistration[] = [];
|
|
4639
5041
|
for (const session of dead) {
|
|
4640
|
-
await broker.index.
|
|
4641
|
-
type: "host_unregistered",
|
|
4642
|
-
sessionId: session.sessionId,
|
|
4643
|
-
locator: session.locator,
|
|
4644
|
-
endpointGeneration: session.endpointGeneration,
|
|
4645
|
-
pid: session.pid,
|
|
4646
|
-
...(session.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: session.endpointMtimeMs }),
|
|
4647
|
-
...(session.lifecycleRequestId ? { lifecycleRequestId: session.lifecycleRequestId } : {}),
|
|
4648
|
-
});
|
|
5042
|
+
if (!(await broker.index.unregisterIfCurrent(session))) continue;
|
|
4649
5043
|
const record = {
|
|
4650
5044
|
sessionId: session.sessionId,
|
|
4651
5045
|
pid: session.pid,
|