@gajae-code/coding-agent 0.18.11 → 0.19.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 +760 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/capability/rule.d.ts +7 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/append-system-prompt.d.ts +6 -0
- package/dist/types/cli/args.d.ts +7 -2
- package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
- package/dist/types/cli/customize-doctor.d.ts +4 -3
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +0 -12
- package/dist/types/cli/update-cli.d.ts +21 -3
- package/dist/types/cli-main.d.ts +0 -6
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/launch.d.ts +0 -6
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +13 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/commit/map-reduce/index.d.ts +4 -4
- package/dist/types/config/autorouting-tier-map.d.ts +53 -1
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +44 -0
- package/dist/types/config/file-lock.d.ts +52 -3
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +79 -9
- package/dist/types/config/model-registry.d.ts +72 -8
- package/dist/types/config/model-resolver.d.ts +7 -4
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +57 -4
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/customization/import.d.ts +2 -0
- package/dist/types/customization/types.d.ts +1 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/dap/session.d.ts +11 -2
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +7 -2
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/index.d.ts +5 -0
- package/dist/types/edit/modes/patch.d.ts +14 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/export/ttsr.d.ts +19 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/content.d.ts +5 -6
- package/dist/types/internal-urls/types.d.ts +2 -0
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
- package/dist/types/main.d.ts +23 -18
- package/dist/types/migrate/action-planner.d.ts +10 -1
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +42 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/hook-module.d.ts +23 -0
- package/dist/types/migrate/report.d.ts +2 -1
- package/dist/types/migrate/types.d.ts +82 -3
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +43 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/utils/host-status.d.ts +15 -11
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +3 -3
- package/dist/types/sdk/broker/broker.d.ts +29 -2
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +141 -9
- package/dist/types/sdk/broker/hop.d.ts +45 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +6 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +44 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/control/operations.d.ts +5 -1
- package/dist/types/sdk/host/events.d.ts +4 -0
- package/dist/types/sdk/host/host.d.ts +2 -1
- package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
- package/dist/types/sdk/host/session-runtime.d.ts +40 -8
- package/dist/types/sdk/host/surface-policy.d.ts +2 -1
- package/dist/types/sdk/lifecycle/service.d.ts +12 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +11 -4
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +107 -9
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +29 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +20 -4
- package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/ssh/ssh-executor.d.ts +8 -1
- package/dist/types/system-prompt.d.ts +3 -2
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/task/role-body.d.ts +11 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/awaiting-input.d.ts +28 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +17 -0
- package/dist/types/tools/bash-skill-urls.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/descriptors.d.ts +15 -2
- package/dist/types/tools/fetch.d.ts +7 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/irc.d.ts +5 -3
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +5 -0
- package/dist/types/utils/image-limits.d.ts +7 -0
- package/dist/types/utils/image-loading.d.ts +0 -1
- package/dist/types/utils/open.d.ts +0 -1
- package/dist/types/utils/pasted-image-loading.d.ts +1 -5
- package/dist/types/utils/prompt-suggestion.d.ts +2 -2
- package/dist/types/utils/shell-snapshot.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +13 -3
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/codex.d.ts +6 -7
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +10 -9
- package/scripts/acp-conformance-agent.ts +12 -2
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +38 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +58 -22
- package/src/capability/index.ts +2 -2
- package/src/capability/rule.ts +11 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/append-system-prompt.ts +12 -0
- package/src/cli/args.ts +11 -7
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/customize-doctor-precedence.ts +31 -0
- package/src/cli/customize-doctor.ts +49 -39
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -3
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +19 -6
- package/src/cli/public-command-errors.ts +6 -0
- package/src/cli/root-flags.ts +2 -4
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +471 -13
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/harness.ts +25 -15
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +277 -50
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/agentic/agent.ts +6 -6
- package/src/commit/analysis/conventional.ts +14 -5
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/map-reduce/index.ts +7 -7
- package/src/commit/message.ts +10 -3
- package/src/commit/pipeline.ts +12 -12
- package/src/config/autorouting-tier-map.ts +50 -1
- package/src/config/config-file.ts +106 -3
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +742 -0
- package/src/config/file-lock.ts +1615 -328
- package/src/config/keybindings.ts +154 -7
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +889 -148
- package/src/config/model-registry.ts +1706 -205
- package/src/config/model-resolver.ts +52 -22
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +73 -4
- package/src/config/settings.ts +460 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/coordinator-mcp/server.ts +37 -2
- package/src/crash/sanitize.ts +7 -1
- package/src/customization/import.ts +279 -8
- package/src/customization/inventory.ts +24 -8
- package/src/customization/types.ts +1 -0
- package/src/dap/client.ts +3 -2
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/dap/session.ts +161 -36
- package/src/debug/crash-diagnostics.ts +89 -6
- package/src/debug/report-bundle.ts +90 -2
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/builtin.ts +4 -3
- package/src/discovery/helpers.ts +25 -16
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +204 -22
- package/src/edit/modes/patch.ts +214 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/eval/py/runtime.ts +44 -4
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/html/index.ts +86 -6
- package/src/export/ttsr.ts +176 -1
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +10 -1
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
- package/src/extensibility/extensions/types.ts +5 -1
- package/src/extensibility/hooks/loader.ts +10 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/extensibility/plugins/marketplace/manager.ts +36 -10
- package/src/extensibility/slash-commands.ts +5 -0
- package/src/gjc-runtime/gc-runtime.ts +37 -7
- package/src/gjc-runtime/launch-tmux.ts +30 -15
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/ralplan-current-final.ts +294 -0
- package/src/gjc-runtime/ralplan-runtime.ts +95 -1
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/gjc-runtime/state-writer.ts +109 -5
- package/src/gjc-runtime/tmux-sessions.ts +93 -13
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
- package/src/gjc-runtime/ultragoal-guard.ts +9 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
- package/src/harness-control-plane/finalize.ts +11 -0
- package/src/harness-control-plane/receipt-spool.ts +105 -28
- package/src/harness-control-plane/storage.ts +124 -6
- package/src/hindsight/content.ts +116 -16
- package/src/hindsight/mental-models.ts +22 -10
- package/src/hindsight/state.ts +7 -4
- package/src/hooks/native-skill-hook.ts +17 -12
- package/src/internal-urls/artifact-protocol.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +23 -22
- package/src/internal-urls/local-protocol.ts +167 -8
- package/src/internal-urls/types.ts +2 -0
- package/src/lsp/client.ts +2 -1
- package/src/lsp/clients/biome-client.ts +2 -0
- package/src/lsp/clients/swiftlint-client.ts +2 -0
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/main.ts +330 -92
- package/src/memories/index.ts +25 -2
- package/src/migrate/action-planner.ts +574 -2
- package/src/migrate/adapters/claude-code.ts +162 -5
- package/src/migrate/adapters/codex.ts +77 -7
- package/src/migrate/adapters/index.ts +258 -5
- package/src/migrate/adapters/opencode.ts +10 -1
- package/src/migrate/executor.ts +222 -7
- package/src/migrate/hook-module.ts +193 -0
- package/src/migrate/report.ts +63 -2
- package/src/migrate/types.ts +84 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/acp-agent.ts +10 -1
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/agent-dashboard.ts +20 -4
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/customization/customization-dashboard.ts +7 -0
- package/src/modes/components/customization/import-wizard.ts +29 -3
- package/src/modes/components/model-selector.ts +157 -103
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/components/welcome.ts +3 -1
- package/src/modes/controllers/command-controller.ts +300 -9
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +31 -1
- package/src/modes/controllers/goal-mode-controller.ts +79 -10
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/plan-mode-controller.ts +66 -11
- package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
- package/src/modes/controllers/selector-controller.ts +76 -7
- package/src/modes/interactive-mode.ts +123 -4
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/theme/theme.ts +15 -0
- package/src/modes/utils/host-status.ts +21 -10
- package/src/prompts/agent-fragments/browser-aside.md +2 -2
- package/src/prompts/system/irc-incoming.md +3 -1
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +40 -3
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/project-host-values.ts +12 -8
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +194 -21
- package/src/runtime-mcp/transports/stdio.ts +2 -1
- package/src/sdk/broker/broker-exit.ts +10 -5
- package/src/sdk/broker/broker.ts +484 -85
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1214 -268
- package/src/sdk/broker/hop.ts +286 -0
- package/src/sdk/broker/lifecycle-ledger.ts +38 -11
- package/src/sdk/broker/lifecycle.ts +102 -6
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +91 -16
- package/src/sdk/broker/transport.ts +17 -1
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +460 -35
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/session-control.ts +2 -0
- package/src/sdk/bus/slack-daemon.ts +105 -9
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -2
- package/src/sdk/bus/threaded-render.ts +20 -1
- package/src/sdk/cli/session-cli.ts +23 -6
- package/src/sdk/host/control/dispatch.ts +13 -1
- package/src/sdk/host/control/operations.ts +10 -1
- package/src/sdk/host/events.ts +67 -4
- package/src/sdk/host/host.ts +8 -2
- package/src/sdk/host/prompt-image-upload.ts +438 -0
- package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
- package/src/sdk/host/session-runtime.test.ts +1737 -129
- package/src/sdk/host/session-runtime.ts +684 -83
- package/src/sdk/host/surface-policy.ts +4 -0
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
- package/src/sdk/host/websocket-transport.ts +19 -7
- package/src/sdk/lifecycle/service.ts +15 -1
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/prompt-status.ts +6 -4
- package/src/sdk/protocol/adapter-validation.ts +81 -12
- package/src/sdk/protocol/operation-inventory.generated.json +261 -2
- package/src/sdk/protocol/operation-registry.ts +33 -3
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +237 -97
- package/src/session/agent-session.ts +2726 -479
- package/src/session/history-storage.ts +182 -12
- package/src/session/internal/managed-session-scope.ts +173 -57
- package/src/session/internal/managed-session-storage.ts +85 -12
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +326 -19
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/redact.ts +53 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +2145 -206
- package/src/slash-commands/builtin-registry.ts +21 -6
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/mcp.ts +11 -3
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/ssh/ssh-executor.ts +9 -2
- package/src/stt/recorder.ts +26 -9
- package/src/system-prompt.ts +61 -16
- package/src/task/executor.ts +115 -20
- package/src/task/index.ts +389 -175
- package/src/task/role-body.ts +84 -0
- package/src/task/token-log-mode.test.ts +162 -0
- package/src/task/token-log.ts +91 -2
- package/src/tools/archive-reader.ts +650 -37
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/ast-grep.ts +7 -2
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +15 -3
- package/src/tools/awaiting-input.ts +114 -0
- package/src/tools/bash-allowed-prefixes.ts +214 -6
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash-skill-urls.ts +58 -5
- package/src/tools/bash.ts +113 -26
- package/src/tools/browser/tab-worker.ts +39 -13
- package/src/tools/browser.ts +17 -7
- package/src/tools/cron.ts +10 -1
- package/src/tools/debug.ts +16 -8
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +50 -6
- package/src/tools/eval.ts +15 -0
- package/src/tools/fetch.ts +93 -4
- package/src/tools/gh.ts +27 -12
- package/src/tools/image-gen.ts +20 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/irc.ts +5 -3
- package/src/tools/monitor.ts +9 -1
- package/src/tools/output-meta.ts +74 -5
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +26 -37
- package/src/tools/recipe/runner.ts +23 -12
- package/src/tools/render-utils.ts +2 -2
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/search.ts +3 -2
- package/src/tools/ssh.ts +274 -8
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +118 -5
- package/src/tools/tmux-self-injection-guard.ts +3301 -122
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +32 -4
- package/src/tools/ultragoal-ask-guard.ts +13 -1
- package/src/tools/write.ts +42 -3
- package/src/tui/tree-list.ts +15 -1
- package/src/utils/commit-message-generator.ts +12 -4
- package/src/utils/external-editor.ts +21 -7
- package/src/utils/file-mentions.ts +75 -7
- package/src/utils/git.ts +38 -7
- package/src/utils/image-limits.ts +8 -0
- package/src/utils/image-loading.ts +1 -1
- package/src/utils/open.ts +73 -4
- package/src/utils/pasted-image-loading.ts +9 -8
- package/src/utils/prompt-suggestion.ts +5 -4
- package/src/utils/shell-snapshot.ts +9 -1
- package/src/utils/title-generator.ts +72 -22
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/index.ts +2 -1
- package/src/web/search/providers/codex.ts +29 -32
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -3,10 +3,15 @@ import type { BigIntStats } from "node:fs";
|
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
exactRemoveDirectoryTree,
|
|
8
|
+
type NativeBrokerRestartIntent,
|
|
9
|
+
type NativeDirectoryTreeSnapshot,
|
|
10
|
+
snapshotDirectoryTree,
|
|
11
|
+
} from "@gajae-code/natives";
|
|
7
12
|
import { logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
8
13
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
9
|
-
import { FileLockAcquireError } from "../../config/file-lock";
|
|
14
|
+
import { FileLockAcquireError, withFileLock } from "../../config/file-lock";
|
|
10
15
|
import type { ModelProfileErrorDetails } from "../../config/model-profile-contract";
|
|
11
16
|
import { planLaunchWorktree } from "../../gjc-runtime/launch-worktree";
|
|
12
17
|
import { readExistingStateForMutation, withWorkflowStateLock } from "../../gjc-runtime/state-writer";
|
|
@@ -80,6 +85,7 @@ import {
|
|
|
80
85
|
type LifecycleState,
|
|
81
86
|
type TerminalReadBack,
|
|
82
87
|
} from "./lifecycle-ledger";
|
|
88
|
+
import { brokerLockHostIdentity } from "./lock-host-identity";
|
|
83
89
|
import {
|
|
84
90
|
cancelManagedTasks,
|
|
85
91
|
createManagedDomainBinding,
|
|
@@ -111,8 +117,15 @@ import {
|
|
|
111
117
|
} from "./managed-task-dag";
|
|
112
118
|
import { reconcileRunningManagedVerification, verifyManagedTaskAttempt } from "./managed-task-verification";
|
|
113
119
|
import { createMasterCapabilityVerifier, readEndpoint } from "./master-capability";
|
|
120
|
+
import { isProcessIncarnation, observeProcessIncarnation } from "./process-incarnation";
|
|
114
121
|
import { sdkInternalRuntimeImage } from "./runtime";
|
|
115
|
-
import {
|
|
122
|
+
import {
|
|
123
|
+
type IndexedSession,
|
|
124
|
+
isSessionAuthorityEligible,
|
|
125
|
+
SessionIndex,
|
|
126
|
+
type SessionList,
|
|
127
|
+
sessionIndexStartupLockOptions,
|
|
128
|
+
} from "./session-index";
|
|
116
129
|
import {
|
|
117
130
|
type ResolvedScopeV1,
|
|
118
131
|
resolveScopeRequest,
|
|
@@ -152,6 +165,21 @@ export interface BrokerSettings {
|
|
|
152
165
|
masterCapabilityVerifier?: MasterCapabilityVerifier;
|
|
153
166
|
/** Grace before an orphaned spawn child closes; schema-bounded in production. */
|
|
154
167
|
masterOrphanGraceMs?: number;
|
|
168
|
+
/**
|
|
169
|
+
* Grace before a broker nobody is left to use exits on its own. The clock only
|
|
170
|
+
* runs while the index proves no live session host and no open spawn authority,
|
|
171
|
+
* so a broker whose client died without closing exits instead of holding its
|
|
172
|
+
* port, its publication, and its lock for the life of the machine.
|
|
173
|
+
*/
|
|
174
|
+
abandonedGraceMs?: number;
|
|
175
|
+
/**
|
|
176
|
+
* Whether this broker may end itself once nobody is left to use it. Only the
|
|
177
|
+
* detached autostart entry point enables this: an operator-supervised
|
|
178
|
+
* `gjc sdk broker run` service owns its own lifetime, and its supervisor reads an
|
|
179
|
+
* owned-root completion as a clean exit, so self-reaping would quietly retire a
|
|
180
|
+
* service that was meant to keep serving.
|
|
181
|
+
*/
|
|
182
|
+
reapWhenAbandoned?: boolean;
|
|
155
183
|
/** Host model resolver override for lifecycle tests and embedders. */
|
|
156
184
|
resolveModelPin?: SdkHostModelResolver;
|
|
157
185
|
/**
|
|
@@ -189,6 +217,8 @@ type ResolvedBrokerSettings = {
|
|
|
189
217
|
spawnSubstrateProvider?: SpawnSubstrateProvider;
|
|
190
218
|
spawnPromptLayer?: SpawnPromptLayer;
|
|
191
219
|
masterOrphanGraceMs: number;
|
|
220
|
+
abandonedGraceMs: number;
|
|
221
|
+
reapWhenAbandoned: boolean;
|
|
192
222
|
restartRequestId?: string;
|
|
193
223
|
};
|
|
194
224
|
export function resolveBrokerPackageGeneration(): string {
|
|
@@ -214,6 +244,7 @@ function modelResolutionCwd(input: Record<string, unknown>): string | undefined
|
|
|
214
244
|
|
|
215
245
|
export type BrokerErrorCode =
|
|
216
246
|
| "idempotency_conflict"
|
|
247
|
+
| "legacy_lifecycle_ambiguous"
|
|
217
248
|
| "terminal_uncertain"
|
|
218
249
|
| "broker_restarting"
|
|
219
250
|
| "unavailable"
|
|
@@ -610,6 +641,7 @@ const SPAWN_HOST_REGISTRATION_TIMEOUT_MS = 10_000;
|
|
|
610
641
|
const SPAWN_HOST_REGISTRATION_POLL_MS = 50;
|
|
611
642
|
const SPAWN_PROMPT_EXCHANGE_TIMEOUT_MS = 10_000;
|
|
612
643
|
const MASTER_ORPHAN_GRACE_DEFAULT_MS = 120_000;
|
|
644
|
+
const BROKER_ABANDONED_GRACE_DEFAULT_MS = 120_000;
|
|
613
645
|
const MANAGED_CLOSE_WAIT_MS = 10_000;
|
|
614
646
|
|
|
615
647
|
function settlesWithin(promise: Promise<unknown>, timeoutMs: number): Promise<boolean> {
|
|
@@ -1088,6 +1120,13 @@ function sameSessionControlAuthority(
|
|
|
1088
1120
|
);
|
|
1089
1121
|
}
|
|
1090
1122
|
|
|
1123
|
+
const SESSION_TARGETED_LIFECYCLE_OPERATIONS: ReadonlySet<string> = new Set([
|
|
1124
|
+
"session.close",
|
|
1125
|
+
"session.resume",
|
|
1126
|
+
"session.delete",
|
|
1127
|
+
"session.reconcile_uncertain",
|
|
1128
|
+
]);
|
|
1129
|
+
|
|
1091
1130
|
function lifecycleTarget(operation: string, input: Record<string, unknown>): unknown {
|
|
1092
1131
|
const target = input.target as Record<string, unknown> | undefined;
|
|
1093
1132
|
const string = (...values: unknown[]): string | undefined =>
|
|
@@ -1163,10 +1202,29 @@ const BROKER_LOCK_RETRY_MS = 10;
|
|
|
1163
1202
|
type BrokerLockSnapshot = {
|
|
1164
1203
|
ownerId?: string;
|
|
1165
1204
|
pid: number;
|
|
1205
|
+
incarnation?: string;
|
|
1206
|
+
ownerHostId?: string;
|
|
1166
1207
|
identity: string;
|
|
1167
1208
|
lockIdentity: string;
|
|
1168
1209
|
};
|
|
1169
1210
|
|
|
1211
|
+
function isBrokerLockOwnerAlive(
|
|
1212
|
+
snapshot: Pick<BrokerLockSnapshot, "pid" | "incarnation" | "ownerHostId">,
|
|
1213
|
+
currentHostId: string | undefined,
|
|
1214
|
+
pidAlive: (pid: number) => boolean = isPidAlive,
|
|
1215
|
+
readProcessIncarnation: (pid: number) => string | undefined = brokerProcessIncarnation,
|
|
1216
|
+
): boolean {
|
|
1217
|
+
if (snapshot.pid <= 0) return false;
|
|
1218
|
+
// Local PID evidence is meaningful only when the owner record identifies this host.
|
|
1219
|
+
// Foreign and legacy host-unqualified owners are never reclaimed from this process table.
|
|
1220
|
+
if (snapshot.ownerHostId === undefined || snapshot.ownerHostId !== currentHostId) return true;
|
|
1221
|
+
if (!pidAlive(snapshot.pid)) return false;
|
|
1222
|
+
// A live PID is retained unless its recorded incarnation proves reuse.
|
|
1223
|
+
if (!snapshot.incarnation) return true;
|
|
1224
|
+
const incarnation = readProcessIncarnation(snapshot.pid);
|
|
1225
|
+
return !incarnation || incarnation === snapshot.incarnation;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1170
1228
|
/** Tombstone prefix used by {@link Broker.reclaimStaleLock} when a dead owner's lock is renamed aside. */
|
|
1171
1229
|
export const BROKER_LOCK_TOMBSTONE_PREFIX = ".broker.lock.stale-";
|
|
1172
1230
|
|
|
@@ -1214,11 +1272,12 @@ function isBrokerLockArtifactName(name: string): boolean {
|
|
|
1214
1272
|
* Decide whether one candidate directory is provably abandoned.
|
|
1215
1273
|
*
|
|
1216
1274
|
* Fail-closed by construction: every branch that cannot prove abandonment
|
|
1217
|
-
* returns a retention reason. A tombstone with an owner record is
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
*
|
|
1275
|
+
* returns a retention reason. A tombstone with an owner record is abandoned
|
|
1276
|
+
* only when its PID is dead or validated incarnation evidence proves PID reuse;
|
|
1277
|
+
* missing or unavailable evidence remains retained. A missing record is
|
|
1278
|
+
* abandoned only when the aged tombstone is empty; non-empty tombstones remain
|
|
1279
|
+
* retained as ambiguous evidence. Backup directories carry no owner contract,
|
|
1280
|
+
* so an absent record there is not ambiguity and age alone governs.
|
|
1222
1281
|
*/
|
|
1223
1282
|
async function classifyBrokerLockArtifact(
|
|
1224
1283
|
directory: string,
|
|
@@ -1226,6 +1285,8 @@ async function classifyBrokerLockArtifact(
|
|
|
1226
1285
|
now: number,
|
|
1227
1286
|
graceMs: number,
|
|
1228
1287
|
pidAlive: (pid: number) => boolean,
|
|
1288
|
+
readProcessIncarnation: (pid: number) => string | undefined,
|
|
1289
|
+
currentHostId: string | undefined,
|
|
1229
1290
|
): Promise<BrokerLockArtifactRetentionReason | "abandoned"> {
|
|
1230
1291
|
const target = path.join(directory, name);
|
|
1231
1292
|
// lstat, never stat: a symlink pointing at live state must never be followed
|
|
@@ -1248,14 +1309,26 @@ async function classifyBrokerLockArtifact(
|
|
|
1248
1309
|
return "owner-record-unreadable";
|
|
1249
1310
|
}
|
|
1250
1311
|
}
|
|
1251
|
-
let
|
|
1312
|
+
let owner: { pid?: unknown; incarnation?: unknown; ownerHostId?: unknown };
|
|
1252
1313
|
try {
|
|
1253
|
-
|
|
1314
|
+
owner = JSON.parse(raw) as { pid?: unknown; incarnation?: unknown; ownerHostId?: unknown };
|
|
1254
1315
|
} catch {
|
|
1255
1316
|
return "owner-record-unreadable";
|
|
1256
1317
|
}
|
|
1257
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
1258
|
-
|
|
1318
|
+
if (typeof owner.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0)
|
|
1319
|
+
return "owner-record-unreadable";
|
|
1320
|
+
return isBrokerLockOwnerAlive(
|
|
1321
|
+
{
|
|
1322
|
+
pid: owner.pid,
|
|
1323
|
+
incarnation: isProcessIncarnation(owner.incarnation) ? owner.incarnation : undefined,
|
|
1324
|
+
ownerHostId: typeof owner.ownerHostId === "string" ? owner.ownerHostId : undefined,
|
|
1325
|
+
},
|
|
1326
|
+
currentHostId,
|
|
1327
|
+
pidAlive,
|
|
1328
|
+
readProcessIncarnation,
|
|
1329
|
+
)
|
|
1330
|
+
? "owner-alive"
|
|
1331
|
+
: "abandoned";
|
|
1259
1332
|
}
|
|
1260
1333
|
|
|
1261
1334
|
/**
|
|
@@ -1274,11 +1347,18 @@ export async function reapStaleBrokerLockArtifacts(input: {
|
|
|
1274
1347
|
now?: number;
|
|
1275
1348
|
graceMs?: number;
|
|
1276
1349
|
pidAlive?: (pid: number) => boolean;
|
|
1350
|
+
processIncarnation?: (pid: number) => string | undefined;
|
|
1277
1351
|
}): Promise<BrokerLockArtifactReapResult> {
|
|
1278
1352
|
const directory = path.join(input.agentDir, "sdk");
|
|
1279
1353
|
const now = input.now ?? Date.now();
|
|
1280
1354
|
const graceMs = input.graceMs ?? BROKER_LOCK_ARTIFACT_GRACE_MS;
|
|
1281
1355
|
const pidAlive = input.pidAlive ?? isPidAlive;
|
|
1356
|
+
const readProcessIncarnation = input.processIncarnation ?? brokerProcessIncarnation;
|
|
1357
|
+
// Without this host's identity no host-qualified owner can be matched, so all of them are retained.
|
|
1358
|
+
const currentHostId = await brokerLockHostIdentity(input.agentDir).then(
|
|
1359
|
+
identity => identity.ownerHostId,
|
|
1360
|
+
() => undefined,
|
|
1361
|
+
);
|
|
1282
1362
|
const removed: string[] = [];
|
|
1283
1363
|
const retained: BrokerLockArtifactRetention[] = [];
|
|
1284
1364
|
const retainedByReason = new Map<BrokerLockArtifactRetentionReason, number>();
|
|
@@ -1295,7 +1375,15 @@ export async function reapStaleBrokerLockArtifacts(input: {
|
|
|
1295
1375
|
const target = path.join(directory, name);
|
|
1296
1376
|
let verdict: BrokerLockArtifactRetentionReason | "abandoned";
|
|
1297
1377
|
try {
|
|
1298
|
-
verdict = await classifyBrokerLockArtifact(
|
|
1378
|
+
verdict = await classifyBrokerLockArtifact(
|
|
1379
|
+
directory,
|
|
1380
|
+
name,
|
|
1381
|
+
now,
|
|
1382
|
+
graceMs,
|
|
1383
|
+
pidAlive,
|
|
1384
|
+
readProcessIncarnation,
|
|
1385
|
+
currentHostId,
|
|
1386
|
+
);
|
|
1299
1387
|
} catch (error) {
|
|
1300
1388
|
// A vanished candidate needs no decision; anything else is ambiguous.
|
|
1301
1389
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
@@ -1528,7 +1616,12 @@ const terminalPersistenceHooksForTest = new WeakMap<Broker, () => void>();
|
|
|
1528
1616
|
const ambiguityGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
1529
1617
|
const publicationObservationOverridesForTest = new WeakMap<Broker, BrokerPublicationObservation>();
|
|
1530
1618
|
const lockArtifactGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
1619
|
+
const brokerLockReclaimHooksForTest = new WeakMap<
|
|
1620
|
+
Broker,
|
|
1621
|
+
{ beforeQuarantine?: () => Promise<void>; onContended?: () => void }
|
|
1622
|
+
>();
|
|
1531
1623
|
const livenessGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
1624
|
+
const abandonedGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
1532
1625
|
const heartbeatStallOverridesForTest = new WeakMap<Broker, PromiseWithResolvers<void>>();
|
|
1533
1626
|
const managedCloseWaitOverridesForTest = new WeakMap<Broker, number>();
|
|
1534
1627
|
|
|
@@ -1539,6 +1632,8 @@ export class Broker {
|
|
|
1539
1632
|
discovery: BrokerDiscovery | null = null;
|
|
1540
1633
|
#lock: string;
|
|
1541
1634
|
#owner = randomBytes(12).toString("hex");
|
|
1635
|
+
#ownedLockIdentity: string | undefined;
|
|
1636
|
+
#ownedLockIncarnation: string | undefined;
|
|
1542
1637
|
/** Public publication-incarnation id, fixed at startup; never the endpoint generation. */
|
|
1543
1638
|
#diagnosticGeneration = randomBytes(16).toString("hex");
|
|
1544
1639
|
/** Trusted build snapshot captured at startup; unknown buildId stays null. */
|
|
@@ -1569,6 +1664,8 @@ export class Broker {
|
|
|
1569
1664
|
#publishedAt: bigint | null = null;
|
|
1570
1665
|
#startedAt: bigint | null = null;
|
|
1571
1666
|
#watchInFlight = false;
|
|
1667
|
+
#abandonedAt: number | null = null;
|
|
1668
|
+
#abandonedReapInFlight = false;
|
|
1572
1669
|
#checkpointInFlight = false;
|
|
1573
1670
|
#stopping = false;
|
|
1574
1671
|
#transport: BrokerTransport | null = null;
|
|
@@ -1608,6 +1705,8 @@ export class Broker {
|
|
|
1608
1705
|
spawnSubstrateProvider: settings.spawnSubstrateProvider,
|
|
1609
1706
|
spawnPromptLayer: settings.spawnPromptLayer,
|
|
1610
1707
|
masterOrphanGraceMs: settings.masterOrphanGraceMs ?? MASTER_ORPHAN_GRACE_DEFAULT_MS,
|
|
1708
|
+
abandonedGraceMs: settings.abandonedGraceMs ?? BROKER_ABANDONED_GRACE_DEFAULT_MS,
|
|
1709
|
+
reapWhenAbandoned: settings.reapWhenAbandoned === true,
|
|
1611
1710
|
...(settings.restartRequestId === undefined ? {} : { restartRequestId: settings.restartRequestId }),
|
|
1612
1711
|
};
|
|
1613
1712
|
this.index = new SessionIndex(settings.agentDir);
|
|
@@ -3581,6 +3680,96 @@ export class Broker {
|
|
|
3581
3680
|
await this.#reapSpawnOrphans();
|
|
3582
3681
|
}
|
|
3583
3682
|
|
|
3683
|
+
/**
|
|
3684
|
+
* Grace before an idle broker exits. The clock starts only after the index has
|
|
3685
|
+
* already stopped proving any host live, so this grace is time on top of that
|
|
3686
|
+
* freshness window, never a substitute for it.
|
|
3687
|
+
*/
|
|
3688
|
+
#abandonedGraceMs(): number {
|
|
3689
|
+
return abandonedGraceOverridesForTest.get(this) ?? this.settings.abandonedGraceMs;
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
/**
|
|
3693
|
+
* What the index proves about the hosts using this agent directory. Liveness is
|
|
3694
|
+
* judged on read -- fresh checkpoint, live pid, matching incarnation -- so a host
|
|
3695
|
+
* that died without closing stops proving it without help. A row that is not live
|
|
3696
|
+
* is absence only once the OS confirms the process is gone: an inconclusive
|
|
3697
|
+
* reading is never death proof on any platform, and a pid that still exists but
|
|
3698
|
+
* cannot be matched to the recorded incarnation leaves the host undecided.
|
|
3699
|
+
*/
|
|
3700
|
+
async #sessionHostLiveness(): Promise<"live" | "absent" | "unknown"> {
|
|
3701
|
+
try {
|
|
3702
|
+
await this.index.refreshIfChanged();
|
|
3703
|
+
} catch (error) {
|
|
3704
|
+
logger.warn(`sdk broker: abandonment probe could not read the session index: ${String(error)}`);
|
|
3705
|
+
return "unknown";
|
|
3706
|
+
}
|
|
3707
|
+
for (const row of this.index.listSessionIdentities()) {
|
|
3708
|
+
if (row.endpointGeneration <= 0) continue;
|
|
3709
|
+
if (row.live) return "live";
|
|
3710
|
+
// The tri-state observer keeps positive absence separate from every
|
|
3711
|
+
// inconclusive outcome, which is the difference between reaping a broker and
|
|
3712
|
+
// killing one out from under a host this platform simply cannot confirm.
|
|
3713
|
+
const observed = observeProcessIncarnation(row.pid);
|
|
3714
|
+
if (observed.status === "unknown") return "unknown";
|
|
3715
|
+
if (observed.status === "absent") continue;
|
|
3716
|
+
const recorded = row.hostIncarnation ?? row.processIncarnation;
|
|
3717
|
+
if (recorded === undefined) return "unknown";
|
|
3718
|
+
if (observed.incarnation === recorded) return "live";
|
|
3719
|
+
// The pid names another process now: the host that registered it is gone.
|
|
3720
|
+
}
|
|
3721
|
+
return "absent";
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
/** Whether any client currently holds a transport connection to this broker. */
|
|
3725
|
+
#hasAttachedClient(): boolean {
|
|
3726
|
+
return (this.#transport?.clientCount ?? 0) > 0;
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
/** Whether this broker still owns a spawn authority it must keep serving. */
|
|
3730
|
+
#hasOpenSpawnAuthority(): boolean {
|
|
3731
|
+
const store = this.#spawnAuthority;
|
|
3732
|
+
if (!store) return false;
|
|
3733
|
+
for (const claim of store.claims()) {
|
|
3734
|
+
const authority = store.authority(claim.lifecycleIdentity);
|
|
3735
|
+
if (authority && (authority.closeState === "active" || authority.closeState === "close_requested"))
|
|
3736
|
+
return true;
|
|
3737
|
+
}
|
|
3738
|
+
return false;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
/**
|
|
3742
|
+
* Ends a broker nobody is left to use. A client that dies without closing leaves
|
|
3743
|
+
* no live session host behind, and nothing else reaps this process: without this
|
|
3744
|
+
* pass it would hold its port, its publication, and its lock until the machine
|
|
3745
|
+
* reboots. The clock only runs while absence is confirmed and no client holds a
|
|
3746
|
+
* connection, and any live host, attached client, open authority, or undecided
|
|
3747
|
+
* reading disarms it, so a slow reattach never races the grace. Brokers an
|
|
3748
|
+
* operator supervises never reap themselves at all.
|
|
3749
|
+
*/
|
|
3750
|
+
async #reapSelfIfAbandoned(): Promise<void> {
|
|
3751
|
+
if (!this.settings.reapWhenAbandoned) return;
|
|
3752
|
+
if (this.#stopping || this.#completionTask !== null || this.#fenceReason !== null) return;
|
|
3753
|
+
if (this.#abandonedReapInFlight) return;
|
|
3754
|
+
this.#abandonedReapInFlight = true;
|
|
3755
|
+
try {
|
|
3756
|
+
const liveness = await this.#sessionHostLiveness();
|
|
3757
|
+
const now = Date.now();
|
|
3758
|
+
if (liveness !== "absent" || this.#hasAttachedClient() || this.#hasOpenSpawnAuthority()) {
|
|
3759
|
+
this.#abandonedAt = null;
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
if (this.#abandonedAt === null) {
|
|
3763
|
+
this.#abandonedAt = now;
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
if (now - this.#abandonedAt < this.#abandonedGraceMs()) return;
|
|
3767
|
+
await this.#complete(this.#provenOwnedRoot() ? "owned-root" : "lost-root", "abandoned");
|
|
3768
|
+
} finally {
|
|
3769
|
+
this.#abandonedReapInFlight = false;
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3584
3773
|
async #reapSpawnOrphans(): Promise<void> {
|
|
3585
3774
|
if (this.#spawnReapInFlight || this.#stopping) return;
|
|
3586
3775
|
this.#spawnReapInFlight = true;
|
|
@@ -3663,7 +3852,12 @@ export class Broker {
|
|
|
3663
3852
|
}
|
|
3664
3853
|
async #lockSnapshot(raw: string, lockIdentity: string): Promise<BrokerLockSnapshot> {
|
|
3665
3854
|
try {
|
|
3666
|
-
const lock = JSON.parse(raw) as {
|
|
3855
|
+
const lock = JSON.parse(raw) as {
|
|
3856
|
+
ownerId?: unknown;
|
|
3857
|
+
pid?: unknown;
|
|
3858
|
+
incarnation?: unknown;
|
|
3859
|
+
ownerHostId?: unknown;
|
|
3860
|
+
};
|
|
3667
3861
|
if (
|
|
3668
3862
|
typeof lock.ownerId === "string" &&
|
|
3669
3863
|
lock.ownerId.length > 0 &&
|
|
@@ -3671,7 +3865,14 @@ export class Broker {
|
|
|
3671
3865
|
Number.isInteger(lock.pid) &&
|
|
3672
3866
|
lock.pid > 0
|
|
3673
3867
|
)
|
|
3674
|
-
return {
|
|
3868
|
+
return {
|
|
3869
|
+
ownerId: lock.ownerId,
|
|
3870
|
+
pid: lock.pid,
|
|
3871
|
+
incarnation: isProcessIncarnation(lock.incarnation) ? lock.incarnation : undefined,
|
|
3872
|
+
ownerHostId: typeof lock.ownerHostId === "string" ? lock.ownerHostId : undefined,
|
|
3873
|
+
identity: `owner:${lock.ownerId}`,
|
|
3874
|
+
lockIdentity,
|
|
3875
|
+
};
|
|
3675
3876
|
} catch {}
|
|
3676
3877
|
return { pid: 0, identity: `contents:${createHash("sha256").update(raw).digest("hex")}`, lockIdentity };
|
|
3677
3878
|
}
|
|
@@ -3703,35 +3904,103 @@ export class Broker {
|
|
|
3703
3904
|
}
|
|
3704
3905
|
return this.#lockSnapshot(raw, lockIdentity);
|
|
3705
3906
|
}
|
|
3706
|
-
async #
|
|
3907
|
+
async #withLockReclaimGuard<T>(operation: () => Promise<T>): Promise<T> {
|
|
3908
|
+
const hostIdentity = await brokerLockHostIdentity(this.settings.agentDir);
|
|
3909
|
+
return withFileLock(`${this.#lock}.reclaim`, operation, {
|
|
3910
|
+
...hostIdentity,
|
|
3911
|
+
onContended: () => brokerLockReclaimHooksForTest.get(this)?.onContended?.(),
|
|
3912
|
+
});
|
|
3913
|
+
}
|
|
3914
|
+
async #hasCanonicalLockOwnership(): Promise<boolean> {
|
|
3915
|
+
if (!this.#ownedLockIdentity || !this.#ownedLockIncarnation) return false;
|
|
3916
|
+
try {
|
|
3917
|
+
const current = await this.#readLock();
|
|
3918
|
+
return (
|
|
3919
|
+
current?.identity === `owner:${this.#owner}` &&
|
|
3920
|
+
current.lockIdentity === this.#ownedLockIdentity &&
|
|
3921
|
+
current.pid === process.pid &&
|
|
3922
|
+
current.incarnation === this.#ownedLockIncarnation
|
|
3923
|
+
);
|
|
3924
|
+
} catch {
|
|
3925
|
+
return false;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
#removeCreatedLockGeneration(
|
|
3929
|
+
lockIdentity: { dev: string; ino: string },
|
|
3930
|
+
parentIdentity: { dev: bigint; ino: bigint },
|
|
3931
|
+
ownerHash?: string,
|
|
3932
|
+
): void {
|
|
3933
|
+
const captured = snapshotDirectoryTree(this.#lock);
|
|
3934
|
+
const snapshot = captured.snapshot;
|
|
3935
|
+
const root = snapshot?.entries.find(entry => entry.relativePath === "");
|
|
3936
|
+
if (
|
|
3937
|
+
!captured.ok ||
|
|
3938
|
+
!snapshot ||
|
|
3939
|
+
root?.kind !== "directory" ||
|
|
3940
|
+
snapshot.rootDev !== lockIdentity.dev ||
|
|
3941
|
+
snapshot.rootIno !== lockIdentity.ino
|
|
3942
|
+
)
|
|
3943
|
+
return;
|
|
3944
|
+
|
|
3945
|
+
const owner = snapshot.entries.find(entry => entry.relativePath === BROKER_LOCK_RECORD);
|
|
3946
|
+
const exactOwner =
|
|
3947
|
+
ownerHash !== undefined &&
|
|
3948
|
+
snapshot.entries.length === 2 &&
|
|
3949
|
+
owner?.kind === "file" &&
|
|
3950
|
+
owner.sha256 === ownerHash;
|
|
3951
|
+
if (!exactOwner && snapshot.entries.length !== 1) return;
|
|
3952
|
+
|
|
3953
|
+
exactRemoveDirectoryTree(this.#lock, snapshot, parentIdentity);
|
|
3954
|
+
}
|
|
3955
|
+
async #createLockInsideGuard(): Promise<void> {
|
|
3956
|
+
const rootDigest = await canonicalServiceRootDigest(this.settings.agentDir);
|
|
3957
|
+
const hostIdentity = await brokerLockHostIdentity(this.settings.agentDir);
|
|
3958
|
+
const parent = await fs.lstat(path.dirname(this.#lock), { bigint: true });
|
|
3707
3959
|
await fs.mkdir(this.#lock, { mode: 0o700 });
|
|
3960
|
+
let createdLockIdentity: { dev: string; ino: string } | undefined;
|
|
3961
|
+
let createdOwnerHash: string | undefined;
|
|
3708
3962
|
try {
|
|
3963
|
+
const lockStat = await fs.lstat(this.#lock, { bigint: true });
|
|
3964
|
+
if (!lockStat.isDirectory()) throw new Error("SDK broker lock is not a directory.");
|
|
3965
|
+
createdLockIdentity = { dev: String(lockStat.dev), ino: String(lockStat.ino) };
|
|
3709
3966
|
const incarnation = brokerProcessIncarnation(process.pid);
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3967
|
+
if (!incarnation) throw new Error("Broker process incarnation is unavailable.");
|
|
3968
|
+
const ownerRecord = JSON.stringify({
|
|
3969
|
+
version: 1,
|
|
3970
|
+
ownerId: this.#owner,
|
|
3971
|
+
pid: process.pid,
|
|
3972
|
+
acquiredAt: Date.now(),
|
|
3973
|
+
incarnation,
|
|
3974
|
+
ownerHostId: hostIdentity.ownerHostId,
|
|
3975
|
+
rootDigest,
|
|
3976
|
+
});
|
|
3977
|
+
createdOwnerHash = createHash("sha256").update(ownerRecord).digest("hex");
|
|
3978
|
+
await fs.writeFile(this.#lockRecordPath(), ownerRecord, { flag: "wx", mode: 0o600 });
|
|
3979
|
+
const current = await this.#readLock();
|
|
3980
|
+
if (
|
|
3981
|
+
!current ||
|
|
3982
|
+
current.ownerId !== this.#owner ||
|
|
3983
|
+
current.pid !== process.pid ||
|
|
3984
|
+
current.incarnation !== incarnation
|
|
3985
|
+
)
|
|
3986
|
+
throw new Error("SDK broker lock ownership changed during startup.");
|
|
3987
|
+
this.#ownedLockIdentity = current.lockIdentity;
|
|
3988
|
+
this.#ownedLockIncarnation = incarnation;
|
|
3989
|
+
} catch (error) {
|
|
3729
3990
|
try {
|
|
3730
|
-
|
|
3991
|
+
if (createdLockIdentity)
|
|
3992
|
+
this.#removeCreatedLockGeneration(
|
|
3993
|
+
createdLockIdentity,
|
|
3994
|
+
{ dev: parent.dev, ino: parent.ino },
|
|
3995
|
+
createdOwnerHash,
|
|
3996
|
+
);
|
|
3731
3997
|
} catch {}
|
|
3732
|
-
throw
|
|
3998
|
+
throw error;
|
|
3733
3999
|
}
|
|
3734
4000
|
}
|
|
4001
|
+
async #createLock(): Promise<void> {
|
|
4002
|
+
await this.#withLockReclaimGuard(() => this.#createLockInsideGuard());
|
|
4003
|
+
}
|
|
3735
4004
|
async #waitForBrokerDiscovery(): Promise<BrokerDiscovery | null> {
|
|
3736
4005
|
const deadline = Date.now() + BROKER_LOCK_STARTUP_WAIT_MS;
|
|
3737
4006
|
while (Date.now() < deadline) {
|
|
@@ -3741,50 +4010,63 @@ export class Broker {
|
|
|
3741
4010
|
}
|
|
3742
4011
|
return readBrokerDiscovery(this.settings.agentDir, this.settings.heartbeatTtlMs);
|
|
3743
4012
|
}
|
|
3744
|
-
async #reclaimStaleLock(snapshot: BrokerLockSnapshot): Promise<
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
4013
|
+
async #reclaimStaleLock(snapshot: BrokerLockSnapshot): Promise<boolean> {
|
|
4014
|
+
return await this.#withLockReclaimGuard(async () => {
|
|
4015
|
+
const current = await this.#readLock();
|
|
4016
|
+
const hostIdentity = await brokerLockHostIdentity(this.settings.agentDir);
|
|
4017
|
+
if (
|
|
4018
|
+
!current ||
|
|
4019
|
+
current.identity !== snapshot.identity ||
|
|
4020
|
+
current.lockIdentity !== snapshot.lockIdentity ||
|
|
4021
|
+
isBrokerLockOwnerAlive(current, hostIdentity.ownerHostId)
|
|
4022
|
+
)
|
|
4023
|
+
return false;
|
|
4024
|
+
await brokerLockReclaimHooksForTest.get(this)?.beforeQuarantine?.();
|
|
4025
|
+
|
|
4026
|
+
// Keep validation, quarantine, and successor lock publication under one
|
|
4027
|
+
// cross-process fence so a stale snapshot can never move a newer lock.
|
|
4028
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4029
|
+
const tombstone = path.join(
|
|
4030
|
+
path.dirname(this.#lock),
|
|
4031
|
+
`.broker.lock.stale-${createHash("sha256").update(snapshot.lockIdentity).digest("hex")}-${randomBytes(8).toString("hex")}`,
|
|
4032
|
+
);
|
|
4033
|
+
try {
|
|
4034
|
+
await fs.rename(this.#lock, tombstone);
|
|
4035
|
+
} catch (error) {
|
|
4036
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
4037
|
+
if (code === "EEXIST" || code === "ENOTEMPTY") continue;
|
|
4038
|
+
if (["ENOENT", "EISDIR", "ENOTDIR"].includes(code ?? "")) return false;
|
|
4039
|
+
if (code === "EPERM") {
|
|
4040
|
+
try {
|
|
4041
|
+
await fs.lstat(tombstone);
|
|
4042
|
+
continue;
|
|
4043
|
+
} catch (statError) {
|
|
4044
|
+
if ((statError as NodeJS.ErrnoException).code !== "ENOENT") throw statError;
|
|
4045
|
+
}
|
|
3777
4046
|
}
|
|
4047
|
+
throw error;
|
|
4048
|
+
}
|
|
4049
|
+
try {
|
|
4050
|
+
await this.#createLockInsideGuard();
|
|
4051
|
+
return true;
|
|
4052
|
+
} catch (error) {
|
|
4053
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
|
|
4054
|
+
throw error;
|
|
3778
4055
|
}
|
|
3779
|
-
throw e;
|
|
3780
4056
|
}
|
|
3781
|
-
|
|
3782
|
-
|
|
4057
|
+
throw new Error(`Broker lock quarantine namespace is saturated for ${this.#lock}`);
|
|
4058
|
+
});
|
|
3783
4059
|
}
|
|
3784
4060
|
async #releaseOwnedLock(): Promise<void> {
|
|
3785
4061
|
try {
|
|
3786
4062
|
const lock = await this.#readLock();
|
|
3787
|
-
if (
|
|
4063
|
+
if (
|
|
4064
|
+
!lock ||
|
|
4065
|
+
lock.ownerId !== this.#owner ||
|
|
4066
|
+
lock.lockIdentity !== this.#ownedLockIdentity ||
|
|
4067
|
+
lock.incarnation !== this.#ownedLockIncarnation
|
|
4068
|
+
)
|
|
4069
|
+
return;
|
|
3788
4070
|
await fs.unlink(this.#lockRecordPath());
|
|
3789
4071
|
await fs.rmdir(this.#lock);
|
|
3790
4072
|
} catch (error) {
|
|
@@ -3833,8 +4115,15 @@ export class Broker {
|
|
|
3833
4115
|
this.#ambiguousAt = null;
|
|
3834
4116
|
this.#publishedAt = null;
|
|
3835
4117
|
this.#startedAt = process.hrtime.bigint();
|
|
4118
|
+
this.#ownedLockIdentity = undefined;
|
|
4119
|
+
this.#ownedLockIncarnation = undefined;
|
|
3836
4120
|
this.#watchInFlight = false;
|
|
3837
4121
|
this.#checkpointInFlight = false;
|
|
4122
|
+
// A restarted instance is a new generation: it holds none of the state the
|
|
4123
|
+
// previous one stopped with, so the abandonment clock it armed (and any reap
|
|
4124
|
+
// still unwinding) must not survive into the generation clients reconnect to.
|
|
4125
|
+
this.#abandonedAt = null;
|
|
4126
|
+
this.#abandonedReapInFlight = false;
|
|
3838
4127
|
this.#throwIfStartupAborted();
|
|
3839
4128
|
await Promise.all([this.ledger.assertSupportedStateVersions(), readBrokerDiscovery(this.settings.agentDir)]);
|
|
3840
4129
|
this.#throwIfStartupAborted();
|
|
@@ -3861,7 +4150,8 @@ export class Broker {
|
|
|
3861
4150
|
}
|
|
3862
4151
|
const snapshot = await this.#readLock();
|
|
3863
4152
|
if (!snapshot) continue;
|
|
3864
|
-
|
|
4153
|
+
const hostIdentity = await brokerLockHostIdentity(this.settings.agentDir);
|
|
4154
|
+
if (isBrokerLockOwnerAlive(snapshot, hostIdentity.ownerHostId)) {
|
|
3865
4155
|
const starting = await this.#waitForBrokerDiscovery();
|
|
3866
4156
|
if (starting) {
|
|
3867
4157
|
logger.info(
|
|
@@ -3872,22 +4162,59 @@ export class Broker {
|
|
|
3872
4162
|
return starting;
|
|
3873
4163
|
}
|
|
3874
4164
|
const current = await this.#readLock();
|
|
3875
|
-
if (
|
|
4165
|
+
if (
|
|
4166
|
+
current &&
|
|
4167
|
+
current.identity === snapshot.identity &&
|
|
4168
|
+
isBrokerLockOwnerAlive(current, hostIdentity.ownerHostId)
|
|
4169
|
+
) {
|
|
4170
|
+
const hostUnqualified = current.ownerHostId === undefined;
|
|
4171
|
+
const foreignHostOwner = !hostUnqualified && current.ownerHostId !== hostIdentity.ownerHostId;
|
|
4172
|
+
let refusal: string;
|
|
4173
|
+
let errorMessage: string;
|
|
4174
|
+
if (hostUnqualified) {
|
|
4175
|
+
refusal = `owner pid ${current.pid} has no host identity; automatic recovery is unsafe for shared agent directories. Stop every broker using this agent directory on all hosts, then follow https://github.com/Yeachan-Heo/gajae-code/blob/dev/docs/sdk.md#recovering-legacy-broker-locks`;
|
|
4176
|
+
errorMessage = `Broker lock owner is host-unqualified (pid ${current.pid}); refusing automatic recovery because its host cannot be verified. Stop every broker using this agent directory on all hosts and follow https://github.com/Yeachan-Heo/gajae-code/blob/dev/docs/sdk.md#recovering-legacy-broker-locks`;
|
|
4177
|
+
} else if (foreignHostOwner) {
|
|
4178
|
+
refusal = `owner pid ${current.pid} is attributed to another host; its liveness cannot be verified here`;
|
|
4179
|
+
errorMessage = `Broker lock owner is attributed to a different host (pid ${current.pid}); refusing automatic recovery because its liveness cannot be verified from this host.`;
|
|
4180
|
+
} else {
|
|
4181
|
+
refusal = `live pid ${current.pid} has published no discovery record`;
|
|
4182
|
+
errorMessage = `Broker lock is held by a live owner (pid ${current.pid})`;
|
|
4183
|
+
}
|
|
3876
4184
|
logger.warn(
|
|
3877
|
-
`sdk broker: lock contention, refusing to start because ${this.#lock} is held by
|
|
4185
|
+
`sdk broker: lock contention, refusing to start because ${this.#lock} is held by ${refusal}`,
|
|
3878
4186
|
);
|
|
3879
|
-
throw new Error(
|
|
4187
|
+
throw new Error(errorMessage);
|
|
3880
4188
|
}
|
|
3881
4189
|
continue;
|
|
3882
4190
|
}
|
|
3883
|
-
await this.#reclaimStaleLock(snapshot);
|
|
4191
|
+
if (await this.#reclaimStaleLock(snapshot)) break;
|
|
3884
4192
|
}
|
|
3885
4193
|
this.#throwIfStartupAborted();
|
|
3886
4194
|
// Only the lock holder reaps, so concurrent brokers cannot race the removal.
|
|
3887
4195
|
await this.#reapLockArtifacts();
|
|
3888
4196
|
try {
|
|
3889
4197
|
this.#throwIfStartupAborted();
|
|
3890
|
-
|
|
4198
|
+
// Recheck under the broker ownership lock: a restart can be committed while
|
|
4199
|
+
// an ordinary startup is waiting to acquire this lock.
|
|
4200
|
+
const restartIntent = await readBrokerRestartIntent(this.settings.agentDir);
|
|
4201
|
+
const activeCommittedIntent =
|
|
4202
|
+
restartIntent?.phase === "committed" && restartIntent.expiresAt > Date.now() ? restartIntent : undefined;
|
|
4203
|
+
if (
|
|
4204
|
+
this.settings.restartRequestId === undefined
|
|
4205
|
+
? activeCommittedIntent !== undefined
|
|
4206
|
+
: activeCommittedIntent?.requestId !== this.settings.restartRequestId
|
|
4207
|
+
)
|
|
4208
|
+
throw new Error("A committed SDK broker restart is active; startup requires its exact request ID.");
|
|
4209
|
+
await this.index.open(
|
|
4210
|
+
this.#startupCheckpointDeadline === undefined
|
|
4211
|
+
? undefined
|
|
4212
|
+
: sessionIndexStartupLockOptions(
|
|
4213
|
+
this.#startupCheckpointDeadline,
|
|
4214
|
+
this.#startupAbortSignal,
|
|
4215
|
+
this.settings.restartRequestId === undefined ? "ordinary" : "authorized-successor",
|
|
4216
|
+
),
|
|
4217
|
+
);
|
|
3891
4218
|
await this.ledger.open();
|
|
3892
4219
|
const brokerIdentityKey = await getBrokerIdentityKey(this.settings.agentDir);
|
|
3893
4220
|
this.#spawnAuthority = new SpawnAuthorityStore(this.settings.agentDir, brokerIdentityKey);
|
|
@@ -3950,6 +4277,8 @@ export class Broker {
|
|
|
3950
4277
|
await this.#startupPrePublicationTestHook?.();
|
|
3951
4278
|
if (this.#startupPrePublicationDelayMs > 0) await Bun.sleep(this.#startupPrePublicationDelayMs);
|
|
3952
4279
|
this.#throwIfStartupAborted();
|
|
4280
|
+
if (!(await this.#hasCanonicalLockOwnership()))
|
|
4281
|
+
throw new Error("SDK broker lock ownership changed before discovery publication.");
|
|
3953
4282
|
this.#publication = await publishBrokerDiscovery(
|
|
3954
4283
|
this.settings.agentDir,
|
|
3955
4284
|
this.discovery,
|
|
@@ -3968,6 +4297,7 @@ export class Broker {
|
|
|
3968
4297
|
this.#heartbeatTimer = setInterval(() => {
|
|
3969
4298
|
void this.#watchPublication();
|
|
3970
4299
|
void this.#reapSpawnOrphans();
|
|
4300
|
+
void this.#reapSelfIfAbandoned();
|
|
3971
4301
|
}, cadenceMs);
|
|
3972
4302
|
if (this.#startupPostPublicationDelayMs > 0) await Bun.sleep(this.#startupPostPublicationDelayMs);
|
|
3973
4303
|
// This process is now the independently verified successor: its own
|
|
@@ -4017,6 +4347,10 @@ export class Broker {
|
|
|
4017
4347
|
this.discovery?.ownerId === this.#owner
|
|
4018
4348
|
);
|
|
4019
4349
|
}
|
|
4350
|
+
/** Whether this broker completed its initial retained discovery publication. */
|
|
4351
|
+
get hasPublishedDiscovery(): boolean {
|
|
4352
|
+
return this.#publishedAt !== null;
|
|
4353
|
+
}
|
|
4020
4354
|
get completion(): Promise<void> {
|
|
4021
4355
|
return this.#completion;
|
|
4022
4356
|
}
|
|
@@ -4851,7 +5185,19 @@ export class Broker {
|
|
|
4851
5185
|
}
|
|
4852
5186
|
|
|
4853
5187
|
// Wire requests are readiness-gated by BrokerTransport before reaching this dispatcher.
|
|
4854
|
-
handleRequest(
|
|
5188
|
+
async handleRequest(
|
|
5189
|
+
operation: string,
|
|
5190
|
+
input: Record<string, unknown>,
|
|
5191
|
+
idempotencyKey?: string,
|
|
5192
|
+
): Promise<BrokerResponse> {
|
|
5193
|
+
if (
|
|
5194
|
+
this.#publication !== null &&
|
|
5195
|
+
this.#publicationState === "healthy-owned" &&
|
|
5196
|
+
!(await this.#hasCanonicalLockOwnership())
|
|
5197
|
+
) {
|
|
5198
|
+
this.#fence("suspect-unpublished");
|
|
5199
|
+
return error("unavailable", "broker lock ownership is not held");
|
|
5200
|
+
}
|
|
4855
5201
|
if (operation === "broker.status") return Promise.resolve({ ok: true, result: this.status() });
|
|
4856
5202
|
if (operation === "broker.prepare_restart")
|
|
4857
5203
|
return this.prepareRestart(input as unknown as BrokerRestartPrepareOptions).then(
|
|
@@ -4876,6 +5222,32 @@ export class Broker {
|
|
|
4876
5222
|
this.#admitted.delete(admission);
|
|
4877
5223
|
});
|
|
4878
5224
|
}
|
|
5225
|
+
/**
|
|
5226
|
+
* Session-targeted lifecycle requests may pass terminal legacy rows recorded
|
|
5227
|
+
* for other sessions. A row equal to this key's legacy target identity for its
|
|
5228
|
+
* recorded session proves cross-target key reuse and keeps the fence; close
|
|
5229
|
+
* targets are reconstructed sessionId-only because legacy close rows may lack
|
|
5230
|
+
* endpoint authority.
|
|
5231
|
+
*/
|
|
5232
|
+
async #legacySessionTarget(
|
|
5233
|
+
operation: string,
|
|
5234
|
+
input: Record<string, unknown>,
|
|
5235
|
+
idempotencyKey: string,
|
|
5236
|
+
): Promise<{ sessionId: string; keyReuseIdentities: ReadonlySet<string> } | undefined> {
|
|
5237
|
+
if (!SESSION_TARGETED_LIFECYCLE_OPERATIONS.has(operation)) return undefined;
|
|
5238
|
+
const { sessionId } = lifecycleTarget(operation, input) as { sessionId?: string };
|
|
5239
|
+
if (sessionId === undefined) return undefined;
|
|
5240
|
+
const keyReuseIdentities = new Set<string>();
|
|
5241
|
+
for (const recordedSessionId of this.ledger.terminalLegacySessionIds()) {
|
|
5242
|
+
const recordedTarget = createHash("sha256")
|
|
5243
|
+
.update(canonicalJson(lifecycleTarget(operation, { sessionId: recordedSessionId })))
|
|
5244
|
+
.digest("hex");
|
|
5245
|
+
keyReuseIdentities.add(
|
|
5246
|
+
await deriveLegacyTargetIdentity(this.settings.agentDir, operation, idempotencyKey, recordedTarget),
|
|
5247
|
+
);
|
|
5248
|
+
}
|
|
5249
|
+
return { sessionId, keyReuseIdentities };
|
|
5250
|
+
}
|
|
4879
5251
|
async #handleRequest(
|
|
4880
5252
|
operation: string,
|
|
4881
5253
|
input: Record<string, unknown>,
|
|
@@ -5094,10 +5466,13 @@ export class Broker {
|
|
|
5094
5466
|
} else if (
|
|
5095
5467
|
this.ledger.hasLegacyIdentity(
|
|
5096
5468
|
new Set(this.#spawnAuthority?.claims().map(claim => claim.lifecycleIdentity) ?? []),
|
|
5097
|
-
{
|
|
5469
|
+
{
|
|
5470
|
+
ignoreFinalRows: operation === "session.create",
|
|
5471
|
+
sessionTarget: await this.#legacySessionTarget(operation, input, idempotencyKey),
|
|
5472
|
+
},
|
|
5098
5473
|
)
|
|
5099
5474
|
) {
|
|
5100
|
-
return error("
|
|
5475
|
+
return error("legacy_lifecycle_ambiguous", "legacy lifecycle request has an ambiguous target");
|
|
5101
5476
|
}
|
|
5102
5477
|
}
|
|
5103
5478
|
}
|
|
@@ -5173,6 +5548,9 @@ export class Broker {
|
|
|
5173
5548
|
const begun = await this.ledger.begin(identity, requestHash, metadata);
|
|
5174
5549
|
if (begun.kind === "replay") {
|
|
5175
5550
|
const replay = begun.entry.response as BrokerResponse;
|
|
5551
|
+
// Proven errors are immutable even when their response carries cleanup
|
|
5552
|
+
// metadata. Only unproven cleanup outcomes may execute again.
|
|
5553
|
+
if (begun.entry.state === "terminal_error") return replay;
|
|
5176
5554
|
const cleanup = cleanupFromResponse(replay) ?? reconstructedDeleteCleanup;
|
|
5177
5555
|
if (!cleanup) {
|
|
5178
5556
|
if (
|
|
@@ -5217,7 +5595,7 @@ export class Broker {
|
|
|
5217
5595
|
const outcome = await executeLifecycle(this, operation, input, identity, cleanup);
|
|
5218
5596
|
const response = outcome.response;
|
|
5219
5597
|
const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
|
|
5220
|
-
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
5598
|
+
const recorded = await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
5221
5599
|
...(operation === "session.delete" &&
|
|
5222
5600
|
typeof input.sessionId === "string" &&
|
|
5223
5601
|
lifecycleResponseState(response) === "terminal_uncertain" &&
|
|
@@ -5229,6 +5607,10 @@ export class Broker {
|
|
|
5229
5607
|
...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
|
|
5230
5608
|
...(outcome.startupFailure ? { startupFailure: outcome.startupFailure } : {}),
|
|
5231
5609
|
});
|
|
5610
|
+
// A late transition may have proven a terminal result already. The ledger
|
|
5611
|
+
// preserves that row; return it instead of the freshly computed response.
|
|
5612
|
+
if (recorded.state === "terminal_error" && recorded.response !== undefined)
|
|
5613
|
+
return recorded.response as BrokerResponse;
|
|
5232
5614
|
return response;
|
|
5233
5615
|
}
|
|
5234
5616
|
if (begun.kind === "idempotency_conflict")
|
|
@@ -5241,7 +5623,7 @@ export class Broker {
|
|
|
5241
5623
|
const outcome = await executeLifecycle(this, operation, input, identity, cleanup);
|
|
5242
5624
|
const response = outcome.response;
|
|
5243
5625
|
const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
|
|
5244
|
-
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
5626
|
+
const recorded = await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
5245
5627
|
...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
|
|
5246
5628
|
...(operation === "session.delete" &&
|
|
5247
5629
|
typeof input.sessionId === "string" &&
|
|
@@ -5254,6 +5636,8 @@ export class Broker {
|
|
|
5254
5636
|
...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
|
|
5255
5637
|
...(outcome.startupFailure ? { startupFailure: outcome.startupFailure } : {}),
|
|
5256
5638
|
});
|
|
5639
|
+
if (recorded.state === "terminal_error" && recorded.response !== undefined)
|
|
5640
|
+
return recorded.response as BrokerResponse;
|
|
5257
5641
|
return response;
|
|
5258
5642
|
}
|
|
5259
5643
|
if (begun.kind === "in_progress") return error("broker_restarting", "lifecycle operation is in progress");
|
|
@@ -5341,6 +5725,21 @@ export function setLockArtifactGraceForTest(broker: Broker, graceMs: number | un
|
|
|
5341
5725
|
else lockArtifactGraceOverridesForTest.set(broker, graceMs);
|
|
5342
5726
|
}
|
|
5343
5727
|
|
|
5728
|
+
/** Test-only barriers for exercising the cross-process broker-lock reclaim fence. */
|
|
5729
|
+
export function setBrokerLockReclaimHooksForTest(
|
|
5730
|
+
broker: Broker,
|
|
5731
|
+
hooks: { beforeQuarantine?: () => Promise<void>; onContended?: () => void } | undefined,
|
|
5732
|
+
): void {
|
|
5733
|
+
if (hooks === undefined) brokerLockReclaimHooksForTest.delete(broker);
|
|
5734
|
+
else brokerLockReclaimHooksForTest.set(broker, hooks);
|
|
5735
|
+
}
|
|
5736
|
+
|
|
5737
|
+
/** Test-only hook for shortening the bounded abandonment grace. */
|
|
5738
|
+
export function setAbandonedGraceForTest(broker: Broker, graceMs: number | undefined): void {
|
|
5739
|
+
if (graceMs === undefined) abandonedGraceOverridesForTest.delete(broker);
|
|
5740
|
+
else abandonedGraceOverridesForTest.set(broker, graceMs);
|
|
5741
|
+
}
|
|
5742
|
+
|
|
5344
5743
|
/** Test-only hook for shortening the bounded liveness deadline. */
|
|
5345
5744
|
export function setLivenessGraceForTest(broker: Broker, graceMs: number | undefined): void {
|
|
5346
5745
|
if (graceMs === undefined) livenessGraceOverridesForTest.delete(broker);
|