@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
4
5
|
import { withFileLock } from "../../config/file-lock";
|
|
5
6
|
import { processIncarnation } from "./process-incarnation";
|
|
6
7
|
import {
|
|
@@ -83,11 +84,16 @@ export interface IndexedSession {
|
|
|
83
84
|
activity?: SessionActivity;
|
|
84
85
|
/** Wall-clock timestamp of the latest admitted heartbeat, when one exists. */
|
|
85
86
|
lastHeartbeatAt?: number;
|
|
86
|
-
/** True when
|
|
87
|
+
/** True when more than one unresolved authority-fencing state-root identity claims this session id. */
|
|
87
88
|
ambiguous: boolean;
|
|
88
|
-
/** True when the identity's latest event is
|
|
89
|
+
/** True when the identity's latest event is terminal (DR-1 retains stopped rows for inspection/offline tail). */
|
|
89
90
|
terminal: boolean;
|
|
90
91
|
}
|
|
92
|
+
|
|
93
|
+
/** A session can grant endpoint or lifecycle authority only when one authority-fencing state root claims its id. */
|
|
94
|
+
export function isSessionAuthorityEligible(session: Pick<IndexedSession, "ambiguous">): boolean {
|
|
95
|
+
return session.ambiguous !== true;
|
|
96
|
+
}
|
|
91
97
|
export interface SessionList {
|
|
92
98
|
indexSeq: number;
|
|
93
99
|
sessions: IndexedSession[];
|
|
@@ -272,101 +278,324 @@ function auditRecords(events: SessionIndexEvent[], ts: number): SessionIndexAudi
|
|
|
272
278
|
}));
|
|
273
279
|
}
|
|
274
280
|
|
|
281
|
+
interface SessionIdentityState {
|
|
282
|
+
identity: string;
|
|
283
|
+
latest: SessionIndexEvent;
|
|
284
|
+
heartbeat: SessionIndexEvent | undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
interface SessionIndexProjection {
|
|
288
|
+
identities: IndexedSession[];
|
|
289
|
+
sessions: IndexedSession[];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function isTerminalEvent(event: SessionIndexEvent): boolean {
|
|
293
|
+
return event.type === "host_unregistered" || event.type === "session_closed" || event.type === "session_deleted";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function isUnresolvedEvent(event: SessionIndexEvent): boolean {
|
|
297
|
+
return !isTerminalEvent(event);
|
|
298
|
+
}
|
|
299
|
+
|
|
275
300
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
301
|
+
* True only for the proven direct-session GC fence row: the `host_registered`
|
|
302
|
+
* event `main.ts` appends at endpoint generation 0 with the agent dir itself as
|
|
303
|
+
* the state root. It has never published an endpoint and can never be attached
|
|
304
|
+
* by SessionRouter (which requires a positive generation and a readable
|
|
305
|
+
* endpoint), so it must not fence a real endpoint root into ambiguity —
|
|
306
|
+
* otherwise every interactive session reads ambiguous and no chat daemon can
|
|
307
|
+
* attach anything.
|
|
308
|
+
*
|
|
309
|
+
* The locator is required provenance, not decoration. Without it any admitted
|
|
310
|
+
* generation-0 `host_registered` from a foreign root would silently stop
|
|
311
|
+
* fencing, which is fail-open relative to the symmetric fence. Generation 0 is
|
|
312
|
+
* also emitted by `recordTerminalUncertain` as an unproven `lifecycle_terminal`
|
|
313
|
+
* claim, and malformed generations are not proof of anything: everything that
|
|
314
|
+
* is not this exact shape keeps fencing (C5/C6).
|
|
315
|
+
*
|
|
316
|
+
* The comparison is symlink-equivalent because the writer and the reader can
|
|
317
|
+
* legitimately spell the same agent dir differently (one via a symlink, one via
|
|
318
|
+
* its realpath). A plain `path.resolve` match would reject the genuine fence
|
|
319
|
+
* row under a symlinked agent dir and re-fence every session — the exact
|
|
320
|
+
* outage this predicate exists to prevent. `rootIdentity` resolves a stateRoot
|
|
321
|
+
* string once per scan so repeated sessions sharing one root never re-run
|
|
322
|
+
* `realpath` on the same spelling.
|
|
285
323
|
*/
|
|
286
|
-
function
|
|
324
|
+
function isDirectSessionGcFenceRow(
|
|
325
|
+
event: SessionIndexEvent,
|
|
326
|
+
agentDirIdentity: string,
|
|
327
|
+
rootIdentity: (stateRoot: string) => string,
|
|
328
|
+
): boolean {
|
|
329
|
+
return (
|
|
330
|
+
event.type === "host_registered" &&
|
|
331
|
+
event.endpointGeneration === 0 &&
|
|
332
|
+
rootIdentity(event.locator.stateRoot) === agentDirIdentity
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function preferredIdentity(states: Iterable<SessionIdentityState>): SessionIdentityState | undefined {
|
|
337
|
+
let preferred: SessionIdentityState | undefined;
|
|
338
|
+
for (const state of states) {
|
|
339
|
+
if (
|
|
340
|
+
preferred === undefined ||
|
|
341
|
+
state.latest.endpointGeneration > preferred.latest.endpointGeneration ||
|
|
342
|
+
(state.latest.endpointGeneration === preferred.latest.endpointGeneration &&
|
|
343
|
+
state.latest.indexSeq > preferred.latest.indexSeq)
|
|
344
|
+
)
|
|
345
|
+
preferred = state;
|
|
346
|
+
}
|
|
347
|
+
return preferred;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function projectIdentity(
|
|
351
|
+
state: SessionIdentityState,
|
|
352
|
+
ambiguous: boolean,
|
|
353
|
+
now: number,
|
|
354
|
+
probedIncarnations?: ReadonlyMap<string, string | undefined>,
|
|
355
|
+
): IndexedSession {
|
|
356
|
+
const { latest, heartbeat } = state;
|
|
357
|
+
const terminal = isTerminalEvent(latest);
|
|
358
|
+
const terminalUncertain = latest.type === "lifecycle_terminal" || latest.terminalUncertain === true;
|
|
359
|
+
const pidAlive = alive(latest.pid);
|
|
360
|
+
// Liveness evidence is host-written: a checkpointed heartbeat, or the
|
|
361
|
+
// `host_registered` event the host appended itself. Counting registration
|
|
362
|
+
// closes the up-to-one-interval window where a just-registered session
|
|
363
|
+
// would read not-live before the first C2 pass — without weakening the
|
|
364
|
+
// pid-reuse fence, because the incarnation match below is still required.
|
|
365
|
+
// Broker-written events (reconciliation, terminal records) are not host
|
|
366
|
+
// evidence and never refresh liveness.
|
|
367
|
+
const evidenceTs = Math.max(heartbeat?.ts ?? 0, latest.type === "host_registered" ? latest.ts : 0);
|
|
368
|
+
const heartbeatFresh = evidenceTs > 0 && now - evidenceTs < 2 * SESSION_HEARTBEAT_INTERVAL_MS;
|
|
369
|
+
const recordedIncarnation = effectiveIncarnation(latest);
|
|
370
|
+
// OS incarnation probes are never taken while the machine-global index lock is
|
|
371
|
+
// held (#4544): callers that already probed pass the observation in, and
|
|
372
|
+
// unlocked callers probe directly. On Windows a probe can spawn powershell.exe
|
|
373
|
+
// — an unbounded OS operation a machine-global critical section must not await.
|
|
374
|
+
// Dead pids are additionally never probed: the probe exists to fence pid reuse
|
|
375
|
+
// for a LIVE process, and for a dead pid it can only return undefined while
|
|
376
|
+
// costing a powershell.exe spawn per stale registration on Windows.
|
|
377
|
+
const probeKey = `${latest.sessionId}\u0000${latest.endpointGeneration}\u0000${latest.pid}`;
|
|
378
|
+
const currentIncarnation =
|
|
379
|
+
recordedIncarnation === undefined || !pidAlive
|
|
380
|
+
? undefined
|
|
381
|
+
: probedIncarnations !== undefined
|
|
382
|
+
? probedIncarnations.get(probeKey)
|
|
383
|
+
: processIncarnation(latest.pid);
|
|
384
|
+
const incarnationMatches = currentIncarnation !== undefined && currentIncarnation === recordedIncarnation;
|
|
385
|
+
return {
|
|
386
|
+
sessionId: latest.sessionId,
|
|
387
|
+
locator: latest.locator,
|
|
388
|
+
endpointGeneration: latest.endpointGeneration,
|
|
389
|
+
pid: latest.pid,
|
|
390
|
+
processIncarnation: latest.processIncarnation,
|
|
391
|
+
endpointMtimeMs: latest.endpointMtimeMs,
|
|
392
|
+
lifecycleRequestId: latest.lifecycleRequestId,
|
|
393
|
+
terminalUncertain,
|
|
394
|
+
indexSeq: latest.indexSeq,
|
|
395
|
+
hostIncarnation: latest.hostIncarnation,
|
|
396
|
+
identityProvenance: recordedIncarnation === undefined ? "legacy" : "composite",
|
|
397
|
+
activity: heartbeat?.activity,
|
|
398
|
+
lastHeartbeatAt: heartbeat?.ts,
|
|
399
|
+
ambiguous,
|
|
400
|
+
terminal,
|
|
401
|
+
live:
|
|
402
|
+
isSessionAuthorityEligible({ ambiguous }) &&
|
|
403
|
+
!terminal &&
|
|
404
|
+
!terminalUncertain &&
|
|
405
|
+
pidAlive &&
|
|
406
|
+
heartbeatFresh &&
|
|
407
|
+
incarnationMatches,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Total-order projection (C5/C6): first retain every current composite-identity
|
|
413
|
+
* row, then select one public row per session. More than one unresolved
|
|
414
|
+
* authority-fencing state root fences authority; the direct-session GC fence
|
|
415
|
+
* row never fences, but still survives as public authority when it is the only
|
|
416
|
+
* unresolved root left. Once exactly one authority-fencing root remains, its
|
|
417
|
+
* current identity becomes the public authority even when a terminated identity
|
|
418
|
+
* from another root has a higher generation. Heartbeats inherit
|
|
419
|
+
* locator/endpoint metadata from their identity's prior event.
|
|
420
|
+
*/
|
|
421
|
+
function reduceEvents(
|
|
422
|
+
events: SessionIndexEvent[],
|
|
423
|
+
now: number,
|
|
424
|
+
agentDir: string,
|
|
425
|
+
probedIncarnations?: ReadonlyMap<string, string | undefined>,
|
|
426
|
+
): SessionIndexProjection {
|
|
427
|
+
// Resolved once: the fence-row check runs per root, and only this side of the
|
|
428
|
+
// comparison is ours to normalize.
|
|
429
|
+
const agentDirIdentity = resolveEquivalentPath(agentDir);
|
|
430
|
+
// Sessions routinely share one stateRoot spelling (agent dir, worktree, ...);
|
|
431
|
+
// resolve each distinct spelling once per scan instead of per root so the
|
|
432
|
+
// symlink-equivalence check stays a cheap map hit on every repeated session.
|
|
433
|
+
const rootIdentities = new Map<string, string>();
|
|
434
|
+
const rootIdentity = (stateRoot: string): string => {
|
|
435
|
+
let identity = rootIdentities.get(stateRoot);
|
|
436
|
+
if (identity === undefined) {
|
|
437
|
+
identity = resolveEquivalentPath(stateRoot);
|
|
438
|
+
rootIdentities.set(stateRoot, identity);
|
|
439
|
+
}
|
|
440
|
+
return identity;
|
|
441
|
+
};
|
|
287
442
|
const { admitted } = admitEvents(events);
|
|
288
443
|
const latestByIdentity = new Map<string, SessionIndexEvent>();
|
|
289
444
|
const latestHeartbeatByIdentity = new Map<string, SessionIndexEvent>();
|
|
290
445
|
for (const event of admitted) {
|
|
291
|
-
const
|
|
446
|
+
const identity = identityKey(event);
|
|
292
447
|
if (event.type === "host_heartbeat") {
|
|
293
|
-
latestHeartbeatByIdentity.set(
|
|
448
|
+
latestHeartbeatByIdentity.set(identity, event);
|
|
294
449
|
continue;
|
|
295
450
|
}
|
|
296
|
-
const previous = latestByIdentity.get(
|
|
297
|
-
if (previous === undefined || event.indexSeq > previous.indexSeq) latestByIdentity.set(
|
|
451
|
+
const previous = latestByIdentity.get(identity);
|
|
452
|
+
if (previous === undefined || event.indexSeq > previous.indexSeq) latestByIdentity.set(identity, event);
|
|
298
453
|
}
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
for (const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
454
|
+
const statesBySession = new Map<string, SessionIdentityState[]>();
|
|
455
|
+
const rootsBySession = new Map<string, Map<string, SessionIdentityState[]>>();
|
|
456
|
+
for (const [identity, latest] of latestByIdentity) {
|
|
457
|
+
const state: SessionIdentityState = { identity, latest, heartbeat: latestHeartbeatByIdentity.get(identity) };
|
|
458
|
+
let states = statesBySession.get(latest.sessionId);
|
|
459
|
+
if (states === undefined) {
|
|
460
|
+
states = [];
|
|
461
|
+
statesBySession.set(latest.sessionId, states);
|
|
306
462
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
463
|
+
states.push(state);
|
|
464
|
+
let roots = rootsBySession.get(latest.sessionId);
|
|
465
|
+
if (roots === undefined) {
|
|
466
|
+
roots = new Map<string, SessionIdentityState[]>();
|
|
467
|
+
rootsBySession.set(latest.sessionId, roots);
|
|
468
|
+
}
|
|
469
|
+
let rootStates = roots.get(latest.locator.stateRoot);
|
|
470
|
+
if (rootStates === undefined) {
|
|
471
|
+
rootStates = [];
|
|
472
|
+
roots.set(latest.locator.stateRoot, rootStates);
|
|
473
|
+
}
|
|
474
|
+
rootStates.push(state);
|
|
475
|
+
}
|
|
476
|
+
// Two views of the same roots. `fencingRoots` holds the roots that may claim
|
|
477
|
+
// authority: more than one fences the session, and exactly one is the sole
|
|
478
|
+
// surviving authority. `unresolvedRoots` additionally holds the
|
|
479
|
+
// direct-session GC fence row, which never fences and only becomes public
|
|
480
|
+
// authority when no fencing root is left at all.
|
|
481
|
+
const unresolvedRoots = new Map<string, Map<string, SessionIdentityState[]>>();
|
|
482
|
+
const fencingRoots = new Map<string, Map<string, SessionIdentityState[]>>();
|
|
483
|
+
for (const [sessionId, roots] of rootsBySession) {
|
|
484
|
+
for (const [root, rootStates] of roots) {
|
|
485
|
+
const current = preferredIdentity(rootStates);
|
|
486
|
+
if (!current || !isUnresolvedEvent(current.latest)) continue;
|
|
487
|
+
let unresolved = unresolvedRoots.get(sessionId);
|
|
488
|
+
if (unresolved === undefined) {
|
|
489
|
+
unresolved = new Map<string, SessionIdentityState[]>();
|
|
490
|
+
unresolvedRoots.set(sessionId, unresolved);
|
|
491
|
+
}
|
|
492
|
+
unresolved.set(root, rootStates);
|
|
493
|
+
if (isDirectSessionGcFenceRow(current.latest, agentDirIdentity, rootIdentity)) continue;
|
|
494
|
+
let fencing = fencingRoots.get(sessionId);
|
|
495
|
+
if (fencing === undefined) {
|
|
496
|
+
fencing = new Map<string, SessionIdentityState[]>();
|
|
497
|
+
fencingRoots.set(sessionId, fencing);
|
|
498
|
+
}
|
|
499
|
+
fencing.set(root, rootStates);
|
|
320
500
|
}
|
|
321
501
|
}
|
|
502
|
+
const identities: IndexedSession[] = [];
|
|
322
503
|
const sessions: IndexedSession[] = [];
|
|
323
|
-
for (const [sessionId,
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const incarnationMatches = currentIncarnation !== undefined && currentIncarnation === recordedIncarnation;
|
|
342
|
-
sessions.push({
|
|
343
|
-
sessionId,
|
|
344
|
-
locator: latest.locator,
|
|
345
|
-
endpointGeneration: latest.endpointGeneration,
|
|
346
|
-
pid: latest.pid,
|
|
347
|
-
processIncarnation: latest.processIncarnation,
|
|
348
|
-
endpointMtimeMs: latest.endpointMtimeMs,
|
|
349
|
-
lifecycleRequestId: latest.lifecycleRequestId,
|
|
350
|
-
terminalUncertain: latest.type === "lifecycle_terminal" || latest.terminalUncertain === true,
|
|
351
|
-
indexSeq: latest.indexSeq,
|
|
352
|
-
hostIncarnation: latest.hostIncarnation,
|
|
353
|
-
identityProvenance: recordedIncarnation === undefined ? "legacy" : "composite",
|
|
354
|
-
activity: heartbeat?.activity,
|
|
355
|
-
lastHeartbeatAt: heartbeat?.ts,
|
|
356
|
-
ambiguous: (roots.get(sessionId)?.size ?? 0) > 1,
|
|
357
|
-
terminal,
|
|
358
|
-
live: !terminal && pidAlive && heartbeatFresh && incarnationMatches,
|
|
359
|
-
});
|
|
504
|
+
for (const [sessionId, states] of statesBySession) {
|
|
505
|
+
const fencing = fencingRoots.get(sessionId);
|
|
506
|
+
const unresolved = unresolvedRoots.get(sessionId);
|
|
507
|
+
const ambiguous = (fencing?.size ?? 0) > 1;
|
|
508
|
+
for (const state of states) identities.push(projectIdentity(state, ambiguous, now, probedIncarnations));
|
|
509
|
+
const defaultAuthority = preferredIdentity(states);
|
|
510
|
+
if (defaultAuthority === undefined || defaultAuthority.latest.type === "session_deleted") continue;
|
|
511
|
+
let authority = defaultAuthority;
|
|
512
|
+
// A sole fencing root outranks any terminated identity from another root,
|
|
513
|
+
// including a higher-generation one. Only when nothing can claim authority
|
|
514
|
+
// does a lone GC fence row become the public survivor.
|
|
515
|
+
const survivingRoots = fencing?.size === 1 ? fencing : (fencing?.size ?? 0) === 0 ? unresolved : undefined;
|
|
516
|
+
if (survivingRoots?.size === 1) {
|
|
517
|
+
const onlyRoot = survivingRoots.values().next().value;
|
|
518
|
+
const survivingAuthority = onlyRoot ? preferredIdentity(onlyRoot) : undefined;
|
|
519
|
+
if (survivingAuthority !== undefined) authority = survivingAuthority;
|
|
520
|
+
}
|
|
521
|
+
sessions.push(projectIdentity(authority, ambiguous, now, probedIncarnations));
|
|
360
522
|
}
|
|
361
|
-
return sessions;
|
|
523
|
+
return { identities, sessions };
|
|
362
524
|
}
|
|
363
525
|
|
|
364
526
|
// Global launch bursts may queue behind legitimate long index transactions. Keep
|
|
365
527
|
// this bounded at one minute while the shared lock's exact dead-owner recovery runs.
|
|
366
528
|
const SESSION_INDEX_LOCK_OPTIONS = { retries: 600, retryDelayMs: 100 } as const;
|
|
367
529
|
|
|
368
|
-
|
|
369
|
-
|
|
530
|
+
/**
|
|
531
|
+
* Observation window for one locked index transaction (#4544). This never times
|
|
532
|
+
* out or interrupts the holder — the machine-global lock's safety (never steal
|
|
533
|
+
* from a live owner) depends on holders finishing — it only names the operation
|
|
534
|
+
* still running once the window elapses, so a wedged Windows sync-family await
|
|
535
|
+
* is attributable from logs instead of surfacing as a bare 600-attempt
|
|
536
|
+
* lock-exhaustion crash on every later launch.
|
|
537
|
+
*/
|
|
538
|
+
const SESSION_INDEX_OP_SLOW_MS = 10_000;
|
|
539
|
+
/**
|
|
540
|
+
* How long a completed incarnation-probe batch stays trustworthy while the
|
|
541
|
+
* heartbeat pass acquires the machine-global index lock (#4544 review). A pid
|
|
542
|
+
* can exit and be reused while the pass queues behind another holder, and
|
|
543
|
+
* `alive(pid)` alone would see the reused pid while the observation still
|
|
544
|
+
* matched the dead process. Beyond this bound the cycle writes no heartbeat
|
|
545
|
+
* (fail-closed); the OS is never probed under the lock. The bound only needs
|
|
546
|
+
* to cover an uncontended acquisition plus scheduler jitter — real contention
|
|
547
|
+
* queues for the retry delay (100ms) or far longer.
|
|
548
|
+
*
|
|
549
|
+
* Both freshness bounds are measured on the MONOTONIC clock
|
|
550
|
+
* (`performance.now()`), never `Date.now()`: a backward wall-clock step
|
|
551
|
+
* (manual clock fix, NTP slew, VM snapshot restore) would make a wall-clock
|
|
552
|
+
* interval negative and pass the bound, consuming an arbitrarily stale
|
|
553
|
+
* observation batch. Monotonic time never steps backward, so the fail-closed
|
|
554
|
+
* guarantee cannot be defeated by the system clock moving.
|
|
555
|
+
*/
|
|
556
|
+
const SESSION_INDEX_PROBE_FRESHNESS_MS = 50;
|
|
557
|
+
/**
|
|
558
|
+
* The same trust bound re-checked AFTER the locked replay (#4544 review):
|
|
559
|
+
* replay re-reads the whole log (up to the 4 MiB rotation bound) and fsyncs
|
|
560
|
+
* pending audit rows, so a large index or a slow Windows sync-family await can
|
|
561
|
+
* stretch the interval between the probe batch and the heartbeat write past
|
|
562
|
+
* the acquisition bound above. A pid can exit and be reused across that
|
|
563
|
+
* awaited replay; consuming the cached observation then would checkpoint the
|
|
564
|
+
* replacement process as the dead row. The replay bound therefore covers the
|
|
565
|
+
* full locked replay cost envelope of a healthy machine instead of scheduler
|
|
566
|
+
* jitter alone; anything slower still fails closed (no heartbeat this cycle)
|
|
567
|
+
* and the next pass re-probes from scratch.
|
|
568
|
+
*/
|
|
569
|
+
const SESSION_INDEX_REPLAY_FRESHNESS_MS = 2_000;
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* One locked session-index transaction (#4544): every critical section over the
|
|
573
|
+
* machine-global lock runs through this single choke point, so (a) the operation
|
|
574
|
+
* is named in a slow-operation warning when it holds the lock beyond the
|
|
575
|
+
* observation window and (b) the lock budget cannot silently drift per call
|
|
576
|
+
* site.
|
|
577
|
+
*/
|
|
578
|
+
function withSessionIndexLock<T>(operation: string, agentDir: string, callback: () => Promise<T>): Promise<T> {
|
|
579
|
+
return withFileLock(
|
|
580
|
+
logFor(agentDir),
|
|
581
|
+
async () => {
|
|
582
|
+
// Armed only once the lock is actually held: queueing time behind another
|
|
583
|
+
// legitimate holder must not be attributed to this operation.
|
|
584
|
+
const note = setTimeout(
|
|
585
|
+
() => logger.warn(`sdk broker: session index "${operation}" still holds the index lock after 10s`),
|
|
586
|
+
SESSION_INDEX_OP_SLOW_MS,
|
|
587
|
+
);
|
|
588
|
+
// A diagnostic timer must never keep the process alive on an exit path
|
|
589
|
+
// that races the 10s window; fast paths always clear it in `finally`.
|
|
590
|
+
note.unref?.();
|
|
591
|
+
try {
|
|
592
|
+
return await callback();
|
|
593
|
+
} finally {
|
|
594
|
+
clearTimeout(note);
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
SESSION_INDEX_LOCK_OPTIONS,
|
|
598
|
+
);
|
|
370
599
|
}
|
|
371
600
|
function isValidSnapshot(snapshot: unknown): snapshot is { indexSeq: number; events: SessionIndexEvent[] } {
|
|
372
601
|
if (!snapshot || typeof snapshot !== "object") return false;
|
|
@@ -574,7 +803,9 @@ export class SessionIndex {
|
|
|
574
803
|
group.promise = SessionIndex.#enqueue(indexPath, () => this.#prepareOpenGroup(indexPath, group!));
|
|
575
804
|
}
|
|
576
805
|
await group.promise;
|
|
577
|
-
await SessionIndex.#enqueue(indexPath, () =>
|
|
806
|
+
await SessionIndex.#enqueue(indexPath, () =>
|
|
807
|
+
withSessionIndexLock("replay", this.#agentDir, () => this.#replayUnderLock()),
|
|
808
|
+
);
|
|
578
809
|
return this;
|
|
579
810
|
}
|
|
580
811
|
async #prepareOpenGroup(indexPath: string, group: SessionIndexOpenGroup): Promise<void> {
|
|
@@ -588,7 +819,9 @@ export class SessionIndex {
|
|
|
588
819
|
}
|
|
589
820
|
async replay(): Promise<void> {
|
|
590
821
|
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
591
|
-
await SessionIndex.#enqueue(indexPath, () =>
|
|
822
|
+
await SessionIndex.#enqueue(indexPath, () =>
|
|
823
|
+
withSessionIndexLock("replay", this.#agentDir, () => this.#replayUnderLock()),
|
|
824
|
+
);
|
|
592
825
|
}
|
|
593
826
|
async #replayUnderLock(): Promise<void> {
|
|
594
827
|
const scan = await this.#scan();
|
|
@@ -779,14 +1012,14 @@ export class SessionIndex {
|
|
|
779
1012
|
}),
|
|
780
1013
|
);
|
|
781
1014
|
if (!exists.some(Boolean)) return { status: "healthy", validPrefixSeq: 0, snapshotSeq: 0 };
|
|
782
|
-
return await withSessionIndexLock(this.#agentDir, async () => (await this.#scan()).diagnosis);
|
|
1015
|
+
return await withSessionIndexLock("diagnose", this.#agentDir, async () => (await this.#scan()).diagnosis);
|
|
783
1016
|
});
|
|
784
1017
|
}
|
|
785
1018
|
async repair(): Promise<SessionIndexRepairResult> {
|
|
786
1019
|
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
787
1020
|
return await SessionIndex.#enqueue(indexPath, async () => {
|
|
788
1021
|
await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
|
|
789
|
-
return await withSessionIndexLock(this.#agentDir, async () => {
|
|
1022
|
+
return await withSessionIndexLock("repair", this.#agentDir, async () => {
|
|
790
1023
|
const scan = await this.#scan();
|
|
791
1024
|
if (scan.diagnosis.status === "unsupported") return { ...scan.diagnosis, repaired: false };
|
|
792
1025
|
if (scan.diagnosis.status === "healthy") return { ...scan.diagnosis, repaired: false };
|
|
@@ -800,15 +1033,25 @@ export class SessionIndex {
|
|
|
800
1033
|
await writeAndSync(path.join(quarantinePath, "index.snapshot.json"), scan.snapshotContents);
|
|
801
1034
|
if (scan.logContents) await writeAndSync(path.join(quarantinePath, "index.jsonl"), scan.logContents);
|
|
802
1035
|
await syncDirectory(path.join(quarantinePath, "index.jsonl"));
|
|
803
|
-
|
|
1036
|
+
// Repair republishes the surviving history as the new snapshot, so it must
|
|
1037
|
+
// apply the same retention the ordinary snapshot path applies. Writing the
|
|
1038
|
+
// raw survivor set instead let one repair of a long-lived index restore an
|
|
1039
|
+
// unbounded snapshot, and every later locked transaction then re-parsed that
|
|
1040
|
+
// whole history while holding the index lock — the broker burns CPU and
|
|
1041
|
+
// unrelated launches time out waiting for the lock.
|
|
1042
|
+
const events = compactEvents([...scan.snapshotEvents, ...scan.validLogEvents], this.#policy);
|
|
804
1043
|
const snapshot = JSON.stringify({
|
|
805
1044
|
version: SESSION_INDEX_SNAPSHOT_VERSION,
|
|
806
1045
|
indexSeq: scan.diagnosis.validPrefixSeq,
|
|
807
1046
|
events,
|
|
808
1047
|
});
|
|
809
|
-
const log = scan.validLogEvents.map(event => JSON.stringify(event)).join("\n");
|
|
810
1048
|
await replaceAtomically(snapshotFor(this.#agentDir), snapshot);
|
|
811
|
-
|
|
1049
|
+
// The republished snapshot already carries the full surviving history (after
|
|
1050
|
+
// compaction), so the log must be truncated to match: leaving the original
|
|
1051
|
+
// events in place keeps every subsequent #scan() parsing them under the lock
|
|
1052
|
+
// — the same starvation the compaction above is meant to end. This mirrors
|
|
1053
|
+
// #rotate(), which writes an empty log after snapshotting.
|
|
1054
|
+
await replaceAtomically(logFor(this.#agentDir), "");
|
|
812
1055
|
await this.#replayUnderLock();
|
|
813
1056
|
return { ...scan.diagnosis, repaired: true, quarantinePath };
|
|
814
1057
|
});
|
|
@@ -869,7 +1112,7 @@ export class SessionIndex {
|
|
|
869
1112
|
async refresh(): Promise<void> {
|
|
870
1113
|
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
871
1114
|
await SessionIndex.#enqueue(indexPath, () =>
|
|
872
|
-
withSessionIndexLock(this.#agentDir, () => this.#refreshUnderLock()),
|
|
1115
|
+
withSessionIndexLock("refresh", this.#agentDir, () => this.#refreshUnderLock()),
|
|
873
1116
|
);
|
|
874
1117
|
}
|
|
875
1118
|
async #refreshUnderLock(): Promise<void> {
|
|
@@ -886,7 +1129,19 @@ export class SessionIndex {
|
|
|
886
1129
|
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
887
1130
|
return await SessionIndex.#enqueue(indexPath, async () => {
|
|
888
1131
|
await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
|
|
889
|
-
|
|
1132
|
+
// Own-pid incarnation derivation runs BEFORE the lock (#4544): on Windows
|
|
1133
|
+
// the probe can spawn powershell.exe, and the machine-global index lock
|
|
1134
|
+
// must not be held across an unbounded OS spawn. It cannot race anything —
|
|
1135
|
+
// this process's own pid is fixed for its lifetime.
|
|
1136
|
+
let ownIncarnation: string | undefined;
|
|
1137
|
+
if (
|
|
1138
|
+
input.hostIncarnation === undefined &&
|
|
1139
|
+
input.pid === process.pid &&
|
|
1140
|
+
Number.isSafeInteger(input.pid) &&
|
|
1141
|
+
input.pid > 0
|
|
1142
|
+
)
|
|
1143
|
+
ownIncarnation = input.processIncarnation ?? processIncarnation(input.pid);
|
|
1144
|
+
return await withSessionIndexLock("append", this.#agentDir, async () => {
|
|
890
1145
|
await this.#replayUnderLock();
|
|
891
1146
|
if (this.#corruptSuffix)
|
|
892
1147
|
throw new Error(
|
|
@@ -898,11 +1153,11 @@ export class SessionIndex {
|
|
|
898
1153
|
indexSeq: this.indexSeq + 1,
|
|
899
1154
|
ts: input.ts ?? Date.now(),
|
|
900
1155
|
};
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1156
|
+
// The host's own OS identity was derived before the lock above;
|
|
1157
|
+
// broker-authored events for another process carry the registration's
|
|
1158
|
+
// persisted binding instead.
|
|
1159
|
+
if (ownIncarnation !== undefined && unsigned.hostIncarnation === undefined)
|
|
1160
|
+
unsigned.hostIncarnation = ownIncarnation;
|
|
906
1161
|
const event: SessionIndexEvent = { ...unsigned, checksum: sessionIndexChecksum(unsigned) };
|
|
907
1162
|
await appendSync(logFor(this.#agentDir), JSON.stringify(event));
|
|
908
1163
|
await this.#refreshUnderLock();
|
|
@@ -911,6 +1166,93 @@ export class SessionIndex {
|
|
|
911
1166
|
});
|
|
912
1167
|
});
|
|
913
1168
|
}
|
|
1169
|
+
async unregisterIfCurrent(expected: IndexedSession): Promise<boolean> {
|
|
1170
|
+
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
1171
|
+
return await SessionIndex.#enqueue(indexPath, async () => {
|
|
1172
|
+
await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
|
|
1173
|
+
// OS incarnation probes run BEFORE the machine-global lock is taken
|
|
1174
|
+
// (#4544 review round 5): this pass projects every composite identity
|
|
1175
|
+
// to find the current authority row, and an unlocked projection probes
|
|
1176
|
+
// the OS once per identity — on Windows that can spawn powershell.exe,
|
|
1177
|
+
// which a machine-global critical section must never await. The
|
|
1178
|
+
// unregister decision below never consults the projected `live` flag —
|
|
1179
|
+
// it compares persisted identity fields against the replayed events —
|
|
1180
|
+
// so the pre-lock observations cannot change the outcome either way;
|
|
1181
|
+
// they exist only so the locked projection performs no OS probes.
|
|
1182
|
+
const probed = new Map<string, string | undefined>();
|
|
1183
|
+
for (const row of reduceEvents(this.#events, this.#policy.clock(), this.#agentDir).identities) {
|
|
1184
|
+
const recordedIncarnation = row.hostIncarnation ?? row.processIncarnation;
|
|
1185
|
+
if (recordedIncarnation === undefined || !alive(row.pid)) continue;
|
|
1186
|
+
probed.set(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`, processIncarnation(row.pid));
|
|
1187
|
+
}
|
|
1188
|
+
return await withSessionIndexLock("conditional unregister", this.#agentDir, async () => {
|
|
1189
|
+
await this.#replayUnderLock();
|
|
1190
|
+
if (this.#corruptSuffix) throw new Error("Cannot conditionally unregister from a corrupt session index");
|
|
1191
|
+
const identities = this.listSessionIdentities(probed);
|
|
1192
|
+
const current = identities.find(
|
|
1193
|
+
session =>
|
|
1194
|
+
session.sessionId === expected.sessionId &&
|
|
1195
|
+
session.endpointGeneration === expected.endpointGeneration &&
|
|
1196
|
+
session.pid === expected.pid &&
|
|
1197
|
+
session.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
1198
|
+
session.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
1199
|
+
session.processIncarnation === expected.processIncarnation &&
|
|
1200
|
+
(session.hostIncarnation ?? session.processIncarnation) ===
|
|
1201
|
+
(expected.hostIncarnation ?? expected.processIncarnation) &&
|
|
1202
|
+
resolveEquivalentPath(session.locator.repo) === resolveEquivalentPath(expected.locator.repo) &&
|
|
1203
|
+
path.resolve(session.locator.stateRoot) === path.resolve(expected.locator.stateRoot),
|
|
1204
|
+
);
|
|
1205
|
+
let currentRoot: IndexedSession | undefined;
|
|
1206
|
+
for (const session of identities) {
|
|
1207
|
+
if (
|
|
1208
|
+
session.sessionId !== expected.sessionId ||
|
|
1209
|
+
session.terminal ||
|
|
1210
|
+
resolveEquivalentPath(session.locator.repo) !== resolveEquivalentPath(expected.locator.repo) ||
|
|
1211
|
+
path.resolve(session.locator.stateRoot) !== path.resolve(expected.locator.stateRoot)
|
|
1212
|
+
)
|
|
1213
|
+
continue;
|
|
1214
|
+
if (
|
|
1215
|
+
currentRoot === undefined ||
|
|
1216
|
+
session.endpointGeneration > currentRoot.endpointGeneration ||
|
|
1217
|
+
(session.endpointGeneration === currentRoot.endpointGeneration &&
|
|
1218
|
+
session.indexSeq > currentRoot.indexSeq)
|
|
1219
|
+
)
|
|
1220
|
+
currentRoot = session;
|
|
1221
|
+
}
|
|
1222
|
+
if (
|
|
1223
|
+
!current ||
|
|
1224
|
+
current !== currentRoot ||
|
|
1225
|
+
current.terminal ||
|
|
1226
|
+
current.terminalUncertain ||
|
|
1227
|
+
this.hostUnregisteredAfter(expected)
|
|
1228
|
+
)
|
|
1229
|
+
return false;
|
|
1230
|
+
const unsigned: Omit<SessionIndexEvent, "checksum"> = {
|
|
1231
|
+
version: SDK_STATE_VERSION,
|
|
1232
|
+
indexSeq: this.indexSeq + 1,
|
|
1233
|
+
ts: Date.now(),
|
|
1234
|
+
type: "host_unregistered",
|
|
1235
|
+
sessionId: expected.sessionId,
|
|
1236
|
+
locator: expected.locator,
|
|
1237
|
+
endpointGeneration: expected.endpointGeneration,
|
|
1238
|
+
pid: expected.pid,
|
|
1239
|
+
...(expected.processIncarnation === undefined
|
|
1240
|
+
? {}
|
|
1241
|
+
: { processIncarnation: expected.processIncarnation }),
|
|
1242
|
+
...(expected.hostIncarnation === undefined ? {} : { hostIncarnation: expected.hostIncarnation }),
|
|
1243
|
+
...(expected.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: expected.endpointMtimeMs }),
|
|
1244
|
+
...(expected.lifecycleRequestId === undefined
|
|
1245
|
+
? {}
|
|
1246
|
+
: { lifecycleRequestId: expected.lifecycleRequestId }),
|
|
1247
|
+
};
|
|
1248
|
+
const event: SessionIndexEvent = { ...unsigned, checksum: sessionIndexChecksum(unsigned) };
|
|
1249
|
+
await appendSync(logFor(this.#agentDir), JSON.stringify(event));
|
|
1250
|
+
await this.#refreshUnderLock();
|
|
1251
|
+
if ((await fs.stat(logFor(this.#agentDir))).size >= ROTATE_BYTES) await this.#rotate();
|
|
1252
|
+
return true;
|
|
1253
|
+
});
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
914
1256
|
|
|
915
1257
|
/** Hold the canonical index lock across an authority-sensitive operation. */
|
|
916
1258
|
async withLocked<T>(callback: () => Promise<T>): Promise<T> {
|
|
@@ -922,7 +1264,7 @@ export class SessionIndex {
|
|
|
922
1264
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return await callback();
|
|
923
1265
|
throw error;
|
|
924
1266
|
}
|
|
925
|
-
return await withSessionIndexLock(this.#agentDir, async () => {
|
|
1267
|
+
return await withSessionIndexLock("authority operation", this.#agentDir, async () => {
|
|
926
1268
|
await this.#replayUnderLock();
|
|
927
1269
|
if (this.#corruptSuffix) throw new Error("Cannot use corrupt session index for artifact reclamation");
|
|
928
1270
|
return await callback();
|
|
@@ -932,7 +1274,7 @@ export class SessionIndex {
|
|
|
932
1274
|
async snapshot(): Promise<void> {
|
|
933
1275
|
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
934
1276
|
await SessionIndex.#enqueue(indexPath, () =>
|
|
935
|
-
withSessionIndexLock(this.#agentDir, () => this.#snapshotUnderLock()),
|
|
1277
|
+
withSessionIndexLock("snapshot", this.#agentDir, () => this.#snapshotUnderLock()),
|
|
936
1278
|
);
|
|
937
1279
|
}
|
|
938
1280
|
async #snapshotUnderLock(): Promise<void> {
|
|
@@ -945,26 +1287,14 @@ export class SessionIndex {
|
|
|
945
1287
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !(error instanceof SyntaxError)) throw error;
|
|
946
1288
|
}
|
|
947
1289
|
if (isValidSnapshot(current) && current.indexSeq > this.indexSeq) return;
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
tmp,
|
|
1290
|
+
await replaceAtomically(
|
|
1291
|
+
file,
|
|
951
1292
|
JSON.stringify({
|
|
952
1293
|
version: SESSION_INDEX_SNAPSHOT_VERSION,
|
|
953
1294
|
indexSeq: this.indexSeq,
|
|
954
1295
|
events: compactEvents(this.#events, this.#policy),
|
|
955
1296
|
}),
|
|
956
|
-
{
|
|
957
|
-
mode: 0o600,
|
|
958
|
-
},
|
|
959
1297
|
);
|
|
960
|
-
const h = await fs.open(tmp, "r");
|
|
961
|
-
try {
|
|
962
|
-
await h.sync();
|
|
963
|
-
} finally {
|
|
964
|
-
await h.close();
|
|
965
|
-
}
|
|
966
|
-
await fs.rename(tmp, file);
|
|
967
|
-
await syncDirectory(file);
|
|
968
1298
|
}
|
|
969
1299
|
/**
|
|
970
1300
|
* Broker-scheduled compaction (C3), independent of rotation size: applies the
|
|
@@ -973,7 +1303,7 @@ export class SessionIndex {
|
|
|
973
1303
|
async compact(): Promise<void> {
|
|
974
1304
|
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
975
1305
|
await SessionIndex.#enqueue(indexPath, () =>
|
|
976
|
-
|
|
1306
|
+
withSessionIndexLock("compact", this.#agentDir, async () => {
|
|
977
1307
|
await this.#replayUnderLock();
|
|
978
1308
|
await this.#rotate();
|
|
979
1309
|
}),
|
|
@@ -992,18 +1322,26 @@ export class SessionIndex {
|
|
|
992
1322
|
listSessions(): SessionList {
|
|
993
1323
|
return {
|
|
994
1324
|
indexSeq: this.indexSeq,
|
|
995
|
-
sessions: reduceEvents(this.#events, this.#policy.clock()),
|
|
1325
|
+
sessions: reduceEvents(this.#events, this.#policy.clock(), this.#agentDir).sessions,
|
|
996
1326
|
warnings: this.#warnings,
|
|
997
1327
|
};
|
|
998
1328
|
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Broker-internal current composite-identity rows. Unlike {@link listSessions},
|
|
1331
|
+
* this retains losing roots so an exact dead registration can be retired without
|
|
1332
|
+
* disturbing the surviving authority.
|
|
1333
|
+
*/
|
|
1334
|
+
listSessionIdentities(probedIncarnations?: ReadonlyMap<string, string | undefined>): IndexedSession[] {
|
|
1335
|
+
return reduceEvents(this.#events, this.#policy.clock(), this.#agentDir, probedIncarnations).identities;
|
|
1336
|
+
}
|
|
999
1337
|
|
|
1000
1338
|
/**
|
|
1001
1339
|
* Production coalesced heartbeat checkpoint pass (C2): appends one
|
|
1002
1340
|
* `host_heartbeat` per session at most once per {@link SESSION_HEARTBEAT_INTERVAL_MS}.
|
|
1003
1341
|
* The pass observes liveness the same way the projection does — the host process
|
|
1004
1342
|
* must be alive and, for composite identities, still carry the recorded OS process
|
|
1005
|
-
* incarnation (a reused PID is never checkpointed). Stopped
|
|
1006
|
-
* whose heartbeat is still fresh are skipped. After a broker restart, sessions whose
|
|
1343
|
+
* incarnation (a reused PID is never checkpointed). Stopped, terminal, and ambiguous rows
|
|
1344
|
+
* and rows whose heartbeat is still fresh are skipped. After a broker restart, sessions whose
|
|
1007
1345
|
* host survived are re-observed as live on the first pass; sessions whose host died
|
|
1008
1346
|
* while the broker was down keep their stale or missing heartbeat and read as
|
|
1009
1347
|
* unknown/not-live (never fresh forever). Returns the number of checkpoints written.
|
|
@@ -1011,20 +1349,70 @@ export class SessionIndex {
|
|
|
1011
1349
|
async checkpointLiveHeartbeats(now = Date.now()): Promise<number> {
|
|
1012
1350
|
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
1013
1351
|
return await SessionIndex.#enqueue(indexPath, async () => {
|
|
1014
|
-
|
|
1015
|
-
|
|
1352
|
+
// An absent index holds no registration to checkpoint, so this pass must read
|
|
1353
|
+
// it as "nothing to do" instead of creating one. Recreating the directory
|
|
1354
|
+
// resurrects a state root its owner already retired: the broker's 5s
|
|
1355
|
+
// publication watch would rebuild a removed agent dir after shutdown.
|
|
1356
|
+
try {
|
|
1357
|
+
await fs.stat(dirFor(this.#agentDir));
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return 0;
|
|
1360
|
+
throw error;
|
|
1361
|
+
}
|
|
1362
|
+
// OS incarnation probes run BEFORE the machine-global lock is taken
|
|
1363
|
+
// (#4544): on Windows a probe can spawn powershell.exe, and holding the
|
|
1364
|
+
// index lock across an unbounded OS spawn starves every later launch.
|
|
1365
|
+
// The probe reads only the last replayed history to pick WHICH pids are
|
|
1366
|
+
// worth observing. The observations are then bound to the instant the
|
|
1367
|
+
// probe batch COMPLETED: if anything delays this pass past that instant —
|
|
1368
|
+
// lock contention queueing behind another holder, or simply time — the
|
|
1369
|
+
// observation set is discarded and this cycle writes no heartbeat. A pid
|
|
1370
|
+
// can exit and be reused in that window, and `alive(pid)` alone would see
|
|
1371
|
+
// the reused pid while a stale observation still matched the dead
|
|
1372
|
+
// process, checkpointing the wrong host. Failing closed is the only
|
|
1373
|
+
// revalidation that never probes the OS under the lock: the next pass
|
|
1374
|
+
// re-probes from scratch.
|
|
1375
|
+
const probed = new Map<string, string | undefined>();
|
|
1376
|
+
for (const row of reduceEvents(this.#events, now, this.#agentDir).sessions) {
|
|
1377
|
+
if (!isSessionAuthorityEligible(row) || row.terminal || row.terminalUncertain) continue;
|
|
1378
|
+
if (row.lastHeartbeatAt !== undefined && now - row.lastHeartbeatAt < SESSION_HEARTBEAT_INTERVAL_MS)
|
|
1379
|
+
continue;
|
|
1380
|
+
if (!alive(row.pid)) continue;
|
|
1381
|
+
const recordedIncarnation = row.hostIncarnation ?? row.processIncarnation;
|
|
1382
|
+
if (recordedIncarnation === undefined) continue;
|
|
1383
|
+
probed.set(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`, processIncarnation(row.pid));
|
|
1384
|
+
}
|
|
1385
|
+
const probedAt = performance.now();
|
|
1386
|
+
return await withSessionIndexLock("heartbeat checkpoint", this.#agentDir, async () => {
|
|
1387
|
+
// Contended or delayed acquisition invalidates the observation set:
|
|
1388
|
+
// never trust identity evidence gathered meaningfully before the
|
|
1389
|
+
// lock was held. The checkpoint is skipped this cycle (fail-closed);
|
|
1390
|
+
// the OS is never probed while the machine-global lock is held. The
|
|
1391
|
+
// bound tolerates scheduler jitter on an uncontended acquisition —
|
|
1392
|
+
// which is the only path where the evidence is trustworthy.
|
|
1393
|
+
if (performance.now() - probedAt > SESSION_INDEX_PROBE_FRESHNESS_MS) return 0;
|
|
1016
1394
|
await this.#replayUnderLock();
|
|
1395
|
+
// Recheck AFTER the awaited replay too (#4544 review round 4): the
|
|
1396
|
+
// replay re-reads the whole log (up to the 4 MiB rotation bound) and
|
|
1397
|
+
// fsyncs pending audit rows, so the probe→write interval can stretch
|
|
1398
|
+
// well past the acquisition bound above while the lock is held. The
|
|
1399
|
+
// same pid-reuse window exists across that awaited replay; consuming
|
|
1400
|
+
// the cached observation then would checkpoint the replacement
|
|
1401
|
+
// process as the dead row. The replay bound covers the full locked
|
|
1402
|
+
// replay cost envelope of a healthy machine; anything slower fails
|
|
1403
|
+
// closed (no heartbeat this cycle) and the next pass re-probes.
|
|
1404
|
+
if (performance.now() - probedAt > SESSION_INDEX_REPLAY_FRESHNESS_MS) return 0;
|
|
1017
1405
|
if (this.#corruptSuffix) return 0;
|
|
1018
1406
|
const events: SessionIndexEvent[] = [];
|
|
1019
|
-
const rows = reduceEvents(this.#events, now);
|
|
1407
|
+
const rows = reduceEvents(this.#events, now, this.#agentDir, probed).sessions;
|
|
1020
1408
|
for (const row of rows) {
|
|
1021
|
-
if (row.terminal) continue;
|
|
1409
|
+
if (!isSessionAuthorityEligible(row) || row.terminal || row.terminalUncertain) continue;
|
|
1022
1410
|
if (row.lastHeartbeatAt !== undefined && now - row.lastHeartbeatAt < SESSION_HEARTBEAT_INTERVAL_MS)
|
|
1023
1411
|
continue;
|
|
1024
1412
|
if (!alive(row.pid)) continue;
|
|
1025
1413
|
const recordedIncarnation = row.hostIncarnation ?? row.processIncarnation;
|
|
1026
1414
|
if (recordedIncarnation === undefined) continue;
|
|
1027
|
-
const current =
|
|
1415
|
+
const current = probed.get(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`);
|
|
1028
1416
|
if (current === undefined || current !== recordedIncarnation) continue;
|
|
1029
1417
|
const unsigned: Omit<SessionIndexEvent, "checksum"> = {
|
|
1030
1418
|
version: SDK_STATE_VERSION,
|
|
@@ -1053,10 +1441,18 @@ export class SessionIndex {
|
|
|
1053
1441
|
hostUnregisteredAfter(
|
|
1054
1442
|
registration: Pick<
|
|
1055
1443
|
IndexedSession,
|
|
1056
|
-
|
|
1444
|
+
| "sessionId"
|
|
1445
|
+
| "locator"
|
|
1446
|
+
| "endpointGeneration"
|
|
1447
|
+
| "pid"
|
|
1448
|
+
| "indexSeq"
|
|
1449
|
+
| "lifecycleRequestId"
|
|
1450
|
+
| "hostIncarnation"
|
|
1451
|
+
| "processIncarnation"
|
|
1057
1452
|
>,
|
|
1058
1453
|
): { indexSeq: number; lifecycleRequestId?: string } | undefined {
|
|
1059
1454
|
const lifecycleRequestId = registration.lifecycleRequestId;
|
|
1455
|
+
const incarnation = registration.hostIncarnation ?? registration.processIncarnation;
|
|
1060
1456
|
const event = this.#events.findLast(
|
|
1061
1457
|
item =>
|
|
1062
1458
|
item.type === "host_unregistered" &&
|
|
@@ -1064,7 +1460,10 @@ export class SessionIndex {
|
|
|
1064
1460
|
item.sessionId === registration.sessionId &&
|
|
1065
1461
|
item.endpointGeneration === registration.endpointGeneration &&
|
|
1066
1462
|
item.pid === registration.pid &&
|
|
1067
|
-
(
|
|
1463
|
+
resolveEquivalentPath(item.locator.repo) === resolveEquivalentPath(registration.locator.repo) &&
|
|
1464
|
+
path.resolve(item.locator.stateRoot) === path.resolve(registration.locator.stateRoot) &&
|
|
1465
|
+
(lifecycleRequestId === undefined || item.lifecycleRequestId === lifecycleRequestId) &&
|
|
1466
|
+
(incarnation === undefined || (item.hostIncarnation ?? item.processIncarnation) === incarnation),
|
|
1068
1467
|
);
|
|
1069
1468
|
return event
|
|
1070
1469
|
? {
|