@sayknow-cli/coding-agent 0.3.16 → 0.4.1
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 +52 -0
- package/package.json +133 -116
- package/scripts/build-sdk-package-smoke.ts +132 -0
- package/scripts/capture-notifications-settings-showcase.ts +435 -0
- package/scripts/compile-args.ts +5 -2
- package/scripts/g005-daemon-smoke.ts +4 -5
- package/scripts/g011-daemon-path-smoke.ts +5 -5
- package/scripts/generate-hotkeys-docs.ts +74 -0
- package/scripts/generate-sdk-adapter-parity-manifest.ts +163 -0
- package/scripts/generate-sdk-operation-inventory.ts +914 -0
- package/scripts/generate-telegram-baseline-manifest.ts +128 -0
- package/scripts/manifest-schema.ts +124 -0
- package/scripts/run-test-manifest.ts +153 -0
- package/scripts/run-threat-model-check.ts +217 -0
- package/scripts/verify-skc-sdk-canonicalization.ts +3647 -0
- package/scripts/verify-skc-sdk-rename.ts +90 -0
- package/src/append-only-mode.ts +1 -1
- package/src/async/job-manager.ts +455 -69
- package/src/cli/args.ts +43 -12
- package/src/cli/auth-gateway-cli.ts +2 -2
- package/src/cli/config-cli.ts +77 -3
- package/src/cli/daemon-cli.ts +63 -14
- package/src/cli/launch-disposition.ts +42 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/notify-cli.ts +373 -283
- package/src/cli/session-picker.ts +20 -3
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +31 -11
- package/src/commands/config.ts +1 -1
- package/src/commands/daemon.ts +50 -6
- package/src/commands/gc.ts +11 -1
- package/src/commands/harness.ts +127 -142
- package/src/commands/launch.ts +11 -6
- package/src/commands/mcp-serve.ts +97 -13
- package/src/commands/mcp.ts +2 -2
- package/src/commands/notify.ts +44 -7
- package/src/commands/sdk.ts +453 -0
- package/src/commands/ultragoal.ts +4 -1
- package/src/commands/worktree.ts +10 -53
- package/src/commit/agentic/agent.ts +1 -0
- package/src/config/atomic-yaml-patch.ts +490 -0
- package/src/config/file-lock.ts +105 -43
- package/src/config/keybindings.ts +64 -20
- package/src/config/model-auth.ts +9 -0
- package/src/config/model-bindings-applier.ts +139 -0
- package/src/config/model-discovery-manager.ts +224 -0
- package/src/config/model-equivalence.ts +66 -0
- package/src/config/model-profile-activation.ts +233 -86
- package/src/config/model-profiles.ts +52 -49
- package/src/config/model-registry.ts +5 -4
- package/src/config/model-resolver.ts +487 -410
- package/src/config/model-selector-value.ts +50 -0
- package/src/config/models-config-schema.ts +17 -23
- package/src/config/settings-schema.ts +372 -21
- package/src/config/settings.ts +1120 -182
- package/src/coordinator-mcp/model-preset.ts +74 -20
- package/src/coordinator-mcp/policy.ts +76 -11
- package/src/coordinator-mcp/question-gate-codec.ts +274 -0
- package/src/coordinator-mcp/question-state.ts +778 -0
- package/src/coordinator-mcp/server.ts +3120 -3111
- package/src/daemon/builtin.ts +8 -5
- package/src/daemon/control-types.ts +1 -1
- package/src/daemon/runtime.ts +2 -2
- package/src/debug/index.ts +1 -1
- package/src/defaults/skc/skills/team/SKILL.md +27 -13
- package/src/defaults/skc/skills/ultragoal/SKILL.md +0 -2
- package/src/discovery/helpers.ts +1 -50
- package/src/discovery/mcp-json.ts +327 -20
- package/src/edit/index.ts +64 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/env.ts +26 -0
- package/src/eval/py/index.ts +2 -2
- package/src/eval/py/kernel.ts +5 -3
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +52 -18
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/compact-handler.ts +3 -3
- package/src/extensibility/extensions/get-commands-handler.ts +2 -2
- package/src/extensibility/extensions/loader.ts +77 -5
- package/src/extensibility/extensions/runner.ts +124 -7
- package/src/extensibility/extensions/types.ts +215 -18
- package/src/extensibility/hooks/runner.ts +2 -2
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +94 -10
- package/src/extensibility/runtime-skill-discovery.ts +33 -13
- package/src/extensibility/shared-events.ts +2 -0
- package/src/extensibility/skc-plugins/installer.ts +16 -16
- package/src/extensibility/slash-commands.ts +3 -3
- package/src/goals/continuation-timeout-guard.ts +79 -0
- package/src/goals/runtime.ts +7 -5
- package/src/goals/state.ts +18 -0
- package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
- package/src/harness-control-plane/operate.ts +9 -9
- package/src/harness-control-plane/owner.ts +341 -41
- package/src/harness-control-plane/phase-rollup.ts +4 -7
- package/src/harness-control-plane/receipt-ingest.ts +5 -0
- package/src/harness-control-plane/receipt-spool.ts +51 -7
- package/src/harness-control-plane/receipts.ts +0 -1
- package/src/harness-control-plane/sdk-transport.ts +395 -0
- package/src/harness-control-plane/session-lease.ts +12 -130
- package/src/harness-control-plane/session-transport.ts +94 -0
- package/src/harness-control-plane/types.ts +1 -1
- package/src/hindsight/backend.ts +26 -27
- package/src/hindsight/client.ts +65 -2
- package/src/hindsight/state.ts +101 -32
- package/src/hooks/skill-state.ts +17 -11
- package/src/i18n/messages/en.ts +1 -0
- package/src/internal-urls/agent-protocol.ts +76 -5
- package/src/internal-urls/docs-index.generated.ts +44 -37
- package/src/internal-urls/local-protocol.ts +420 -18
- package/src/lsp/client.ts +43 -7
- package/src/lsp/config.ts +145 -26
- package/src/lsp/index.ts +7 -91
- package/src/lsp/lspmux.ts +39 -10
- package/src/lsp/path-trust.ts +81 -0
- package/src/main.ts +451 -236
- package/src/memories/index.ts +1 -0
- package/src/memories/storage.ts +6 -2
- package/src/modes/DESIGN.md +258 -0
- package/src/modes/acp/acp-agent.ts +1236 -2612
- package/src/modes/acp/acp-client-bridge.ts +2 -20
- package/src/modes/acp/acp-event-mapper.ts +18 -2
- package/src/modes/acp/acp-mode.ts +8 -36
- package/src/modes/acp/permission-mode.ts +25 -0
- package/src/modes/acp/startup-options.ts +6 -0
- package/src/modes/action-registry.ts +165 -0
- package/src/modes/bridge/bridge-mode.ts +6 -4
- package/src/modes/components/agent-dashboard.ts +37 -17
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/chrome.ts +40 -0
- package/src/modes/components/command-palette.ts +168 -0
- package/src/modes/components/custom-editor.ts +105 -33
- package/src/modes/components/custom-provider-wizard.ts +77 -15
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/model-selector.ts +93 -35
- package/src/modes/components/notifications-settings-editor.ts +1403 -0
- package/src/modes/components/pet-capability.ts +115 -0
- package/src/modes/components/pet-selector.ts +15 -23
- package/src/modes/components/plan-preview-overlay.ts +295 -0
- package/src/modes/components/queue-mode-selector.ts +10 -27
- package/src/modes/components/queue-pane.ts +79 -0
- package/src/modes/components/read-tool-group.ts +13 -0
- package/src/modes/components/sayknow-pet-widget.ts +169 -20
- package/src/modes/components/session-observer-overlay.ts +213 -775
- package/src/modes/components/sessions-dashboard.ts +181 -0
- package/src/modes/components/settings-selector.ts +89 -12
- package/src/modes/components/show-images-selector.ts +10 -24
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +1 -969
- package/src/modes/components/tasks-pane.ts +73 -0
- package/src/modes/components/theme-selector.ts +11 -31
- package/src/modes/components/tool-execution.ts +22 -0
- package/src/modes/components/tool-status-header.ts +1056 -0
- package/src/modes/components/tool-transcript-format.ts +393 -0
- package/src/modes/components/transcript-viewer-overlay.ts +446 -0
- package/src/modes/components/welcome.ts +4 -4
- package/src/modes/controllers/btw-controller.ts +1 -0
- package/src/modes/controllers/command-controller.ts +25 -17
- package/src/modes/controllers/event-controller.ts +14 -18
- package/src/modes/controllers/extension-ui-controller.ts +441 -37
- package/src/modes/controllers/goal-mode-controller.ts +493 -0
- package/src/modes/controllers/input-controller.ts +889 -263
- package/src/modes/controllers/mode-gate.ts +24 -0
- package/src/modes/controllers/plan-mode-controller.ts +519 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +957 -54
- package/src/modes/controllers/stt-controller.ts +125 -0
- package/src/modes/index.ts +0 -22
- package/src/modes/interactive-mode.ts +376 -1130
- package/src/modes/jobs-observer.ts +3 -0
- package/src/modes/prompt-action-autocomplete.ts +7 -0
- package/src/modes/rpc/rpc-mode.ts +3 -1
- package/src/modes/runtime-init.ts +320 -6
- package/src/modes/shared/agent-wire/approval-gate.ts +9 -11
- package/src/modes/shared/agent-wire/command-dispatch.ts +3 -1
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +65 -111
- package/src/modes/shared/agent-wire/event-contract.ts +7 -8
- package/src/modes/shared/agent-wire/event-envelope.ts +9 -17
- package/src/modes/shared/agent-wire/event-observation.ts +16 -7
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +1331 -89
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +19 -17
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +407 -0
- package/src/modes/tasks-aggregator.ts +199 -0
- package/src/modes/transcript-item-registry.ts +229 -0
- package/src/modes/types.ts +63 -21
- package/src/modes/utils/ui-helpers.ts +17 -9
- package/src/notifications/index.ts +8 -2
- package/src/plan-mode/approved-plan.ts +3 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -0
- package/src/prompts/agents/architect.md +3 -8
- package/src/prompts/agents/critic.md +3 -8
- package/src/prompts/agents/executor.md +3 -0
- package/src/prompts/agents/planner.md +4 -12
- package/src/prompts/system/custom-system-prompt.md +2 -24
- package/src/prompts/system/plan-mode-approved.md +8 -0
- package/src/prompts/system/system-prompt.md +18 -94
- package/src/prompts/tools/bash.md +4 -4
- package/src/prompts/tools/browser.md +15 -52
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/hashline.md +13 -55
- package/src/prompts/tools/lsp.md +3 -3
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/search-tool-bm25.md +0 -2
- package/src/prompts/tools/search.md +2 -4
- package/src/prompts/tools/skill-discovery.md +1 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-mcp/client.ts +94 -19
- package/src/runtime-mcp/config.ts +27 -15
- package/src/runtime-mcp/discoverable-tool-metadata.ts +16 -13
- package/src/runtime-mcp/manager.ts +359 -106
- package/src/runtime-mcp/oauth-discovery.ts +116 -26
- package/src/runtime-mcp/oauth-flow.ts +42 -12
- package/src/runtime-mcp/transports/http.ts +49 -19
- package/src/runtime-mcp/transports/stdio.ts +47 -28
- package/src/runtime-mcp/types.ts +10 -0
- package/src/sdk/acp/adapter.ts +418 -0
- package/src/sdk/acp/index.ts +2 -0
- package/src/sdk/broker/broker.ts +877 -0
- package/src/sdk/broker/discovery.ts +238 -0
- package/src/sdk/broker/ensure.ts +393 -0
- package/src/sdk/broker/identity.ts +42 -0
- package/src/sdk/broker/index.ts +8 -0
- package/src/sdk/broker/internal-source-marker-2178.txt +1 -0
- package/src/sdk/broker/internal-source.bunfig.toml +1 -0
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/lifecycle-codec.ts +11 -0
- package/src/sdk/broker/lifecycle-ledger.ts +583 -0
- package/src/sdk/broker/lifecycle.ts +3413 -0
- package/src/sdk/broker/process-incarnation.ts +151 -0
- package/src/sdk/broker/runtime.ts +164 -0
- package/src/sdk/broker/session-index.ts +678 -0
- package/src/sdk/broker/state-version.ts +47 -0
- package/src/sdk/broker/transport.ts +125 -0
- package/src/sdk/bus/attachment-registry.ts +23 -0
- package/src/sdk/bus/chat-adapters.ts +147 -0
- package/src/sdk/bus/chat-command-policy.ts +107 -0
- package/src/sdk/bus/chat-daemon-cli.ts +278 -0
- package/src/sdk/bus/chat-daemon-control.ts +1101 -0
- package/src/sdk/bus/chat-daemon-runtime.ts +527 -0
- package/src/sdk/bus/chat-effect-journal.ts +363 -0
- package/src/sdk/bus/config-commands.ts +192 -0
- package/src/sdk/bus/config.ts +568 -0
- package/src/sdk/bus/conversation-store.ts +367 -0
- package/src/sdk/bus/daemon-paths.ts +34 -0
- package/src/sdk/bus/discord-conversation.ts +85 -0
- package/src/sdk/bus/discord-daemon.ts +2292 -0
- package/src/sdk/bus/discord-live-provider.ts +521 -0
- package/src/sdk/bus/discord-provider.ts +56 -0
- package/src/sdk/bus/engine.ts +100 -0
- package/src/sdk/bus/helpers.ts +135 -0
- package/src/sdk/bus/html-format.ts +518 -0
- package/src/sdk/bus/index.ts +4961 -0
- package/src/sdk/bus/lifecycle-commands.ts +291 -0
- package/src/sdk/bus/lifecycle-control-runtime.ts +1409 -0
- package/src/sdk/bus/lifecycle-orchestrator.ts +458 -0
- package/src/sdk/bus/managed-daemon.ts +163 -0
- package/src/sdk/bus/native-runtime-compatibility.ts +42 -0
- package/src/sdk/bus/natives-augment.d.ts +63 -0
- package/src/sdk/bus/notification-orchestration.ts +535 -0
- package/src/sdk/bus/notification-service.ts +1174 -0
- package/src/sdk/bus/operator-runtime.ts +171 -0
- package/src/sdk/bus/rate-limit-pool.ts +276 -0
- package/src/sdk/bus/recent-activity.ts +279 -0
- package/src/sdk/bus/reply-sent-store.ts +134 -0
- package/src/sdk/bus/rich-document.ts +258 -0
- package/src/sdk/bus/rich-draft.ts +107 -0
- package/src/sdk/bus/rich-render.ts +135 -0
- package/src/sdk/bus/session-control.ts +439 -0
- package/src/sdk/bus/slack-conversation.ts +74 -0
- package/src/sdk/bus/slack-daemon.ts +2144 -0
- package/src/sdk/bus/slack-live-provider.ts +380 -0
- package/src/sdk/bus/slack-provider.ts +76 -0
- package/src/sdk/bus/telegram-cli.ts +194 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +292 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +27 -0
- package/src/sdk/bus/telegram-daemon-control.ts +642 -0
- package/src/sdk/bus/telegram-daemon.ts +6675 -0
- package/src/sdk/bus/telegram-reference.ts +436 -0
- package/src/sdk/bus/telegram-setup.ts +663 -0
- package/src/sdk/bus/threaded-inbound.ts +141 -0
- package/src/sdk/bus/threaded-render.ts +294 -0
- package/src/sdk/bus/topic-registry.ts +364 -0
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/session-cli.ts +275 -0
- package/src/sdk/client/client.ts +1 -0
- package/src/sdk/client/discovery.ts +108 -0
- package/src/sdk/client/index.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +388 -0
- package/src/sdk/host/control/index.ts +10 -0
- package/src/sdk/host/control/operations.ts +68 -0
- package/src/sdk/host/events.ts +91 -0
- package/src/sdk/host/host.ts +338 -0
- package/src/sdk/host/index.ts +4 -0
- package/src/sdk/host/query/cursor.ts +189 -0
- package/src/sdk/host/query/handlers.ts +531 -0
- package/src/sdk/host/query/index.ts +3 -0
- package/src/sdk/host/query/revision-store.ts +1149 -0
- package/src/sdk/host/reverse-leases.ts +325 -0
- package/src/sdk/host/types.ts +29 -0
- package/src/sdk/index.ts +14 -0
- package/src/sdk/lifecycle-session.ts +39 -0
- package/src/sdk/mcp/index.ts +1 -0
- package/src/sdk/mcp/server.ts +332 -0
- package/src/sdk/models.ts +168 -0
- package/src/sdk/protocol/adapter-validation.ts +48 -0
- package/src/sdk/protocol/operation-inventory.generated.json +4719 -0
- package/src/sdk/protocol/operation-registry.ts +312 -0
- package/src/sdk/session-directory.ts +141 -0
- package/src/sdk/session.ts +2869 -0
- package/src/sdk/startup-capability.ts +189 -0
- package/src/secrets/index.ts +53 -10
- package/src/secrets/obfuscator.ts +21 -18
- package/src/secrets/regex.ts +2 -0
- package/src/session/agent-session.ts +5602 -2138
- package/src/session/agent-storage.ts +18 -0
- package/src/session/artifacts.ts +188 -23
- package/src/session/cache-economics.ts +25 -8
- package/src/session/context-estimation.ts +7 -2
- package/src/session/default-model-selection.ts +93 -0
- package/src/session/fallback-chain-controller.ts +186 -0
- package/src/session/internal/managed-session-scope.ts +2402 -0
- package/src/session/internal/managed-session-storage.ts +1559 -0
- package/src/session/messages.ts +91 -2
- package/src/session/session-manager-internal.ts +33 -0
- package/src/session/session-manager.ts +2578 -392
- package/src/session/session-storage.ts +550 -40
- package/src/session/streaming-output.ts +6 -0
- package/src/session/volatile-context-pruning.ts +78 -0
- package/src/session/yield-queue.ts +20 -0
- package/src/setup/credential-auto-import.ts +291 -35
- package/src/setup/hermes/templates/operator-instructions.v1.md +4 -6
- package/src/setup/provider-onboarding.ts +36 -61
- package/src/setup/provider-presets.json +5 -0
- package/src/skc-runtime/deep-interview-recorder.ts +70 -3
- package/src/skc-runtime/deep-interview-state.ts +441 -5
- package/src/skc-runtime/gc-render.ts +19 -1
- package/src/skc-runtime/gc-runtime.ts +68 -3
- package/src/skc-runtime/goal-mode-request.ts +55 -8
- package/src/skc-runtime/launch-worktree.ts +5 -5
- package/src/skc-runtime/linux-proc.ts +71 -0
- package/src/skc-runtime/managed-owner-admission.ts +352 -0
- package/src/skc-runtime/managed-owner-supervisor.ts +162 -0
- package/src/skc-runtime/psmux-detect.ts +82 -4
- package/src/skc-runtime/ralplan-runtime.ts +74 -90
- package/src/skc-runtime/session-resolution.ts +18 -3
- package/src/skc-runtime/session-state-sidecar.ts +47 -138
- package/src/skc-runtime/state-argv.ts +292 -0
- package/src/skc-runtime/state-migrations.ts +1 -1
- package/src/skc-runtime/state-runtime.ts +387 -451
- package/src/skc-runtime/state-writer.ts +36 -40
- package/src/skc-runtime/team-launch.ts +319 -0
- package/src/skc-runtime/team-notify.ts +326 -0
- package/src/skc-runtime/team-runtime.ts +8 -2
- package/src/skc-runtime/team-store.ts +828 -0
- package/src/skc-runtime/team-workers.ts +962 -0
- package/src/skc-runtime/tmux-owner-isolation-cli.ts +55 -28
- package/src/skc-runtime/tmux-owner-isolation.ts +136 -11
- package/src/skc-runtime/tmux-sessions.ts +23 -34
- package/src/skc-runtime/ultragoal-change-set.ts +172 -0
- package/src/skc-runtime/ultragoal-evidence.ts +874 -0
- package/src/skc-runtime/ultragoal-guard.ts +136 -18
- package/src/skc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
- package/src/skc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +102 -1
- package/src/skc-runtime/ultragoal-runtime.ts +325 -1322
- package/src/skc-runtime/windows-powershell-command.ts +58 -0
- package/src/skc-runtime/workflow-cli-common.ts +30 -0
- package/src/skc-runtime/workflow-manifest.generated.json +58 -0
- package/src/skc-runtime/workflow-manifest.ts +18 -0
- package/src/skill-state/active-state.ts +3 -13
- package/src/skill-state/workflow-mutation-guard.ts +661 -0
- package/src/skill-state/workflow-state-contract.ts +6 -41
- package/src/slash-commands/builtin-registry.ts +115 -41
- package/src/system-prompt.ts +37 -44
- package/src/task/agents.ts +19 -1
- package/src/task/executor.ts +223 -45
- package/src/task/index.ts +124 -56
- package/src/thinking.ts +21 -6
- package/src/tool-discovery/tool-index.ts +20 -134
- package/src/tools/ask-answer-registry.ts +16 -2
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +40 -2
- package/src/tools/browser/STEALTH-THIRD-PARTY-NOTICES.md +26 -0
- package/src/tools/browser/benchmark/detector-report.ts +157 -0
- package/src/tools/browser/benchmark/run-suite.ts +39 -0
- package/src/tools/browser/dead-tab-recovery.ts +78 -0
- package/src/tools/browser/launch.ts +127 -20
- package/src/tools/browser/profile-discovery.ts +63 -0
- package/src/tools/browser/profile-posture.ts +59 -0
- package/src/tools/browser/profile-reuse.ts +102 -0
- package/src/tools/browser/profile-warmup.ts +109 -0
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/registry.ts +87 -11
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +392 -142
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +8 -6
- package/src/tools/browser.ts +6 -0
- package/src/tools/computer.ts +10 -3
- package/src/tools/cron.ts +30 -16
- package/src/tools/fetch.ts +11 -4
- package/src/tools/index.ts +88 -32
- package/src/tools/output-meta.ts +19 -7
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/puppeteer/14_stealth_permissions.txt +57 -0
- package/src/tools/puppeteer/15_stealth_webrtc.txt +60 -0
- package/src/tools/read.ts +226 -65
- package/src/tools/resource-gc.ts +125 -36
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent-render.ts +19 -8
- package/src/tools/subagent.ts +129 -34
- package/src/tools/telegram-send.ts +2 -2
- package/src/utils/event-bus.ts +9 -4
- package/src/utils/file-mentions.ts +4 -4
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/index.ts +3 -3
- package/src/web/search/providers/openai-compatible.ts +76 -23
- package/dist/types/append-only-mode.d.ts +0 -31
- package/dist/types/async/index.d.ts +0 -2
- package/dist/types/async/job-manager.d.ts +0 -328
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/autoresearch/dashboard.d.ts +0 -4
- package/dist/types/autoresearch/git.d.ts +0 -36
- package/dist/types/autoresearch/helpers.d.ts +0 -24
- package/dist/types/autoresearch/index.d.ts +0 -2
- package/dist/types/autoresearch/state.d.ts +0 -17
- package/dist/types/autoresearch/storage.d.ts +0 -142
- package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
- package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
- package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
- package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
- package/dist/types/autoresearch/types.d.ts +0 -154
- package/dist/types/build-metadata.d.ts +0 -7
- package/dist/types/capability/context-file.d.ts +0 -17
- package/dist/types/capability/extension-module.d.ts +0 -15
- package/dist/types/capability/extension.d.ts +0 -28
- package/dist/types/capability/fs.d.ts +0 -20
- package/dist/types/capability/hook.d.ts +0 -19
- package/dist/types/capability/index.d.ts +0 -80
- package/dist/types/capability/instruction.d.ts +0 -17
- package/dist/types/capability/mcp.d.ts +0 -49
- package/dist/types/capability/prompt.d.ts +0 -15
- package/dist/types/capability/rule.d.ts +0 -69
- package/dist/types/capability/settings.d.ts +0 -15
- package/dist/types/capability/skill.d.ts +0 -36
- package/dist/types/capability/slash-command.d.ts +0 -17
- package/dist/types/capability/ssh.d.ts +0 -23
- package/dist/types/capability/system-prompt.d.ts +0 -15
- package/dist/types/capability/tool.d.ts +0 -19
- package/dist/types/capability/types.d.ts +0 -154
- package/dist/types/cli/agents-cli.d.ts +0 -12
- package/dist/types/cli/args.d.ts +0 -57
- package/dist/types/cli/auth-broker-cli.d.ts +0 -25
- package/dist/types/cli/auth-gateway-cli.d.ts +0 -18
- package/dist/types/cli/classify-install-target.d.ts +0 -8
- package/dist/types/cli/commands/init-xdg.d.ts +0 -1
- package/dist/types/cli/completion-cli.d.ts +0 -41
- package/dist/types/cli/config-cli.d.ts +0 -23
- package/dist/types/cli/daemon-cli.d.ts +0 -27
- package/dist/types/cli/fast-help.d.ts +0 -1
- package/dist/types/cli/file-processor.d.ts +0 -11
- package/dist/types/cli/fixture-report.d.ts +0 -20
- package/dist/types/cli/fixture-report.test.d.ts +0 -1
- package/dist/types/cli/grep-cli.d.ts +0 -17
- package/dist/types/cli/initial-message.d.ts +0 -17
- package/dist/types/cli/list-models.d.ts +0 -36
- package/dist/types/cli/local-provider-smoke.d.ts +0 -59
- package/dist/types/cli/malloc-env-guard.d.ts +0 -65
- package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
- package/dist/types/cli/mcp-cli.d.ts +0 -25
- package/dist/types/cli/migrate-cli.d.ts +0 -20
- package/dist/types/cli/nofile-limit.d.ts +0 -11
- package/dist/types/cli/notify-cli.d.ts +0 -39
- package/dist/types/cli/plugin-cli.d.ts +0 -34
- package/dist/types/cli/read-cli.d.ts +0 -4
- package/dist/types/cli/session-picker.d.ts +0 -4
- package/dist/types/cli/setup-cli.d.ts +0 -73
- package/dist/types/cli/shell-cli.d.ts +0 -8
- package/dist/types/cli/skills-cli.d.ts +0 -9
- package/dist/types/cli/ssh-cli.d.ts +0 -21
- package/dist/types/cli/stats-cli.d.ts +0 -17
- package/dist/types/cli/telegram-cli.d.ts +0 -13
- package/dist/types/cli/update-cli.d.ts +0 -108
- package/dist/types/cli/web-search-cli.d.ts +0 -32
- package/dist/types/cli/worktree-cli.d.ts +0 -26
- package/dist/types/cli.d.ts +0 -140
- package/dist/types/commands/acp.d.ts +0 -12
- package/dist/types/commands/agents.d.ts +0 -34
- package/dist/types/commands/auth-broker.d.ts +0 -54
- package/dist/types/commands/auth-gateway.d.ts +0 -32
- package/dist/types/commands/codex-native-hook.d.ts +0 -6
- package/dist/types/commands/commit.d.ts +0 -27
- package/dist/types/commands/completion.d.ts +0 -29
- package/dist/types/commands/config.d.ts +0 -33
- package/dist/types/commands/contribution-prep.d.ts +0 -18
- package/dist/types/commands/coordinator.d.ts +0 -19
- package/dist/types/commands/daemon.d.ts +0 -45
- package/dist/types/commands/deep-interview.d.ts +0 -54
- package/dist/types/commands/gc.d.ts +0 -26
- package/dist/types/commands/grep.d.ts +0 -42
- package/dist/types/commands/harness.d.ts +0 -43
- package/dist/types/commands/launch.d.ts +0 -131
- package/dist/types/commands/local-provider.d.ts +0 -34
- package/dist/types/commands/mcp-serve.d.ts +0 -24
- package/dist/types/commands/mcp.d.ts +0 -70
- package/dist/types/commands/migrate.d.ts +0 -33
- package/dist/types/commands/notify.d.ts +0 -47
- package/dist/types/commands/plugin.d.ts +0 -58
- package/dist/types/commands/ralplan.d.ts +0 -7
- package/dist/types/commands/read.d.ts +0 -15
- package/dist/types/commands/rlm.d.ts +0 -10
- package/dist/types/commands/session.d.ts +0 -30
- package/dist/types/commands/setup.d.ts +0 -105
- package/dist/types/commands/shell.d.ts +0 -21
- package/dist/types/commands/skills.d.ts +0 -26
- package/dist/types/commands/ssh.d.ts +0 -48
- package/dist/types/commands/state.d.ts +0 -7
- package/dist/types/commands/stats.d.ts +0 -25
- package/dist/types/commands/team.d.ts +0 -24
- package/dist/types/commands/telegram.d.ts +0 -22
- package/dist/types/commands/ultragoal.d.ts +0 -8
- package/dist/types/commands/update.d.ts +0 -20
- package/dist/types/commands/web-search.d.ts +0 -87
- package/dist/types/commands/worktree.d.ts +0 -34
- package/dist/types/commit/agentic/agent.d.ts +0 -31
- package/dist/types/commit/agentic/fallback.d.ts +0 -5
- package/dist/types/commit/agentic/index.d.ts +0 -2
- package/dist/types/commit/agentic/state.d.ts +0 -58
- package/dist/types/commit/agentic/tools/analyze-file.d.ts +0 -19
- package/dist/types/commit/agentic/tools/git-file-diff.d.ts +0 -10
- package/dist/types/commit/agentic/tools/git-hunk.d.ts +0 -9
- package/dist/types/commit/agentic/tools/git-overview.d.ts +0 -9
- package/dist/types/commit/agentic/tools/index.d.ts +0 -16
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +0 -28
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +0 -36
- package/dist/types/commit/agentic/tools/recent-commits.d.ts +0 -7
- package/dist/types/commit/agentic/tools/schemas.d.ts +0 -27
- package/dist/types/commit/agentic/tools/split-commit.d.ts +0 -53
- package/dist/types/commit/agentic/topo-sort.d.ts +0 -4
- package/dist/types/commit/agentic/trivial.d.ts +0 -7
- package/dist/types/commit/agentic/validation.d.ts +0 -20
- package/dist/types/commit/analysis/conventional.d.ts +0 -22
- package/dist/types/commit/analysis/index.d.ts +0 -4
- package/dist/types/commit/analysis/scope.d.ts +0 -6
- package/dist/types/commit/analysis/summary.d.ts +0 -19
- package/dist/types/commit/analysis/validation.d.ts +0 -8
- package/dist/types/commit/changelog/detect.d.ts +0 -2
- package/dist/types/commit/changelog/generate.d.ts +0 -30
- package/dist/types/commit/changelog/index.d.ts +0 -30
- package/dist/types/commit/changelog/parse.d.ts +0 -2
- package/dist/types/commit/cli.d.ts +0 -3
- package/dist/types/commit/git/diff.d.ts +0 -5
- package/dist/types/commit/index.d.ts +0 -4
- package/dist/types/commit/map-reduce/index.d.ts +0 -28
- package/dist/types/commit/map-reduce/map-phase.d.ts +0 -17
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +0 -13
- package/dist/types/commit/map-reduce/utils.d.ts +0 -2
- package/dist/types/commit/message.d.ts +0 -2
- package/dist/types/commit/model-selection.d.ts +0 -15
- package/dist/types/commit/pipeline.d.ts +0 -5
- package/dist/types/commit/shared-llm.d.ts +0 -54
- package/dist/types/commit/types.d.ts +0 -78
- package/dist/types/commit/utils/exclusions.d.ts +0 -4
- package/dist/types/commit/utils.d.ts +0 -20
- package/dist/types/config/config-file.d.ts +0 -58
- package/dist/types/config/file-lock-gc.d.ts +0 -5
- package/dist/types/config/file-lock.d.ts +0 -36
- package/dist/types/config/keybindings.d.ts +0 -364
- package/dist/types/config/model-equivalence.d.ts +0 -24
- package/dist/types/config/model-profile-activation.d.ts +0 -65
- package/dist/types/config/model-profiles.d.ts +0 -39
- package/dist/types/config/model-registry.d.ts +0 -473
- package/dist/types/config/model-resolver.d.ts +0 -217
- package/dist/types/config/models-config-schema.d.ts +0 -700
- package/dist/types/config/prompt-templates.d.ts +0 -32
- package/dist/types/config/resolve-config-value.d.ts +0 -17
- package/dist/types/config/settings-schema.d.ts +0 -4223
- package/dist/types/config/settings.d.ts +0 -158
- package/dist/types/config/skill-settings-defaults.d.ts +0 -14
- package/dist/types/config/telegram-autostart.d.ts +0 -14
- package/dist/types/config/telegram-env-bridge.d.ts +0 -18
- package/dist/types/config.d.ts +0 -66
- package/dist/types/coordinator/contract.d.ts +0 -4
- package/dist/types/coordinator-mcp/model-preset.d.ts +0 -45
- package/dist/types/coordinator-mcp/policy.d.ts +0 -27
- package/dist/types/coordinator-mcp/safety.d.ts +0 -26
- package/dist/types/coordinator-mcp/server.d.ts +0 -139
- package/dist/types/coordinator-mcp/session-reaper.d.ts +0 -53
- package/dist/types/cursor.d.ts +0 -24
- package/dist/types/daemon/builtin.d.ts +0 -20
- package/dist/types/daemon/control-types.d.ts +0 -67
- package/dist/types/daemon/operator-contract.d.ts +0 -47
- package/dist/types/daemon/runtime.d.ts +0 -25
- package/dist/types/dap/client.d.ts +0 -39
- package/dist/types/dap/config.d.ts +0 -6
- package/dist/types/dap/index.d.ts +0 -4
- package/dist/types/dap/session.d.ts +0 -108
- package/dist/types/dap/types.d.ts +0 -524
- package/dist/types/debug/crash-diagnostics.d.ts +0 -45
- package/dist/types/debug/index.d.ts +0 -15
- package/dist/types/debug/log-formatting.d.ts +0 -4
- package/dist/types/debug/log-viewer.d.ts +0 -68
- package/dist/types/debug/profiler.d.ts +0 -24
- package/dist/types/debug/raw-sse-buffer.d.ts +0 -44
- package/dist/types/debug/raw-sse.d.ts +0 -16
- package/dist/types/debug/report-bundle.d.ts +0 -55
- package/dist/types/debug/runtime-gauges.d.ts +0 -6
- package/dist/types/debug/system-info.d.ts +0 -26
- package/dist/types/deep-interview/plaintext-gate-guard.d.ts +0 -11
- package/dist/types/deep-interview/render-middleware.d.ts +0 -6
- package/dist/types/defaults/skc/extensions/grok-build/index.d.ts +0 -1
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/index.d.ts +0 -1
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +0 -29
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +0 -27
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/billing.d.ts +0 -8
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/register.d.ts +0 -5
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/stream.d.ts +0 -10
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/usage.d.ts +0 -2
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +0 -2
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +0 -38
- package/dist/types/defaults/skc-defaults.d.ts +0 -64
- package/dist/types/defaults/skc-grok-cli.d.ts +0 -5
- package/dist/types/discovery/agents-md.d.ts +0 -1
- package/dist/types/discovery/agents.d.ts +0 -12
- package/dist/types/discovery/builtin.d.ts +0 -1
- package/dist/types/discovery/claude-plugins.d.ts +0 -1
- package/dist/types/discovery/claude.d.ts +0 -1
- package/dist/types/discovery/cline.d.ts +0 -1
- package/dist/types/discovery/codex.d.ts +0 -1
- package/dist/types/discovery/cursor.d.ts +0 -19
- package/dist/types/discovery/gemini.d.ts +0 -1
- package/dist/types/discovery/github.d.ts +0 -1
- package/dist/types/discovery/helpers.d.ts +0 -271
- package/dist/types/discovery/index.d.ts +0 -48
- package/dist/types/discovery/mcp-json.d.ts +0 -1
- package/dist/types/discovery/opencode.d.ts +0 -1
- package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
- package/dist/types/discovery/ssh.d.ts +0 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -1
- package/dist/types/discovery/vscode.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +0 -15
- package/dist/types/edit/apply-patch/index.d.ts +0 -35
- package/dist/types/edit/apply-patch/parser.d.ts +0 -34
- package/dist/types/edit/diff.d.ts +0 -75
- package/dist/types/edit/file-read-cache.d.ts +0 -25
- package/dist/types/edit/index.d.ts +0 -58
- package/dist/types/edit/modes/apply-patch.d.ts +0 -24
- package/dist/types/edit/modes/patch.d.ts +0 -99
- package/dist/types/edit/modes/replace.d.ts +0 -149
- package/dist/types/edit/normalize.d.ts +0 -43
- package/dist/types/edit/notebook.d.ts +0 -26
- package/dist/types/edit/read-file.d.ts +0 -8
- package/dist/types/edit/renderer.d.ts +0 -110
- package/dist/types/edit/streaming.d.ts +0 -66
- package/dist/types/eval/backend.d.ts +0 -40
- package/dist/types/eval/index.d.ts +0 -4
- package/dist/types/eval/js/context-manager.d.ts +0 -27
- package/dist/types/eval/js/executor.d.ts +0 -29
- package/dist/types/eval/js/index.d.ts +0 -10
- package/dist/types/eval/js/shared/helpers.d.ts +0 -38
- package/dist/types/eval/js/shared/indirect-eval.d.ts +0 -14
- package/dist/types/eval/js/shared/prelude.d.ts +0 -1
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +0 -6
- package/dist/types/eval/js/shared/runtime.d.ts +0 -47
- package/dist/types/eval/js/shared/types.d.ts +0 -24
- package/dist/types/eval/js/tool-bridge.d.ts +0 -18
- package/dist/types/eval/js/worker-core.d.ts +0 -5
- package/dist/types/eval/js/worker-entry.d.ts +0 -1
- package/dist/types/eval/js/worker-protocol.d.ts +0 -77
- package/dist/types/eval/py/display.d.ts +0 -25
- package/dist/types/eval/py/executor.d.ts +0 -88
- package/dist/types/eval/py/index.d.ts +0 -10
- package/dist/types/eval/py/kernel.d.ts +0 -65
- package/dist/types/eval/py/prelude.d.ts +0 -1
- package/dist/types/eval/py/runtime.d.ts +0 -29
- package/dist/types/eval/py/tool-bridge.d.ts +0 -20
- package/dist/types/eval/types.d.ts +0 -52
- package/dist/types/exa/factory.d.ts +0 -13
- package/dist/types/exa/index.d.ts +0 -20
- package/dist/types/exa/mcp-client.d.ts +0 -45
- package/dist/types/exa/render.d.ts +0 -19
- package/dist/types/exa/researcher.d.ts +0 -9
- package/dist/types/exa/search.d.ts +0 -9
- package/dist/types/exa/types.d.ts +0 -155
- package/dist/types/exa/websets.d.ts +0 -9
- package/dist/types/exec/bash-executor.d.ts +0 -64
- package/dist/types/exec/exec.d.ts +0 -25
- package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
- package/dist/types/exec/non-interactive-env.d.ts +0 -1
- package/dist/types/export/custom-share.d.ts +0 -20
- package/dist/types/export/html/index.d.ts +0 -10
- package/dist/types/export/html/template.generated.d.ts +0 -1
- package/dist/types/export/html/template.macro.d.ts +0 -5
- package/dist/types/export/ttsr.d.ts +0 -55
- package/dist/types/extensibility/custom-commands/bundled/ci-green/index.d.ts +0 -9
- package/dist/types/extensibility/custom-commands/index.d.ts +0 -2
- package/dist/types/extensibility/custom-commands/loader.d.ts +0 -29
- package/dist/types/extensibility/custom-commands/types.d.ts +0 -106
- package/dist/types/extensibility/custom-tools/index.d.ts +0 -6
- package/dist/types/extensibility/custom-tools/loader.d.ts +0 -69
- package/dist/types/extensibility/custom-tools/types.d.ts +0 -229
- package/dist/types/extensibility/custom-tools/wrapper.d.ts +0 -24
- package/dist/types/extensibility/extensions/compact-handler.d.ts +0 -26
- package/dist/types/extensibility/extensions/get-commands-handler.d.ts +0 -29
- package/dist/types/extensibility/extensions/index.d.ts +0 -9
- package/dist/types/extensibility/extensions/loader.d.ts +0 -43
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -35
- package/dist/types/extensibility/extensions/runner.d.ts +0 -135
- package/dist/types/extensibility/extensions/types.d.ts +0 -920
- package/dist/types/extensibility/extensions/wrapper.d.ts +0 -55
- package/dist/types/extensibility/hooks/index.d.ts +0 -5
- package/dist/types/extensibility/hooks/loader.d.ts +0 -89
- package/dist/types/extensibility/hooks/runner.d.ts +0 -126
- package/dist/types/extensibility/hooks/tool-wrapper.d.ts +0 -25
- package/dist/types/extensibility/hooks/types.d.ts +0 -431
- package/dist/types/extensibility/plugins/doctor.d.ts +0 -3
- package/dist/types/extensibility/plugins/git-url.d.ts +0 -34
- package/dist/types/extensibility/plugins/index.d.ts +0 -8
- package/dist/types/extensibility/plugins/installer.d.ts +0 -5
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +0 -2
- package/dist/types/extensibility/plugins/loader.d.ts +0 -31
- package/dist/types/extensibility/plugins/manager.d.ts +0 -65
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +0 -41
- package/dist/types/extensibility/plugins/marketplace/fetcher.d.ts +0 -48
- package/dist/types/extensibility/plugins/marketplace/index.d.ts +0 -6
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +0 -57
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +0 -34
- package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +0 -28
- package/dist/types/extensibility/plugins/marketplace/types.d.ts +0 -130
- package/dist/types/extensibility/plugins/parser.d.ts +0 -52
- package/dist/types/extensibility/plugins/security-scanner.d.ts +0 -31
- package/dist/types/extensibility/plugins/types.d.ts +0 -149
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +0 -20
- package/dist/types/extensibility/shared-events.d.ts +0 -281
- package/dist/types/extensibility/skc-plugins/activation.d.ts +0 -14
- package/dist/types/extensibility/skc-plugins/compiler.d.ts +0 -19
- package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +0 -29
- package/dist/types/extensibility/skc-plugins/index.d.ts +0 -18
- package/dist/types/extensibility/skc-plugins/injection.d.ts +0 -40
- package/dist/types/extensibility/skc-plugins/installer.d.ts +0 -13
- package/dist/types/extensibility/skc-plugins/loader.d.ts +0 -3
- package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +0 -26
- package/dist/types/extensibility/skc-plugins/observability.d.ts +0 -27
- package/dist/types/extensibility/skc-plugins/paths.d.ts +0 -8
- package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +0 -16
- package/dist/types/extensibility/skc-plugins/registry.d.ts +0 -32
- package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +0 -64
- package/dist/types/extensibility/skc-plugins/schema.d.ts +0 -3
- package/dist/types/extensibility/skc-plugins/session-validation.d.ts +0 -42
- package/dist/types/extensibility/skc-plugins/state.d.ts +0 -9
- package/dist/types/extensibility/skc-plugins/tools.d.ts +0 -8
- package/dist/types/extensibility/skc-plugins/types.d.ts +0 -220
- package/dist/types/extensibility/skc-plugins/validation.d.ts +0 -11
- package/dist/types/extensibility/skills.d.ts +0 -82
- package/dist/types/extensibility/slash-commands.d.ts +0 -48
- package/dist/types/extensibility/tool-proxy.d.ts +0 -4
- package/dist/types/extensibility/typebox.d.ts +0 -155
- package/dist/types/extensibility/utils.d.ts +0 -12
- package/dist/types/goals/index.d.ts +0 -3
- package/dist/types/goals/runtime.d.ts +0 -63
- package/dist/types/goals/state.d.ts +0 -32
- package/dist/types/goals/tools/goal-tool.d.ts +0 -62
- package/dist/types/harness-control-plane/classifier.d.ts +0 -13
- package/dist/types/harness-control-plane/control-endpoint.d.ts +0 -31
- package/dist/types/harness-control-plane/finalize.d.ts +0 -60
- package/dist/types/harness-control-plane/gc-adapter.d.ts +0 -3
- package/dist/types/harness-control-plane/operate.d.ts +0 -35
- package/dist/types/harness-control-plane/owner.d.ts +0 -53
- package/dist/types/harness-control-plane/phase-rollup.d.ts +0 -23
- package/dist/types/harness-control-plane/preserve.d.ts +0 -19
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +0 -19
- package/dist/types/harness-control-plane/receipt-spool.d.ts +0 -19
- package/dist/types/harness-control-plane/receipts.d.ts +0 -149
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +0 -69
- package/dist/types/harness-control-plane/seams.d.ts +0 -21
- package/dist/types/harness-control-plane/session-lease.d.ts +0 -65
- package/dist/types/harness-control-plane/state-machine.d.ts +0 -24
- package/dist/types/harness-control-plane/storage.d.ts +0 -81
- package/dist/types/harness-control-plane/types.d.ts +0 -187
- package/dist/types/hashline/anchors.d.ts +0 -20
- package/dist/types/hashline/apply.d.ts +0 -14
- package/dist/types/hashline/constants.d.ts +0 -19
- package/dist/types/hashline/diff-preview.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +0 -17
- package/dist/types/hashline/execute.d.ts +0 -4
- package/dist/types/hashline/hash.d.ts +0 -123
- package/dist/types/hashline/index.d.ts +0 -13
- package/dist/types/hashline/input.d.ts +0 -11
- package/dist/types/hashline/parser.d.ts +0 -7
- package/dist/types/hashline/prefixes.d.ts +0 -7
- package/dist/types/hashline/recovery.d.ts +0 -27
- package/dist/types/hashline/stream.d.ts +0 -2
- package/dist/types/hashline/types.d.ts +0 -77
- package/dist/types/hindsight/backend.d.ts +0 -13
- package/dist/types/hindsight/bank.d.ts +0 -54
- package/dist/types/hindsight/client.d.ts +0 -224
- package/dist/types/hindsight/config.d.ts +0 -51
- package/dist/types/hindsight/content.d.ts +0 -70
- package/dist/types/hindsight/index.d.ts +0 -8
- package/dist/types/hindsight/mental-models.d.ts +0 -125
- package/dist/types/hindsight/state.d.ts +0 -99
- package/dist/types/hindsight/transcript.d.ts +0 -28
- package/dist/types/hooks/codex-native-hooks-config.d.ts +0 -29
- package/dist/types/hooks/native-skill-hook.d.ts +0 -28
- package/dist/types/hooks/skill-keywords.d.ts +0 -18
- package/dist/types/hooks/skill-state.d.ts +0 -93
- package/dist/types/i18n/index.d.ts +0 -38
- package/dist/types/i18n/messages/de.d.ts +0 -2
- package/dist/types/i18n/messages/de.messages.d.ts +0 -1
- package/dist/types/i18n/messages/de.settings.d.ts +0 -1
- package/dist/types/i18n/messages/en.d.ts +0 -81
- package/dist/types/i18n/messages/es.d.ts +0 -2
- package/dist/types/i18n/messages/es.messages.d.ts +0 -1
- package/dist/types/i18n/messages/es.settings.d.ts +0 -1
- package/dist/types/i18n/messages/fr.d.ts +0 -2
- package/dist/types/i18n/messages/fr.messages.d.ts +0 -1
- package/dist/types/i18n/messages/fr.settings.d.ts +0 -1
- package/dist/types/i18n/messages/ja.d.ts +0 -2
- package/dist/types/i18n/messages/ja.messages.d.ts +0 -1
- package/dist/types/i18n/messages/ja.settings.d.ts +0 -1
- package/dist/types/i18n/messages/ko.d.ts +0 -2
- package/dist/types/i18n/messages/ko.messages.d.ts +0 -1
- package/dist/types/i18n/messages/ko.settings.d.ts +0 -1
- package/dist/types/i18n/messages/zh.d.ts +0 -2
- package/dist/types/i18n/messages/zh.messages.d.ts +0 -1
- package/dist/types/i18n/messages/zh.settings.d.ts +0 -1
- package/dist/types/index.d.ts +0 -33
- package/dist/types/internal-urls/agent-protocol.d.ts +0 -12
- package/dist/types/internal-urls/artifact-protocol.d.ts +0 -6
- package/dist/types/internal-urls/docs-index.generated.d.ts +0 -2
- package/dist/types/internal-urls/index.d.ts +0 -20
- package/dist/types/internal-urls/issue-pr-protocol.d.ts +0 -17
- package/dist/types/internal-urls/json-query.d.ts +0 -30
- package/dist/types/internal-urls/local-protocol.d.ts +0 -43
- package/dist/types/internal-urls/mcp-protocol.d.ts +0 -12
- package/dist/types/internal-urls/memory-protocol.d.ts +0 -23
- package/dist/types/internal-urls/parse.d.ts +0 -19
- package/dist/types/internal-urls/registry-helpers.d.ts +0 -11
- package/dist/types/internal-urls/router.d.ts +0 -14
- package/dist/types/internal-urls/rule-protocol.d.ts +0 -6
- package/dist/types/internal-urls/skc-protocol.d.ts +0 -12
- package/dist/types/internal-urls/skill-protocol.d.ts +0 -13
- package/dist/types/internal-urls/types.d.ts +0 -109
- package/dist/types/lsp/client.d.ts +0 -66
- package/dist/types/lsp/clients/biome-client.d.ts +0 -16
- package/dist/types/lsp/clients/index.d.ts +0 -19
- package/dist/types/lsp/clients/lsp-linter-client.d.ts +0 -16
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -20
- package/dist/types/lsp/config.d.ts +0 -65
- package/dist/types/lsp/edits.d.ts +0 -23
- package/dist/types/lsp/index.d.ts +0 -130
- package/dist/types/lsp/lspmux.d.ts +0 -58
- package/dist/types/lsp/render.d.ts +0 -32
- package/dist/types/lsp/startup-events.d.ts +0 -12
- package/dist/types/lsp/types.d.ts +0 -306
- package/dist/types/lsp/utils.d.ts +0 -108
- package/dist/types/main.d.ts +0 -139
- package/dist/types/memories/index.d.ts +0 -28
- package/dist/types/memories/storage.d.ts +0 -110
- package/dist/types/memory-backend/index.d.ts +0 -4
- package/dist/types/memory-backend/local-backend.d.ts +0 -9
- package/dist/types/memory-backend/off-backend.d.ts +0 -7
- package/dist/types/memory-backend/resolve.d.ts +0 -15
- package/dist/types/memory-backend/types.d.ts +0 -61
- package/dist/types/migrate/action-planner.d.ts +0 -11
- package/dist/types/migrate/adapters/claude-code.d.ts +0 -2
- package/dist/types/migrate/adapters/codex.d.ts +0 -5
- package/dist/types/migrate/adapters/index.d.ts +0 -45
- package/dist/types/migrate/adapters/opencode.d.ts +0 -2
- package/dist/types/migrate/executor.d.ts +0 -2
- package/dist/types/migrate/mcp-mapper.d.ts +0 -20
- package/dist/types/migrate/report.d.ts +0 -18
- package/dist/types/migrate/skill-normalizer.d.ts +0 -27
- package/dist/types/migrate/types.d.ts +0 -126
- package/dist/types/modes/acp/acp-agent.d.ts +0 -78
- package/dist/types/modes/acp/acp-client-bridge.d.ts +0 -9
- package/dist/types/modes/acp/acp-event-mapper.d.ts +0 -36
- package/dist/types/modes/acp/acp-mode.d.ts +0 -17
- package/dist/types/modes/acp/index.d.ts +0 -2
- package/dist/types/modes/acp/terminal-auth.d.ts +0 -6
- package/dist/types/modes/bridge/auth.d.ts +0 -12
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +0 -9
- package/dist/types/modes/bridge/bridge-mode.d.ts +0 -47
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +0 -88
- package/dist/types/modes/bridge/event-stream.d.ts +0 -8
- package/dist/types/modes/components/agent-dashboard.d.ts +0 -21
- package/dist/types/modes/components/assistant-message.d.ts +0 -20
- package/dist/types/modes/components/bash-execution.d.ts +0 -29
- package/dist/types/modes/components/bordered-loader.d.ts +0 -11
- package/dist/types/modes/components/branch-summary-message.d.ts +0 -13
- package/dist/types/modes/components/btw-panel.d.ts +0 -16
- package/dist/types/modes/components/compaction-summary-message.d.ts +0 -13
- package/dist/types/modes/components/countdown-timer.d.ts +0 -14
- package/dist/types/modes/components/custom-editor.d.ts +0 -69
- package/dist/types/modes/components/custom-message.d.ts +0 -15
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +0 -12
- package/dist/types/modes/components/custom-provider-wizard.d.ts +0 -10
- package/dist/types/modes/components/diff.d.ts +0 -11
- package/dist/types/modes/components/dynamic-border.d.ts +0 -14
- package/dist/types/modes/components/eval-execution.d.ts +0 -23
- package/dist/types/modes/components/execution-shared.d.ts +0 -46
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +0 -26
- package/dist/types/modes/components/extensions/extension-list.d.ts +0 -39
- package/dist/types/modes/components/extensions/index.d.ts +0 -8
- package/dist/types/modes/components/extensions/inspector-panel.d.ts +0 -8
- package/dist/types/modes/components/extensions/state-manager.d.ts +0 -50
- package/dist/types/modes/components/extensions/types.d.ts +0 -151
- package/dist/types/modes/components/footer.d.ts +0 -30
- package/dist/types/modes/components/history-search.d.ts +0 -7
- package/dist/types/modes/components/hook-editor.d.ts +0 -18
- package/dist/types/modes/components/hook-input.d.ts +0 -15
- package/dist/types/modes/components/hook-message.d.ts +0 -15
- package/dist/types/modes/components/hook-selector.d.ts +0 -59
- package/dist/types/modes/components/index.d.ts +0 -35
- package/dist/types/modes/components/irc-sidebar.d.ts +0 -32
- package/dist/types/modes/components/jobs-overlay-model.d.ts +0 -31
- package/dist/types/modes/components/jobs-overlay.d.ts +0 -30
- package/dist/types/modes/components/keybinding-hints.d.ts +0 -40
- package/dist/types/modes/components/login-dialog.d.ts +0 -32
- package/dist/types/modes/components/message-frame.d.ts +0 -42
- package/dist/types/modes/components/model-selector.d.ts +0 -60
- package/dist/types/modes/components/oauth-selector.d.ts +0 -17
- package/dist/types/modes/components/pet-selector.d.ts +0 -11
- package/dist/types/modes/components/plugin-selector.d.ts +0 -26
- package/dist/types/modes/components/plugin-settings.d.ts +0 -66
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +0 -7
- package/dist/types/modes/components/queue-mode-selector.d.ts +0 -9
- package/dist/types/modes/components/queued-message-selector.d.ts +0 -12
- package/dist/types/modes/components/read-tool-group.d.ts +0 -30
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +0 -26
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +0 -59
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/modes/components/session-selector.d.ts +0 -43
- package/dist/types/modes/components/settings-defs.d.ts +0 -50
- package/dist/types/modes/components/settings-selector.d.ts +0 -64
- package/dist/types/modes/components/show-images-selector.d.ts +0 -9
- package/dist/types/modes/components/skill-hud/render.d.ts +0 -2
- package/dist/types/modes/components/skill-message.d.ts +0 -10
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +0 -4
- package/dist/types/modes/components/status-line/git-utils.d.ts +0 -28
- package/dist/types/modes/components/status-line/index.d.ts +0 -4
- package/dist/types/modes/components/status-line/presets.d.ts +0 -3
- package/dist/types/modes/components/status-line/segments.d.ts +0 -5
- package/dist/types/modes/components/status-line/separators.d.ts +0 -3
- package/dist/types/modes/components/status-line/token-rate.d.ts +0 -10
- package/dist/types/modes/components/status-line/types.d.ts +0 -83
- package/dist/types/modes/components/status-line.d.ts +0 -94
- package/dist/types/modes/components/theme-selector.d.ts +0 -10
- package/dist/types/modes/components/thinking-selector.d.ts +0 -15
- package/dist/types/modes/components/todo-reminder.d.ts +0 -13
- package/dist/types/modes/components/tool-execution.d.ts +0 -55
- package/dist/types/modes/components/tree-selector.d.ts +0 -31
- package/dist/types/modes/components/ttsr-notification.d.ts +0 -13
- package/dist/types/modes/components/two-column-body.d.ts +0 -9
- package/dist/types/modes/components/user-message-selector.d.ts +0 -28
- package/dist/types/modes/components/user-message.d.ts +0 -8
- package/dist/types/modes/components/visual-truncate.d.ts +0 -19
- package/dist/types/modes/components/welcome.d.ts +0 -46
- package/dist/types/modes/controllers/btw-controller.d.ts +0 -10
- package/dist/types/modes/controllers/command-controller-shared.d.ts +0 -47
- package/dist/types/modes/controllers/command-controller.d.ts +0 -40
- package/dist/types/modes/controllers/event-controller.d.ts +0 -21
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -80
- package/dist/types/modes/controllers/input-controller.d.ts +0 -42
- package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +0 -10
- package/dist/types/modes/controllers/selector-controller.d.ts +0 -68
- package/dist/types/modes/controllers/ssh-command-controller.d.ts +0 -10
- package/dist/types/modes/controllers/todo-command-controller.d.ts +0 -7
- package/dist/types/modes/emoji-autocomplete.d.ts +0 -16
- package/dist/types/modes/index.d.ts +0 -10
- package/dist/types/modes/interactive-mode.d.ts +0 -297
- package/dist/types/modes/irc-observation-ledger.d.ts +0 -22
- package/dist/types/modes/jobs-observer.d.ts +0 -57
- package/dist/types/modes/oauth-manual-input.d.ts +0 -8
- package/dist/types/modes/print-mode.d.ts +0 -44
- package/dist/types/modes/prompt-action-autocomplete.d.ts +0 -50
- package/dist/types/modes/rpc/host-tools.d.ts +0 -1
- package/dist/types/modes/rpc/host-uris.d.ts +0 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +0 -299
- package/dist/types/modes/rpc/rpc-mode.d.ts +0 -86
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +0 -10
- package/dist/types/modes/rpc/rpc-types.d.ts +0 -810
- package/dist/types/modes/runtime-init.d.ts +0 -21
- package/dist/types/modes/session-observer-registry.d.ts +0 -26
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +0 -57
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +0 -37
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +0 -2
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +0 -102
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +0 -103
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +0 -60
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +0 -37
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +0 -56
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +0 -16
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +0 -17
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/responses.d.ts +0 -4
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +0 -42
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +0 -27
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +0 -29
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +0 -68
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +0 -166
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +0 -127
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +0 -154
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +0 -39
- package/dist/types/modes/shared.d.ts +0 -15
- package/dist/types/modes/slash-command-visibility.d.ts +0 -1
- package/dist/types/modes/theme/defaults/index.d.ts +0 -652
- package/dist/types/modes/theme/mermaid-cache.d.ts +0 -9
- package/dist/types/modes/theme/shimmer.d.ts +0 -38
- package/dist/types/modes/theme/theme.d.ts +0 -275
- package/dist/types/modes/tmux-scroll.d.ts +0 -18
- package/dist/types/modes/types.d.ts +0 -318
- package/dist/types/modes/utils/abort-message.d.ts +0 -4
- package/dist/types/modes/utils/context-usage.d.ts +0 -36
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +0 -5
- package/dist/types/modes/utils/injected-user-submission.d.ts +0 -52
- package/dist/types/modes/utils/irc-message.d.ts +0 -38
- package/dist/types/modes/utils/keybinding-matchers.d.ts +0 -10
- package/dist/types/modes/utils/terminal-bell.d.ts +0 -3
- package/dist/types/modes/utils/tools-markdown.d.ts +0 -5
- package/dist/types/modes/utils/ui-helpers.d.ts +0 -75
- package/dist/types/notifications/attachment-registry.d.ts +0 -17
- package/dist/types/notifications/chat-adapters.d.ts +0 -9
- package/dist/types/notifications/config-commands.d.ts +0 -64
- package/dist/types/notifications/config.d.ts +0 -132
- package/dist/types/notifications/daemon-paths.d.ts +0 -17
- package/dist/types/notifications/engine.d.ts +0 -59
- package/dist/types/notifications/helpers.d.ts +0 -55
- package/dist/types/notifications/html-format.d.ts +0 -75
- package/dist/types/notifications/index.d.ts +0 -219
- package/dist/types/notifications/lifecycle-commands.d.ts +0 -69
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +0 -107
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +0 -144
- package/dist/types/notifications/managed-daemon.d.ts +0 -48
- package/dist/types/notifications/notification-service.d.ts +0 -144
- package/dist/types/notifications/operator-runtime.d.ts +0 -52
- package/dist/types/notifications/rate-limit-pool.d.ts +0 -113
- package/dist/types/notifications/recent-activity.d.ts +0 -39
- package/dist/types/notifications/reply-sent-store.d.ts +0 -53
- package/dist/types/notifications/rich-draft.d.ts +0 -68
- package/dist/types/notifications/rich-render.d.ts +0 -90
- package/dist/types/notifications/telegram-cli.d.ts +0 -19
- package/dist/types/notifications/telegram-daemon-cli.d.ts +0 -28
- package/dist/types/notifications/telegram-daemon-control.d.ts +0 -70
- package/dist/types/notifications/telegram-daemon.d.ts +0 -558
- package/dist/types/notifications/telegram-reference.d.ts +0 -136
- package/dist/types/notifications/threaded-inbound.d.ts +0 -77
- package/dist/types/notifications/threaded-render.d.ts +0 -88
- package/dist/types/notifications/topic-registry.d.ts +0 -89
- package/dist/types/plan-mode/approved-plan.d.ts +0 -49
- package/dist/types/plan-mode/state.d.ts +0 -6
- package/dist/types/registry/agent-registry.d.ts +0 -64
- package/dist/types/reminders/star-reminder.d.ts +0 -115
- package/dist/types/research-plan/index.d.ts +0 -1
- package/dist/types/research-plan/ledger.d.ts +0 -33
- package/dist/types/rlm/artifacts.d.ts +0 -9
- package/dist/types/rlm/complete-research-tool.d.ts +0 -35
- package/dist/types/rlm/data-context.d.ts +0 -6
- package/dist/types/rlm/index.d.ts +0 -47
- package/dist/types/rlm/notebook.d.ts +0 -12
- package/dist/types/rlm/preset.d.ts +0 -23
- package/dist/types/rlm/python-tool.d.ts +0 -16
- package/dist/types/rlm/report.d.ts +0 -14
- package/dist/types/rlm/types.d.ts +0 -37
- package/dist/types/runtime/process-lifecycle.d.ts +0 -108
- package/dist/types/runtime-api-key.d.ts +0 -7
- package/dist/types/runtime-credential-selector.d.ts +0 -7
- package/dist/types/runtime-mcp/client.d.ts +0 -74
- package/dist/types/runtime-mcp/config-writer.d.ts +0 -89
- package/dist/types/runtime-mcp/config.d.ts +0 -77
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
- package/dist/types/runtime-mcp/index.d.ts +0 -18
- package/dist/types/runtime-mcp/json-rpc.d.ts +0 -22
- package/dist/types/runtime-mcp/loader.d.ts +0 -45
- package/dist/types/runtime-mcp/manager.d.ts +0 -195
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +0 -40
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -59
- package/dist/types/runtime-mcp/render.d.ts +0 -25
- package/dist/types/runtime-mcp/smithery-auth.d.ts +0 -16
- package/dist/types/runtime-mcp/smithery-connect.d.ts +0 -38
- package/dist/types/runtime-mcp/smithery-registry.d.ts +0 -51
- package/dist/types/runtime-mcp/tool-bridge.d.ts +0 -86
- package/dist/types/runtime-mcp/tool-cache.d.ts +0 -8
- package/dist/types/runtime-mcp/transports/http.d.ts +0 -36
- package/dist/types/runtime-mcp/transports/index.d.ts +0 -5
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -29
- package/dist/types/runtime-mcp/types.d.ts +0 -349
- package/dist/types/sdk.d.ts +0 -256
- package/dist/types/secrets/index.d.ts +0 -9
- package/dist/types/secrets/obfuscator.d.ts +0 -23
- package/dist/types/secrets/regex.d.ts +0 -2
- package/dist/types/session/agent-session.d.ts +0 -1238
- package/dist/types/session/agent-storage.d.ts +0 -101
- package/dist/types/session/artifacts.d.ts +0 -64
- package/dist/types/session/auth-broker-config.d.ts +0 -13
- package/dist/types/session/auth-storage.d.ts +0 -6
- package/dist/types/session/blob-store.d.ts +0 -160
- package/dist/types/session/cache-economics.d.ts +0 -79
- package/dist/types/session/client-bridge.d.ts +0 -88
- package/dist/types/session/context-estimation.d.ts +0 -31
- package/dist/types/session/contribution-prep.d.ts +0 -47
- package/dist/types/session/file-mention-pruning.d.ts +0 -8
- package/dist/types/session/history-storage.d.ts +0 -16
- package/dist/types/session/messages.d.ts +0 -169
- package/dist/types/session/provider-safety-stop.d.ts +0 -5
- package/dist/types/session/session-dump-format.d.ts +0 -22
- package/dist/types/session/session-manager.d.ts +0 -781
- package/dist/types/session/session-storage.d.ts +0 -230
- package/dist/types/session/streaming-output.d.ts +0 -209
- package/dist/types/session/tool-choice-queue.d.ts +0 -84
- package/dist/types/session/yield-queue.d.ts +0 -24
- package/dist/types/setup/credential-auto-import.d.ts +0 -63
- package/dist/types/setup/credential-import.d.ts +0 -82
- package/dist/types/setup/hermes-setup.d.ts +0 -78
- package/dist/types/setup/host-plugin-setup.d.ts +0 -39
- package/dist/types/setup/model-onboarding-guidance.d.ts +0 -9
- package/dist/types/setup/provider-onboarding.d.ts +0 -52
- package/dist/types/skc-runtime/cli-write-receipt.d.ts +0 -24
- package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +0 -39
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +0 -114
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +0 -36
- package/dist/types/skc-runtime/deep-interview-state.d.ts +0 -127
- package/dist/types/skc-runtime/gc-render.d.ts +0 -6
- package/dist/types/skc-runtime/gc-runtime.d.ts +0 -134
- package/dist/types/skc-runtime/goal-mode-request.d.ts +0 -53
- package/dist/types/skc-runtime/launch-tmux.d.ts +0 -78
- package/dist/types/skc-runtime/launch-worktree.d.ts +0 -46
- package/dist/types/skc-runtime/ledger-event-renderer.d.ts +0 -68
- package/dist/types/skc-runtime/psmux-detect.d.ts +0 -78
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +0 -26
- package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +0 -3
- package/dist/types/skc-runtime/session-layout.d.ts +0 -59
- package/dist/types/skc-runtime/session-resolution.d.ts +0 -47
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +0 -70
- package/dist/types/skc-runtime/state-graph.d.ts +0 -4
- package/dist/types/skc-runtime/state-migrations.d.ts +0 -33
- package/dist/types/skc-runtime/state-renderer.d.ts +0 -70
- package/dist/types/skc-runtime/state-runtime.d.ts +0 -38
- package/dist/types/skc-runtime/state-schema.d.ts +0 -319
- package/dist/types/skc-runtime/state-validation.d.ts +0 -6
- package/dist/types/skc-runtime/state-writer.d.ts +0 -244
- package/dist/types/skc-runtime/team-gc.d.ts +0 -7
- package/dist/types/skc-runtime/team-runtime.d.ts +0 -355
- package/dist/types/skc-runtime/tmux-common.d.ts +0 -111
- package/dist/types/skc-runtime/tmux-gc.d.ts +0 -7
- package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +0 -5
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +0 -309
- package/dist/types/skc-runtime/tmux-sessions.d.ts +0 -94
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +0 -93
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +0 -42
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -410
- package/dist/types/skc-runtime/workflow-command-ref.d.ts +0 -43
- package/dist/types/skc-runtime/workflow-manifest.d.ts +0 -54
- package/dist/types/skill-state/active-state.d.ts +0 -126
- package/dist/types/skill-state/canonical-skills.d.ts +0 -3
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +0 -38
- package/dist/types/skill-state/initial-phase.d.ts +0 -12
- package/dist/types/skill-state/workflow-hud.d.ts +0 -83
- package/dist/types/skill-state/workflow-state-contract.d.ts +0 -57
- package/dist/types/skill-state/workflow-state-version.d.ts +0 -3
- package/dist/types/slash-commands/acp-builtins.d.ts +0 -18
- package/dist/types/slash-commands/builtin-registry.d.ts +0 -24
- package/dist/types/slash-commands/helpers/context-report.d.ts +0 -7
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +0 -82
- package/dist/types/slash-commands/helpers/format.d.ts +0 -10
- package/dist/types/slash-commands/helpers/mcp.d.ts +0 -3
- package/dist/types/slash-commands/helpers/parse.d.ts +0 -33
- package/dist/types/slash-commands/helpers/ssh.d.ts +0 -3
- package/dist/types/slash-commands/helpers/todo.d.ts +0 -3
- package/dist/types/slash-commands/helpers/usage-report.d.ts +0 -7
- package/dist/types/slash-commands/types.d.ts +0 -120
- package/dist/types/ssh/config-writer.d.ts +0 -49
- package/dist/types/ssh/connection-manager.d.ts +0 -33
- package/dist/types/ssh/ssh-executor.d.ts +0 -37
- package/dist/types/ssh/sshfs-mount.d.ts +0 -6
- package/dist/types/ssh/utils.d.ts +0 -2
- package/dist/types/stt/downloader.d.ts +0 -9
- package/dist/types/stt/index.d.ts +0 -3
- package/dist/types/stt/recorder.d.ts +0 -13
- package/dist/types/stt/setup.d.ts +0 -18
- package/dist/types/stt/stt-controller.d.ts +0 -16
- package/dist/types/stt/transcriber.d.ts +0 -16
- package/dist/types/system-prompt.d.ts +0 -118
- package/dist/types/task/agents.d.ts +0 -28
- package/dist/types/task/commands.d.ts +0 -35
- package/dist/types/task/discovery.d.ts +0 -19
- package/dist/types/task/executor.d.ts +0 -127
- package/dist/types/task/fork-context-advisory.d.ts +0 -13
- package/dist/types/task/fork-context-budget.d.ts +0 -7
- package/dist/types/task/id.d.ts +0 -7
- package/dist/types/task/index.d.ts +0 -43
- package/dist/types/task/name-generator.d.ts +0 -16
- package/dist/types/task/output-manager.d.ts +0 -36
- package/dist/types/task/parallel.d.ts +0 -34
- package/dist/types/task/receipt.d.ts +0 -89
- package/dist/types/task/render.d.ts +0 -28
- package/dist/types/task/roi-reconciliation.d.ts +0 -27
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/dist/types/task/skc-command.d.ts +0 -7
- package/dist/types/task/spawn-gate.d.ts +0 -29
- package/dist/types/task/subprocess-tool-registry.d.ts +0 -72
- package/dist/types/task/token-log.d.ts +0 -30
- package/dist/types/task/token-log.test.d.ts +0 -1
- package/dist/types/task/types.d.ts +0 -516
- package/dist/types/task/worktree.d.ts +0 -94
- package/dist/types/thinking-metadata.d.ts +0 -16
- package/dist/types/thinking.d.ts +0 -22
- package/dist/types/tool-discovery/tool-index.d.ts +0 -112
- package/dist/types/tools/archive-reader.d.ts +0 -40
- package/dist/types/tools/ask-answer-registry.d.ts +0 -18
- package/dist/types/tools/ask.d.ts +0 -123
- package/dist/types/tools/ast-edit.d.ts +0 -77
- package/dist/types/tools/ast-grep.d.ts +0 -69
- package/dist/types/tools/auto-generated-guard.d.ts +0 -17
- package/dist/types/tools/bash-allowed-prefixes.d.ts +0 -10
- package/dist/types/tools/bash-command-fixup.d.ts +0 -11
- package/dist/types/tools/bash-interactive.d.ts +0 -16
- package/dist/types/tools/bash-interceptor.d.ts +0 -24
- package/dist/types/tools/bash-pty-selection.d.ts +0 -7
- package/dist/types/tools/bash-skill-urls.d.ts +0 -31
- package/dist/types/tools/bash.d.ts +0 -157
- package/dist/types/tools/bisect.d.ts +0 -81
- package/dist/types/tools/browser/actions.d.ts +0 -54
- package/dist/types/tools/browser/attach.d.ts +0 -50
- package/dist/types/tools/browser/launch.d.ts +0 -62
- package/dist/types/tools/browser/readable.d.ts +0 -17
- package/dist/types/tools/browser/registry.d.ts +0 -57
- package/dist/types/tools/browser/render.d.ts +0 -53
- package/dist/types/tools/browser/tab-protocol.d.ts +0 -144
- package/dist/types/tools/browser/tab-supervisor.d.ts +0 -109
- package/dist/types/tools/browser/tab-worker-entry.d.ts +0 -1
- package/dist/types/tools/browser/tab-worker.d.ts +0 -19
- package/dist/types/tools/browser.d.ts +0 -211
- package/dist/types/tools/calculator.d.ts +0 -76
- package/dist/types/tools/checkpoint.d.ts +0 -63
- package/dist/types/tools/composer-bash-policy.d.ts +0 -14
- package/dist/types/tools/computer/render.d.ts +0 -17
- package/dist/types/tools/computer-gc.d.ts +0 -23
- package/dist/types/tools/computer.d.ts +0 -465
- package/dist/types/tools/conflict-detect.d.ts +0 -205
- package/dist/types/tools/context.d.ts +0 -19
- package/dist/types/tools/cron.d.ts +0 -91
- package/dist/types/tools/debug.d.ts +0 -209
- package/dist/types/tools/eval.d.ts +0 -108
- package/dist/types/tools/fetch.d.ts +0 -84
- package/dist/types/tools/file-recorder.d.ts +0 -13
- package/dist/types/tools/find.d.ts +0 -95
- package/dist/types/tools/fs-cache-invalidation.d.ts +0 -15
- package/dist/types/tools/gh-format.d.ts +0 -6
- package/dist/types/tools/gh-renderer.d.ts +0 -25
- package/dist/types/tools/gh.d.ts +0 -356
- package/dist/types/tools/github-cache.d.ts +0 -103
- package/dist/types/tools/grouped-file-output.d.ts +0 -36
- package/dist/types/tools/hindsight-recall.d.ts +0 -21
- package/dist/types/tools/hindsight-reflect.d.ts +0 -23
- package/dist/types/tools/hindsight-retain.d.ts +0 -27
- package/dist/types/tools/image-gen.d.ts +0 -79
- package/dist/types/tools/index.d.ts +0 -371
- package/dist/types/tools/irc.d.ts +0 -79
- package/dist/types/tools/job.d.ts +0 -78
- package/dist/types/tools/json-tree.d.ts +0 -23
- package/dist/types/tools/jtd-to-json-schema.d.ts +0 -29
- package/dist/types/tools/jtd-to-typescript.d.ts +0 -26
- package/dist/types/tools/jtd-utils.d.ts +0 -42
- package/dist/types/tools/list-limit.d.ts +0 -12
- package/dist/types/tools/match-line-format.d.ts +0 -11
- package/dist/types/tools/monitor.d.ts +0 -54
- package/dist/types/tools/output-meta.d.ts +0 -204
- package/dist/types/tools/path-utils.d.ts +0 -147
- package/dist/types/tools/plan-mode-guard.d.ts +0 -18
- package/dist/types/tools/read.d.ts +0 -85
- package/dist/types/tools/recipe/index.d.ts +0 -45
- package/dist/types/tools/recipe/render.d.ts +0 -36
- package/dist/types/tools/recipe/runner.d.ts +0 -60
- package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
- package/dist/types/tools/recipe/runners/index.d.ts +0 -2
- package/dist/types/tools/recipe/runners/just.d.ts +0 -2
- package/dist/types/tools/recipe/runners/make.d.ts +0 -2
- package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
- package/dist/types/tools/recipe/runners/task.d.ts +0 -2
- package/dist/types/tools/render-mermaid.d.ts +0 -37
- package/dist/types/tools/render-utils.d.ts +0 -158
- package/dist/types/tools/renderers.d.ts +0 -26
- package/dist/types/tools/resolve.d.ts +0 -80
- package/dist/types/tools/resource-gc.d.ts +0 -54
- package/dist/types/tools/review.d.ts +0 -63
- package/dist/types/tools/search-tool-bm25.d.ts +0 -65
- package/dist/types/tools/search.d.ts +0 -93
- package/dist/types/tools/skill-discovery.d.ts +0 -40
- package/dist/types/tools/skill.d.ts +0 -48
- package/dist/types/tools/sqlite-reader.d.ts +0 -94
- package/dist/types/tools/ssh.d.ts +0 -67
- package/dist/types/tools/subagent-render.d.ts +0 -31
- package/dist/types/tools/subagent.d.ts +0 -117
- package/dist/types/tools/telegram-send.d.ts +0 -39
- package/dist/types/tools/todo-write.d.ts +0 -120
- package/dist/types/tools/tool-errors.d.ts +0 -33
- package/dist/types/tools/tool-result.d.ts +0 -30
- package/dist/types/tools/tool-timeouts.d.ts +0 -56
- package/dist/types/tools/ultragoal-ask-guard.d.ts +0 -12
- package/dist/types/tools/vim.d.ts +0 -58
- package/dist/types/tools/write.d.ts +0 -59
- package/dist/types/tools/yield.d.ts +0 -25
- package/dist/types/tui/code-cell.d.ts +0 -32
- package/dist/types/tui/file-list.d.ts +0 -22
- package/dist/types/tui/hyperlink.d.ts +0 -42
- package/dist/types/tui/index.d.ts +0 -11
- package/dist/types/tui/output-block.d.ts +0 -28
- package/dist/types/tui/status-line.d.ts +0 -18
- package/dist/types/tui/tree-list.d.ts +0 -19
- package/dist/types/tui/types.d.ts +0 -13
- package/dist/types/tui/utils.d.ts +0 -36
- package/dist/types/utils/changelog.d.ts +0 -37
- package/dist/types/utils/clipboard.d.ts +0 -22
- package/dist/types/utils/cmux-workspace.d.ts +0 -52
- package/dist/types/utils/command-args.d.ts +0 -9
- package/dist/types/utils/commit-message-generator.d.ts +0 -7
- package/dist/types/utils/edit-mode.d.ts +0 -13
- package/dist/types/utils/event-bus.d.ts +0 -6
- package/dist/types/utils/external-editor.d.ts +0 -19
- package/dist/types/utils/file-display-mode.d.ts +0 -27
- package/dist/types/utils/file-mentions.d.ts +0 -25
- package/dist/types/utils/git.d.ts +0 -393
- package/dist/types/utils/image-loading.d.ts +0 -26
- package/dist/types/utils/image-resize.d.ts +0 -39
- package/dist/types/utils/lang-from-path.d.ts +0 -8
- package/dist/types/utils/linkedom.d.ts +0 -4
- package/dist/types/utils/markit.d.ts +0 -7
- package/dist/types/utils/open.d.ts +0 -2
- package/dist/types/utils/pasted-image-path.d.ts +0 -32
- package/dist/types/utils/session-color.d.ts +0 -10
- package/dist/types/utils/shell-snapshot.d.ts +0 -5
- package/dist/types/utils/sixel.d.ts +0 -21
- package/dist/types/utils/title-generator.d.ts +0 -31
- package/dist/types/utils/tool-choice.d.ts +0 -20
- package/dist/types/utils/tools-manager.d.ts +0 -9
- package/dist/types/vim/buffer.d.ts +0 -41
- package/dist/types/vim/commands.d.ts +0 -6
- package/dist/types/vim/engine.d.ts +0 -47
- package/dist/types/vim/parser.d.ts +0 -3
- package/dist/types/vim/render.d.ts +0 -25
- package/dist/types/vim/types.d.ts +0 -182
- package/dist/types/web/insane/bridge.d.ts +0 -103
- package/dist/types/web/insane/url-guard.d.ts +0 -25
- package/dist/types/web/kagi.d.ts +0 -23
- package/dist/types/web/parallel.d.ts +0 -58
- package/dist/types/web/scrapers/artifacthub.d.ts +0 -6
- package/dist/types/web/scrapers/arxiv.d.ts +0 -5
- package/dist/types/web/scrapers/aur.d.ts +0 -5
- package/dist/types/web/scrapers/biorxiv.d.ts +0 -5
- package/dist/types/web/scrapers/bluesky.d.ts +0 -5
- package/dist/types/web/scrapers/brew.d.ts +0 -5
- package/dist/types/web/scrapers/cheatsh.d.ts +0 -8
- package/dist/types/web/scrapers/chocolatey.d.ts +0 -5
- package/dist/types/web/scrapers/choosealicense.d.ts +0 -2
- package/dist/types/web/scrapers/cisa-kev.d.ts +0 -5
- package/dist/types/web/scrapers/clojars.d.ts +0 -5
- package/dist/types/web/scrapers/coingecko.d.ts +0 -5
- package/dist/types/web/scrapers/crates-io.d.ts +0 -5
- package/dist/types/web/scrapers/crossref.d.ts +0 -2
- package/dist/types/web/scrapers/devto.d.ts +0 -5
- package/dist/types/web/scrapers/discogs.d.ts +0 -8
- package/dist/types/web/scrapers/discourse.d.ts +0 -5
- package/dist/types/web/scrapers/dockerhub.d.ts +0 -5
- package/dist/types/web/scrapers/docs-rs.d.ts +0 -2
- package/dist/types/web/scrapers/fdroid.d.ts +0 -5
- package/dist/types/web/scrapers/firefox-addons.d.ts +0 -2
- package/dist/types/web/scrapers/flathub.d.ts +0 -2
- package/dist/types/web/scrapers/github-gist.d.ts +0 -5
- package/dist/types/web/scrapers/github.d.ts +0 -12
- package/dist/types/web/scrapers/gitlab.d.ts +0 -5
- package/dist/types/web/scrapers/go-pkg.d.ts +0 -5
- package/dist/types/web/scrapers/hackage.d.ts +0 -5
- package/dist/types/web/scrapers/hackernews.d.ts +0 -2
- package/dist/types/web/scrapers/hex.d.ts +0 -5
- package/dist/types/web/scrapers/huggingface.d.ts +0 -2
- package/dist/types/web/scrapers/iacr.d.ts +0 -5
- package/dist/types/web/scrapers/index.d.ts +0 -84
- package/dist/types/web/scrapers/jetbrains-marketplace.d.ts +0 -2
- package/dist/types/web/scrapers/lemmy.d.ts +0 -2
- package/dist/types/web/scrapers/lobsters.d.ts +0 -5
- package/dist/types/web/scrapers/mastodon.d.ts +0 -5
- package/dist/types/web/scrapers/maven.d.ts +0 -6
- package/dist/types/web/scrapers/mdn.d.ts +0 -2
- package/dist/types/web/scrapers/metacpan.d.ts +0 -5
- package/dist/types/web/scrapers/musicbrainz.d.ts +0 -5
- package/dist/types/web/scrapers/npm.d.ts +0 -5
- package/dist/types/web/scrapers/nuget.d.ts +0 -5
- package/dist/types/web/scrapers/nvd.d.ts +0 -5
- package/dist/types/web/scrapers/ollama.d.ts +0 -2
- package/dist/types/web/scrapers/open-vsx.d.ts +0 -5
- package/dist/types/web/scrapers/opencorporates.d.ts +0 -5
- package/dist/types/web/scrapers/openlibrary.d.ts +0 -5
- package/dist/types/web/scrapers/orcid.d.ts +0 -5
- package/dist/types/web/scrapers/osv.d.ts +0 -5
- package/dist/types/web/scrapers/packagist.d.ts +0 -5
- package/dist/types/web/scrapers/pub-dev.d.ts +0 -5
- package/dist/types/web/scrapers/pubmed.d.ts +0 -5
- package/dist/types/web/scrapers/pypi.d.ts +0 -5
- package/dist/types/web/scrapers/rawg.d.ts +0 -2
- package/dist/types/web/scrapers/readthedocs.d.ts +0 -2
- package/dist/types/web/scrapers/reddit.d.ts +0 -5
- package/dist/types/web/scrapers/repology.d.ts +0 -5
- package/dist/types/web/scrapers/rfc.d.ts +0 -5
- package/dist/types/web/scrapers/rubygems.d.ts +0 -5
- package/dist/types/web/scrapers/searchcode.d.ts +0 -2
- package/dist/types/web/scrapers/sec-edgar.d.ts +0 -5
- package/dist/types/web/scrapers/semantic-scholar.d.ts +0 -2
- package/dist/types/web/scrapers/snapcraft.d.ts +0 -2
- package/dist/types/web/scrapers/sourcegraph.d.ts +0 -2
- package/dist/types/web/scrapers/spdx.d.ts +0 -5
- package/dist/types/web/scrapers/spotify.d.ts +0 -8
- package/dist/types/web/scrapers/stackoverflow.d.ts +0 -6
- package/dist/types/web/scrapers/terraform.d.ts +0 -5
- package/dist/types/web/scrapers/tldr.d.ts +0 -7
- package/dist/types/web/scrapers/twitter.d.ts +0 -5
- package/dist/types/web/scrapers/types.d.ts +0 -83
- package/dist/types/web/scrapers/utils.d.ts +0 -32
- package/dist/types/web/scrapers/vimeo.d.ts +0 -5
- package/dist/types/web/scrapers/vscode-marketplace.d.ts +0 -5
- package/dist/types/web/scrapers/w3c.d.ts +0 -2
- package/dist/types/web/scrapers/wikidata.d.ts +0 -5
- package/dist/types/web/scrapers/wikipedia.d.ts +0 -5
- package/dist/types/web/scrapers/youtube.d.ts +0 -13
- package/dist/types/web/search/index.d.ts +0 -118
- package/dist/types/web/search/provider.d.ts +0 -55
- package/dist/types/web/search/providers/anthropic.d.ts +0 -32
- package/dist/types/web/search/providers/base.d.ts +0 -90
- package/dist/types/web/search/providers/brave.d.ts +0 -27
- package/dist/types/web/search/providers/codex.d.ts +0 -35
- package/dist/types/web/search/providers/duckduckgo.d.ts +0 -57
- package/dist/types/web/search/providers/exa.d.ts +0 -52
- package/dist/types/web/search/providers/gemini.d.ts +0 -57
- package/dist/types/web/search/providers/insane.d.ts +0 -53
- package/dist/types/web/search/providers/jina.d.ts +0 -26
- package/dist/types/web/search/providers/kagi.d.ts +0 -24
- package/dist/types/web/search/providers/kimi.d.ts +0 -27
- package/dist/types/web/search/providers/openai-compatible.d.ts +0 -9
- package/dist/types/web/search/providers/parallel.d.ts +0 -15
- package/dist/types/web/search/providers/perplexity.d.ts +0 -38
- package/dist/types/web/search/providers/searxng.d.ts +0 -44
- package/dist/types/web/search/providers/synthetic.d.ts +0 -21
- package/dist/types/web/search/providers/tavily.d.ts +0 -29
- package/dist/types/web/search/providers/text-citations.d.ts +0 -23
- package/dist/types/web/search/providers/utils.d.ts +0 -88
- package/dist/types/web/search/providers/xai.d.ts +0 -64
- package/dist/types/web/search/providers/zai.d.ts +0 -28
- package/dist/types/web/search/render.d.ts +0 -36
- package/dist/types/web/search/types.d.ts +0 -372
- package/dist/types/web/search/utils.d.ts +0 -4
- package/dist/types/workflow/workflow-intent-diff.d.ts +0 -25
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +0 -50
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +0 -2
- package/dist/types/workflow/workflow-intent-report.d.ts +0 -48
- package/dist/types/workspace-tree.d.ts +0 -42
- package/src/sdk.ts +0 -2623
|
@@ -0,0 +1,3413 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import * as fsSync from "node:fs";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import type { NativeExactUnlinkResult } from "@sayknow-cli/natives";
|
|
7
|
+
import * as native from "@sayknow-cli/natives";
|
|
8
|
+
import { resolveEquivalentPath } from "@sayknow-cli/utils";
|
|
9
|
+
import { validateManagedArtifactTree } from "../../session/internal/managed-session-storage";
|
|
10
|
+
import {
|
|
11
|
+
FileSessionStorage,
|
|
12
|
+
SessionDeleteVerificationError,
|
|
13
|
+
type SessionStorageFileIdentity,
|
|
14
|
+
type SessionStorageSnapshot,
|
|
15
|
+
type VerifiedSessionDeleteResult,
|
|
16
|
+
type VerifiedSessionDeleteTarget,
|
|
17
|
+
} from "../../session/session-storage";
|
|
18
|
+
import {
|
|
19
|
+
ensureLaunchWorktree,
|
|
20
|
+
ensureReusableNodeModules,
|
|
21
|
+
planLaunchWorktree,
|
|
22
|
+
type SkcLaunchWorktreePlan,
|
|
23
|
+
} from "../../skc-runtime/launch-worktree";
|
|
24
|
+
import { SdkClient, SdkClientError } from "../client/client";
|
|
25
|
+
import {
|
|
26
|
+
type LogicalSessionCandidate,
|
|
27
|
+
listManagedSessionCandidates,
|
|
28
|
+
type ManagedSessionScope,
|
|
29
|
+
resolveManagedSessionScope,
|
|
30
|
+
} from "../session-directory";
|
|
31
|
+
import type { SdkStartupFailure, SdkStartupRollbackResult } from "../startup-capability";
|
|
32
|
+
import type { Broker, BrokerCleanupEvidence, BrokerCleanupIdentity, BrokerResponse } from "./broker";
|
|
33
|
+
import { decodeLifecycleUtf8, parseLifecycleJson } from "./lifecycle-codec";
|
|
34
|
+
import type {
|
|
35
|
+
LifecycleCleanupProof,
|
|
36
|
+
LifecycleDurableEffectsReceipt,
|
|
37
|
+
LifecycleEffectIntent,
|
|
38
|
+
LifecycleStartupFailureReceipt,
|
|
39
|
+
LifecycleWorktreeIntent,
|
|
40
|
+
} from "./lifecycle-ledger";
|
|
41
|
+
import {
|
|
42
|
+
type ProcessIncarnationCommandRunner,
|
|
43
|
+
type ProcessIncarnationOptions,
|
|
44
|
+
parseDarwinProcessIncarnation,
|
|
45
|
+
processIncarnation,
|
|
46
|
+
} from "./process-incarnation";
|
|
47
|
+
import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
type ProcessIncarnationCommandRunner,
|
|
51
|
+
type ProcessIncarnationOptions,
|
|
52
|
+
parseDarwinProcessIncarnation,
|
|
53
|
+
processIncarnation,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const READY_TIMEOUT_MS = 10_000;
|
|
57
|
+
const MIN_READY_TIMEOUT_MS = 4_000;
|
|
58
|
+
const MAX_READY_TIMEOUT_MS = 60_000;
|
|
59
|
+
const POLL_MS = 50;
|
|
60
|
+
const CLOSE_TIMEOUT_MS = 2_000;
|
|
61
|
+
const MAX_RECEIVED_AT_SKEW_MS = 5_000;
|
|
62
|
+
const MAX_LIFECYCLE_METADATA_BYTES = 4096;
|
|
63
|
+
const MAX_EFFECT_MARKER_LENGTH = 128;
|
|
64
|
+
const MAX_PROCESS_INCARNATION_LENGTH = 256;
|
|
65
|
+
|
|
66
|
+
export interface LifecycleDeadlines {
|
|
67
|
+
receivedAt: number;
|
|
68
|
+
requestedReadinessTimeoutMs: number;
|
|
69
|
+
semanticReadyDeadlineAt: number;
|
|
70
|
+
terminationStartDeadlineAt: number;
|
|
71
|
+
lifecycleCleanupDeadlineAt: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function deriveLifecycleDeadlines(receivedAt: number, requestedReadinessTimeoutMs: number): LifecycleDeadlines {
|
|
75
|
+
if (
|
|
76
|
+
!Number.isSafeInteger(receivedAt) ||
|
|
77
|
+
!Number.isSafeInteger(requestedReadinessTimeoutMs) ||
|
|
78
|
+
requestedReadinessTimeoutMs < MIN_READY_TIMEOUT_MS ||
|
|
79
|
+
requestedReadinessTimeoutMs > MAX_READY_TIMEOUT_MS
|
|
80
|
+
)
|
|
81
|
+
throw new Error("Lifecycle timing values must be safe integers in the approved readiness range.");
|
|
82
|
+
const phaseWindowMs = Math.min(1_000, Math.max(500, Math.floor(requestedReadinessTimeoutMs / 4)));
|
|
83
|
+
const lifecycleCleanupDeadlineAt = receivedAt + requestedReadinessTimeoutMs;
|
|
84
|
+
const semanticReadyDeadlineAt = lifecycleCleanupDeadlineAt - phaseWindowMs * 2;
|
|
85
|
+
const terminationStartDeadlineAt = lifecycleCleanupDeadlineAt - phaseWindowMs;
|
|
86
|
+
if (
|
|
87
|
+
!Number.isSafeInteger(phaseWindowMs) ||
|
|
88
|
+
!Number.isSafeInteger(lifecycleCleanupDeadlineAt) ||
|
|
89
|
+
!Number.isSafeInteger(semanticReadyDeadlineAt) ||
|
|
90
|
+
!Number.isSafeInteger(terminationStartDeadlineAt)
|
|
91
|
+
)
|
|
92
|
+
throw new Error("Lifecycle timing values overflow the safe integer range.");
|
|
93
|
+
return {
|
|
94
|
+
receivedAt,
|
|
95
|
+
requestedReadinessTimeoutMs,
|
|
96
|
+
semanticReadyDeadlineAt,
|
|
97
|
+
terminationStartDeadlineAt,
|
|
98
|
+
lifecycleCleanupDeadlineAt,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface LifecycleTiming {
|
|
103
|
+
now(): number;
|
|
104
|
+
sleep(ms: number): Promise<void>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const defaultLifecycleTiming: LifecycleTiming = {
|
|
108
|
+
now: Date.now,
|
|
109
|
+
sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
110
|
+
};
|
|
111
|
+
const lifecycleTimingsForTest = new WeakMap<Broker, LifecycleTiming>();
|
|
112
|
+
type LifecycleCommand = SdkInternalSpawnCommand | { file: string; args: string[] };
|
|
113
|
+
type LifecycleCommandResolver = () => LifecycleCommand;
|
|
114
|
+
const lifecycleCommandResolversForTest = new WeakMap<Broker, LifecycleCommandResolver>();
|
|
115
|
+
const lifecycleCleanupHooksForTest = new WeakMap<Broker, () => void>();
|
|
116
|
+
|
|
117
|
+
/** Test-only hook for simulating a crash immediately after one exact lifecycle detach. */
|
|
118
|
+
export function setLifecycleCleanupHookForTest(broker: Broker, hook: (() => void) | undefined): void {
|
|
119
|
+
if (hook) lifecycleCleanupHooksForTest.set(broker, hook);
|
|
120
|
+
else lifecycleCleanupHooksForTest.delete(broker);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function setLifecycleCommandResolverForTest(
|
|
124
|
+
broker: Broker,
|
|
125
|
+
resolver: LifecycleCommandResolver | undefined,
|
|
126
|
+
): void {
|
|
127
|
+
if (resolver) lifecycleCommandResolversForTest.set(broker, resolver);
|
|
128
|
+
else lifecycleCommandResolversForTest.delete(broker);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function setLifecycleTimingForTest(broker: Broker, timing: LifecycleTiming | undefined): void {
|
|
132
|
+
if (timing) lifecycleTimingsForTest.set(broker, timing);
|
|
133
|
+
else lifecycleTimingsForTest.delete(broker);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function lifecycleTiming(broker: Broker): LifecycleTiming {
|
|
137
|
+
return lifecycleTimingsForTest.get(broker) ?? defaultLifecycleTiming;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function hasValidLifecycleDeadlines(value: LifecycleDeadlines, now = Date.now()): boolean {
|
|
141
|
+
const {
|
|
142
|
+
receivedAt,
|
|
143
|
+
requestedReadinessTimeoutMs,
|
|
144
|
+
semanticReadyDeadlineAt,
|
|
145
|
+
terminationStartDeadlineAt,
|
|
146
|
+
lifecycleCleanupDeadlineAt,
|
|
147
|
+
} = value;
|
|
148
|
+
if (
|
|
149
|
+
!Number.isSafeInteger(receivedAt) ||
|
|
150
|
+
!Number.isSafeInteger(requestedReadinessTimeoutMs) ||
|
|
151
|
+
!Number.isSafeInteger(semanticReadyDeadlineAt) ||
|
|
152
|
+
!Number.isSafeInteger(terminationStartDeadlineAt) ||
|
|
153
|
+
!Number.isSafeInteger(lifecycleCleanupDeadlineAt) ||
|
|
154
|
+
!Number.isSafeInteger(now) ||
|
|
155
|
+
(receivedAt > now && receivedAt - now > MAX_RECEIVED_AT_SKEW_MS)
|
|
156
|
+
)
|
|
157
|
+
return false;
|
|
158
|
+
try {
|
|
159
|
+
const expected = deriveLifecycleDeadlines(receivedAt, requestedReadinessTimeoutMs);
|
|
160
|
+
return (
|
|
161
|
+
semanticReadyDeadlineAt === expected.semanticReadyDeadlineAt &&
|
|
162
|
+
terminationStartDeadlineAt === expected.terminationStartDeadlineAt &&
|
|
163
|
+
lifecycleCleanupDeadlineAt === expected.lifecycleCleanupDeadlineAt
|
|
164
|
+
);
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
type Input = Record<string, unknown>;
|
|
170
|
+
export const isCanonicalSessionId = (value: string): boolean => /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
|
|
171
|
+
const defaultStateRoot = (cwd: string) => path.join(path.resolve(cwd), ".skc", "state");
|
|
172
|
+
const hasDefaultStateRoot = (cwd: string, root: string) => path.resolve(root) === defaultStateRoot(cwd);
|
|
173
|
+
|
|
174
|
+
export interface SessionLifecycleWorktreeTarget {
|
|
175
|
+
enabled: true;
|
|
176
|
+
name?: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface SessionLifecycleWorktreeReceipt {
|
|
180
|
+
enabled: true;
|
|
181
|
+
cwd: string;
|
|
182
|
+
created: boolean;
|
|
183
|
+
reused: boolean;
|
|
184
|
+
branch?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface SessionLifecycleTranscriptIdentity {
|
|
188
|
+
dev: string;
|
|
189
|
+
ino: string;
|
|
190
|
+
size: number;
|
|
191
|
+
mtimeMs: number;
|
|
192
|
+
mtimeNs: string;
|
|
193
|
+
sha256: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface SessionLifecycleLaunchRequest {
|
|
197
|
+
operation: "session.create" | "session.fork" | "session.resume";
|
|
198
|
+
sessionId: string;
|
|
199
|
+
cwd: string;
|
|
200
|
+
stateRoot: string;
|
|
201
|
+
sourceSessionId?: string;
|
|
202
|
+
sourceSessionPath?: string;
|
|
203
|
+
sourceSessionIdentity?: SessionLifecycleTranscriptIdentity;
|
|
204
|
+
sourceCwd?: string;
|
|
205
|
+
sessionPath?: string;
|
|
206
|
+
sessionIdentity?: SessionLifecycleTranscriptIdentity;
|
|
207
|
+
/** Broker-issued effect marker which the child echoes only after host readiness. */
|
|
208
|
+
effectMarker?: string;
|
|
209
|
+
modelPreset?: string;
|
|
210
|
+
worktree?: SessionLifecycleWorktreeTarget;
|
|
211
|
+
receivedAt: number;
|
|
212
|
+
requestedReadinessTimeoutMs: number;
|
|
213
|
+
semanticReadyDeadlineAt: number;
|
|
214
|
+
terminationStartDeadlineAt: number;
|
|
215
|
+
lifecycleCleanupDeadlineAt: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isSessionLifecycleTranscriptIdentity(value: unknown): value is SessionLifecycleTranscriptIdentity {
|
|
219
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
220
|
+
const identity = value as Record<string, unknown>;
|
|
221
|
+
return (
|
|
222
|
+
typeof identity.dev === "string" &&
|
|
223
|
+
/^\d+$/.test(identity.dev) &&
|
|
224
|
+
typeof identity.ino === "string" &&
|
|
225
|
+
/^\d+$/.test(identity.ino) &&
|
|
226
|
+
typeof identity.size === "number" &&
|
|
227
|
+
Number.isSafeInteger(identity.size) &&
|
|
228
|
+
identity.size >= 0 &&
|
|
229
|
+
typeof identity.mtimeMs === "number" &&
|
|
230
|
+
Number.isFinite(identity.mtimeMs) &&
|
|
231
|
+
identity.mtimeMs >= 0 &&
|
|
232
|
+
typeof identity.mtimeNs === "string" &&
|
|
233
|
+
/^\d+$/.test(identity.mtimeNs) &&
|
|
234
|
+
typeof identity.sha256 === "string" &&
|
|
235
|
+
/^[a-f0-9]{64}$/.test(identity.sha256)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function hasValidTranscriptAuthority(path: unknown, identity: unknown): path is string {
|
|
240
|
+
return typeof path === "string" && path.length > 0 && isSessionLifecycleTranscriptIdentity(identity);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function readSessionLifecycleLaunchRequest(
|
|
244
|
+
value: string | undefined,
|
|
245
|
+
now = Date.now(),
|
|
246
|
+
): SessionLifecycleLaunchRequest {
|
|
247
|
+
if (!value) throw new Error("SKC_SDK_LIFECYCLE_REQUEST is required.");
|
|
248
|
+
const request = JSON.parse(value) as Partial<SessionLifecycleLaunchRequest>;
|
|
249
|
+
if (
|
|
250
|
+
(request.operation !== "session.create" &&
|
|
251
|
+
request.operation !== "session.fork" &&
|
|
252
|
+
request.operation !== "session.resume") ||
|
|
253
|
+
typeof request.sessionId !== "string" ||
|
|
254
|
+
!isCanonicalSessionId(request.sessionId) ||
|
|
255
|
+
typeof request.cwd !== "string" ||
|
|
256
|
+
!request.cwd ||
|
|
257
|
+
typeof request.stateRoot !== "string" ||
|
|
258
|
+
!request.stateRoot ||
|
|
259
|
+
!hasDefaultStateRoot(request.cwd, request.stateRoot) ||
|
|
260
|
+
(request.sourceSessionId !== undefined &&
|
|
261
|
+
(typeof request.sourceSessionId !== "string" || !isCanonicalSessionId(request.sourceSessionId))) ||
|
|
262
|
+
(request.sourceSessionPath !== undefined &&
|
|
263
|
+
!hasValidTranscriptAuthority(request.sourceSessionPath, request.sourceSessionIdentity)) ||
|
|
264
|
+
(request.sourceSessionIdentity !== undefined &&
|
|
265
|
+
!isSessionLifecycleTranscriptIdentity(request.sourceSessionIdentity)) ||
|
|
266
|
+
(request.sourceCwd !== undefined && (typeof request.sourceCwd !== "string" || !request.sourceCwd)) ||
|
|
267
|
+
(request.sessionPath !== undefined &&
|
|
268
|
+
!hasValidTranscriptAuthority(request.sessionPath, request.sessionIdentity)) ||
|
|
269
|
+
(request.sessionIdentity !== undefined && !isSessionLifecycleTranscriptIdentity(request.sessionIdentity)) ||
|
|
270
|
+
(request.effectMarker !== undefined &&
|
|
271
|
+
(typeof request.effectMarker !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(request.effectMarker))) ||
|
|
272
|
+
(request.modelPreset !== undefined && (typeof request.modelPreset !== "string" || !request.modelPreset)) ||
|
|
273
|
+
!hasValidLifecycleDeadlines(
|
|
274
|
+
{
|
|
275
|
+
receivedAt: request.receivedAt as number,
|
|
276
|
+
requestedReadinessTimeoutMs: request.requestedReadinessTimeoutMs as number,
|
|
277
|
+
semanticReadyDeadlineAt: request.semanticReadyDeadlineAt as number,
|
|
278
|
+
terminationStartDeadlineAt: request.terminationStartDeadlineAt as number,
|
|
279
|
+
lifecycleCleanupDeadlineAt: request.lifecycleCleanupDeadlineAt as number,
|
|
280
|
+
},
|
|
281
|
+
now,
|
|
282
|
+
) ||
|
|
283
|
+
(request.worktree !== undefined && !isLifecycleWorktreeTarget(request.worktree)) ||
|
|
284
|
+
(request.operation === "session.resume" &&
|
|
285
|
+
!hasValidTranscriptAuthority(request.sessionPath, request.sessionIdentity)) ||
|
|
286
|
+
(request.operation === "session.fork" &&
|
|
287
|
+
(!hasValidTranscriptAuthority(request.sourceSessionPath, request.sourceSessionIdentity) ||
|
|
288
|
+
request.sourceSessionId === undefined))
|
|
289
|
+
)
|
|
290
|
+
throw new Error("SKC_SDK_LIFECYCLE_REQUEST is invalid.");
|
|
291
|
+
return request as SessionLifecycleLaunchRequest;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
type SessionLaunch = {
|
|
295
|
+
id: string;
|
|
296
|
+
cwd: string;
|
|
297
|
+
root: string;
|
|
298
|
+
sourceSessionId?: string;
|
|
299
|
+
sourceSessionPath?: string;
|
|
300
|
+
sourceSessionIdentity?: SessionLifecycleTranscriptIdentity;
|
|
301
|
+
sourceCwd?: string;
|
|
302
|
+
sessionPath?: string;
|
|
303
|
+
sessionIdentity?: SessionLifecycleTranscriptIdentity;
|
|
304
|
+
modelPreset?: string;
|
|
305
|
+
worktree?: SessionLifecycleWorktreeTarget;
|
|
306
|
+
worktreePlan?: SkcLaunchWorktreePlan;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
type CleanupEvidence = BrokerCleanupEvidence;
|
|
310
|
+
type CleanupIdentity = {
|
|
311
|
+
dev: bigint;
|
|
312
|
+
ino: bigint;
|
|
313
|
+
size: number;
|
|
314
|
+
mtimeNs: bigint;
|
|
315
|
+
sha256: string;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
function serializeCleanupIdentity(identity: CleanupIdentity): BrokerCleanupIdentity {
|
|
319
|
+
return {
|
|
320
|
+
dev: identity.dev.toString(),
|
|
321
|
+
ino: identity.ino.toString(),
|
|
322
|
+
size: identity.size,
|
|
323
|
+
mtimeNs: identity.mtimeNs.toString(),
|
|
324
|
+
sha256: identity.sha256,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const fail = (code: string, message: string, cleanup?: CleanupEvidence): BrokerResponse => ({
|
|
329
|
+
ok: false,
|
|
330
|
+
error: { code: code as never, message, ...(cleanup ? { cleanup } : {}) },
|
|
331
|
+
});
|
|
332
|
+
function text(value: unknown): string | undefined {
|
|
333
|
+
return typeof value === "string" && value ? value : undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function readinessTimeout(input: Input): number | BrokerResponse {
|
|
337
|
+
const value = input.readinessTimeoutMs;
|
|
338
|
+
if (value === undefined) return READY_TIMEOUT_MS;
|
|
339
|
+
if (
|
|
340
|
+
typeof value !== "number" ||
|
|
341
|
+
!Number.isSafeInteger(value) ||
|
|
342
|
+
value < MIN_READY_TIMEOUT_MS ||
|
|
343
|
+
value > MAX_READY_TIMEOUT_MS
|
|
344
|
+
)
|
|
345
|
+
return fail(
|
|
346
|
+
"invalid_input",
|
|
347
|
+
`readinessTimeoutMs must be an integer between ${MIN_READY_TIMEOUT_MS} and ${MAX_READY_TIMEOUT_MS}.`,
|
|
348
|
+
);
|
|
349
|
+
return value;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function lifecycleDeadlines(input: Input, now: number): LifecycleDeadlines | BrokerResponse {
|
|
353
|
+
const supplied = [
|
|
354
|
+
input.receivedAt,
|
|
355
|
+
input.requestedReadinessTimeoutMs,
|
|
356
|
+
input.semanticReadyDeadlineAt,
|
|
357
|
+
input.terminationStartDeadlineAt,
|
|
358
|
+
input.lifecycleCleanupDeadlineAt,
|
|
359
|
+
];
|
|
360
|
+
if (supplied.some(value => value !== undefined)) {
|
|
361
|
+
if (!supplied.every(value => typeof value === "number" && Number.isSafeInteger(value)))
|
|
362
|
+
return fail("invalid_input", "Lifecycle deadline fields must be supplied together as safe integers.");
|
|
363
|
+
const value: LifecycleDeadlines = {
|
|
364
|
+
receivedAt: input.receivedAt as number,
|
|
365
|
+
requestedReadinessTimeoutMs: input.requestedReadinessTimeoutMs as number,
|
|
366
|
+
semanticReadyDeadlineAt: input.semanticReadyDeadlineAt as number,
|
|
367
|
+
terminationStartDeadlineAt: input.terminationStartDeadlineAt as number,
|
|
368
|
+
lifecycleCleanupDeadlineAt: input.lifecycleCleanupDeadlineAt as number,
|
|
369
|
+
};
|
|
370
|
+
return hasValidLifecycleDeadlines(value, now)
|
|
371
|
+
? value
|
|
372
|
+
: fail("invalid_input", "Lifecycle deadlines do not satisfy the exact approved timing contract.");
|
|
373
|
+
}
|
|
374
|
+
const timeout = readinessTimeout(input);
|
|
375
|
+
return typeof timeout === "number" ? deriveLifecycleDeadlines(now, timeout) : timeout;
|
|
376
|
+
}
|
|
377
|
+
function sessionId(input: Input): string | undefined {
|
|
378
|
+
return text(input.sessionId) ?? text(input.id);
|
|
379
|
+
}
|
|
380
|
+
function lifecycleCwd(input: Input): string | undefined {
|
|
381
|
+
const target = input.target as Record<string, unknown> | undefined;
|
|
382
|
+
const cwd = text(input.cwd) ?? text(input.path) ?? text(target?.path);
|
|
383
|
+
return cwd ? path.resolve(cwd) : undefined;
|
|
384
|
+
}
|
|
385
|
+
function stateRoot(input: Input, cwd: string | undefined): string | undefined {
|
|
386
|
+
const target = input.target as Record<string, unknown> | undefined;
|
|
387
|
+
const root = text(input.stateRoot) ?? text(target?.stateRoot);
|
|
388
|
+
if (root) return path.resolve(root);
|
|
389
|
+
return cwd ? path.join(cwd, ".skc", "state") : undefined;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isLifecycleWorktreeTarget(value: unknown): value is SessionLifecycleWorktreeTarget {
|
|
393
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
394
|
+
const target = value as Record<string, unknown>;
|
|
395
|
+
return (
|
|
396
|
+
target.enabled === true &&
|
|
397
|
+
(target.name === undefined || (typeof target.name === "string" && target.name.length > 0))
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function lifecycleWorktreeTarget(input: Input): SessionLifecycleWorktreeTarget | null | undefined {
|
|
402
|
+
const target = input.target as Record<string, unknown> | undefined;
|
|
403
|
+
const worktree = target?.worktree;
|
|
404
|
+
if (worktree === undefined) return undefined;
|
|
405
|
+
return isLifecycleWorktreeTarget(worktree) ? worktree : null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
type LiveResumeRecord = {
|
|
409
|
+
locator: { repo: string; stateRoot: string };
|
|
410
|
+
endpointGeneration: number;
|
|
411
|
+
pid: number;
|
|
412
|
+
endpointMtimeMs?: number;
|
|
413
|
+
live: boolean;
|
|
414
|
+
};
|
|
415
|
+
type ResumeScope = {
|
|
416
|
+
cwd: string;
|
|
417
|
+
stateRoot: string;
|
|
418
|
+
sessionPath: string;
|
|
419
|
+
sessionIdentity: {
|
|
420
|
+
dev: bigint;
|
|
421
|
+
ino: bigint;
|
|
422
|
+
size: number;
|
|
423
|
+
mtimeMs: number;
|
|
424
|
+
mtimeNs: bigint;
|
|
425
|
+
sha256: string;
|
|
426
|
+
};
|
|
427
|
+
};
|
|
428
|
+
function sameResumeLocator(record: LiveResumeRecord, cwd: string, root: string): boolean {
|
|
429
|
+
return (
|
|
430
|
+
resolveEquivalentPath(record.locator.repo) === resolveEquivalentPath(cwd) &&
|
|
431
|
+
resolveEquivalentPath(record.locator.stateRoot) === resolveEquivalentPath(root)
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
function sameResumeSessionIdentity(left: ResumeScope, right: ResumeScope): boolean {
|
|
435
|
+
return (
|
|
436
|
+
left.sessionPath === right.sessionPath &&
|
|
437
|
+
left.sessionIdentity.dev === right.sessionIdentity.dev &&
|
|
438
|
+
left.sessionIdentity.ino === right.sessionIdentity.ino &&
|
|
439
|
+
left.sessionIdentity.size === right.sessionIdentity.size &&
|
|
440
|
+
left.sessionIdentity.mtimeMs === right.sessionIdentity.mtimeMs &&
|
|
441
|
+
left.sessionIdentity.mtimeNs === right.sessionIdentity.mtimeNs &&
|
|
442
|
+
left.sessionIdentity.sha256 === right.sessionIdentity.sha256
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
function sameLiveResumeRecord(expected: LiveResumeRecord, current: LiveResumeRecord): boolean {
|
|
446
|
+
return (
|
|
447
|
+
current.live &&
|
|
448
|
+
current.endpointGeneration === expected.endpointGeneration &&
|
|
449
|
+
current.pid === expected.pid &&
|
|
450
|
+
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
451
|
+
sameResumeLocator(current, expected.locator.repo, expected.locator.stateRoot)
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
type ValidatedTranscript = {
|
|
456
|
+
path: string;
|
|
457
|
+
id: string;
|
|
458
|
+
identity: SessionLifecycleTranscriptIdentity;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
function serializeTranscriptIdentity(identity: {
|
|
462
|
+
dev: bigint;
|
|
463
|
+
ino: bigint;
|
|
464
|
+
size: number;
|
|
465
|
+
mtimeMs: number;
|
|
466
|
+
mtimeNs: bigint;
|
|
467
|
+
sha256: string;
|
|
468
|
+
}): SessionLifecycleTranscriptIdentity {
|
|
469
|
+
return {
|
|
470
|
+
dev: identity.dev.toString(),
|
|
471
|
+
ino: identity.ino.toString(),
|
|
472
|
+
size: identity.size,
|
|
473
|
+
mtimeMs: identity.mtimeMs,
|
|
474
|
+
mtimeNs: identity.mtimeNs.toString(),
|
|
475
|
+
sha256: identity.sha256,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
async function managedCandidates(
|
|
479
|
+
broker: Broker,
|
|
480
|
+
cwd: string,
|
|
481
|
+
label: "Saved" | "Source",
|
|
482
|
+
): Promise<
|
|
483
|
+
| {
|
|
484
|
+
candidates: readonly LogicalSessionCandidate[];
|
|
485
|
+
migrationPolicy: "copy-retain" | "disabled";
|
|
486
|
+
scope: ManagedSessionScope;
|
|
487
|
+
}
|
|
488
|
+
| BrokerResponse
|
|
489
|
+
> {
|
|
490
|
+
const resolved = await resolveManagedSessionScope({ cwd, agentDir: broker.settings.agentDir });
|
|
491
|
+
if (resolved.kind !== "resolved")
|
|
492
|
+
return fail("invalid_input", `${label} session scope is invalid: ${resolved.message}`);
|
|
493
|
+
const migration = await broker.settings.resolveDirectoryMigration(cwd);
|
|
494
|
+
if (migration !== "copy-retain" && migration !== "disabled")
|
|
495
|
+
return fail("invalid_input", "Broker directory migration policy is invalid.");
|
|
496
|
+
const listed = await listManagedSessionCandidates({ scope: resolved.scope });
|
|
497
|
+
if (listed.kind !== "complete")
|
|
498
|
+
return fail("invalid_input", `${label} session storage could not be verified for the requested workspace.`);
|
|
499
|
+
return { candidates: listed.owned, migrationPolicy: migration, scope: resolved.scope };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function validateSavedTranscript(
|
|
503
|
+
broker: Broker,
|
|
504
|
+
cwd: string,
|
|
505
|
+
suppliedPath: string | undefined,
|
|
506
|
+
expectedSessionId: string | undefined,
|
|
507
|
+
label: "Saved" | "Source",
|
|
508
|
+
): Promise<ValidatedTranscript | BrokerResponse> {
|
|
509
|
+
const inventory = await managedCandidates(broker, cwd, label);
|
|
510
|
+
if ("ok" in inventory) return inventory;
|
|
511
|
+
const canonicalPath = suppliedPath ? path.resolve(suppliedPath) : undefined;
|
|
512
|
+
const matches = inventory.candidates.filter(
|
|
513
|
+
candidate =>
|
|
514
|
+
(canonicalPath === undefined || candidate.path === canonicalPath) &&
|
|
515
|
+
(expectedSessionId === undefined || candidate.sessionId === expectedSessionId),
|
|
516
|
+
);
|
|
517
|
+
if (matches.length !== 1 || !isCanonicalSessionId(matches[0]!.sessionId))
|
|
518
|
+
return fail("invalid_input", `${label} saved session does not match the requested workspace and session id.`);
|
|
519
|
+
const match = matches[0]!;
|
|
520
|
+
if (inventory.migrationPolicy === "disabled" && match.provenance === "legacy")
|
|
521
|
+
return fail("legacy_migration_disabled", `${label} legacy session migration is disabled for this workspace.`);
|
|
522
|
+
return { path: match.path, id: match.sessionId, identity: serializeTranscriptIdentity(match.identity) };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function validateLiveResumeScope(
|
|
526
|
+
broker: Broker,
|
|
527
|
+
input: Input,
|
|
528
|
+
requestedSessionId: string,
|
|
529
|
+
record: LiveResumeRecord,
|
|
530
|
+
): Promise<ResumeScope | BrokerResponse> {
|
|
531
|
+
const requestedCwd = lifecycleCwd(input);
|
|
532
|
+
if (!requestedCwd) return fail("invalid_input", "A target path is required.");
|
|
533
|
+
const suppliedRoot = stateRoot(input, requestedCwd);
|
|
534
|
+
if (!suppliedRoot || !hasDefaultStateRoot(requestedCwd, suppliedRoot))
|
|
535
|
+
return fail("invalid_input", "stateRoot must be the default .skc/state for cwd.");
|
|
536
|
+
try {
|
|
537
|
+
if (!(await fs.stat(requestedCwd)).isDirectory())
|
|
538
|
+
return fail("invalid_input", "Lifecycle worktree must be a directory.");
|
|
539
|
+
} catch {
|
|
540
|
+
return fail("invalid_input", "Lifecycle worktree does not exist.");
|
|
541
|
+
}
|
|
542
|
+
const worktree = lifecycleWorktreeTarget(input);
|
|
543
|
+
if (worktree === null) return fail("invalid_input", "Lifecycle worktree target is invalid.");
|
|
544
|
+
let cwd = requestedCwd;
|
|
545
|
+
if (worktree) {
|
|
546
|
+
try {
|
|
547
|
+
const planned = planLaunchWorktree(
|
|
548
|
+
requestedCwd,
|
|
549
|
+
worktree.name
|
|
550
|
+
? { enabled: true, detached: false, name: worktree.name }
|
|
551
|
+
: { enabled: true, detached: true, name: null },
|
|
552
|
+
);
|
|
553
|
+
if (!planned.enabled) return fail("invalid_input", "Lifecycle worktree target is invalid.");
|
|
554
|
+
cwd = path.resolve(planned.worktreePath);
|
|
555
|
+
} catch (error) {
|
|
556
|
+
return fail(
|
|
557
|
+
"invalid_input",
|
|
558
|
+
`Unable to validate lifecycle worktree: ${error instanceof Error ? error.message : String(error)}`,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const root = defaultStateRoot(cwd);
|
|
563
|
+
if (!sameResumeLocator(record, cwd, root))
|
|
564
|
+
return fail("endpoint_stale", "Live session does not match the requested resume scope.");
|
|
565
|
+
const sessionPath = text(input.sessionPath);
|
|
566
|
+
if (!sessionPath) return fail("invalid_input", "sessionPath is required to resume a saved session.");
|
|
567
|
+
const inventory = await managedCandidates(broker, cwd, "Saved");
|
|
568
|
+
if ("ok" in inventory)
|
|
569
|
+
return fail("endpoint_stale", "Requested saved session could not be verified for the requested workspace.");
|
|
570
|
+
const canonicalSessionPath = path.resolve(sessionPath);
|
|
571
|
+
const matches = inventory.candidates.filter(
|
|
572
|
+
candidate => candidate.sessionId === requestedSessionId && candidate.path === canonicalSessionPath,
|
|
573
|
+
);
|
|
574
|
+
if (matches.length !== 1)
|
|
575
|
+
return fail("endpoint_stale", "Requested saved session does not match the live session scope.");
|
|
576
|
+
const session = matches[0]!;
|
|
577
|
+
if (inventory.migrationPolicy === "disabled" && matches[0]!.provenance === "legacy")
|
|
578
|
+
return fail("legacy_migration_disabled", "Saved legacy session migration is disabled for this workspace.");
|
|
579
|
+
return {
|
|
580
|
+
cwd,
|
|
581
|
+
stateRoot: root,
|
|
582
|
+
sessionPath: canonicalSessionPath,
|
|
583
|
+
sessionIdentity: session.identity,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async function reconcileReadyScope(broker: Broker, id: string, scope: string | undefined): Promise<void> {
|
|
587
|
+
if (!scope) return;
|
|
588
|
+
await broker.index.refresh();
|
|
589
|
+
const record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
590
|
+
if (!record || record.locator.repo === scope) return;
|
|
591
|
+
// The host records its physical cwd, which Darwin canonicalizes from /var to
|
|
592
|
+
// /private/var. Preserve the lifecycle caller's lexical cwd for ACP's scoped
|
|
593
|
+
// listing while retaining the host-provided state root for endpoint binding.
|
|
594
|
+
await broker.index.append({
|
|
595
|
+
type: "record_reconciled",
|
|
596
|
+
sessionId: id,
|
|
597
|
+
locator: { ...record.locator, repo: scope },
|
|
598
|
+
endpointGeneration: record.endpointGeneration,
|
|
599
|
+
pid: record.pid,
|
|
600
|
+
endpointMtimeMs: record.endpointMtimeMs,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function command(broker: Broker): LifecycleCommand {
|
|
605
|
+
const configured = process.env.SKC_SDK_SESSION_COMMAND;
|
|
606
|
+
if (configured) {
|
|
607
|
+
const [file, ...args] = configured.trim().split(/\s+/);
|
|
608
|
+
if (file) return { file, args };
|
|
609
|
+
}
|
|
610
|
+
return lifecycleCommandResolversForTest.get(broker)?.() ?? resolveSdkInternalSpawnCommand("session-host-internal");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const lifecycleMarkerPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.json`);
|
|
614
|
+
const lifecycleReadyPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.ready.json`);
|
|
615
|
+
const lifecycleFailurePath = (root: string, id: string, effectMarker: string) =>
|
|
616
|
+
path.join(root, "sdk", `${id}.lifecycle.failure.${effectMarker}.json`);
|
|
617
|
+
type EffectMarker = { pid: number; effectMarker: string; incarnation: string };
|
|
618
|
+
type ReadyAuthority = {
|
|
619
|
+
endpoint: Record<string, unknown>;
|
|
620
|
+
endpointSource: string;
|
|
621
|
+
endpointMtimeMs: number;
|
|
622
|
+
endpointGeneration: number;
|
|
623
|
+
};
|
|
624
|
+
type ReadinessResult =
|
|
625
|
+
| { kind: "ready"; authority: ReadyAuthority }
|
|
626
|
+
| { kind: "startup_failed"; message: string }
|
|
627
|
+
| { kind: "child_exited" }
|
|
628
|
+
| { kind: "timeout" };
|
|
629
|
+
const processIncarnationReadersForTest = new WeakMap<Broker, (pid: number) => string | undefined>();
|
|
630
|
+
|
|
631
|
+
export function setProcessIncarnationForTest(
|
|
632
|
+
broker: Broker,
|
|
633
|
+
value: ((pid: number) => string | undefined) | undefined,
|
|
634
|
+
): void {
|
|
635
|
+
if (value) processIncarnationReadersForTest.set(broker, value);
|
|
636
|
+
else processIncarnationReadersForTest.delete(broker);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function processIncarnationForBroker(broker: Broker, pid: number): string | undefined {
|
|
640
|
+
const reader = processIncarnationReadersForTest.get(broker);
|
|
641
|
+
return reader ? reader(pid) : processIncarnation(pid);
|
|
642
|
+
}
|
|
643
|
+
function hasProcessIncarnationAuthority(): boolean {
|
|
644
|
+
return processIncarnation(process.pid) !== undefined;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
type ProcessObservation = "alive" | "exited" | "uncertain";
|
|
648
|
+
|
|
649
|
+
/** Only ESRCH or a changed, readable incarnation proves the owned process exited. */
|
|
650
|
+
function observeProcess(
|
|
651
|
+
pid: number,
|
|
652
|
+
expectedIncarnation: string | undefined,
|
|
653
|
+
readIncarnation: (pid: number) => string | undefined = processIncarnation,
|
|
654
|
+
): ProcessObservation {
|
|
655
|
+
try {
|
|
656
|
+
process.kill(pid, 0);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
return (error as NodeJS.ErrnoException).code === "ESRCH" ? "exited" : "uncertain";
|
|
659
|
+
}
|
|
660
|
+
if (!expectedIncarnation) return "uncertain";
|
|
661
|
+
const actualIncarnation = readIncarnation(pid);
|
|
662
|
+
if (!actualIncarnation) return "uncertain";
|
|
663
|
+
return actualIncarnation === expectedIncarnation ? "alive" : "exited";
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function hasObservedProcessExit(pid: number): boolean {
|
|
667
|
+
try {
|
|
668
|
+
process.kill(pid, 0);
|
|
669
|
+
return false;
|
|
670
|
+
} catch (error) {
|
|
671
|
+
return (error as NodeJS.ErrnoException).code === "ESRCH";
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function isEffectMarker(value: unknown): value is EffectMarker {
|
|
676
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
677
|
+
const marker = value as Record<string, unknown>;
|
|
678
|
+
return (
|
|
679
|
+
typeof marker.pid === "number" &&
|
|
680
|
+
Number.isSafeInteger(marker.pid) &&
|
|
681
|
+
marker.pid > 0 &&
|
|
682
|
+
typeof marker.effectMarker === "string" &&
|
|
683
|
+
/^[A-Za-z0-9._-]+$/.test(marker.effectMarker) &&
|
|
684
|
+
marker.effectMarker.length <= MAX_EFFECT_MARKER_LENGTH &&
|
|
685
|
+
typeof marker.incarnation === "string" &&
|
|
686
|
+
marker.incarnation.length > 0 &&
|
|
687
|
+
marker.incarnation.length <= MAX_PROCESS_INCARNATION_LENGTH
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function isExactEffectMarker(value: unknown): value is EffectMarker {
|
|
692
|
+
return (
|
|
693
|
+
isEffectMarker(value) &&
|
|
694
|
+
Object.keys(value).length === 3 &&
|
|
695
|
+
Object.keys(value).every(key => key === "pid" || key === "effectMarker" || key === "incarnation")
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function sameEffectMarker(left: EffectMarker, right: EffectMarker): boolean {
|
|
700
|
+
return left.pid === right.pid && left.effectMarker === right.effectMarker && left.incarnation === right.incarnation;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function readEffectMarker(file: string): Promise<EffectMarker | undefined> {
|
|
704
|
+
try {
|
|
705
|
+
const captured = captureLifecycleFile(file, true, true);
|
|
706
|
+
if (!captured) return undefined;
|
|
707
|
+
const marker: unknown = parseLifecycleJson(captured.bytes);
|
|
708
|
+
return isExactEffectMarker(marker) ? marker : undefined;
|
|
709
|
+
} catch {
|
|
710
|
+
return undefined;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function writeEffectMarker(root: string, id: string, marker: EffectMarker): Promise<void> {
|
|
715
|
+
const directory = path.join(root, "sdk");
|
|
716
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
717
|
+
const temporary = path.join(directory, `.${id}.lifecycle.${randomUUID()}.tmp`);
|
|
718
|
+
const handle = await fs.open(
|
|
719
|
+
temporary,
|
|
720
|
+
fsSync.constants.O_CREAT | fsSync.constants.O_EXCL | fsSync.constants.O_WRONLY,
|
|
721
|
+
0o600,
|
|
722
|
+
);
|
|
723
|
+
try {
|
|
724
|
+
await handle.writeFile(canonicalJson(marker));
|
|
725
|
+
await handle.sync();
|
|
726
|
+
} finally {
|
|
727
|
+
await handle.close();
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
await fs.rename(temporary, lifecycleMarkerPath(root, id));
|
|
731
|
+
await syncDirectory(directory);
|
|
732
|
+
} finally {
|
|
733
|
+
await fs.rm(temporary, { force: true });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** The child writes this only after its endpoint and semantic ready event are both live. */
|
|
738
|
+
export async function writeSessionLifecycleReady(root: string, id: string, effectMarker: string): Promise<void> {
|
|
739
|
+
const incarnation = processIncarnation(process.pid);
|
|
740
|
+
if (!incarnation) throw new Error("Lifecycle child has no readable OS incarnation.");
|
|
741
|
+
await fs.mkdir(path.join(root, "sdk"), { recursive: true, mode: 0o700 });
|
|
742
|
+
await fs.writeFile(lifecycleReadyPath(root, id), JSON.stringify({ pid: process.pid, effectMarker, incarnation }), {
|
|
743
|
+
mode: 0o600,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export interface LifecycleTranscriptEvidence {
|
|
748
|
+
digest: string;
|
|
749
|
+
identity: SessionLifecycleTranscriptIdentity;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
type LifecycleFailureArtifact = EffectMarker &
|
|
753
|
+
SdkStartupFailure & {
|
|
754
|
+
rollback: SdkStartupRollbackResult;
|
|
755
|
+
transcript?: LifecycleTranscriptEvidence;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
function canonicalJson(value: unknown): string {
|
|
759
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
760
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
761
|
+
const record = value as Record<string, unknown>;
|
|
762
|
+
return `{${Object.keys(record)
|
|
763
|
+
.sort()
|
|
764
|
+
.map(key => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
|
765
|
+
.join(",")}}`;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function isRollbackResult(value: unknown): value is SdkStartupRollbackResult {
|
|
769
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
770
|
+
const record = value as Record<string, unknown>;
|
|
771
|
+
return (
|
|
772
|
+
Object.keys(record).length === 5 &&
|
|
773
|
+
(record.endpointGeneration === null ||
|
|
774
|
+
(typeof record.endpointGeneration === "number" &&
|
|
775
|
+
Number.isSafeInteger(record.endpointGeneration) &&
|
|
776
|
+
record.endpointGeneration > 0)) &&
|
|
777
|
+
typeof record.fenced === "boolean" &&
|
|
778
|
+
typeof record.runtimeRemoved === "boolean" &&
|
|
779
|
+
typeof record.hostStopped === "boolean" &&
|
|
780
|
+
typeof record.brokerRegistrationReleased === "boolean"
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function isLifecycleTranscriptEvidence(value: unknown): value is LifecycleTranscriptEvidence {
|
|
785
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
786
|
+
const record = value as Record<string, unknown>;
|
|
787
|
+
return (
|
|
788
|
+
Object.keys(record).length === 2 &&
|
|
789
|
+
typeof record.digest === "string" &&
|
|
790
|
+
/^[a-f0-9]{64}$/.test(record.digest) &&
|
|
791
|
+
isSessionLifecycleTranscriptIdentity(record.identity) &&
|
|
792
|
+
record.digest === record.identity.sha256
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function isSdkStartupFailure(value: unknown): value is SdkStartupFailure {
|
|
797
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
798
|
+
const failure = value as Record<string, unknown>;
|
|
799
|
+
return (
|
|
800
|
+
Object.keys(failure).length === 3 &&
|
|
801
|
+
(failure.phase === "registration" || failure.phase === "startup") &&
|
|
802
|
+
(failure.reason === "disabled" ||
|
|
803
|
+
failure.reason === "ineligible" ||
|
|
804
|
+
failure.reason === "factory_absent" ||
|
|
805
|
+
failure.reason === "runner_absent" ||
|
|
806
|
+
failure.reason === "pending" ||
|
|
807
|
+
failure.reason === "failed") &&
|
|
808
|
+
typeof failure.message === "string" &&
|
|
809
|
+
Buffer.byteLength(failure.message) > 0 &&
|
|
810
|
+
Buffer.byteLength(failure.message) <= 512
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function isLifecycleFailureArtifact(value: unknown): value is LifecycleFailureArtifact {
|
|
815
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
816
|
+
const record = value as Record<string, unknown>;
|
|
817
|
+
if (!isEffectMarker(record)) return false;
|
|
818
|
+
const artifact = value as LifecycleFailureArtifact;
|
|
819
|
+
return (
|
|
820
|
+
Object.keys(record).length === (artifact.transcript === undefined ? 7 : 8) &&
|
|
821
|
+
isSdkStartupFailure({ phase: artifact.phase, reason: artifact.reason, message: artifact.message }) &&
|
|
822
|
+
isRollbackResult(artifact.rollback) &&
|
|
823
|
+
(artifact.transcript === undefined || isLifecycleTranscriptEvidence(artifact.transcript))
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function syncDirectory(directory: string): Promise<void> {
|
|
828
|
+
const handle = await fs.open(directory, fsSync.constants.O_RDONLY);
|
|
829
|
+
try {
|
|
830
|
+
await handle.sync();
|
|
831
|
+
} finally {
|
|
832
|
+
await handle.close();
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** The child writes bounded startup diagnostics before exiting without readiness. */
|
|
837
|
+
export async function writeSessionLifecycleFailure(
|
|
838
|
+
root: string,
|
|
839
|
+
id: string,
|
|
840
|
+
effectMarker: string,
|
|
841
|
+
failure: SdkStartupFailure,
|
|
842
|
+
rollback: SdkStartupRollbackResult,
|
|
843
|
+
transcript?: LifecycleTranscriptEvidence,
|
|
844
|
+
ownerIncarnation?: string,
|
|
845
|
+
): Promise<void> {
|
|
846
|
+
if (!isSdkStartupFailure(failure))
|
|
847
|
+
throw new Error("Lifecycle startup failure does not satisfy the canonical failure contract.");
|
|
848
|
+
if (transcript && !isLifecycleTranscriptEvidence(transcript))
|
|
849
|
+
throw new Error(
|
|
850
|
+
"Lifecycle startup failure transcript evidence does not bind its content digest to its identity.",
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
const incarnation = ownerIncarnation ?? processIncarnation(process.pid);
|
|
854
|
+
if (!incarnation) return;
|
|
855
|
+
const directory = path.join(root, "sdk");
|
|
856
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
857
|
+
await fs.chmod(directory, 0o700);
|
|
858
|
+
const artifact: LifecycleFailureArtifact = {
|
|
859
|
+
pid: process.pid,
|
|
860
|
+
effectMarker,
|
|
861
|
+
incarnation,
|
|
862
|
+
...failure,
|
|
863
|
+
rollback,
|
|
864
|
+
...(transcript ? { transcript } : {}),
|
|
865
|
+
};
|
|
866
|
+
const bytes = Buffer.from(canonicalJson(artifact), "utf8");
|
|
867
|
+
const target = lifecycleFailurePath(root, id, effectMarker);
|
|
868
|
+
const temporary = path.join(directory, `.${id}.lifecycle.failure.${effectMarker}.${randomUUID()}.tmp`);
|
|
869
|
+
let published = false;
|
|
870
|
+
try {
|
|
871
|
+
const handle = await fs.open(
|
|
872
|
+
temporary,
|
|
873
|
+
fsSync.constants.O_CREAT | fsSync.constants.O_EXCL | fsSync.constants.O_WRONLY,
|
|
874
|
+
0o600,
|
|
875
|
+
);
|
|
876
|
+
try {
|
|
877
|
+
await handle.writeFile(bytes);
|
|
878
|
+
await handle.sync();
|
|
879
|
+
} finally {
|
|
880
|
+
await handle.close();
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
await fs.link(temporary, target);
|
|
884
|
+
published = true;
|
|
885
|
+
} catch (writeError) {
|
|
886
|
+
if ((writeError as NodeJS.ErrnoException).code !== "EEXIST") throw writeError;
|
|
887
|
+
const existing = await readLifecycleFailureArtifact(target, artifact);
|
|
888
|
+
if (!existing?.bytes.equals(bytes)) throw new Error("Lifecycle startup failure artifact collision.");
|
|
889
|
+
}
|
|
890
|
+
} finally {
|
|
891
|
+
await fs.rm(temporary, { force: true });
|
|
892
|
+
if (published) await syncDirectory(directory);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async function readLifecycleFailureArtifact(
|
|
897
|
+
file: string,
|
|
898
|
+
expected: EffectMarker,
|
|
899
|
+
): Promise<
|
|
900
|
+
| {
|
|
901
|
+
artifact: LifecycleFailureArtifact;
|
|
902
|
+
bytes: Buffer;
|
|
903
|
+
digest: string;
|
|
904
|
+
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string };
|
|
905
|
+
}
|
|
906
|
+
| undefined
|
|
907
|
+
> {
|
|
908
|
+
let handle: fs.FileHandle | undefined;
|
|
909
|
+
try {
|
|
910
|
+
handle = await fs.open(file, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
|
|
911
|
+
const stat = await handle.stat({ bigint: true });
|
|
912
|
+
if (!stat.isFile() || stat.size > 4096n) return undefined;
|
|
913
|
+
const bytes = Buffer.alloc(Number(stat.size) + 1);
|
|
914
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
915
|
+
if (bytesRead > 4096) return undefined;
|
|
916
|
+
const raw = bytes.subarray(0, bytesRead);
|
|
917
|
+
const value: unknown = parseLifecycleJson(raw);
|
|
918
|
+
if (
|
|
919
|
+
!isLifecycleFailureArtifact(value) ||
|
|
920
|
+
!sameEffectMarker(value, expected) ||
|
|
921
|
+
canonicalJson(value) !== decodeLifecycleUtf8(raw)
|
|
922
|
+
)
|
|
923
|
+
return undefined;
|
|
924
|
+
return {
|
|
925
|
+
artifact: value,
|
|
926
|
+
bytes: raw,
|
|
927
|
+
digest: createHash("sha256").update(raw).digest("hex"),
|
|
928
|
+
identity: {
|
|
929
|
+
dev: stat.dev,
|
|
930
|
+
ino: stat.ino,
|
|
931
|
+
size: stat.size,
|
|
932
|
+
mtimeNs: stat.mtimeNs,
|
|
933
|
+
sha256: createHash("sha256").update(raw).digest("hex"),
|
|
934
|
+
},
|
|
935
|
+
};
|
|
936
|
+
} catch {
|
|
937
|
+
return undefined;
|
|
938
|
+
} finally {
|
|
939
|
+
if (handle) await handle.close();
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function exactUnlinkLifecycleFile(
|
|
944
|
+
file: string,
|
|
945
|
+
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string },
|
|
946
|
+
plannedPath: string,
|
|
947
|
+
): NativeExactUnlinkResult {
|
|
948
|
+
return native.exactUnlink(file, { ...identity, quarantineName: path.basename(plannedPath) });
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
type LifecycleCleanupFile = NonNullable<BrokerCleanupEvidence["lifecycleFiles"]>[number];
|
|
952
|
+
|
|
953
|
+
function sameLifecycleCleanupIdentity(
|
|
954
|
+
left: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string },
|
|
955
|
+
right: BrokerCleanupIdentity,
|
|
956
|
+
): boolean {
|
|
957
|
+
return (
|
|
958
|
+
left.dev.toString() === right.dev &&
|
|
959
|
+
left.ino.toString() === right.ino &&
|
|
960
|
+
left.size === BigInt(right.size) &&
|
|
961
|
+
left.mtimeNs.toString() === right.mtimeNs &&
|
|
962
|
+
left.sha256 === right.sha256
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function lifecycleCleanupPlan(
|
|
967
|
+
root: string,
|
|
968
|
+
id: string,
|
|
969
|
+
expected: EffectMarker,
|
|
970
|
+
evidence: { identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string } },
|
|
971
|
+
): CleanupEvidence {
|
|
972
|
+
const directory = path.join(root, "sdk");
|
|
973
|
+
const candidates = [
|
|
974
|
+
lifecycleFailurePath(root, id, expected.effectMarker),
|
|
975
|
+
path.join(directory, `${id}.json`),
|
|
976
|
+
lifecycleReadyPath(root, id),
|
|
977
|
+
lifecycleMarkerPath(root, id),
|
|
978
|
+
];
|
|
979
|
+
const files: LifecycleCleanupFile[] = candidates.flatMap(file => {
|
|
980
|
+
const captured = captureLifecycleFile(
|
|
981
|
+
file,
|
|
982
|
+
true,
|
|
983
|
+
file === lifecycleMarkerPath(root, id) || file === lifecycleReadyPath(root, id),
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
if (!captured) return [];
|
|
987
|
+
if (file === lifecycleMarkerPath(root, id) || file === lifecycleReadyPath(root, id)) {
|
|
988
|
+
let marker: unknown;
|
|
989
|
+
try {
|
|
990
|
+
marker = parseLifecycleJson(captured.bytes);
|
|
991
|
+
} catch {
|
|
992
|
+
throw new Error("Lifecycle marker changed before cleanup intent persistence.");
|
|
993
|
+
}
|
|
994
|
+
if (!isExactEffectMarker(marker) || !sameEffectMarker(marker, expected))
|
|
995
|
+
throw new Error("Lifecycle marker changed before cleanup intent persistence.");
|
|
996
|
+
}
|
|
997
|
+
if (file.endsWith(`${id}.json`)) {
|
|
998
|
+
let endpoint: { pid?: unknown };
|
|
999
|
+
try {
|
|
1000
|
+
endpoint = parseLifecycleJson(captured.bytes) as { pid?: unknown };
|
|
1001
|
+
} catch {
|
|
1002
|
+
throw new Error("Lifecycle endpoint changed before cleanup intent persistence.");
|
|
1003
|
+
}
|
|
1004
|
+
if (endpoint.pid !== expected.pid)
|
|
1005
|
+
throw new Error("Lifecycle endpoint changed before cleanup intent persistence.");
|
|
1006
|
+
}
|
|
1007
|
+
const identity = file === candidates[0] ? evidence.identity : captured.identity;
|
|
1008
|
+
if (
|
|
1009
|
+
file === candidates[0] &&
|
|
1010
|
+
!sameLifecycleCleanupIdentity(
|
|
1011
|
+
captured.identity,
|
|
1012
|
+
serializeCleanupIdentity({ ...identity, size: Number(identity.size) }),
|
|
1013
|
+
)
|
|
1014
|
+
)
|
|
1015
|
+
throw new Error("Lifecycle failure artifact changed before cleanup intent persistence.");
|
|
1016
|
+
const attempt = 1;
|
|
1017
|
+
const suffix = randomUUID();
|
|
1018
|
+
return [
|
|
1019
|
+
{
|
|
1020
|
+
path: file,
|
|
1021
|
+
identity: serializeCleanupIdentity({ ...identity, size: Number(identity.size) }),
|
|
1022
|
+
attempt,
|
|
1023
|
+
plannedPath: path.join(directory, `.skc-delete-${suffix}-${path.basename(file)}`),
|
|
1024
|
+
},
|
|
1025
|
+
];
|
|
1026
|
+
});
|
|
1027
|
+
return { phase: "lifecycle", sessionId: id, metadataRoot: root, lifecycleFiles: files };
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function escapeRegExp(value: string): string {
|
|
1031
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function isCanonicalLifecycleCleanupOriginal(root: string, id: string, original: string): boolean {
|
|
1035
|
+
const directory = path.join(path.resolve(root), "sdk");
|
|
1036
|
+
if (path.dirname(original) !== directory) return false;
|
|
1037
|
+
const basename = path.basename(original);
|
|
1038
|
+
return (
|
|
1039
|
+
basename === `${id}.json` ||
|
|
1040
|
+
basename === `${id}.lifecycle.json` ||
|
|
1041
|
+
basename === `${id}.lifecycle.ready.json` ||
|
|
1042
|
+
new RegExp(`^${escapeRegExp(id)}\\.lifecycle\\.failure\\.[A-Za-z0-9._-]{1,128}\\.json$`).test(basename)
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function lifecycleCleanupHasMixedMetadataSchema(cleanup: CleanupEvidence): boolean {
|
|
1047
|
+
return [
|
|
1048
|
+
cleanup.metadataIdentity,
|
|
1049
|
+
cleanup.metadataPath,
|
|
1050
|
+
cleanup.metadataAttempt,
|
|
1051
|
+
cleanup.plannedMetadataPath,
|
|
1052
|
+
cleanup.detachedMetadataPath,
|
|
1053
|
+
cleanup.metadataCompleted,
|
|
1054
|
+
].some(value => value !== undefined);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function isCleanupIdentity(value: unknown): value is BrokerCleanupIdentity {
|
|
1058
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1059
|
+
const identity = value as Record<string, unknown>;
|
|
1060
|
+
return (
|
|
1061
|
+
Object.keys(identity).length === 5 &&
|
|
1062
|
+
Object.keys(identity).every(
|
|
1063
|
+
key => key === "dev" || key === "ino" || key === "size" || key === "mtimeNs" || key === "sha256",
|
|
1064
|
+
) &&
|
|
1065
|
+
typeof identity.dev === "string" &&
|
|
1066
|
+
/^\d+$/.test(identity.dev) &&
|
|
1067
|
+
typeof identity.ino === "string" &&
|
|
1068
|
+
/^\d+$/.test(identity.ino) &&
|
|
1069
|
+
typeof identity.size === "number" &&
|
|
1070
|
+
Number.isSafeInteger(identity.size) &&
|
|
1071
|
+
identity.size >= 0 &&
|
|
1072
|
+
typeof identity.mtimeNs === "string" &&
|
|
1073
|
+
/^\d+$/.test(identity.mtimeNs) &&
|
|
1074
|
+
typeof identity.sha256 === "string" &&
|
|
1075
|
+
/^[a-f0-9]{64}$/.test(identity.sha256)
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function isLifecycleCleanupFile(value: unknown): value is LifecycleCleanupFile {
|
|
1080
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1081
|
+
const file = value as Record<string, unknown>;
|
|
1082
|
+
const allowed = new Set(["path", "identity", "attempt", "plannedPath", "detachedPath", "completed"]);
|
|
1083
|
+
return (
|
|
1084
|
+
Object.keys(file).every(key => allowed.has(key)) &&
|
|
1085
|
+
typeof file.path === "string" &&
|
|
1086
|
+
file.path.length > 0 &&
|
|
1087
|
+
typeof file.plannedPath === "string" &&
|
|
1088
|
+
file.plannedPath.length > 0 &&
|
|
1089
|
+
isCleanupIdentity(file.identity) &&
|
|
1090
|
+
typeof file.attempt === "number" &&
|
|
1091
|
+
Number.isSafeInteger(file.attempt) &&
|
|
1092
|
+
file.attempt > 0 &&
|
|
1093
|
+
(file.detachedPath === undefined || (typeof file.detachedPath === "string" && file.detachedPath.length > 0)) &&
|
|
1094
|
+
(file.completed === undefined || file.completed === true)
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function isLifecycleCleanupEvidence(cleanup: CleanupEvidence): boolean {
|
|
1099
|
+
if (typeof cleanup !== "object" || cleanup === null || Array.isArray(cleanup)) return false;
|
|
1100
|
+
const record = cleanup as Record<string, unknown>;
|
|
1101
|
+
const allowed = new Set(["phase", "sessionId", "metadataRoot", "lifecycleDeleteMetadata", "lifecycleFiles"]);
|
|
1102
|
+
return (
|
|
1103
|
+
Object.keys(record).every(key => allowed.has(key)) &&
|
|
1104
|
+
record.phase === "lifecycle" &&
|
|
1105
|
+
typeof record.sessionId === "string" &&
|
|
1106
|
+
isCanonicalSessionId(record.sessionId) &&
|
|
1107
|
+
typeof record.metadataRoot === "string" &&
|
|
1108
|
+
record.metadataRoot.length > 0 &&
|
|
1109
|
+
Array.isArray(record.lifecycleFiles) &&
|
|
1110
|
+
record.lifecycleFiles.length > 0 &&
|
|
1111
|
+
record.lifecycleFiles.length <= 4 &&
|
|
1112
|
+
(record.lifecycleDeleteMetadata === undefined || record.lifecycleDeleteMetadata === true) &&
|
|
1113
|
+
record.lifecycleFiles.every(isLifecycleCleanupFile)
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function validateLifecycleCleanupShape(cleanup: CleanupEvidence): BrokerResponse | undefined {
|
|
1118
|
+
if (
|
|
1119
|
+
!isLifecycleCleanupEvidence(cleanup) ||
|
|
1120
|
+
lifecycleCleanupHasMixedMetadataSchema(cleanup) ||
|
|
1121
|
+
(cleanup.lifecycleDeleteMetadata === true && cleanup.lifecycleFiles!.length > 2)
|
|
1122
|
+
)
|
|
1123
|
+
return fail("terminal_uncertain", "Lifecycle cleanup replay lacks a complete unambiguous schema.");
|
|
1124
|
+
const files = cleanup.lifecycleFiles!;
|
|
1125
|
+
const paths = new Set<string>();
|
|
1126
|
+
for (const file of files) {
|
|
1127
|
+
if (!validateLifecycleCleanupFile(cleanup.metadataRoot!, cleanup.sessionId!, file))
|
|
1128
|
+
return fail("terminal_uncertain", "Lifecycle cleanup replay contains an invalid path authority.");
|
|
1129
|
+
const entryPaths = new Map<string, "path" | "plannedPath" | "detachedPath">();
|
|
1130
|
+
for (const [field, candidate] of [
|
|
1131
|
+
["path", file.path],
|
|
1132
|
+
["plannedPath", file.plannedPath],
|
|
1133
|
+
["detachedPath", file.detachedPath],
|
|
1134
|
+
] as const) {
|
|
1135
|
+
if (candidate === undefined) continue;
|
|
1136
|
+
const resolved = path.resolve(candidate);
|
|
1137
|
+
const previousField = entryPaths.get(resolved);
|
|
1138
|
+
if (previousField !== undefined) {
|
|
1139
|
+
if (
|
|
1140
|
+
(previousField === "plannedPath" && field === "detachedPath") ||
|
|
1141
|
+
(previousField === "detachedPath" && field === "plannedPath")
|
|
1142
|
+
)
|
|
1143
|
+
continue;
|
|
1144
|
+
return fail("terminal_uncertain", "Lifecycle cleanup replay contains duplicate path authority.");
|
|
1145
|
+
}
|
|
1146
|
+
entryPaths.set(resolved, field);
|
|
1147
|
+
if (paths.has(resolved))
|
|
1148
|
+
return fail("terminal_uncertain", "Lifecycle cleanup replay contains duplicate path authority.");
|
|
1149
|
+
paths.add(resolved);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return undefined;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function validateLifecycleCleanupFile(root: string, id: string, file: LifecycleCleanupFile): boolean {
|
|
1156
|
+
const directory = path.join(path.resolve(root), "sdk");
|
|
1157
|
+
const original = path.resolve(file.path);
|
|
1158
|
+
const planned = path.resolve(file.plannedPath);
|
|
1159
|
+
if (
|
|
1160
|
+
!isCanonicalLifecycleCleanupOriginal(root, id, original) ||
|
|
1161
|
+
path.dirname(planned) !== directory ||
|
|
1162
|
+
!path.basename(planned).startsWith(".skc-delete-") ||
|
|
1163
|
+
(file.detachedPath !== undefined && path.dirname(path.resolve(file.detachedPath)) !== directory)
|
|
1164
|
+
)
|
|
1165
|
+
return false;
|
|
1166
|
+
return isCleanupIdentity(file.identity);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function lifecycleCleanupCandidates(file: LifecycleCleanupFile): string[] {
|
|
1170
|
+
return [file.path, file.detachedPath, file.plannedPath].filter(
|
|
1171
|
+
(value, index, values): value is string => typeof value === "string" && values.indexOf(value) === index,
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function lifecycleMetadataReplayFiles(cleanup: CleanupEvidence): LifecycleCleanupFile[] | undefined {
|
|
1176
|
+
if (cleanup.lifecycleDeleteMetadata !== true) return undefined;
|
|
1177
|
+
return cleanup.lifecycleFiles?.length ? cleanup.lifecycleFiles : undefined;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function absentLifecyclePath(file: string): boolean {
|
|
1181
|
+
try {
|
|
1182
|
+
fsSync.lstatSync(file);
|
|
1183
|
+
return false;
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function isLifecycleCleanupResponse(value: LifecycleFileCapture | BrokerResponse | undefined): value is BrokerResponse {
|
|
1190
|
+
return typeof value === "object" && value !== null && "ok" in value;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function validateLifecycleMetadataReplay(cleanup: CleanupEvidence): BrokerResponse | undefined {
|
|
1194
|
+
const files = lifecycleMetadataReplayFiles(cleanup);
|
|
1195
|
+
if (!files) return undefined;
|
|
1196
|
+
const root = path.resolve(cleanup.metadataRoot!);
|
|
1197
|
+
const id = cleanup.sessionId!;
|
|
1198
|
+
const markerPath = lifecycleMarkerPath(root, id);
|
|
1199
|
+
const readyPath = lifecycleReadyPath(root, id);
|
|
1200
|
+
const authorities = new Set<string>();
|
|
1201
|
+
const candidates = new Set<string>();
|
|
1202
|
+
for (const file of files) {
|
|
1203
|
+
const authority = path.resolve(file.path);
|
|
1204
|
+
if ((authority !== markerPath && authority !== readyPath) || authorities.has(authority))
|
|
1205
|
+
return fail("terminal_uncertain", "Lifecycle metadata replay contains duplicate or non-canonical authority.");
|
|
1206
|
+
authorities.add(authority);
|
|
1207
|
+
for (const candidate of lifecycleCleanupCandidates(file)) {
|
|
1208
|
+
const resolved = path.resolve(candidate);
|
|
1209
|
+
if (candidates.has(resolved))
|
|
1210
|
+
return fail("terminal_uncertain", "Lifecycle metadata replay contains duplicate candidate authority.");
|
|
1211
|
+
candidates.add(resolved);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
const markerEntry = files.find(file => path.resolve(file.path) === markerPath);
|
|
1215
|
+
const readyEntry = files.find(file => path.resolve(file.path) === readyPath);
|
|
1216
|
+
const capture = (file: string): LifecycleFileCapture | undefined | BrokerResponse => {
|
|
1217
|
+
try {
|
|
1218
|
+
return captureLifecycleFile(file, true, true);
|
|
1219
|
+
} catch {
|
|
1220
|
+
return fail("terminal_uncertain", "Lifecycle metadata sibling could not be safely inspected.");
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
const marker = capture(markerPath);
|
|
1224
|
+
const ready = capture(readyPath);
|
|
1225
|
+
if (isLifecycleCleanupResponse(marker)) return marker;
|
|
1226
|
+
if (isLifecycleCleanupResponse(ready)) return ready;
|
|
1227
|
+
if (marker && (!markerEntry || !sameLifecycleCleanupIdentity(marker.identity, markerEntry.identity)))
|
|
1228
|
+
return fail("terminal_uncertain", "Lifecycle marker sibling lacks exact replay authority.");
|
|
1229
|
+
if (ready && (!readyEntry || !sameLifecycleCleanupIdentity(ready.identity, readyEntry.identity)))
|
|
1230
|
+
return fail("terminal_uncertain", "Lifecycle readiness sibling lacks exact replay authority.");
|
|
1231
|
+
for (const file of files) {
|
|
1232
|
+
let activeCandidates = 0;
|
|
1233
|
+
for (const candidate of lifecycleCleanupCandidates(file)) {
|
|
1234
|
+
let current: LifecycleFileCapture | undefined;
|
|
1235
|
+
try {
|
|
1236
|
+
current = captureLifecycleFile(candidate, true, true);
|
|
1237
|
+
} catch {
|
|
1238
|
+
return fail("terminal_uncertain", "Lifecycle metadata candidate could not be safely inspected.");
|
|
1239
|
+
}
|
|
1240
|
+
if (!current) continue;
|
|
1241
|
+
if (!sameLifecycleCleanupIdentity(current.identity, file.identity))
|
|
1242
|
+
return fail("terminal_uncertain", "Lifecycle metadata candidate lacks exact replay authority.");
|
|
1243
|
+
activeCandidates++;
|
|
1244
|
+
}
|
|
1245
|
+
if (file.completed && activeCandidates > 0)
|
|
1246
|
+
return fail(
|
|
1247
|
+
"terminal_uncertain",
|
|
1248
|
+
"Lifecycle cleanup receipt marks a metadata target complete while a candidate remains.",
|
|
1249
|
+
);
|
|
1250
|
+
if (!file.completed && activeCandidates > 1)
|
|
1251
|
+
return fail("terminal_uncertain", "Lifecycle metadata replay has multiple active candidates.");
|
|
1252
|
+
}
|
|
1253
|
+
if (ready && !markerEntry)
|
|
1254
|
+
return fail("terminal_uncertain", "Lifecycle readiness metadata lacks canonical marker authority.");
|
|
1255
|
+
if (!ready) return undefined;
|
|
1256
|
+
let readyMarker: EffectMarker;
|
|
1257
|
+
try {
|
|
1258
|
+
const value: unknown = parseLifecycleJson(ready.bytes);
|
|
1259
|
+
if (!isExactEffectMarker(value)) throw new Error("invalid ready marker");
|
|
1260
|
+
|
|
1261
|
+
readyMarker = value;
|
|
1262
|
+
} catch {
|
|
1263
|
+
return fail("terminal_uncertain", "Lifecycle readiness metadata ownership could not be verified.");
|
|
1264
|
+
}
|
|
1265
|
+
if (!markerEntry)
|
|
1266
|
+
return fail("terminal_uncertain", "Lifecycle readiness metadata lacks canonical marker authority.");
|
|
1267
|
+
if (!marker) {
|
|
1268
|
+
if (createHash("sha256").update(canonicalJson(readyMarker)).digest("hex") !== markerEntry.identity.sha256)
|
|
1269
|
+
return fail(
|
|
1270
|
+
"terminal_uncertain",
|
|
1271
|
+
"Lifecycle readiness metadata is not bound to the completed marker authority.",
|
|
1272
|
+
);
|
|
1273
|
+
return undefined;
|
|
1274
|
+
}
|
|
1275
|
+
try {
|
|
1276
|
+
const value: unknown = parseLifecycleJson(marker.bytes);
|
|
1277
|
+
if (!isExactEffectMarker(value) || !sameEffectMarker(value, readyMarker)) throw new Error("mismatched marker");
|
|
1278
|
+
} catch {
|
|
1279
|
+
return fail("terminal_uncertain", "Lifecycle metadata siblings do not share one owner marker.");
|
|
1280
|
+
}
|
|
1281
|
+
return undefined;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function lifecycleMetadataReplayAbsent(cleanup: CleanupEvidence): boolean {
|
|
1285
|
+
const files = lifecycleMetadataReplayFiles(cleanup);
|
|
1286
|
+
if (!files) return true;
|
|
1287
|
+
const root = path.resolve(cleanup.metadataRoot!);
|
|
1288
|
+
const id = cleanup.sessionId!;
|
|
1289
|
+
const required = new Set<string>([lifecycleMarkerPath(root, id), lifecycleReadyPath(root, id)]);
|
|
1290
|
+
for (const file of files) for (const candidate of lifecycleCleanupCandidates(file)) required.add(candidate);
|
|
1291
|
+
return [...required].every(absentLifecyclePath);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Base dev persisted metadata cleanup one file at a time. Accept only its
|
|
1296
|
+
* identity-bound marker receipt and translate it into the current replay plan.
|
|
1297
|
+
*/
|
|
1298
|
+
function hasExactLegacyMetadataCleanupKeys(cleanup: CleanupEvidence): boolean {
|
|
1299
|
+
if (typeof cleanup !== "object" || cleanup === null || Array.isArray(cleanup)) return false;
|
|
1300
|
+
const allowed = new Set([
|
|
1301
|
+
"phase",
|
|
1302
|
+
"sessionId",
|
|
1303
|
+
"metadataRoot",
|
|
1304
|
+
"metadataIdentity",
|
|
1305
|
+
"metadataPath",
|
|
1306
|
+
"metadataAttempt",
|
|
1307
|
+
"plannedMetadataPath",
|
|
1308
|
+
"detachedMetadataPath",
|
|
1309
|
+
"metadataCompleted",
|
|
1310
|
+
]);
|
|
1311
|
+
return Object.keys(cleanup as Record<string, unknown>).every(key => allowed.has(key));
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function legacyMetadataCleanupPlan(cleanup: CleanupEvidence): CleanupEvidence | undefined {
|
|
1315
|
+
if (
|
|
1316
|
+
!hasExactLegacyMetadataCleanupKeys(cleanup) ||
|
|
1317
|
+
cleanup.phase !== "metadata" ||
|
|
1318
|
+
typeof cleanup.sessionId !== "string" ||
|
|
1319
|
+
!isCanonicalSessionId(cleanup.sessionId) ||
|
|
1320
|
+
typeof cleanup.metadataRoot !== "string" ||
|
|
1321
|
+
cleanup.metadataRoot.length === 0 ||
|
|
1322
|
+
typeof cleanup.metadataPath !== "string" ||
|
|
1323
|
+
cleanup.metadataPath.length === 0 ||
|
|
1324
|
+
!cleanup.metadataIdentity ||
|
|
1325
|
+
typeof cleanup.plannedMetadataPath !== "string" ||
|
|
1326
|
+
cleanup.plannedMetadataPath.length === 0 ||
|
|
1327
|
+
(cleanup.detachedMetadataPath !== undefined &&
|
|
1328
|
+
(typeof cleanup.detachedMetadataPath !== "string" || cleanup.detachedMetadataPath.length === 0)) ||
|
|
1329
|
+
(cleanup.metadataCompleted !== undefined && cleanup.metadataCompleted !== true)
|
|
1330
|
+
)
|
|
1331
|
+
return undefined;
|
|
1332
|
+
const root = path.resolve(cleanup.metadataRoot);
|
|
1333
|
+
const directory = path.join(root, "sdk");
|
|
1334
|
+
const markerPath = lifecycleMarkerPath(root, cleanup.sessionId);
|
|
1335
|
+
const readyPath = lifecycleReadyPath(root, cleanup.sessionId);
|
|
1336
|
+
const metadataPath = path.resolve(cleanup.metadataPath);
|
|
1337
|
+
const plannedPath = path.resolve(cleanup.plannedMetadataPath);
|
|
1338
|
+
const detachedPath = cleanup.detachedMetadataPath && path.resolve(cleanup.detachedMetadataPath);
|
|
1339
|
+
if (
|
|
1340
|
+
metadataPath !== markerPath ||
|
|
1341
|
+
path.dirname(plannedPath) !== directory ||
|
|
1342
|
+
!path.basename(plannedPath).startsWith(".skc-delete-") ||
|
|
1343
|
+
(detachedPath !== undefined && path.dirname(detachedPath) !== directory) ||
|
|
1344
|
+
(cleanup.metadataAttempt !== undefined &&
|
|
1345
|
+
(!Number.isSafeInteger(cleanup.metadataAttempt) || cleanup.metadataAttempt < 1))
|
|
1346
|
+
)
|
|
1347
|
+
return undefined;
|
|
1348
|
+
const persistedIdentity = cleanupIdentity(cleanup.metadataIdentity);
|
|
1349
|
+
if (!persistedIdentity) return undefined;
|
|
1350
|
+
|
|
1351
|
+
const captureExactRegular = (
|
|
1352
|
+
file: string,
|
|
1353
|
+
): { kind: "absent" } | { kind: "present"; capture: LifecycleFileCapture } | undefined => {
|
|
1354
|
+
try {
|
|
1355
|
+
const stat = fsSync.lstatSync(file);
|
|
1356
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return undefined;
|
|
1357
|
+
const capture = captureLifecycleFile(file, true, true);
|
|
1358
|
+
|
|
1359
|
+
return capture ? { kind: "present", capture } : undefined;
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT" ? { kind: "absent" } : undefined;
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
const markerCandidates = [metadataPath, detachedPath, plannedPath].filter(
|
|
1366
|
+
(candidate, index, candidates): candidate is string =>
|
|
1367
|
+
typeof candidate === "string" && candidates.indexOf(candidate) === index,
|
|
1368
|
+
);
|
|
1369
|
+
let activeMarker: LifecycleFileCapture | undefined;
|
|
1370
|
+
for (const candidate of markerCandidates) {
|
|
1371
|
+
const current = captureExactRegular(candidate);
|
|
1372
|
+
if (!current) return undefined;
|
|
1373
|
+
if (current.kind === "absent") continue;
|
|
1374
|
+
if (!sameLifecycleCleanupIdentity(current.capture.identity, serializeCleanupIdentity(persistedIdentity)))
|
|
1375
|
+
return undefined;
|
|
1376
|
+
if (activeMarker) return undefined;
|
|
1377
|
+
activeMarker = current.capture;
|
|
1378
|
+
}
|
|
1379
|
+
const markerCompleted = !activeMarker;
|
|
1380
|
+
// Legacy receipts can crash after exact marker unlink and before recording metadataCompleted.
|
|
1381
|
+
// With every authorized marker candidate absent, only the persisted marker digest may bind a ready sibling.
|
|
1382
|
+
let marker: EffectMarker | undefined;
|
|
1383
|
+
if (activeMarker) {
|
|
1384
|
+
try {
|
|
1385
|
+
const value: unknown = parseLifecycleJson(activeMarker.bytes);
|
|
1386
|
+
if (!isExactEffectMarker(value)) return undefined;
|
|
1387
|
+
|
|
1388
|
+
marker = value;
|
|
1389
|
+
} catch {
|
|
1390
|
+
return undefined;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
const ready = captureExactRegular(readyPath);
|
|
1395
|
+
if (!ready) return undefined;
|
|
1396
|
+
let readyMarker: EffectMarker | undefined;
|
|
1397
|
+
if (ready.kind === "present") {
|
|
1398
|
+
try {
|
|
1399
|
+
const value: unknown = parseLifecycleJson(ready.capture.bytes);
|
|
1400
|
+
if (!isExactEffectMarker(value)) return undefined;
|
|
1401
|
+
|
|
1402
|
+
readyMarker = value;
|
|
1403
|
+
} catch {
|
|
1404
|
+
return undefined;
|
|
1405
|
+
}
|
|
1406
|
+
if (marker && !sameEffectMarker(marker, readyMarker)) return undefined;
|
|
1407
|
+
if (!marker && createHash("sha256").update(canonicalJson(readyMarker)).digest("hex") !== persistedIdentity.sha256)
|
|
1408
|
+
return undefined;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
return {
|
|
1412
|
+
phase: "lifecycle",
|
|
1413
|
+
sessionId: cleanup.sessionId,
|
|
1414
|
+
metadataRoot: root,
|
|
1415
|
+
lifecycleDeleteMetadata: true,
|
|
1416
|
+
lifecycleFiles: [
|
|
1417
|
+
{
|
|
1418
|
+
path: metadataPath,
|
|
1419
|
+
identity: serializeCleanupIdentity({
|
|
1420
|
+
...(activeMarker?.identity ?? persistedIdentity),
|
|
1421
|
+
size: Number((activeMarker?.identity ?? persistedIdentity).size),
|
|
1422
|
+
}),
|
|
1423
|
+
attempt: cleanup.metadataAttempt ?? 1,
|
|
1424
|
+
plannedPath,
|
|
1425
|
+
...(detachedPath ? { detachedPath } : {}),
|
|
1426
|
+
...(markerCompleted ? { completed: true as const } : {}),
|
|
1427
|
+
},
|
|
1428
|
+
...(ready.kind === "present"
|
|
1429
|
+
? [
|
|
1430
|
+
{
|
|
1431
|
+
path: readyPath,
|
|
1432
|
+
identity: serializeCleanupIdentity({
|
|
1433
|
+
...ready.capture.identity,
|
|
1434
|
+
size: Number(ready.capture.identity.size),
|
|
1435
|
+
}),
|
|
1436
|
+
attempt: 1,
|
|
1437
|
+
plannedPath: path.join(directory, `.skc-delete-${randomUUID()}-${path.basename(readyPath)}`),
|
|
1438
|
+
},
|
|
1439
|
+
]
|
|
1440
|
+
: []),
|
|
1441
|
+
],
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function lifecycleDeleteMetadataCleanupPlan(
|
|
1446
|
+
metadataRoot: string,
|
|
1447
|
+
id: string,
|
|
1448
|
+
files: ReadonlyArray<{ metadataPath: string; metadata: LifecycleFileCapture }>,
|
|
1449
|
+
): CleanupEvidence {
|
|
1450
|
+
return {
|
|
1451
|
+
phase: "lifecycle",
|
|
1452
|
+
sessionId: id,
|
|
1453
|
+
metadataRoot,
|
|
1454
|
+
lifecycleDeleteMetadata: true,
|
|
1455
|
+
lifecycleFiles: files.map(({ metadataPath, metadata }) => ({
|
|
1456
|
+
path: metadataPath,
|
|
1457
|
+
identity: serializeCleanupIdentity({
|
|
1458
|
+
dev: metadata.identity.dev,
|
|
1459
|
+
ino: metadata.identity.ino,
|
|
1460
|
+
size: Number(metadata.identity.size),
|
|
1461
|
+
mtimeNs: metadata.identity.mtimeNs,
|
|
1462
|
+
sha256: metadata.identity.sha256,
|
|
1463
|
+
}),
|
|
1464
|
+
attempt: 1,
|
|
1465
|
+
plannedPath: path.join(
|
|
1466
|
+
path.dirname(metadataPath),
|
|
1467
|
+
`.skc-delete-${randomUUID()}-${path.basename(metadataPath)}`,
|
|
1468
|
+
),
|
|
1469
|
+
})),
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
type LifecycleDeleteMetadataPreflight = { cleanup: CleanupEvidence } | BrokerResponse;
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* Capture lifecycle metadata before deleting any saved user data. A fresh delete
|
|
1477
|
+
* may only clean metadata owned by one dead lifecycle process.
|
|
1478
|
+
*/
|
|
1479
|
+
function preflightLifecycleDeleteMetadata(
|
|
1480
|
+
root: string,
|
|
1481
|
+
id: string,
|
|
1482
|
+
record: { pid: number } | undefined,
|
|
1483
|
+
readIncarnation: (pid: number) => string | undefined,
|
|
1484
|
+
): LifecycleDeleteMetadataPreflight {
|
|
1485
|
+
const metadataPaths = [lifecycleMarkerPath(root, id), lifecycleReadyPath(root, id)];
|
|
1486
|
+
const lifecycleMetadata: Array<{
|
|
1487
|
+
metadataPath: string;
|
|
1488
|
+
metadata: LifecycleFileCapture;
|
|
1489
|
+
marker: EffectMarker;
|
|
1490
|
+
}> = [];
|
|
1491
|
+
for (const metadataPath of metadataPaths) {
|
|
1492
|
+
let metadata: LifecycleFileCapture | undefined;
|
|
1493
|
+
try {
|
|
1494
|
+
metadata = captureLifecycleFile(metadataPath, true, true);
|
|
1495
|
+
} catch {
|
|
1496
|
+
return fail("terminal_uncertain", "Lifecycle metadata path is occupied by an unsafe object.");
|
|
1497
|
+
}
|
|
1498
|
+
if (!metadata) continue;
|
|
1499
|
+
let marker: unknown;
|
|
1500
|
+
try {
|
|
1501
|
+
marker = parseLifecycleJson(metadata.bytes);
|
|
1502
|
+
} catch {
|
|
1503
|
+
return fail("terminal_uncertain", "Lifecycle metadata ownership could not be verified.");
|
|
1504
|
+
}
|
|
1505
|
+
if (
|
|
1506
|
+
!isExactEffectMarker(marker) ||
|
|
1507
|
+
(record && marker.pid !== record.pid) ||
|
|
1508
|
+
observeProcess(marker.pid, marker.incarnation, readIncarnation) !== "exited"
|
|
1509
|
+
)
|
|
1510
|
+
return fail("terminal_uncertain", "Lifecycle metadata ownership could not be verified.");
|
|
1511
|
+
lifecycleMetadata.push({ metadataPath, metadata, marker });
|
|
1512
|
+
}
|
|
1513
|
+
const canonicalLifecycleMarker = lifecycleMetadata.find(
|
|
1514
|
+
metadata => metadata.metadataPath === lifecycleMarkerPath(root, id),
|
|
1515
|
+
);
|
|
1516
|
+
const lifecycleReadyMarker = lifecycleMetadata.find(
|
|
1517
|
+
metadata => metadata.metadataPath === lifecycleReadyPath(root, id),
|
|
1518
|
+
);
|
|
1519
|
+
if (lifecycleReadyMarker && !canonicalLifecycleMarker)
|
|
1520
|
+
return fail(
|
|
1521
|
+
"terminal_uncertain",
|
|
1522
|
+
"Lifecycle readiness metadata lacks canonical marker authority for fresh cleanup.",
|
|
1523
|
+
);
|
|
1524
|
+
if (
|
|
1525
|
+
canonicalLifecycleMarker &&
|
|
1526
|
+
lifecycleReadyMarker &&
|
|
1527
|
+
!sameEffectMarker(canonicalLifecycleMarker.marker, lifecycleReadyMarker.marker)
|
|
1528
|
+
)
|
|
1529
|
+
return fail("terminal_uncertain", "Lifecycle metadata siblings do not share one owner marker.");
|
|
1530
|
+
return { cleanup: lifecycleDeleteMetadataCleanupPlan(root, id, lifecycleMetadata) };
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
async function reconcileLifecycleCleanup(
|
|
1534
|
+
broker: Broker,
|
|
1535
|
+
identity: string,
|
|
1536
|
+
cleanup: CleanupEvidence,
|
|
1537
|
+
completion: BrokerResponse = fail("spawn_failed", "No ready SDK endpoint remains available."),
|
|
1538
|
+
): Promise<BrokerResponse> {
|
|
1539
|
+
const shapeValidation = validateLifecycleCleanupShape(cleanup);
|
|
1540
|
+
if (shapeValidation) return shapeValidation;
|
|
1541
|
+
let activeCleanup =
|
|
1542
|
+
cleanup.lifecycleDeleteMetadata === true || completion.ok
|
|
1543
|
+
? { ...cleanup, lifecycleDeleteMetadata: true as const }
|
|
1544
|
+
: cleanup;
|
|
1545
|
+
const metadataReplayValidation = validateLifecycleMetadataReplay(activeCleanup);
|
|
1546
|
+
if (metadataReplayValidation) return metadataReplayValidation;
|
|
1547
|
+
for (let index = 0; index < activeCleanup.lifecycleFiles!.length; index++) {
|
|
1548
|
+
const file = activeCleanup.lifecycleFiles![index];
|
|
1549
|
+
if (!validateLifecycleCleanupFile(activeCleanup.metadataRoot!, activeCleanup.sessionId!, file))
|
|
1550
|
+
return fail("terminal_uncertain", "Lifecycle cleanup replay contains an invalid path authority.");
|
|
1551
|
+
const candidates = lifecycleCleanupCandidates(file);
|
|
1552
|
+
if (file.completed) {
|
|
1553
|
+
for (const candidate of candidates) {
|
|
1554
|
+
try {
|
|
1555
|
+
fsSync.lstatSync(candidate);
|
|
1556
|
+
return fail(
|
|
1557
|
+
"terminal_uncertain",
|
|
1558
|
+
"Lifecycle cleanup receipt marks a target complete while an authorized candidate remains.",
|
|
1559
|
+
);
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT")
|
|
1562
|
+
return fail("terminal_uncertain", "Lifecycle cleanup completion could not be safely inspected.");
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
let activePath: string | undefined;
|
|
1568
|
+
let captured: LifecycleFileCapture | undefined;
|
|
1569
|
+
let foundUnauthorized = false;
|
|
1570
|
+
for (const candidate of candidates) {
|
|
1571
|
+
try {
|
|
1572
|
+
const stat = fsSync.lstatSync(candidate);
|
|
1573
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
1574
|
+
foundUnauthorized = true;
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
1579
|
+
foundUnauthorized = true;
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
const current = captureLifecycleFile(candidate, true, true);
|
|
1583
|
+
|
|
1584
|
+
if (!current) {
|
|
1585
|
+
foundUnauthorized = true;
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
if (sameLifecycleCleanupIdentity(current.identity, file.identity) && !activePath) {
|
|
1589
|
+
activePath = candidate;
|
|
1590
|
+
captured = current;
|
|
1591
|
+
} else {
|
|
1592
|
+
foundUnauthorized = true;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (foundUnauthorized)
|
|
1596
|
+
return fail("terminal_uncertain", "Lifecycle cleanup target identity changed before reconciliation.");
|
|
1597
|
+
if (!captured || !activePath) {
|
|
1598
|
+
const lifecycleFiles = activeCleanup.lifecycleFiles!.map((candidate, candidateIndex) =>
|
|
1599
|
+
candidateIndex === index ? { ...candidate, completed: true as const } : candidate,
|
|
1600
|
+
);
|
|
1601
|
+
activeCleanup = { ...activeCleanup, lifecycleFiles };
|
|
1602
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
1603
|
+
response: fail("cleanup_pending", "Lifecycle cleanup completion was durably reconciled.", activeCleanup),
|
|
1604
|
+
});
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
if (file.detachedPath || activePath === file.plannedPath) {
|
|
1609
|
+
const nextFile: LifecycleCleanupFile = {
|
|
1610
|
+
...file,
|
|
1611
|
+
detachedPath: activePath,
|
|
1612
|
+
attempt: (file.attempt ?? 1) + 1,
|
|
1613
|
+
plannedPath: path.join(path.dirname(file.path), `.skc-delete-${randomUUID()}-${path.basename(file.path)}`),
|
|
1614
|
+
};
|
|
1615
|
+
const lifecycleFiles = activeCleanup.lifecycleFiles!.map((candidate, candidateIndex) =>
|
|
1616
|
+
candidateIndex === index ? nextFile : candidate,
|
|
1617
|
+
);
|
|
1618
|
+
activeCleanup = { ...activeCleanup, lifecycleFiles };
|
|
1619
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
1620
|
+
response: fail(
|
|
1621
|
+
"cleanup_pending",
|
|
1622
|
+
"Lifecycle retry cleanup is preauthorized for durable reconciliation.",
|
|
1623
|
+
activeCleanup,
|
|
1624
|
+
),
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
const currentFile = activeCleanup.lifecycleFiles![index];
|
|
1628
|
+
const result = native.exactUnlink(activePath, {
|
|
1629
|
+
...captured.identity,
|
|
1630
|
+
quarantineName: path.basename(currentFile.plannedPath),
|
|
1631
|
+
});
|
|
1632
|
+
if (!result.ok) {
|
|
1633
|
+
const lifecycleFiles = activeCleanup.lifecycleFiles!.map((candidate, candidateIndex) =>
|
|
1634
|
+
candidateIndex === index && result.detachedPath
|
|
1635
|
+
? { ...candidate, detachedPath: result.detachedPath }
|
|
1636
|
+
: candidate,
|
|
1637
|
+
);
|
|
1638
|
+
return fail("cleanup_pending", `Lifecycle cleanup remains pending: ${result.code ?? "unknown"}`, {
|
|
1639
|
+
...activeCleanup,
|
|
1640
|
+
lifecycleFiles,
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
const lifecycleFiles = activeCleanup.lifecycleFiles!.map((candidate, candidateIndex) =>
|
|
1644
|
+
candidateIndex === index ? { ...candidate, completed: true as const } : candidate,
|
|
1645
|
+
);
|
|
1646
|
+
activeCleanup = { ...activeCleanup, lifecycleFiles };
|
|
1647
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
1648
|
+
response: fail("cleanup_pending", "Lifecycle cleanup completion was durably reconciled.", activeCleanup),
|
|
1649
|
+
});
|
|
1650
|
+
lifecycleCleanupHooksForTest.get(broker)?.();
|
|
1651
|
+
}
|
|
1652
|
+
if (!lifecycleMetadataReplayAbsent(activeCleanup))
|
|
1653
|
+
return fail("terminal_uncertain", "Lifecycle metadata replay left an authorized sibling behind.");
|
|
1654
|
+
await syncDirectory(path.join(activeCleanup.metadataRoot!, "sdk"));
|
|
1655
|
+
return completion;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
async function readSessionLifecycleFailure(
|
|
1659
|
+
root: string,
|
|
1660
|
+
id: string,
|
|
1661
|
+
expected: EffectMarker,
|
|
1662
|
+
): Promise<LifecycleFailureArtifact | undefined> {
|
|
1663
|
+
return (await readLifecycleFailureArtifact(lifecycleFailurePath(root, id, expected.effectMarker), expected))
|
|
1664
|
+
?.artifact;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
async function hasDurableProcessIdentity(
|
|
1668
|
+
root: string,
|
|
1669
|
+
id: string,
|
|
1670
|
+
pid: number,
|
|
1671
|
+
expected?: EffectMarker,
|
|
1672
|
+
): Promise<boolean> {
|
|
1673
|
+
const marker = await readEffectMarker(lifecycleMarkerPath(root, id));
|
|
1674
|
+
if (!marker || marker.pid !== pid || (expected && !sameEffectMarker(marker, expected))) return false;
|
|
1675
|
+
return marker.incarnation === processIncarnation(pid);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
async function hasOwnedReadinessEvidence(
|
|
1679
|
+
broker: Broker,
|
|
1680
|
+
root: string,
|
|
1681
|
+
id: string,
|
|
1682
|
+
expected: EffectMarker,
|
|
1683
|
+
): Promise<boolean> {
|
|
1684
|
+
if (
|
|
1685
|
+
observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) !==
|
|
1686
|
+
"alive"
|
|
1687
|
+
)
|
|
1688
|
+
return false;
|
|
1689
|
+
const [effect, ready] = await Promise.all([
|
|
1690
|
+
readEffectMarker(lifecycleMarkerPath(root, id)),
|
|
1691
|
+
readEffectMarker(lifecycleReadyPath(root, id)),
|
|
1692
|
+
]);
|
|
1693
|
+
return (
|
|
1694
|
+
effect !== undefined &&
|
|
1695
|
+
ready !== undefined &&
|
|
1696
|
+
sameEffectMarker(effect, expected) &&
|
|
1697
|
+
sameEffectMarker(ready, expected)
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
type LifecycleFileCapture = {
|
|
1702
|
+
bytes: Buffer;
|
|
1703
|
+
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string };
|
|
1704
|
+
digest: string;
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
function captureLifecycleFile(file: string, requireRegular = false, bounded = false): LifecycleFileCapture | undefined {
|
|
1708
|
+
let descriptor: number | undefined;
|
|
1709
|
+
try {
|
|
1710
|
+
const preflight = bounded ? fsSync.lstatSync(file, { bigint: true }) : undefined;
|
|
1711
|
+
if (
|
|
1712
|
+
preflight &&
|
|
1713
|
+
(!preflight.isFile() || preflight.size === 0n || preflight.size > BigInt(MAX_LIFECYCLE_METADATA_BYTES))
|
|
1714
|
+
) {
|
|
1715
|
+
if (requireRegular) throw new Error("Lifecycle metadata is not a bounded regular file.");
|
|
1716
|
+
return undefined;
|
|
1717
|
+
}
|
|
1718
|
+
descriptor = fsSync.openSync(file, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
|
|
1719
|
+
const stat = fsSync.fstatSync(descriptor, { bigint: true });
|
|
1720
|
+
if (
|
|
1721
|
+
!stat.isFile() ||
|
|
1722
|
+
(bounded &&
|
|
1723
|
+
(stat.size === 0n ||
|
|
1724
|
+
stat.size > BigInt(MAX_LIFECYCLE_METADATA_BYTES) ||
|
|
1725
|
+
!preflight ||
|
|
1726
|
+
stat.dev !== preflight.dev ||
|
|
1727
|
+
stat.ino !== preflight.ino))
|
|
1728
|
+
) {
|
|
1729
|
+
if (requireRegular) throw new Error("Lifecycle cleanup candidate is not an exact bounded regular file.");
|
|
1730
|
+
return undefined;
|
|
1731
|
+
}
|
|
1732
|
+
const bytes = fsSync.readFileSync(descriptor);
|
|
1733
|
+
const current = fsSync.fstatSync(descriptor, { bigint: true });
|
|
1734
|
+
if (
|
|
1735
|
+
!current.isFile() ||
|
|
1736
|
+
current.dev !== stat.dev ||
|
|
1737
|
+
current.ino !== stat.ino ||
|
|
1738
|
+
current.size !== stat.size ||
|
|
1739
|
+
current.mtimeNs !== stat.mtimeNs
|
|
1740
|
+
)
|
|
1741
|
+
return undefined;
|
|
1742
|
+
return {
|
|
1743
|
+
bytes,
|
|
1744
|
+
identity: {
|
|
1745
|
+
dev: stat.dev,
|
|
1746
|
+
ino: stat.ino,
|
|
1747
|
+
size: stat.size,
|
|
1748
|
+
mtimeNs: stat.mtimeNs,
|
|
1749
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1750
|
+
},
|
|
1751
|
+
digest: createHash("sha256").update(bytes).digest("hex"),
|
|
1752
|
+
};
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
1755
|
+
throw error;
|
|
1756
|
+
} finally {
|
|
1757
|
+
if (descriptor !== undefined) fsSync.closeSync(descriptor);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
async function removeOwnedLifecycleArtifacts(root: string, id: string, expected: EffectMarker): Promise<boolean> {
|
|
1762
|
+
const marker = await readEffectMarker(lifecycleMarkerPath(root, id));
|
|
1763
|
+
if (!marker || !sameEffectMarker(marker, expected)) return false;
|
|
1764
|
+
const endpointPath = path.join(root, "sdk", `${id}.json`);
|
|
1765
|
+
const plannedEndpointPath = path.join(
|
|
1766
|
+
path.dirname(endpointPath),
|
|
1767
|
+
`.skc-delete-endpoint-${expected.effectMarker}-${path.basename(endpointPath)}`,
|
|
1768
|
+
);
|
|
1769
|
+
const retryEndpointPath = path.join(
|
|
1770
|
+
path.dirname(endpointPath),
|
|
1771
|
+
`.skc-delete-endpoint-retry-${expected.effectMarker}-${path.basename(endpointPath)}`,
|
|
1772
|
+
);
|
|
1773
|
+
const finalEndpointPath = path.join(
|
|
1774
|
+
path.dirname(endpointPath),
|
|
1775
|
+
`.skc-delete-endpoint-final-${expected.effectMarker}-${path.basename(endpointPath)}`,
|
|
1776
|
+
);
|
|
1777
|
+
const endpointSource = [endpointPath, plannedEndpointPath, retryEndpointPath, finalEndpointPath].find(candidate => {
|
|
1778
|
+
try {
|
|
1779
|
+
return fsSync.lstatSync(candidate).isFile();
|
|
1780
|
+
} catch {
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
const endpoint = endpointSource ? captureLifecycleFile(endpointSource) : undefined;
|
|
1785
|
+
if (endpoint && endpointSource) {
|
|
1786
|
+
let parsed: { pid?: unknown };
|
|
1787
|
+
try {
|
|
1788
|
+
parsed = parseLifecycleJson(endpoint.bytes) as { pid?: unknown };
|
|
1789
|
+
} catch {
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
if (parsed.pid !== expected.pid || !hasObservedProcessExit(expected.pid)) return false;
|
|
1793
|
+
if (createHash("sha256").update(endpoint.bytes).digest("hex") !== endpoint.digest) return false;
|
|
1794
|
+
const endpointRemoval = exactUnlinkLifecycleFile(
|
|
1795
|
+
endpointSource,
|
|
1796
|
+
endpoint.identity,
|
|
1797
|
+
endpointSource === endpointPath
|
|
1798
|
+
? plannedEndpointPath
|
|
1799
|
+
: endpointSource === plannedEndpointPath
|
|
1800
|
+
? retryEndpointPath
|
|
1801
|
+
: finalEndpointPath,
|
|
1802
|
+
);
|
|
1803
|
+
if (!endpointRemoval.ok) return false;
|
|
1804
|
+
}
|
|
1805
|
+
const currentMarker = await readEffectMarker(lifecycleMarkerPath(root, id));
|
|
1806
|
+
if (!currentMarker || !sameEffectMarker(currentMarker, expected)) return false;
|
|
1807
|
+
const readyPath = lifecycleReadyPath(root, id);
|
|
1808
|
+
const ready = captureLifecycleFile(readyPath, true, true);
|
|
1809
|
+
if (ready && createHash("sha256").update(ready.bytes).digest("hex") !== ready.digest) return false;
|
|
1810
|
+
// Readiness mutation is deferred to the same ledger-backed cleanup transaction.
|
|
1811
|
+
return true;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
async function recordTerminalUncertain(broker: Broker, id: string, root: string, pid: number): Promise<void> {
|
|
1815
|
+
await broker.index.refresh();
|
|
1816
|
+
const registered = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
1817
|
+
if (registered)
|
|
1818
|
+
await broker.index.append({
|
|
1819
|
+
type: "lifecycle_terminal",
|
|
1820
|
+
sessionId: id,
|
|
1821
|
+
locator: registered.locator,
|
|
1822
|
+
endpointGeneration: registered.endpointGeneration,
|
|
1823
|
+
pid: registered.pid,
|
|
1824
|
+
terminalUncertain: true,
|
|
1825
|
+
});
|
|
1826
|
+
else
|
|
1827
|
+
await broker.index.append({
|
|
1828
|
+
type: "lifecycle_terminal",
|
|
1829
|
+
sessionId: id,
|
|
1830
|
+
locator: { repo: "unknown", stateRoot: root },
|
|
1831
|
+
endpointGeneration: 0,
|
|
1832
|
+
pid,
|
|
1833
|
+
terminalUncertain: true,
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
async function waitUntil(timing: LifecycleTiming, deadline: number): Promise<void> {
|
|
1838
|
+
while (timing.now() < deadline) await timing.sleep(Math.max(0, Math.min(POLL_MS, deadline - timing.now())));
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
async function terminateSpawnedChild(
|
|
1842
|
+
child: ChildProcess,
|
|
1843
|
+
broker: Broker,
|
|
1844
|
+
id: string,
|
|
1845
|
+
root: string,
|
|
1846
|
+
deadline: number,
|
|
1847
|
+
terminationStartDeadlineAt: number,
|
|
1848
|
+
expected: EffectMarker | undefined,
|
|
1849
|
+
timing: LifecycleTiming,
|
|
1850
|
+
): Promise<boolean> {
|
|
1851
|
+
const pid = child.pid;
|
|
1852
|
+
if (!pid || (expected && pid !== expected.pid)) return false;
|
|
1853
|
+
const incarnation = expected?.incarnation ?? processIncarnationForBroker(broker, pid);
|
|
1854
|
+
await broker.index.refresh();
|
|
1855
|
+
const observe = (): ProcessObservation =>
|
|
1856
|
+
child.exitCode !== null
|
|
1857
|
+
? "exited"
|
|
1858
|
+
: observeProcess(pid, incarnation, value => processIncarnationForBroker(broker, value));
|
|
1859
|
+
const waitForExit = async (until: number): Promise<ProcessObservation> => {
|
|
1860
|
+
let observation = observe();
|
|
1861
|
+
while (observation !== "exited" && timing.now() < until) {
|
|
1862
|
+
await timing.sleep(Math.max(0, Math.min(POLL_MS, until - timing.now())));
|
|
1863
|
+
observation = observe();
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
return observation;
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
let observation = observe();
|
|
1870
|
+
if (observation === "alive") {
|
|
1871
|
+
await waitUntil(timing, terminationStartDeadlineAt);
|
|
1872
|
+
observation = observe();
|
|
1873
|
+
}
|
|
1874
|
+
if (observation === "alive") {
|
|
1875
|
+
if (!(await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGTERM", expected))) {
|
|
1876
|
+
observation = observe();
|
|
1877
|
+
if (observation !== "exited") {
|
|
1878
|
+
await recordTerminalUncertain(broker, id, root, pid);
|
|
1879
|
+
return false;
|
|
1880
|
+
}
|
|
1881
|
+
} else {
|
|
1882
|
+
const remaining = Math.max(0, deadline - timing.now());
|
|
1883
|
+
const gracefulDeadline = timing.now() + Math.min(CLOSE_TIMEOUT_MS, Math.floor(remaining / 2));
|
|
1884
|
+
observation = await waitForExit(gracefulDeadline);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
if (observation === "alive") {
|
|
1888
|
+
if (!(await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGKILL", expected))) {
|
|
1889
|
+
observation = observe();
|
|
1890
|
+
if (observation !== "exited") {
|
|
1891
|
+
await recordTerminalUncertain(broker, id, root, pid);
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
} else {
|
|
1895
|
+
observation = await waitForExit(deadline);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
if (observation !== "exited") {
|
|
1899
|
+
await recordTerminalUncertain(broker, id, root, pid);
|
|
1900
|
+
return false;
|
|
1901
|
+
}
|
|
1902
|
+
let rollbackGeneration: number | null | undefined;
|
|
1903
|
+
if (expected) {
|
|
1904
|
+
const failure = await readLifecycleFailureArtifact(
|
|
1905
|
+
lifecycleFailurePath(root, id, expected.effectMarker),
|
|
1906
|
+
expected,
|
|
1907
|
+
);
|
|
1908
|
+
if (
|
|
1909
|
+
!failure?.artifact.rollback.fenced ||
|
|
1910
|
+
!failure.artifact.rollback.runtimeRemoved ||
|
|
1911
|
+
!failure.artifact.rollback.hostStopped ||
|
|
1912
|
+
!failure.artifact.rollback.brokerRegistrationReleased
|
|
1913
|
+
) {
|
|
1914
|
+
await recordTerminalUncertain(broker, id, root, pid);
|
|
1915
|
+
return false;
|
|
1916
|
+
}
|
|
1917
|
+
rollbackGeneration = failure.artifact.rollback.endpointGeneration;
|
|
1918
|
+
}
|
|
1919
|
+
if (expected && !(await removeOwnedLifecycleArtifacts(root, id, expected))) {
|
|
1920
|
+
await recordTerminalUncertain(broker, id, root, pid);
|
|
1921
|
+
return false;
|
|
1922
|
+
}
|
|
1923
|
+
await broker.index.refresh();
|
|
1924
|
+
if (
|
|
1925
|
+
rollbackGeneration === null &&
|
|
1926
|
+
expected &&
|
|
1927
|
+
!broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker)
|
|
1928
|
+
)
|
|
1929
|
+
return true;
|
|
1930
|
+
const registeredBeforeTermination =
|
|
1931
|
+
rollbackGeneration === undefined || rollbackGeneration === null
|
|
1932
|
+
? undefined
|
|
1933
|
+
: broker.index.findHostRegistration(id, rollbackGeneration, pid, expected?.effectMarker);
|
|
1934
|
+
const unregistered = registeredBeforeTermination
|
|
1935
|
+
? broker.index.hostUnregisteredAfter(registeredBeforeTermination)
|
|
1936
|
+
: undefined;
|
|
1937
|
+
if (!registeredBeforeTermination || !unregistered) {
|
|
1938
|
+
await recordTerminalUncertain(broker, id, root, pid);
|
|
1939
|
+
return false;
|
|
1940
|
+
}
|
|
1941
|
+
return endpointRemoved(root, id);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
async function signalVerifiedSession(
|
|
1945
|
+
record: { locator: { stateRoot: string }; pid: number },
|
|
1946
|
+
id: string,
|
|
1947
|
+
signal: NodeJS.Signals,
|
|
1948
|
+
expected?: EffectMarker,
|
|
1949
|
+
): Promise<boolean> {
|
|
1950
|
+
if (!(await hasDurableProcessIdentity(record.locator.stateRoot, id, record.pid, expected))) return false;
|
|
1951
|
+
try {
|
|
1952
|
+
if (!(await hasDurableProcessIdentity(record.locator.stateRoot, id, record.pid, expected))) return false;
|
|
1953
|
+
process.kill(record.pid, signal);
|
|
1954
|
+
return true;
|
|
1955
|
+
} catch {
|
|
1956
|
+
return false;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
async function endpointRemoved(root: string, id: string): Promise<boolean> {
|
|
1961
|
+
try {
|
|
1962
|
+
await fs.access(path.join(root, "sdk", `${id}.json`));
|
|
1963
|
+
return false;
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
async function waitForClose(
|
|
1970
|
+
broker: Broker,
|
|
1971
|
+
id: string,
|
|
1972
|
+
record: { locator: { stateRoot: string }; endpointGeneration: number; pid: number; lifecycleRequestId?: string },
|
|
1973
|
+
timeoutMs: number,
|
|
1974
|
+
): Promise<boolean> {
|
|
1975
|
+
const timing = lifecycleTiming(broker);
|
|
1976
|
+
const deadline = timing.now() + timeoutMs;
|
|
1977
|
+
while (timing.now() < deadline) {
|
|
1978
|
+
await broker.index.refresh();
|
|
1979
|
+
const registration = broker.index.findHostRegistration(
|
|
1980
|
+
id,
|
|
1981
|
+
record.endpointGeneration,
|
|
1982
|
+
record.pid,
|
|
1983
|
+
record.lifecycleRequestId,
|
|
1984
|
+
);
|
|
1985
|
+
if (
|
|
1986
|
+
registration &&
|
|
1987
|
+
broker.index.hostUnregisteredAfter(registration) &&
|
|
1988
|
+
(await endpointRemoved(record.locator.stateRoot, id)) &&
|
|
1989
|
+
hasObservedProcessExit(record.pid)
|
|
1990
|
+
)
|
|
1991
|
+
return true;
|
|
1992
|
+
await timing.sleep(POLL_MS);
|
|
1993
|
+
}
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
async function currentReadyAuthority(
|
|
1998
|
+
broker: Broker,
|
|
1999
|
+
id: string,
|
|
2000
|
+
root: string,
|
|
2001
|
+
expected: EffectMarker,
|
|
2002
|
+
): Promise<ReadyAuthority | undefined> {
|
|
2003
|
+
if (!(await hasOwnedReadinessEvidence(broker, root, id, expected))) return undefined;
|
|
2004
|
+
const endpointPath = path.join(root, "sdk", `${id}.json`);
|
|
2005
|
+
try {
|
|
2006
|
+
const [endpointSource, endpointMetadata] = await Promise.all([
|
|
2007
|
+
fs.readFile(endpointPath, "utf8"),
|
|
2008
|
+
fs.stat(endpointPath),
|
|
2009
|
+
]);
|
|
2010
|
+
const endpoint = JSON.parse(endpointSource) as {
|
|
2011
|
+
sessionId?: unknown;
|
|
2012
|
+
url?: unknown;
|
|
2013
|
+
token?: unknown;
|
|
2014
|
+
pid?: unknown;
|
|
2015
|
+
};
|
|
2016
|
+
await broker.index.refresh();
|
|
2017
|
+
const record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
2018
|
+
if (
|
|
2019
|
+
!record?.live ||
|
|
2020
|
+
record.pid !== expected.pid ||
|
|
2021
|
+
resolveEquivalentPath(record.locator.stateRoot) !== resolveEquivalentPath(root) ||
|
|
2022
|
+
record.endpointMtimeMs !== endpointMetadata.mtimeMs ||
|
|
2023
|
+
endpoint.pid !== expected.pid ||
|
|
2024
|
+
endpoint.sessionId !== id ||
|
|
2025
|
+
typeof endpoint.url !== "string" ||
|
|
2026
|
+
typeof endpoint.token !== "string"
|
|
2027
|
+
)
|
|
2028
|
+
return undefined;
|
|
2029
|
+
return {
|
|
2030
|
+
endpoint: endpoint as Record<string, unknown>,
|
|
2031
|
+
endpointSource,
|
|
2032
|
+
endpointMtimeMs: endpointMetadata.mtimeMs,
|
|
2033
|
+
endpointGeneration: record.endpointGeneration,
|
|
2034
|
+
};
|
|
2035
|
+
} catch {
|
|
2036
|
+
return undefined;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
function sameReadyAuthority(left: ReadyAuthority, right: ReadyAuthority): boolean {
|
|
2041
|
+
return (
|
|
2042
|
+
left.endpointSource === right.endpointSource &&
|
|
2043
|
+
left.endpointMtimeMs === right.endpointMtimeMs &&
|
|
2044
|
+
left.endpointGeneration === right.endpointGeneration
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
async function waitForReady(
|
|
2049
|
+
broker: Broker,
|
|
2050
|
+
id: string,
|
|
2051
|
+
root: string,
|
|
2052
|
+
deadline: number,
|
|
2053
|
+
expected: EffectMarker,
|
|
2054
|
+
timing: LifecycleTiming,
|
|
2055
|
+
): Promise<ReadinessResult> {
|
|
2056
|
+
while (timing.now() < deadline) {
|
|
2057
|
+
const startupFailure = await readSessionLifecycleFailure(root, id, expected);
|
|
2058
|
+
if (startupFailure) return { kind: "startup_failed", message: startupFailure.message };
|
|
2059
|
+
if (
|
|
2060
|
+
observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
|
|
2061
|
+
"exited"
|
|
2062
|
+
) {
|
|
2063
|
+
const finalStartupFailure = await readSessionLifecycleFailure(root, id, expected);
|
|
2064
|
+
return finalStartupFailure
|
|
2065
|
+
? { kind: "startup_failed", message: finalStartupFailure.message }
|
|
2066
|
+
: { kind: "child_exited" };
|
|
2067
|
+
}
|
|
2068
|
+
try {
|
|
2069
|
+
const authority = await currentReadyAuthority(broker, id, root, expected);
|
|
2070
|
+
if (!authority) {
|
|
2071
|
+
const remaining = deadline - timing.now();
|
|
2072
|
+
if (remaining > 0) await timing.sleep(Math.min(POLL_MS, remaining));
|
|
2073
|
+
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
const connectionTimeoutMs = Math.min(2_000, deadline - timing.now());
|
|
2077
|
+
|
|
2078
|
+
if (connectionTimeoutMs <= 0) break;
|
|
2079
|
+
const endpoint = authority.endpoint as { url: string; token: string };
|
|
2080
|
+
const client = await SdkClient.connect(endpoint.url, endpoint.token, {
|
|
2081
|
+
timeoutMs: connectionTimeoutMs,
|
|
2082
|
+
deadline,
|
|
2083
|
+
reconnectAttempts: 0,
|
|
2084
|
+
});
|
|
2085
|
+
try {
|
|
2086
|
+
const requestTimeoutMs = Math.min(2_000, deadline - timing.now());
|
|
2087
|
+
|
|
2088
|
+
if (requestTimeoutMs <= 0) break;
|
|
2089
|
+
const replay = await client.request(
|
|
2090
|
+
{
|
|
2091
|
+
type: "event_replay",
|
|
2092
|
+
sinceGeneration: authority.endpointGeneration,
|
|
2093
|
+
sinceSeq: 0,
|
|
2094
|
+
},
|
|
2095
|
+
{ timeoutMs: requestTimeoutMs },
|
|
2096
|
+
);
|
|
2097
|
+
const events = (replay.events as unknown[]) ?? [];
|
|
2098
|
+
if (
|
|
2099
|
+
events.some(event => {
|
|
2100
|
+
const frame = event as Record<string, unknown>;
|
|
2101
|
+
return (
|
|
2102
|
+
frame.type === "event" &&
|
|
2103
|
+
frame.name === "session_ready" &&
|
|
2104
|
+
frame.sessionId === id &&
|
|
2105
|
+
frame.generation === authority.endpointGeneration
|
|
2106
|
+
);
|
|
2107
|
+
})
|
|
2108
|
+
) {
|
|
2109
|
+
const current = await currentReadyAuthority(broker, id, root, expected);
|
|
2110
|
+
if (current && sameReadyAuthority(authority, current)) return { kind: "ready", authority: current };
|
|
2111
|
+
}
|
|
2112
|
+
} finally {
|
|
2113
|
+
await client.close();
|
|
2114
|
+
}
|
|
2115
|
+
} catch {
|
|
2116
|
+
// A partially initialized or unauthenticated endpoint is not ready yet.
|
|
2117
|
+
}
|
|
2118
|
+
const remaining = deadline - timing.now();
|
|
2119
|
+
if (remaining > 0) await timing.sleep(Math.min(POLL_MS, remaining));
|
|
2120
|
+
}
|
|
2121
|
+
return { kind: "timeout" };
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
function worktreeIntent(plan: SkcLaunchWorktreePlan | undefined): LifecycleWorktreeIntent | undefined {
|
|
2125
|
+
if (!plan) return undefined;
|
|
2126
|
+
return {
|
|
2127
|
+
repoRoot: path.resolve(plan.repoRoot),
|
|
2128
|
+
worktreePath: path.resolve(plan.worktreePath),
|
|
2129
|
+
detached: plan.detached,
|
|
2130
|
+
baseRef: plan.baseRef,
|
|
2131
|
+
...(plan.branchName ? { branchName: plan.branchName } : {}),
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function preparePlannedWorktree(plan: SkcLaunchWorktreePlan): SessionLifecycleWorktreeReceipt {
|
|
2136
|
+
const prepared = ensureLaunchWorktree(plan);
|
|
2137
|
+
if (!prepared.enabled || path.resolve(prepared.worktreePath) !== path.resolve(plan.worktreePath))
|
|
2138
|
+
throw new Error("Lifecycle worktree preparation did not preserve the durable worktree identity.");
|
|
2139
|
+
return {
|
|
2140
|
+
enabled: true,
|
|
2141
|
+
cwd: path.resolve(prepared.worktreePath),
|
|
2142
|
+
created: prepared.created,
|
|
2143
|
+
reused: prepared.reused,
|
|
2144
|
+
...(prepared.branchName ? { branch: prepared.branchName } : {}),
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
async function launchInput(
|
|
2148
|
+
broker: Broker,
|
|
2149
|
+
operation: "session.create" | "session.fork" | "session.resume",
|
|
2150
|
+
input: Input,
|
|
2151
|
+
): Promise<SessionLaunch | BrokerResponse> {
|
|
2152
|
+
const requestedCwd = lifecycleCwd(input);
|
|
2153
|
+
if (!requestedCwd) return fail("invalid_input", "A target path is required.");
|
|
2154
|
+
const sourceCwd = requestedCwd;
|
|
2155
|
+
const suppliedRoot = stateRoot(input, requestedCwd);
|
|
2156
|
+
if (!suppliedRoot || !hasDefaultStateRoot(requestedCwd, suppliedRoot))
|
|
2157
|
+
return fail("invalid_input", "stateRoot must be the default .skc/state for cwd.");
|
|
2158
|
+
|
|
2159
|
+
try {
|
|
2160
|
+
if (!(await fs.stat(sourceCwd)).isDirectory())
|
|
2161
|
+
return fail("invalid_input", "Lifecycle worktree must be a directory.");
|
|
2162
|
+
} catch {
|
|
2163
|
+
return fail("invalid_input", "Lifecycle worktree does not exist.");
|
|
2164
|
+
}
|
|
2165
|
+
const worktree = lifecycleWorktreeTarget(input);
|
|
2166
|
+
if (worktree === null || (worktree !== undefined && requestedCwd === undefined))
|
|
2167
|
+
return fail("invalid_input", "Lifecycle worktree target is invalid.");
|
|
2168
|
+
let cwd = sourceCwd;
|
|
2169
|
+
let worktreePlan: SkcLaunchWorktreePlan | undefined;
|
|
2170
|
+
if (worktree) {
|
|
2171
|
+
try {
|
|
2172
|
+
const planned = planLaunchWorktree(
|
|
2173
|
+
sourceCwd,
|
|
2174
|
+
worktree.name
|
|
2175
|
+
? { enabled: true, detached: false, name: worktree.name }
|
|
2176
|
+
: { enabled: true, detached: true, name: null },
|
|
2177
|
+
);
|
|
2178
|
+
if (!planned.enabled) return fail("invalid_input", "Lifecycle worktree target is invalid.");
|
|
2179
|
+
worktreePlan = planned;
|
|
2180
|
+
cwd = path.resolve(planned.worktreePath);
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
return fail(
|
|
2183
|
+
"invalid_input",
|
|
2184
|
+
`Unable to plan lifecycle worktree: ${error instanceof Error ? error.message : String(error)}`,
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
const resolvedRoot = defaultStateRoot(cwd);
|
|
2189
|
+
|
|
2190
|
+
const requested = sessionId(input);
|
|
2191
|
+
if (requested !== undefined && !isCanonicalSessionId(requested))
|
|
2192
|
+
return fail("invalid_input", "sessionId must be a canonical safe identifier.");
|
|
2193
|
+
const modelPreset = text(input.modelPreset);
|
|
2194
|
+
|
|
2195
|
+
if (operation === "session.create")
|
|
2196
|
+
return { id: randomUUID(), cwd, root: resolvedRoot, modelPreset, worktree, worktreePlan };
|
|
2197
|
+
if (operation === "session.resume") {
|
|
2198
|
+
if (!requested) return fail("invalid_input", "sessionId is required to resume a saved session.");
|
|
2199
|
+
const savedPath = text(input.sessionPath);
|
|
2200
|
+
if (!savedPath) return fail("invalid_input", "sessionPath is required to resume a saved session.");
|
|
2201
|
+
const saved = await validateSavedTranscript(broker, cwd, savedPath, requested, "Saved");
|
|
2202
|
+
if ("ok" in saved) return saved;
|
|
2203
|
+
return {
|
|
2204
|
+
id: requested,
|
|
2205
|
+
cwd,
|
|
2206
|
+
root: resolvedRoot,
|
|
2207
|
+
sessionPath: saved.path,
|
|
2208
|
+
sessionIdentity: saved.identity,
|
|
2209
|
+
modelPreset,
|
|
2210
|
+
worktree,
|
|
2211
|
+
worktreePlan,
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
const sourceSessionId = text(input.sourceSessionId) ?? text(input.sourceId);
|
|
2215
|
+
if (sourceSessionId !== undefined && !isCanonicalSessionId(sourceSessionId))
|
|
2216
|
+
return fail("invalid_input", "sourceSessionId must be a canonical safe identifier.");
|
|
2217
|
+
const sourceSessionPath = text(input.sourceSessionPath) ?? text(input.sourcePath) ?? text(input.sessionPath);
|
|
2218
|
+
if (!sourceSessionId && !sourceSessionPath)
|
|
2219
|
+
return fail("invalid_input", "sourceSessionId or sourceSessionPath is required to fork a session.");
|
|
2220
|
+
const source = await validateSavedTranscript(broker, sourceCwd, sourceSessionPath, sourceSessionId, "Source");
|
|
2221
|
+
if ("ok" in source) return source;
|
|
2222
|
+
return {
|
|
2223
|
+
id: randomUUID(),
|
|
2224
|
+
cwd,
|
|
2225
|
+
root: resolvedRoot,
|
|
2226
|
+
sourceSessionId: source.id,
|
|
2227
|
+
sourceSessionPath: source.path,
|
|
2228
|
+
sourceSessionIdentity: source.identity,
|
|
2229
|
+
sourceCwd,
|
|
2230
|
+
modelPreset,
|
|
2231
|
+
worktree,
|
|
2232
|
+
worktreePlan,
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
type ValidatedDelete = {
|
|
2237
|
+
storage: FileSessionStorage;
|
|
2238
|
+
target: VerifiedSessionDeleteTarget;
|
|
2239
|
+
metadataRoot: string;
|
|
2240
|
+
};
|
|
2241
|
+
function cleanupIdentity(
|
|
2242
|
+
identity: BrokerCleanupEvidence["transcriptIdentity"],
|
|
2243
|
+
allowEmptySha256 = false,
|
|
2244
|
+
): SessionStorageFileIdentity | undefined {
|
|
2245
|
+
if (
|
|
2246
|
+
!identity ||
|
|
2247
|
+
!/^[0-9]+$/.test(identity.dev) ||
|
|
2248
|
+
!/^[0-9]+$/.test(identity.ino) ||
|
|
2249
|
+
!Number.isSafeInteger(identity.size) ||
|
|
2250
|
+
identity.size < 0 ||
|
|
2251
|
+
!/^[0-9]+$/.test(identity.mtimeNs) ||
|
|
2252
|
+
(!allowEmptySha256 && !/^[a-f0-9]{64}$/.test(identity.sha256)) ||
|
|
2253
|
+
(allowEmptySha256 && identity.sha256 !== "" && !/^[a-f0-9]{64}$/.test(identity.sha256))
|
|
2254
|
+
)
|
|
2255
|
+
return undefined;
|
|
2256
|
+
return {
|
|
2257
|
+
dev: BigInt(identity.dev),
|
|
2258
|
+
ino: BigInt(identity.ino),
|
|
2259
|
+
size: identity.size,
|
|
2260
|
+
mtimeNs: BigInt(identity.mtimeNs),
|
|
2261
|
+
sha256: identity.sha256,
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerResponse {
|
|
2266
|
+
const transcriptIdentity = cleanupIdentity(cleanup.transcriptIdentity);
|
|
2267
|
+
if (
|
|
2268
|
+
(cleanup.phase !== "artifacts" && cleanup.phase !== "transcript") ||
|
|
2269
|
+
!cleanup.sessionId ||
|
|
2270
|
+
!isCanonicalSessionId(cleanup.sessionId) ||
|
|
2271
|
+
!cleanup.sessionsRoot ||
|
|
2272
|
+
!cleanup.transcriptPath ||
|
|
2273
|
+
!cleanup.cwd ||
|
|
2274
|
+
!cleanup.metadataRoot ||
|
|
2275
|
+
!transcriptIdentity
|
|
2276
|
+
) {
|
|
2277
|
+
return fail("terminal_uncertain", "Cleanup replay lacks a complete ledger-bound deletion target.");
|
|
2278
|
+
}
|
|
2279
|
+
const artifactsIdentity = cleanupIdentity(cleanup.artifactsIdentity, true);
|
|
2280
|
+
const artifactTreeIdentity =
|
|
2281
|
+
cleanup.artifactsRemoved === true || !cleanup.artifactTree
|
|
2282
|
+
? undefined
|
|
2283
|
+
: cleanupIdentity(cleanup.artifactTree.identity, true);
|
|
2284
|
+
if (cleanup.artifactsRemoved !== true && cleanup.artifactTree && !artifactTreeIdentity)
|
|
2285
|
+
return fail("terminal_uncertain", "Artifact tree cleanup lacks its ledger-bound identity.");
|
|
2286
|
+
|
|
2287
|
+
if (cleanup.detachedArtifactsPath && !artifactsIdentity)
|
|
2288
|
+
return fail("terminal_uncertain", "Detached artifact cleanup lacks its ledger-bound identity.");
|
|
2289
|
+
const plannedArtifactsPath = cleanup.plannedArtifactsPath;
|
|
2290
|
+
const plannedTranscriptPath = cleanup.plannedTranscriptPath;
|
|
2291
|
+
if (
|
|
2292
|
+
(plannedArtifactsPath &&
|
|
2293
|
+
(path.dirname(plannedArtifactsPath) !== path.dirname(cleanup.transcriptPath) ||
|
|
2294
|
+
!path.basename(plannedArtifactsPath).startsWith(".skc-delete-"))) ||
|
|
2295
|
+
(plannedTranscriptPath &&
|
|
2296
|
+
(path.dirname(plannedTranscriptPath) !== path.dirname(cleanup.transcriptPath) ||
|
|
2297
|
+
!path.basename(plannedTranscriptPath).startsWith(".skc-delete-"))) ||
|
|
2298
|
+
(cleanup.artifactsRemoved !== true &&
|
|
2299
|
+
cleanup.artifactTree &&
|
|
2300
|
+
(path.dirname(cleanup.artifactTree.plannedPath) !== path.dirname(cleanup.transcriptPath) ||
|
|
2301
|
+
!path.basename(cleanup.artifactTree.plannedPath).startsWith(".skc-delete-") ||
|
|
2302
|
+
(cleanup.artifactTree.detachedPath !== undefined &&
|
|
2303
|
+
path.dirname(cleanup.artifactTree.detachedPath) !== path.dirname(cleanup.transcriptPath))))
|
|
2304
|
+
)
|
|
2305
|
+
return fail("terminal_uncertain", "Cleanup replay has invalid preauthorized quarantine paths.");
|
|
2306
|
+
if (
|
|
2307
|
+
cleanup.phase === "transcript" &&
|
|
2308
|
+
cleanup.artifactsRemoved === true &&
|
|
2309
|
+
![cleanup.transcriptPath, cleanup.detachedTranscriptPath, plannedTranscriptPath].some(
|
|
2310
|
+
candidate => typeof candidate === "string" && fsSync.existsSync(candidate),
|
|
2311
|
+
)
|
|
2312
|
+
)
|
|
2313
|
+
return { ok: true, result: { sessionId: cleanup.sessionId } };
|
|
2314
|
+
const artifactRemovingPath =
|
|
2315
|
+
cleanup.artifactsRemoved === true
|
|
2316
|
+
? undefined
|
|
2317
|
+
: cleanup.artifactTree
|
|
2318
|
+
? `${cleanup.artifactTree.plannedPath}.removing`
|
|
2319
|
+
: plannedArtifactsPath
|
|
2320
|
+
? `${plannedArtifactsPath}.removing`
|
|
2321
|
+
: undefined;
|
|
2322
|
+
const recoveredDetachedArtifactsPath =
|
|
2323
|
+
cleanup.detachedArtifactsPath && fsSync.existsSync(cleanup.detachedArtifactsPath)
|
|
2324
|
+
? cleanup.detachedArtifactsPath
|
|
2325
|
+
: artifactRemovingPath && fsSync.existsSync(artifactRemovingPath)
|
|
2326
|
+
? artifactRemovingPath
|
|
2327
|
+
: plannedArtifactsPath &&
|
|
2328
|
+
!fsSync.existsSync(cleanup.transcriptPath.slice(0, -6)) &&
|
|
2329
|
+
fsSync.existsSync(plannedArtifactsPath)
|
|
2330
|
+
? plannedArtifactsPath
|
|
2331
|
+
: undefined;
|
|
2332
|
+
const recoveredDetachedTranscriptPath =
|
|
2333
|
+
cleanup.detachedTranscriptPath && fsSync.existsSync(cleanup.detachedTranscriptPath)
|
|
2334
|
+
? cleanup.detachedTranscriptPath
|
|
2335
|
+
: plannedTranscriptPath &&
|
|
2336
|
+
!fsSync.existsSync(cleanup.transcriptPath) &&
|
|
2337
|
+
fsSync.existsSync(plannedTranscriptPath)
|
|
2338
|
+
? plannedTranscriptPath
|
|
2339
|
+
: undefined;
|
|
2340
|
+
return {
|
|
2341
|
+
storage: new FileSessionStorage(),
|
|
2342
|
+
target: {
|
|
2343
|
+
sessionsRoot: cleanup.sessionsRoot,
|
|
2344
|
+
transcriptPath: cleanup.transcriptPath,
|
|
2345
|
+
sessionId: cleanup.sessionId,
|
|
2346
|
+
cwd: cleanup.cwd,
|
|
2347
|
+
transcriptIdentity,
|
|
2348
|
+
...(cleanup.artifactsRemoved === true ? { artifactsRemoved: true } : {}),
|
|
2349
|
+
...(cleanup.artifactsRemoved !== true && artifactsIdentity
|
|
2350
|
+
? { expectedArtifactsIdentity: artifactsIdentity }
|
|
2351
|
+
: {}),
|
|
2352
|
+
...(cleanup.artifactsRemoved !== true && recoveredDetachedArtifactsPath
|
|
2353
|
+
? { detachedArtifactsPath: recoveredDetachedArtifactsPath }
|
|
2354
|
+
: {}),
|
|
2355
|
+
...(recoveredDetachedTranscriptPath ? { detachedTranscriptPath: recoveredDetachedTranscriptPath } : {}),
|
|
2356
|
+
...(plannedArtifactsPath ? { plannedArtifactsPath } : {}),
|
|
2357
|
+
...(plannedTranscriptPath ? { plannedTranscriptPath } : {}),
|
|
2358
|
+
...(cleanup.artifactsRemoved !== true && cleanup.artifactTree && artifactTreeIdentity
|
|
2359
|
+
? {
|
|
2360
|
+
expectedArtifactsIdentity: artifactTreeIdentity,
|
|
2361
|
+
expectedArtifactsTree: cleanup.artifactTree.snapshot,
|
|
2362
|
+
detachedArtifactsPath: cleanup.artifactTree.detachedPath ?? recoveredDetachedArtifactsPath,
|
|
2363
|
+
plannedArtifactsPath: cleanup.artifactTree.plannedPath,
|
|
2364
|
+
}
|
|
2365
|
+
: {}),
|
|
2366
|
+
},
|
|
2367
|
+
metadataRoot: cleanup.metadataRoot,
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
async function validateDeletePath(
|
|
2372
|
+
broker: Broker,
|
|
2373
|
+
input: Input,
|
|
2374
|
+
id: string,
|
|
2375
|
+
record: { locator: { repo: string; stateRoot: string } } | undefined,
|
|
2376
|
+
cleanup?: CleanupEvidence,
|
|
2377
|
+
): Promise<ValidatedDelete | BrokerResponse> {
|
|
2378
|
+
if (cleanup) return replayDeleteTarget(cleanup);
|
|
2379
|
+
const sessionPath = text(input.sessionPath);
|
|
2380
|
+
const cwd = lifecycleCwd(input);
|
|
2381
|
+
if (!sessionPath || !cwd)
|
|
2382
|
+
return fail("invalid_input", "session.delete requires sessionPath and its configured cwd.");
|
|
2383
|
+
const requestedRoot = stateRoot(input, cwd);
|
|
2384
|
+
if (!requestedRoot || !hasDefaultStateRoot(cwd, requestedRoot))
|
|
2385
|
+
return fail("invalid_input", "stateRoot must be the default .skc/state for cwd.");
|
|
2386
|
+
if (
|
|
2387
|
+
record &&
|
|
2388
|
+
(path.resolve(record.locator.repo) !== cwd || path.resolve(record.locator.stateRoot) !== requestedRoot)
|
|
2389
|
+
)
|
|
2390
|
+
return fail("invalid_input", "session.delete locator does not match the indexed session.");
|
|
2391
|
+
|
|
2392
|
+
const inventory = await managedCandidates(broker, cwd, "Saved");
|
|
2393
|
+
if ("ok" in inventory) return inventory;
|
|
2394
|
+
const candidatePath = path.resolve(sessionPath);
|
|
2395
|
+
const matches = inventory.candidates.filter(
|
|
2396
|
+
candidate => candidate.path === candidatePath && candidate.sessionId === id,
|
|
2397
|
+
);
|
|
2398
|
+
if (matches.length !== 1)
|
|
2399
|
+
return fail("invalid_input", "session.delete path is not an owned managed session for the configured cwd.");
|
|
2400
|
+
const match = matches[0]!;
|
|
2401
|
+
if (inventory.migrationPolicy === "disabled" && match.provenance === "legacy")
|
|
2402
|
+
return fail("legacy_migration_disabled", "Saved legacy session migration is disabled for this workspace.");
|
|
2403
|
+
|
|
2404
|
+
const storage = new FileSessionStorage();
|
|
2405
|
+
let snapshot: SessionStorageSnapshot;
|
|
2406
|
+
try {
|
|
2407
|
+
snapshot = storage.readSnapshotSync(candidatePath);
|
|
2408
|
+
} catch {
|
|
2409
|
+
return fail("not_found", "Requested saved session does not exist or cannot be read.");
|
|
2410
|
+
}
|
|
2411
|
+
const digest = createHash("sha256").update(snapshot.bytes).digest("hex");
|
|
2412
|
+
if (
|
|
2413
|
+
snapshot.stat.dev !== match.identity.dev ||
|
|
2414
|
+
snapshot.stat.ino !== match.identity.ino ||
|
|
2415
|
+
snapshot.stat.size !== match.identity.size ||
|
|
2416
|
+
snapshot.stat.mtimeNs !== match.identity.mtimeNs ||
|
|
2417
|
+
digest !== match.identity.sha256
|
|
2418
|
+
)
|
|
2419
|
+
return fail("invalid_input", "session.delete session changed after managed ownership was verified.");
|
|
2420
|
+
return {
|
|
2421
|
+
storage,
|
|
2422
|
+
target: {
|
|
2423
|
+
sessionsRoot: inventory.scope.sessionsRoot,
|
|
2424
|
+
transcriptPath: candidatePath,
|
|
2425
|
+
sessionId: id,
|
|
2426
|
+
cwd,
|
|
2427
|
+
transcriptIdentity: {
|
|
2428
|
+
dev: snapshot.stat.dev,
|
|
2429
|
+
ino: snapshot.stat.ino,
|
|
2430
|
+
size: snapshot.stat.size,
|
|
2431
|
+
mtimeNs: snapshot.stat.mtimeNs,
|
|
2432
|
+
sha256: digest,
|
|
2433
|
+
},
|
|
2434
|
+
},
|
|
2435
|
+
metadataRoot: requestedRoot,
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
type CloseAuthority = { endpointGeneration: number; endpointIncarnation: string };
|
|
2439
|
+
type CloseRecord = {
|
|
2440
|
+
locator: { repo: string; stateRoot: string };
|
|
2441
|
+
endpointGeneration: number;
|
|
2442
|
+
pid: number;
|
|
2443
|
+
endpointMtimeMs?: number;
|
|
2444
|
+
lifecycleRequestId?: string;
|
|
2445
|
+
};
|
|
2446
|
+
|
|
2447
|
+
function endpointIncarnation(record: CloseRecord, sessionId: string): string | undefined {
|
|
2448
|
+
if (
|
|
2449
|
+
!Number.isSafeInteger(record.endpointGeneration) ||
|
|
2450
|
+
record.endpointGeneration <= 0 ||
|
|
2451
|
+
!Number.isSafeInteger(record.pid) ||
|
|
2452
|
+
record.pid <= 0 ||
|
|
2453
|
+
typeof record.endpointMtimeMs !== "number" ||
|
|
2454
|
+
!Number.isFinite(record.endpointMtimeMs) ||
|
|
2455
|
+
record.endpointMtimeMs <= 0
|
|
2456
|
+
)
|
|
2457
|
+
return undefined;
|
|
2458
|
+
return createHash("sha256")
|
|
2459
|
+
.update(
|
|
2460
|
+
JSON.stringify({
|
|
2461
|
+
endpointGeneration: record.endpointGeneration,
|
|
2462
|
+
endpointMtimeMs: record.endpointMtimeMs,
|
|
2463
|
+
pid: record.pid,
|
|
2464
|
+
sessionId,
|
|
2465
|
+
}),
|
|
2466
|
+
)
|
|
2467
|
+
.digest("hex");
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
function requestedCloseAuthority(input: Input): { authority: CloseAuthority | undefined } | { error: BrokerResponse } {
|
|
2471
|
+
const endpointGeneration = input.endpointGeneration;
|
|
2472
|
+
const endpointIncarnation = input.endpointIncarnation;
|
|
2473
|
+
if (endpointGeneration === undefined && endpointIncarnation === undefined) return { authority: undefined };
|
|
2474
|
+
if (
|
|
2475
|
+
typeof endpointGeneration !== "number" ||
|
|
2476
|
+
!Number.isSafeInteger(endpointGeneration) ||
|
|
2477
|
+
endpointGeneration <= 0 ||
|
|
2478
|
+
typeof endpointIncarnation !== "string" ||
|
|
2479
|
+
!/^[a-f0-9]{64}$/.test(endpointIncarnation)
|
|
2480
|
+
)
|
|
2481
|
+
return {
|
|
2482
|
+
error: fail("invalid_input", "session.close endpoint authority is invalid"),
|
|
2483
|
+
};
|
|
2484
|
+
return { authority: { endpointGeneration, endpointIncarnation } };
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
function sameCloseAuthority(authority: CloseAuthority, record: CloseRecord, sessionId: string): boolean {
|
|
2488
|
+
return (
|
|
2489
|
+
authority.endpointGeneration === record.endpointGeneration &&
|
|
2490
|
+
authority.endpointIncarnation === endpointIncarnation(record, sessionId)
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function sameCloseProcessIdentity(expected: CloseRecord, current: CloseRecord & { live: boolean }): boolean {
|
|
2495
|
+
return (
|
|
2496
|
+
current.live &&
|
|
2497
|
+
current.pid === expected.pid &&
|
|
2498
|
+
typeof expected.lifecycleRequestId === "string" &&
|
|
2499
|
+
expected.lifecycleRequestId.length > 0 &&
|
|
2500
|
+
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
2501
|
+
path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
|
|
2502
|
+
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
function sameCloseGeneration(expected: CloseRecord, current: CloseRecord & { live: boolean }): boolean {
|
|
2507
|
+
return (
|
|
2508
|
+
current.live &&
|
|
2509
|
+
current.endpointGeneration === expected.endpointGeneration &&
|
|
2510
|
+
current.pid === expected.pid &&
|
|
2511
|
+
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
2512
|
+
path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
|
|
2513
|
+
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
async function revalidateCloseGeneration(
|
|
2518
|
+
broker: Broker,
|
|
2519
|
+
id: string,
|
|
2520
|
+
expected: CloseRecord,
|
|
2521
|
+
authority: CloseAuthority | undefined,
|
|
2522
|
+
): Promise<BrokerResponse | undefined> {
|
|
2523
|
+
await broker.index.refresh();
|
|
2524
|
+
const current = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
2525
|
+
return current &&
|
|
2526
|
+
sameCloseGeneration(expected, current) &&
|
|
2527
|
+
(!authority || sameCloseAuthority(authority, current, id))
|
|
2528
|
+
? undefined
|
|
2529
|
+
: fail("endpoint_stale", "session endpoint is stale");
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
function isTransportFailure(error: unknown): error is SdkClientError {
|
|
2533
|
+
return (
|
|
2534
|
+
error instanceof SdkClientError &&
|
|
2535
|
+
["unavailable", "timeout", "connection_closed", "reconnect_exhausted"].includes(error.code)
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function closeEndpoint(endpoint: unknown): { url: string; token: string } | undefined {
|
|
2540
|
+
if (typeof endpoint !== "object" || endpoint === null) return undefined;
|
|
2541
|
+
const value = endpoint as { url?: unknown; token?: unknown };
|
|
2542
|
+
return typeof value.url === "string" && typeof value.token === "string"
|
|
2543
|
+
? { url: value.url, token: value.token }
|
|
2544
|
+
: undefined;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
/** Executes broker-owned global lifecycle effects. */
|
|
2548
|
+
async function executeLifecycleResponse(
|
|
2549
|
+
broker: Broker,
|
|
2550
|
+
operation: string,
|
|
2551
|
+
input: Input,
|
|
2552
|
+
identity: string,
|
|
2553
|
+
cleanup?: CleanupEvidence,
|
|
2554
|
+
): Promise<BrokerResponse> {
|
|
2555
|
+
const requestedSessionId = cleanup && operation === "session.delete" ? cleanup.sessionId : sessionId(input);
|
|
2556
|
+
if (requestedSessionId !== undefined && !isCanonicalSessionId(requestedSessionId))
|
|
2557
|
+
return fail("invalid_input", "sessionId must be a canonical safe identifier.");
|
|
2558
|
+
const requestedSourceSessionId = text(input.sourceSessionId) ?? text(input.sourceId);
|
|
2559
|
+
if (requestedSourceSessionId !== undefined && !isCanonicalSessionId(requestedSourceSessionId))
|
|
2560
|
+
return fail("invalid_input", "sourceSessionId must be a canonical safe identifier.");
|
|
2561
|
+
if (operation === "session.create" || operation === "session.fork" || operation === "session.resume") {
|
|
2562
|
+
await broker.index.refresh();
|
|
2563
|
+
if (operation === "session.resume") {
|
|
2564
|
+
const requestedSessionId = sessionId(input);
|
|
2565
|
+
const existing = requestedSessionId
|
|
2566
|
+
? broker.index.listSessions().sessions.find(session => session.sessionId === requestedSessionId)
|
|
2567
|
+
: undefined;
|
|
2568
|
+
if (existing?.live) {
|
|
2569
|
+
const initialScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
|
|
2570
|
+
if ("ok" in initialScope) return initialScope;
|
|
2571
|
+
const initialIncarnation = endpointIncarnation(existing, requestedSessionId!);
|
|
2572
|
+
if (!initialIncarnation)
|
|
2573
|
+
return fail("live_session", "Session is already live but its endpoint incarnation is unavailable.");
|
|
2574
|
+
const endpoint = await broker.handleRequest("session.get_endpoint", {
|
|
2575
|
+
sessionId: requestedSessionId,
|
|
2576
|
+
endpointGeneration: existing.endpointGeneration,
|
|
2577
|
+
endpointIncarnation: initialIncarnation,
|
|
2578
|
+
});
|
|
2579
|
+
if (!endpoint.ok)
|
|
2580
|
+
return fail(
|
|
2581
|
+
"live_session",
|
|
2582
|
+
"Session is already live but its incarnation-bound endpoint is unavailable.",
|
|
2583
|
+
);
|
|
2584
|
+
await broker.index.refresh();
|
|
2585
|
+
const current = broker.index
|
|
2586
|
+
.listSessions()
|
|
2587
|
+
.sessions.find(session => session.sessionId === requestedSessionId);
|
|
2588
|
+
if (!current || !sameLiveResumeRecord(existing, current))
|
|
2589
|
+
return fail("endpoint_stale", "Live session changed while its resume authority was being verified.");
|
|
2590
|
+
const finalScope = await validateLiveResumeScope(broker, input, requestedSessionId!, current);
|
|
2591
|
+
if ("ok" in finalScope) return finalScope;
|
|
2592
|
+
if (!sameResumeSessionIdentity(initialScope, finalScope))
|
|
2593
|
+
return fail("endpoint_stale", "Saved session changed while its resume authority was being verified.");
|
|
2594
|
+
return {
|
|
2595
|
+
ok: true,
|
|
2596
|
+
result: {
|
|
2597
|
+
sessionId: requestedSessionId,
|
|
2598
|
+
cwd: finalScope.cwd,
|
|
2599
|
+
endpointGeneration: current.endpointGeneration,
|
|
2600
|
+
endpoint: endpoint.result,
|
|
2601
|
+
reused: true,
|
|
2602
|
+
},
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
const timing = lifecycleTiming(broker);
|
|
2607
|
+
|
|
2608
|
+
const deadlines = lifecycleDeadlines(input, timing.now());
|
|
2609
|
+
|
|
2610
|
+
if ("ok" in deadlines) return deadlines;
|
|
2611
|
+
const lifecycleDeadline = deadlines.lifecycleCleanupDeadlineAt;
|
|
2612
|
+
const readinessDeadline = deadlines.semanticReadyDeadlineAt;
|
|
2613
|
+
const terminationStartDeadline = deadlines.terminationStartDeadlineAt;
|
|
2614
|
+
|
|
2615
|
+
const launch = await launchInput(broker, operation, input);
|
|
2616
|
+
if ("ok" in launch) return launch;
|
|
2617
|
+
if (!hasProcessIncarnationAuthority())
|
|
2618
|
+
return fail(
|
|
2619
|
+
"incarnation_unavailable",
|
|
2620
|
+
"OS process incarnation authority is unavailable; refusing to spawn a lifecycle session.",
|
|
2621
|
+
);
|
|
2622
|
+
const effectMarker = randomUUID();
|
|
2623
|
+
const plannedWorktreeIntent = worktreeIntent(launch.worktreePlan);
|
|
2624
|
+
const effectIntent: LifecycleEffectIntent = {
|
|
2625
|
+
sessionId: launch.id,
|
|
2626
|
+
stateRoot: launch.root,
|
|
2627
|
+
childOwnershipEstablished: false,
|
|
2628
|
+
...(plannedWorktreeIntent ? { worktree: plannedWorktreeIntent } : {}),
|
|
2629
|
+
};
|
|
2630
|
+
|
|
2631
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
2632
|
+
intendedSessionId: launch.id,
|
|
2633
|
+
effectMarker,
|
|
2634
|
+
effectIntent,
|
|
2635
|
+
});
|
|
2636
|
+
if (!hasProcessIncarnationAuthority())
|
|
2637
|
+
return fail(
|
|
2638
|
+
"incarnation_unavailable",
|
|
2639
|
+
"OS process incarnation authority is unavailable; refusing to prepare a lifecycle worktree.",
|
|
2640
|
+
);
|
|
2641
|
+
let worktreeReceipt: SessionLifecycleWorktreeReceipt | undefined;
|
|
2642
|
+
try {
|
|
2643
|
+
if (launch.worktreePlan) {
|
|
2644
|
+
worktreeReceipt = preparePlannedWorktree(launch.worktreePlan);
|
|
2645
|
+
const worktree = {
|
|
2646
|
+
cwdDigest: createHash("sha256").update(worktreeReceipt.cwd, "utf8").digest("hex"),
|
|
2647
|
+
created: worktreeReceipt.created,
|
|
2648
|
+
reused: worktreeReceipt.reused,
|
|
2649
|
+
...(worktreeReceipt.branch
|
|
2650
|
+
? { branchDigest: createHash("sha256").update(worktreeReceipt.branch, "utf8").digest("hex") }
|
|
2651
|
+
: {}),
|
|
2652
|
+
};
|
|
2653
|
+
const durableEffects: LifecycleDurableEffectsReceipt = {
|
|
2654
|
+
worktree,
|
|
2655
|
+
digest: createHash("sha256").update(canonicalJson({ worktree })).digest("hex"),
|
|
2656
|
+
};
|
|
2657
|
+
await broker.ledger.transition(identity, "effect_started", { durableEffects });
|
|
2658
|
+
ensureReusableNodeModules(launch.worktreePlan.repoRoot, launch.worktreePlan.worktreePath);
|
|
2659
|
+
}
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
return fail(
|
|
2662
|
+
"spawn_failed",
|
|
2663
|
+
`Unable to prepare lifecycle worktree: ${error instanceof Error ? error.message : String(error)}`,
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
if (timing.now() >= readinessDeadline)
|
|
2667
|
+
return fail(
|
|
2668
|
+
"readiness_timeout",
|
|
2669
|
+
"Lifecycle preparation exhausted the semantic readiness deadline before spawning.",
|
|
2670
|
+
);
|
|
2671
|
+
if (!hasProcessIncarnationAuthority())
|
|
2672
|
+
return fail(
|
|
2673
|
+
"incarnation_unavailable",
|
|
2674
|
+
"OS process incarnation authority is unavailable; refusing to spawn a lifecycle session.",
|
|
2675
|
+
);
|
|
2676
|
+
|
|
2677
|
+
const request: SessionLifecycleLaunchRequest = {
|
|
2678
|
+
operation,
|
|
2679
|
+
sessionId: launch.id,
|
|
2680
|
+
cwd: launch.cwd,
|
|
2681
|
+
stateRoot: launch.root,
|
|
2682
|
+
effectMarker,
|
|
2683
|
+
receivedAt: deadlines.receivedAt,
|
|
2684
|
+
requestedReadinessTimeoutMs: deadlines.requestedReadinessTimeoutMs,
|
|
2685
|
+
semanticReadyDeadlineAt: deadlines.semanticReadyDeadlineAt,
|
|
2686
|
+
terminationStartDeadlineAt: deadlines.terminationStartDeadlineAt,
|
|
2687
|
+
lifecycleCleanupDeadlineAt: deadlines.lifecycleCleanupDeadlineAt,
|
|
2688
|
+
...(launch.sourceSessionId ? { sourceSessionId: launch.sourceSessionId } : {}),
|
|
2689
|
+
...(launch.sourceSessionPath ? { sourceSessionPath: launch.sourceSessionPath } : {}),
|
|
2690
|
+
...(launch.sourceSessionIdentity ? { sourceSessionIdentity: launch.sourceSessionIdentity } : {}),
|
|
2691
|
+
...(launch.sourceCwd ? { sourceCwd: launch.sourceCwd } : {}),
|
|
2692
|
+
...(launch.sessionPath ? { sessionPath: launch.sessionPath } : {}),
|
|
2693
|
+
...(launch.sessionIdentity ? { sessionIdentity: launch.sessionIdentity } : {}),
|
|
2694
|
+
...(launch.modelPreset ? { modelPreset: launch.modelPreset } : {}),
|
|
2695
|
+
...(launch.worktree ? { worktree: launch.worktree } : {}),
|
|
2696
|
+
};
|
|
2697
|
+
let child: ChildProcess | undefined;
|
|
2698
|
+
let spawnedAuthority: EffectMarker | undefined;
|
|
2699
|
+
try {
|
|
2700
|
+
const cmd = command(broker);
|
|
2701
|
+
const spawned = spawn(cmd.file, cmd.args, {
|
|
2702
|
+
cwd: launch.cwd,
|
|
2703
|
+
detached: true,
|
|
2704
|
+
stdio: "ignore",
|
|
2705
|
+
env: {
|
|
2706
|
+
...("kind" in cmd ? cmd.env : process.env),
|
|
2707
|
+
SKC_AGENT_DIR: broker.settings.agentDir,
|
|
2708
|
+
SKC_CODING_AGENT_DIR: broker.settings.agentDir,
|
|
2709
|
+
SKC_SESSION_ID: launch.id,
|
|
2710
|
+
SKC_STATE_ROOT: launch.root,
|
|
2711
|
+
SKC_LIFECYCLE_REQUEST_ID: effectMarker,
|
|
2712
|
+
SKC_SDK_LIFECYCLE_REQUEST: JSON.stringify(request),
|
|
2713
|
+
},
|
|
2714
|
+
});
|
|
2715
|
+
child = spawned;
|
|
2716
|
+
const pid = spawned.pid;
|
|
2717
|
+
if (!pid) throw new Error("spawned session has no pid");
|
|
2718
|
+
const incarnation = processIncarnationForBroker(broker, pid);
|
|
2719
|
+
if (!incarnation) throw new Error("spawned session has no readable OS incarnation");
|
|
2720
|
+
spawnedAuthority = { pid, effectMarker, incarnation };
|
|
2721
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
2722
|
+
effectIntent: { ...effectIntent, childOwnershipEstablished: true },
|
|
2723
|
+
});
|
|
2724
|
+
await writeEffectMarker(launch.root, launch.id, spawnedAuthority);
|
|
2725
|
+
spawned.unref();
|
|
2726
|
+
} catch (error) {
|
|
2727
|
+
const terminated = child
|
|
2728
|
+
? await terminateSpawnedChild(
|
|
2729
|
+
child,
|
|
2730
|
+
broker,
|
|
2731
|
+
launch.id,
|
|
2732
|
+
launch.root,
|
|
2733
|
+
lifecycleDeadline,
|
|
2734
|
+
terminationStartDeadline,
|
|
2735
|
+
spawnedAuthority,
|
|
2736
|
+
timing,
|
|
2737
|
+
)
|
|
2738
|
+
: true;
|
|
2739
|
+
|
|
2740
|
+
return terminated
|
|
2741
|
+
? fail("spawn_failed", `Unable to spawn session: ${error instanceof Error ? error.message : String(error)}`)
|
|
2742
|
+
: fail(
|
|
2743
|
+
"terminal_uncertain",
|
|
2744
|
+
`Unable to establish spawned-session ownership and could not prove the child dead: ${error instanceof Error ? error.message : String(error)}`,
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
if (!child || !spawnedAuthority)
|
|
2748
|
+
return fail("spawn_failed", "Unable to retain the spawned session process identity.");
|
|
2749
|
+
await broker.ledger.transition(identity, "awaiting_ready", { intendedSessionId: launch.id, effectMarker });
|
|
2750
|
+
const readiness = await waitForReady(broker, launch.id, launch.root, readinessDeadline, spawnedAuthority, timing);
|
|
2751
|
+
|
|
2752
|
+
if (readiness.kind !== "ready") {
|
|
2753
|
+
const terminated = await terminateSpawnedChild(
|
|
2754
|
+
child,
|
|
2755
|
+
broker,
|
|
2756
|
+
launch.id,
|
|
2757
|
+
launch.root,
|
|
2758
|
+
lifecycleDeadline,
|
|
2759
|
+
terminationStartDeadline,
|
|
2760
|
+
spawnedAuthority,
|
|
2761
|
+
timing,
|
|
2762
|
+
);
|
|
2763
|
+
|
|
2764
|
+
if (!terminated)
|
|
2765
|
+
return fail(
|
|
2766
|
+
"terminal_uncertain",
|
|
2767
|
+
`Session ${launch.id} did not become ready and its spawned process could not be verified dead.`,
|
|
2768
|
+
);
|
|
2769
|
+
return readiness.kind === "startup_failed"
|
|
2770
|
+
? fail("spawn_failed", readiness.message)
|
|
2771
|
+
: readiness.kind === "child_exited"
|
|
2772
|
+
? fail("spawn_failed", `Session ${launch.id} exited before registering readiness.`)
|
|
2773
|
+
: fail(
|
|
2774
|
+
"readiness_timeout",
|
|
2775
|
+
`Session ${launch.id} did not register an endpoint before the readiness timeout.`,
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
await reconcileReadyScope(broker, launch.id, launch.cwd);
|
|
2779
|
+
const verified = await currentReadyAuthority(broker, launch.id, launch.root, spawnedAuthority);
|
|
2780
|
+
if (!verified || !sameReadyAuthority(readiness.authority, verified)) {
|
|
2781
|
+
const terminated = await terminateSpawnedChild(
|
|
2782
|
+
child,
|
|
2783
|
+
broker,
|
|
2784
|
+
launch.id,
|
|
2785
|
+
launch.root,
|
|
2786
|
+
lifecycleDeadline,
|
|
2787
|
+
terminationStartDeadline,
|
|
2788
|
+
spawnedAuthority,
|
|
2789
|
+
timing,
|
|
2790
|
+
);
|
|
2791
|
+
return terminated
|
|
2792
|
+
? fail("endpoint_stale", "Session endpoint changed while lifecycle readiness was being verified.")
|
|
2793
|
+
: fail(
|
|
2794
|
+
"terminal_uncertain",
|
|
2795
|
+
"Session readiness authority changed and its spawned process could not be verified dead.",
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
return {
|
|
2799
|
+
ok: true,
|
|
2800
|
+
result: {
|
|
2801
|
+
sessionId: launch.id,
|
|
2802
|
+
cwd: launch.cwd,
|
|
2803
|
+
endpoint: verified.endpoint,
|
|
2804
|
+
...(worktreeReceipt ? { worktree: worktreeReceipt } : {}),
|
|
2805
|
+
},
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
const id = cleanup && operation === "session.delete" ? cleanup.sessionId : sessionId(input);
|
|
2810
|
+
if (!id) return fail("invalid_input", "sessionId is required.");
|
|
2811
|
+
if (!isCanonicalSessionId(id)) return fail("invalid_input", "sessionId must be a canonical safe identifier.");
|
|
2812
|
+
await broker.index.refresh();
|
|
2813
|
+
let record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
2814
|
+
if (operation === "session.close") {
|
|
2815
|
+
if (!record) return fail("not_found", "session is not indexed");
|
|
2816
|
+
if (record.terminalUncertain)
|
|
2817
|
+
return fail("terminal_uncertain", "Session ownership is uncertain and cannot be closed safely.");
|
|
2818
|
+
const requestedAuthority = requestedCloseAuthority(input);
|
|
2819
|
+
if ("error" in requestedAuthority) return requestedAuthority.error;
|
|
2820
|
+
if (requestedAuthority.authority && !sameCloseAuthority(requestedAuthority.authority, record, id))
|
|
2821
|
+
return fail("endpoint_stale", "session endpoint is stale");
|
|
2822
|
+
await broker.ledger.transition(identity, "effect_started", { intendedSessionId: id, effectMarker: randomUUID() });
|
|
2823
|
+
|
|
2824
|
+
let usedSignalFallback = false;
|
|
2825
|
+
let note: string | undefined;
|
|
2826
|
+
let endpointResult = await broker.handleRequest("session.get_endpoint", {
|
|
2827
|
+
sessionId: id,
|
|
2828
|
+
endpointGeneration: record.endpointGeneration,
|
|
2829
|
+
});
|
|
2830
|
+
if (!endpointResult.ok && endpointResult.error.code === "endpoint_stale" && !requestedAuthority.authority) {
|
|
2831
|
+
await broker.index.refresh();
|
|
2832
|
+
const refreshed = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
2833
|
+
if (refreshed && sameCloseProcessIdentity(record, refreshed)) {
|
|
2834
|
+
record = refreshed;
|
|
2835
|
+
endpointResult = await broker.handleRequest("session.get_endpoint", {
|
|
2836
|
+
sessionId: id,
|
|
2837
|
+
endpointGeneration: record.endpointGeneration,
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
if (!endpointResult.ok) {
|
|
2842
|
+
if (endpointResult.error.code === "endpoint_stale") return endpointResult;
|
|
2843
|
+
if (endpointResult.error.code !== "resource_gone") return endpointResult;
|
|
2844
|
+
usedSignalFallback = true;
|
|
2845
|
+
} else {
|
|
2846
|
+
const endpoint = closeEndpoint(endpointResult.result);
|
|
2847
|
+
if (!endpoint) return fail("close_refused", "Session endpoint is malformed.");
|
|
2848
|
+
let client: SdkClient | undefined;
|
|
2849
|
+
try {
|
|
2850
|
+
client = await SdkClient.connect(endpoint.url, endpoint.token, {
|
|
2851
|
+
timeoutMs: 2_000,
|
|
2852
|
+
reconnectAttempts: 0,
|
|
2853
|
+
});
|
|
2854
|
+
const refreshedEndpointResult = await broker.handleRequest("session.get_endpoint", {
|
|
2855
|
+
sessionId: id,
|
|
2856
|
+
endpointGeneration: record.endpointGeneration,
|
|
2857
|
+
});
|
|
2858
|
+
if (!refreshedEndpointResult.ok) return refreshedEndpointResult;
|
|
2859
|
+
const refreshedEndpoint = closeEndpoint(refreshedEndpointResult.result);
|
|
2860
|
+
if (
|
|
2861
|
+
!refreshedEndpoint ||
|
|
2862
|
+
refreshedEndpoint.url !== endpoint.url ||
|
|
2863
|
+
refreshedEndpoint.token !== endpoint.token
|
|
2864
|
+
)
|
|
2865
|
+
return fail("endpoint_stale", "session endpoint is stale");
|
|
2866
|
+
const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
|
|
2867
|
+
if (stale) return stale;
|
|
2868
|
+
const response = await client.control("session.close");
|
|
2869
|
+
if ((response as { ok?: unknown }).ok !== true)
|
|
2870
|
+
return fail("close_refused", "Session endpoint rejected session.close.");
|
|
2871
|
+
} catch (error) {
|
|
2872
|
+
if (isTransportFailure(error)) usedSignalFallback = true;
|
|
2873
|
+
else if (error instanceof SdkClientError) return fail(error.code, error.message);
|
|
2874
|
+
else
|
|
2875
|
+
return fail(
|
|
2876
|
+
"close_refused",
|
|
2877
|
+
`Session endpoint close failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2878
|
+
);
|
|
2879
|
+
} finally {
|
|
2880
|
+
await client?.close();
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
if (usedSignalFallback) {
|
|
2885
|
+
const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
|
|
2886
|
+
if (stale) return stale;
|
|
2887
|
+
if (!(await signalVerifiedSession(record, id, "SIGTERM")))
|
|
2888
|
+
return fail(
|
|
2889
|
+
"close_refused",
|
|
2890
|
+
"Session endpoint is unavailable and its durable process identity could not be verified.",
|
|
2891
|
+
);
|
|
2892
|
+
note = "Endpoint close was unreachable; sent SIGTERM to the durably identified session process.";
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
let closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS);
|
|
2896
|
+
if (!closed && !usedSignalFallback) {
|
|
2897
|
+
const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
|
|
2898
|
+
if (stale) return stale;
|
|
2899
|
+
if (!(await signalVerifiedSession(record, id, "SIGTERM"))) {
|
|
2900
|
+
await recordTerminalUncertain(broker, id, record.locator.stateRoot, record.pid);
|
|
2901
|
+
return fail(
|
|
2902
|
+
"terminal_uncertain",
|
|
2903
|
+
"Session acknowledged session.close but its durable process identity could not be verified for shutdown escalation.",
|
|
2904
|
+
);
|
|
2905
|
+
}
|
|
2906
|
+
note =
|
|
2907
|
+
"Session acknowledged session.close but graceful teardown did not complete within the bounded deadline; sent SIGTERM to the durably identified session process.";
|
|
2908
|
+
closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS);
|
|
2909
|
+
}
|
|
2910
|
+
if (!closed) {
|
|
2911
|
+
const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
|
|
2912
|
+
if (stale) return stale;
|
|
2913
|
+
if (!(await signalVerifiedSession(record, id, "SIGKILL"))) {
|
|
2914
|
+
await recordTerminalUncertain(broker, id, record.locator.stateRoot, record.pid);
|
|
2915
|
+
return fail(
|
|
2916
|
+
"terminal_uncertain",
|
|
2917
|
+
"Session did not close after SIGTERM and its durable process identity could not be verified for SIGKILL.",
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
note =
|
|
2921
|
+
"Session teardown did not complete after SIGTERM within the bounded deadline; sent SIGKILL to the durably identified session process.";
|
|
2922
|
+
closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS);
|
|
2923
|
+
}
|
|
2924
|
+
if (!closed) {
|
|
2925
|
+
await recordTerminalUncertain(broker, id, record.locator.stateRoot, record.pid);
|
|
2926
|
+
return fail(
|
|
2927
|
+
"terminal_uncertain",
|
|
2928
|
+
"Session did not unregister, remove its endpoint, and exit after bounded shutdown escalation.",
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
return { ok: true, result: { sessionId: id, ...(note ? { note } : {}) } };
|
|
2933
|
+
}
|
|
2934
|
+
if (operation === "session.delete") {
|
|
2935
|
+
if (record?.terminalUncertain)
|
|
2936
|
+
return fail("terminal_uncertain", "Session ownership is uncertain and cannot be deleted safely.");
|
|
2937
|
+
if (record?.live) return fail("live_session", "Refusing to delete a live session; close it first.");
|
|
2938
|
+
const validated = await validateDeletePath(broker, input, id, record, cleanup);
|
|
2939
|
+
if ("ok" in validated) return validated;
|
|
2940
|
+
const metadataPreflight = preflightLifecycleDeleteMetadata(validated.metadataRoot, id, record, value =>
|
|
2941
|
+
processIncarnationForBroker(broker, value),
|
|
2942
|
+
);
|
|
2943
|
+
if ("ok" in metadataPreflight) return metadataPreflight;
|
|
2944
|
+
const metadataCleanup = metadataPreflight.cleanup;
|
|
2945
|
+
let cleanupTarget: VerifiedSessionDeleteTarget = {
|
|
2946
|
+
...validated.target,
|
|
2947
|
+
...(validated.target.plannedArtifactsPath &&
|
|
2948
|
+
validated.target.detachedArtifactsPath !== validated.target.plannedArtifactsPath
|
|
2949
|
+
? {}
|
|
2950
|
+
: {
|
|
2951
|
+
plannedArtifactsPath: path.join(
|
|
2952
|
+
path.dirname(validated.target.transcriptPath),
|
|
2953
|
+
`.skc-delete-${randomUUID()}-artifacts`,
|
|
2954
|
+
),
|
|
2955
|
+
}),
|
|
2956
|
+
...(validated.target.plannedTranscriptPath &&
|
|
2957
|
+
validated.target.detachedTranscriptPath !== validated.target.plannedTranscriptPath
|
|
2958
|
+
? {}
|
|
2959
|
+
: {
|
|
2960
|
+
plannedTranscriptPath: path.join(
|
|
2961
|
+
path.dirname(validated.target.transcriptPath),
|
|
2962
|
+
`.skc-delete-${randomUUID()}-transcript`,
|
|
2963
|
+
),
|
|
2964
|
+
}),
|
|
2965
|
+
};
|
|
2966
|
+
if (!cleanupTarget.artifactsRemoved && !cleanupTarget.expectedArtifactsIdentity) {
|
|
2967
|
+
const artifactsPath = cleanupTarget.transcriptPath.slice(0, -6);
|
|
2968
|
+
try {
|
|
2969
|
+
const stat = fsSync.lstatSync(artifactsPath, { bigint: true });
|
|
2970
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
2971
|
+
return fail("terminal_uncertain", "Artifact cleanup target is not an exact directory.");
|
|
2972
|
+
validateManagedArtifactTree(artifactsPath);
|
|
2973
|
+
const tree = native.snapshotDirectoryTree(artifactsPath);
|
|
2974
|
+
if (!tree.ok || !tree.snapshot)
|
|
2975
|
+
return fail(
|
|
2976
|
+
"terminal_uncertain",
|
|
2977
|
+
`Artifact tree authority could not be captured: ${tree.code ?? "unknown"}`,
|
|
2978
|
+
);
|
|
2979
|
+
cleanupTarget = {
|
|
2980
|
+
...cleanupTarget,
|
|
2981
|
+
expectedArtifactsIdentity: {
|
|
2982
|
+
dev: stat.dev,
|
|
2983
|
+
ino: stat.ino,
|
|
2984
|
+
size: Number(stat.size),
|
|
2985
|
+
mtimeNs: stat.mtimeNs,
|
|
2986
|
+
sha256: "",
|
|
2987
|
+
},
|
|
2988
|
+
expectedArtifactsTree: tree.snapshot,
|
|
2989
|
+
};
|
|
2990
|
+
} catch (error) {
|
|
2991
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
const preauthorizedCleanup: CleanupEvidence = {
|
|
2995
|
+
phase: cleanupTarget.artifactsRemoved ? "transcript" : "artifacts",
|
|
2996
|
+
sessionId: cleanupTarget.sessionId,
|
|
2997
|
+
sessionsRoot: cleanupTarget.sessionsRoot,
|
|
2998
|
+
transcriptPath: cleanupTarget.transcriptPath,
|
|
2999
|
+
cwd: cleanupTarget.cwd,
|
|
3000
|
+
...(cleanupTarget.artifactsRemoved ? { artifactsRemoved: true } : {}),
|
|
3001
|
+
metadataRoot: validated.metadataRoot,
|
|
3002
|
+
transcriptIdentity: serializeCleanupIdentity(cleanupTarget.transcriptIdentity),
|
|
3003
|
+
...(cleanupTarget.expectedArtifactsIdentity
|
|
3004
|
+
? { artifactsIdentity: serializeCleanupIdentity(cleanupTarget.expectedArtifactsIdentity) }
|
|
3005
|
+
: {}),
|
|
3006
|
+
...(cleanupTarget.expectedArtifactsIdentity && cleanupTarget.plannedArtifactsPath
|
|
3007
|
+
? {
|
|
3008
|
+
artifactTree: {
|
|
3009
|
+
identity: serializeCleanupIdentity(cleanupTarget.expectedArtifactsIdentity),
|
|
3010
|
+
snapshot: cleanupTarget.expectedArtifactsTree!,
|
|
3011
|
+
plannedPath: cleanupTarget.plannedArtifactsPath,
|
|
3012
|
+
...(cleanupTarget.detachedArtifactsPath
|
|
3013
|
+
? { detachedPath: cleanupTarget.detachedArtifactsPath }
|
|
3014
|
+
: {}),
|
|
3015
|
+
},
|
|
3016
|
+
}
|
|
3017
|
+
: {}),
|
|
3018
|
+
|
|
3019
|
+
...(cleanupTarget.plannedArtifactsPath ? { plannedArtifactsPath: cleanupTarget.plannedArtifactsPath } : {}),
|
|
3020
|
+
...(cleanupTarget.plannedTranscriptPath ? { plannedTranscriptPath: cleanupTarget.plannedTranscriptPath } : {}),
|
|
3021
|
+
};
|
|
3022
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
3023
|
+
intendedSessionId: id,
|
|
3024
|
+
effectMarker: randomUUID(),
|
|
3025
|
+
response: fail(
|
|
3026
|
+
"cleanup_pending",
|
|
3027
|
+
"Saved session cleanup is preauthorized for durable reconciliation.",
|
|
3028
|
+
preauthorizedCleanup,
|
|
3029
|
+
),
|
|
3030
|
+
});
|
|
3031
|
+
let deleted: VerifiedSessionDeleteResult;
|
|
3032
|
+
try {
|
|
3033
|
+
deleted = await validated.storage.deleteSessionVerified(cleanupTarget);
|
|
3034
|
+
} catch (error) {
|
|
3035
|
+
if (error instanceof SessionDeleteVerificationError)
|
|
3036
|
+
return fail(
|
|
3037
|
+
"invalid_input",
|
|
3038
|
+
`Saved session deletion verification failed (${error.kind}): ${error.message}`,
|
|
3039
|
+
);
|
|
3040
|
+
return fail(
|
|
3041
|
+
"unavailable",
|
|
3042
|
+
`Unable to delete saved session artifacts: ${error instanceof Error ? error.message : String(error)}`,
|
|
3043
|
+
);
|
|
3044
|
+
}
|
|
3045
|
+
if (deleted.kind === "artifacts_removed") {
|
|
3046
|
+
const transcriptPhaseCleanup = {
|
|
3047
|
+
...preauthorizedCleanup,
|
|
3048
|
+
phase: "transcript" as const,
|
|
3049
|
+
artifactsRemoved: true,
|
|
3050
|
+
...(preauthorizedCleanup.artifactTree
|
|
3051
|
+
? { artifactTree: { ...preauthorizedCleanup.artifactTree, completed: true as const } }
|
|
3052
|
+
: {}),
|
|
3053
|
+
};
|
|
3054
|
+
|
|
3055
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
3056
|
+
intendedSessionId: id,
|
|
3057
|
+
response: fail(
|
|
3058
|
+
"cleanup_pending",
|
|
3059
|
+
"Saved session artifacts were removed; transcript cleanup is preauthorized.",
|
|
3060
|
+
transcriptPhaseCleanup,
|
|
3061
|
+
),
|
|
3062
|
+
});
|
|
3063
|
+
deleted = await validated.storage.deleteSessionVerified({
|
|
3064
|
+
...cleanupTarget,
|
|
3065
|
+
expectedArtifactsIdentity: undefined,
|
|
3066
|
+
detachedArtifactsPath: undefined,
|
|
3067
|
+
artifactsRemoved: true,
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
if (deleted.kind === "cleanup_pending")
|
|
3071
|
+
return fail(
|
|
3072
|
+
"cleanup_pending",
|
|
3073
|
+
`Saved session cleanup is pending in ${deleted.phase}: ${deleted.error.message}`,
|
|
3074
|
+
{
|
|
3075
|
+
phase: deleted.phase,
|
|
3076
|
+
sessionId: validated.target.sessionId,
|
|
3077
|
+
sessionsRoot: validated.target.sessionsRoot,
|
|
3078
|
+
transcriptPath: validated.target.transcriptPath,
|
|
3079
|
+
cwd: validated.target.cwd,
|
|
3080
|
+
metadataRoot: validated.metadataRoot,
|
|
3081
|
+
transcriptIdentity: serializeCleanupIdentity(deleted.transcriptIdentity),
|
|
3082
|
+
...(deleted.phase === "artifacts" && deleted.artifactsIdentity
|
|
3083
|
+
? { artifactsIdentity: serializeCleanupIdentity(deleted.artifactsIdentity) }
|
|
3084
|
+
: {}),
|
|
3085
|
+
...(deleted.phase === "artifacts" && deleted.artifactsIdentity && cleanupTarget.plannedArtifactsPath
|
|
3086
|
+
? {
|
|
3087
|
+
artifactTree: {
|
|
3088
|
+
identity: serializeCleanupIdentity(deleted.artifactsIdentity),
|
|
3089
|
+
snapshot: deleted.artifactsTree,
|
|
3090
|
+
plannedPath: cleanupTarget.plannedArtifactsPath,
|
|
3091
|
+
...(deleted.detachedArtifactsPath ? { detachedPath: deleted.detachedArtifactsPath } : {}),
|
|
3092
|
+
},
|
|
3093
|
+
}
|
|
3094
|
+
: {}),
|
|
3095
|
+
...(deleted.phase === "artifacts" ? { detachedArtifactsPath: deleted.detachedArtifactsPath } : {}),
|
|
3096
|
+
...(deleted.phase === "transcript" && deleted.detachedTranscriptPath
|
|
3097
|
+
? { detachedTranscriptPath: deleted.detachedTranscriptPath }
|
|
3098
|
+
: {}),
|
|
3099
|
+
...(deleted.phase === "transcript" ? { artifactsRemoved: true } : {}),
|
|
3100
|
+
...(deleted.phase === "transcript" &&
|
|
3101
|
+
cleanupTarget.plannedArtifactsPath &&
|
|
3102
|
+
cleanupTarget.expectedArtifactsIdentity
|
|
3103
|
+
? {
|
|
3104
|
+
artifactTree: {
|
|
3105
|
+
identity: serializeCleanupIdentity(cleanupTarget.expectedArtifactsIdentity),
|
|
3106
|
+
snapshot: cleanupTarget.expectedArtifactsTree!,
|
|
3107
|
+
plannedPath: cleanupTarget.plannedArtifactsPath,
|
|
3108
|
+
completed: true as const,
|
|
3109
|
+
},
|
|
3110
|
+
}
|
|
3111
|
+
: {}),
|
|
3112
|
+
...(cleanupTarget.plannedArtifactsPath
|
|
3113
|
+
? { plannedArtifactsPath: cleanupTarget.plannedArtifactsPath }
|
|
3114
|
+
: {}),
|
|
3115
|
+
...(cleanupTarget.plannedTranscriptPath
|
|
3116
|
+
? { plannedTranscriptPath: cleanupTarget.plannedTranscriptPath }
|
|
3117
|
+
: {}),
|
|
3118
|
+
},
|
|
3119
|
+
);
|
|
3120
|
+
|
|
3121
|
+
const completion = { ok: true, result: { sessionId: id } } as const;
|
|
3122
|
+
if (metadataCleanup.lifecycleFiles?.length) {
|
|
3123
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
3124
|
+
intendedSessionId: id,
|
|
3125
|
+
response: fail(
|
|
3126
|
+
"cleanup_pending",
|
|
3127
|
+
"Lifecycle metadata cleanup is preauthorized for durable reconciliation.",
|
|
3128
|
+
metadataCleanup,
|
|
3129
|
+
),
|
|
3130
|
+
});
|
|
3131
|
+
const reconciled = await reconcileLifecycleCleanup(broker, identity, metadataCleanup, completion);
|
|
3132
|
+
if (!reconciled.ok) return reconciled;
|
|
3133
|
+
}
|
|
3134
|
+
if (record)
|
|
3135
|
+
await broker.index.append({
|
|
3136
|
+
type: "session_closed",
|
|
3137
|
+
sessionId: id,
|
|
3138
|
+
locator: record.locator,
|
|
3139
|
+
endpointGeneration: record.endpointGeneration,
|
|
3140
|
+
pid: record.pid,
|
|
3141
|
+
});
|
|
3142
|
+
return completion;
|
|
3143
|
+
}
|
|
3144
|
+
return fail("invalid_input", "Unknown lifecycle operation.");
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
async function exactCleanupProof(
|
|
3148
|
+
broker: Broker,
|
|
3149
|
+
root: string | undefined,
|
|
3150
|
+
id: string | undefined,
|
|
3151
|
+
expected: EffectMarker | undefined,
|
|
3152
|
+
evidence: { artifact: LifecycleFailureArtifact } | undefined,
|
|
3153
|
+
): Promise<LifecycleCleanupProof | undefined> {
|
|
3154
|
+
const rollback = evidence?.artifact.rollback;
|
|
3155
|
+
if (
|
|
3156
|
+
!root ||
|
|
3157
|
+
!id ||
|
|
3158
|
+
!expected ||
|
|
3159
|
+
!rollback?.fenced ||
|
|
3160
|
+
!rollback.runtimeRemoved ||
|
|
3161
|
+
!rollback.hostStopped ||
|
|
3162
|
+
!rollback.brokerRegistrationReleased ||
|
|
3163
|
+
observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) !==
|
|
3164
|
+
"exited" ||
|
|
3165
|
+
!(await endpointRemoved(root, id))
|
|
3166
|
+
)
|
|
3167
|
+
return undefined;
|
|
3168
|
+
await broker.index.refresh();
|
|
3169
|
+
if (rollback.endpointGeneration === null) {
|
|
3170
|
+
if (broker.index.hasHostRegistrationForLifecycle(id, expected.pid, expected.effectMarker)) return undefined;
|
|
3171
|
+
return {
|
|
3172
|
+
processExited: true,
|
|
3173
|
+
endpointRemoved: true,
|
|
3174
|
+
hostUnregistered: { state: "not_registered" },
|
|
3175
|
+
rollback: {
|
|
3176
|
+
endpointGeneration: null,
|
|
3177
|
+
fenced: true,
|
|
3178
|
+
runtimeRemoved: true,
|
|
3179
|
+
hostStopped: true,
|
|
3180
|
+
brokerRegistrationReleased: true,
|
|
3181
|
+
},
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
const registration = broker.index.findHostRegistration(
|
|
3185
|
+
id,
|
|
3186
|
+
rollback.endpointGeneration,
|
|
3187
|
+
expected.pid,
|
|
3188
|
+
expected.effectMarker,
|
|
3189
|
+
);
|
|
3190
|
+
const hostUnregistered = registration ? broker.index.hostUnregisteredAfter(registration) : undefined;
|
|
3191
|
+
return hostUnregistered
|
|
3192
|
+
? {
|
|
3193
|
+
processExited: true,
|
|
3194
|
+
endpointRemoved: true,
|
|
3195
|
+
hostUnregistered: { state: "unregistered", ...hostUnregistered },
|
|
3196
|
+
rollback: {
|
|
3197
|
+
endpointGeneration: rollback.endpointGeneration,
|
|
3198
|
+
fenced: true,
|
|
3199
|
+
runtimeRemoved: true,
|
|
3200
|
+
hostStopped: true,
|
|
3201
|
+
brokerRegistrationReleased: true,
|
|
3202
|
+
},
|
|
3203
|
+
}
|
|
3204
|
+
: undefined;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
function validateLifecycleDeleteMetadataBinding(
|
|
3208
|
+
broker: Broker,
|
|
3209
|
+
operation: string,
|
|
3210
|
+
input: Input,
|
|
3211
|
+
identity: string,
|
|
3212
|
+
cleanup: CleanupEvidence,
|
|
3213
|
+
): BrokerResponse | undefined {
|
|
3214
|
+
if (operation !== "session.delete")
|
|
3215
|
+
return fail("terminal_uncertain", "Lifecycle delete metadata cleanup is not authorized for this operation.");
|
|
3216
|
+
const requestedId = sessionId(input);
|
|
3217
|
+
const cwd = lifecycleCwd(input);
|
|
3218
|
+
const requestedRoot = stateRoot(input, cwd);
|
|
3219
|
+
if (
|
|
3220
|
+
!requestedId ||
|
|
3221
|
+
!isCanonicalSessionId(requestedId) ||
|
|
3222
|
+
!cwd ||
|
|
3223
|
+
!requestedRoot ||
|
|
3224
|
+
!hasDefaultStateRoot(cwd, requestedRoot) ||
|
|
3225
|
+
cleanup.sessionId !== requestedId ||
|
|
3226
|
+
!cleanup.metadataRoot ||
|
|
3227
|
+
path.resolve(cleanup.metadataRoot) !== requestedRoot
|
|
3228
|
+
)
|
|
3229
|
+
return fail("terminal_uncertain", "Lifecycle delete metadata cleanup does not match the normalized request.");
|
|
3230
|
+
const recordedRoot = broker.ledger.get(identity)?.effectIntent?.stateRoot;
|
|
3231
|
+
if (recordedRoot && path.resolve(recordedRoot) !== requestedRoot)
|
|
3232
|
+
return fail("terminal_uncertain", "Lifecycle delete metadata cleanup does not match the recorded workspace.");
|
|
3233
|
+
return undefined;
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
export interface LifecycleExecutionOutcome {
|
|
3237
|
+
response: BrokerResponse;
|
|
3238
|
+
durableEffects?: LifecycleDurableEffectsReceipt;
|
|
3239
|
+
startupFailure?: LifecycleStartupFailureReceipt;
|
|
3240
|
+
deferredArtifactCleanup?: () => Promise<void>;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
/** Returns the response together with every durable lifecycle fact needed for truthful replay. */
|
|
3244
|
+
export async function executeLifecycle(
|
|
3245
|
+
broker: Broker,
|
|
3246
|
+
operation: string,
|
|
3247
|
+
input: Input,
|
|
3248
|
+
identity: string,
|
|
3249
|
+
cleanup?: CleanupEvidence,
|
|
3250
|
+
): Promise<LifecycleExecutionOutcome> {
|
|
3251
|
+
if (cleanup?.phase === "metadata") {
|
|
3252
|
+
if (operation !== "session.delete")
|
|
3253
|
+
return {
|
|
3254
|
+
response: fail(
|
|
3255
|
+
"terminal_uncertain",
|
|
3256
|
+
"Legacy metadata cleanup is not authorized for this lifecycle operation.",
|
|
3257
|
+
),
|
|
3258
|
+
};
|
|
3259
|
+
const migrated = legacyMetadataCleanupPlan(cleanup);
|
|
3260
|
+
if (!migrated)
|
|
3261
|
+
return {
|
|
3262
|
+
response: fail(
|
|
3263
|
+
"terminal_uncertain",
|
|
3264
|
+
"Legacy metadata cleanup replay lacks immutable identity-bound intent.",
|
|
3265
|
+
),
|
|
3266
|
+
};
|
|
3267
|
+
const binding = validateLifecycleDeleteMetadataBinding(broker, operation, input, identity, migrated);
|
|
3268
|
+
if (binding) return { response: binding };
|
|
3269
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
3270
|
+
response: fail(
|
|
3271
|
+
"cleanup_pending",
|
|
3272
|
+
"Legacy lifecycle metadata cleanup is preauthorized for durable reconciliation.",
|
|
3273
|
+
migrated,
|
|
3274
|
+
),
|
|
3275
|
+
});
|
|
3276
|
+
return {
|
|
3277
|
+
response: await reconcileLifecycleCleanup(broker, identity, migrated, {
|
|
3278
|
+
ok: true,
|
|
3279
|
+
result: { sessionId: migrated.sessionId },
|
|
3280
|
+
}),
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
if (cleanup?.phase === "lifecycle") {
|
|
3284
|
+
const shapeValidation = validateLifecycleCleanupShape(cleanup);
|
|
3285
|
+
if (shapeValidation) return { response: shapeValidation };
|
|
3286
|
+
if (cleanup.lifecycleDeleteMetadata === true || operation === "session.delete") {
|
|
3287
|
+
const binding = validateLifecycleDeleteMetadataBinding(broker, operation, input, identity, cleanup);
|
|
3288
|
+
if (binding) return { response: binding };
|
|
3289
|
+
}
|
|
3290
|
+
return {
|
|
3291
|
+
response: await reconcileLifecycleCleanup(
|
|
3292
|
+
broker,
|
|
3293
|
+
identity,
|
|
3294
|
+
cleanup,
|
|
3295
|
+
operation === "session.delete"
|
|
3296
|
+
? { ok: true, result: { sessionId: cleanup.sessionId } }
|
|
3297
|
+
: fail("spawn_failed", "No ready SDK endpoint remains available."),
|
|
3298
|
+
),
|
|
3299
|
+
};
|
|
3300
|
+
}
|
|
3301
|
+
const response = await executeLifecycleResponse(broker, operation, input, identity, cleanup);
|
|
3302
|
+
const entry = broker.ledger.get(identity);
|
|
3303
|
+
const priorDurableEffects = entry?.durableEffects;
|
|
3304
|
+
const evidenceCwd = entry?.effectIntent?.worktree?.worktreePath ?? lifecycleCwd(input);
|
|
3305
|
+
const root = entry?.effectIntent?.stateRoot ?? stateRoot(input, evidenceCwd);
|
|
3306
|
+
const marker =
|
|
3307
|
+
entry?.effectMarker && entry.intendedSessionId && root
|
|
3308
|
+
? await readEffectMarker(lifecycleMarkerPath(root, entry.intendedSessionId))
|
|
3309
|
+
: undefined;
|
|
3310
|
+
const expected = marker && marker.effectMarker === entry?.effectMarker ? marker : undefined;
|
|
3311
|
+
const evidence =
|
|
3312
|
+
root && entry?.intendedSessionId && expected
|
|
3313
|
+
? await readLifecycleFailureArtifact(
|
|
3314
|
+
lifecycleFailurePath(root, entry.intendedSessionId, expected.effectMarker),
|
|
3315
|
+
expected,
|
|
3316
|
+
)
|
|
3317
|
+
: undefined;
|
|
3318
|
+
const cleanupProof = await exactCleanupProof(broker, root, entry?.intendedSessionId, expected, evidence);
|
|
3319
|
+
const startupFailure: LifecycleStartupFailureReceipt | undefined = evidence
|
|
3320
|
+
? {
|
|
3321
|
+
artifactDigest: evidence.digest,
|
|
3322
|
+
phase: evidence.artifact.phase,
|
|
3323
|
+
reason: evidence.artifact.reason,
|
|
3324
|
+
message: evidence.artifact.message,
|
|
3325
|
+
rollback: {
|
|
3326
|
+
endpointGeneration: evidence.artifact.rollback.endpointGeneration,
|
|
3327
|
+
fenced: evidence.artifact.rollback.fenced,
|
|
3328
|
+
runtimeRemoved: evidence.artifact.rollback.runtimeRemoved,
|
|
3329
|
+
hostStopped: evidence.artifact.rollback.hostStopped,
|
|
3330
|
+
brokerRegistrationReleased: evidence.artifact.rollback.brokerRegistrationReleased,
|
|
3331
|
+
},
|
|
3332
|
+
...(cleanupProof ? { cleanupProof } : {}),
|
|
3333
|
+
}
|
|
3334
|
+
: undefined;
|
|
3335
|
+
const durableEffectsBody: Omit<LifecycleDurableEffectsReceipt, "digest"> = {
|
|
3336
|
+
...(priorDurableEffects?.worktree ? { worktree: priorDurableEffects.worktree } : {}),
|
|
3337
|
+
...(evidence?.artifact.transcript
|
|
3338
|
+
? {
|
|
3339
|
+
transcript: {
|
|
3340
|
+
identityDigest: createHash("sha256")
|
|
3341
|
+
.update(canonicalJson(evidence.artifact.transcript.identity))
|
|
3342
|
+
.digest("hex"),
|
|
3343
|
+
contentDigest: evidence.artifact.transcript.digest,
|
|
3344
|
+
},
|
|
3345
|
+
}
|
|
3346
|
+
: {}),
|
|
3347
|
+
...(startupFailure ? { startup: startupFailure } : {}),
|
|
3348
|
+
};
|
|
3349
|
+
const durableEffects =
|
|
3350
|
+
Object.keys(durableEffectsBody).length > 0
|
|
3351
|
+
? {
|
|
3352
|
+
...durableEffectsBody,
|
|
3353
|
+
digest: createHash("sha256").update(canonicalJson(durableEffectsBody)).digest("hex"),
|
|
3354
|
+
}
|
|
3355
|
+
: undefined;
|
|
3356
|
+
const lifecycleCleanupResponse =
|
|
3357
|
+
evidence && root && entry?.intendedSessionId && expected && cleanupProof
|
|
3358
|
+
? await (async () => {
|
|
3359
|
+
const cleanupIntent = lifecycleCleanupPlan(root, entry.intendedSessionId!, expected, evidence);
|
|
3360
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
3361
|
+
response: fail(
|
|
3362
|
+
"cleanup_pending",
|
|
3363
|
+
"Lifecycle failure cleanup is preauthorized for durable reconciliation.",
|
|
3364
|
+
cleanupIntent,
|
|
3365
|
+
),
|
|
3366
|
+
});
|
|
3367
|
+
return reconcileLifecycleCleanup(broker, identity, cleanupIntent);
|
|
3368
|
+
})()
|
|
3369
|
+
: undefined;
|
|
3370
|
+
if (
|
|
3371
|
+
lifecycleCleanupResponse &&
|
|
3372
|
+
!lifecycleCleanupResponse.ok &&
|
|
3373
|
+
lifecycleCleanupResponse.error.code !== "spawn_failed"
|
|
3374
|
+
)
|
|
3375
|
+
return {
|
|
3376
|
+
response: lifecycleCleanupResponse,
|
|
3377
|
+
...(durableEffects ? { durableEffects } : {}),
|
|
3378
|
+
...(startupFailure ? { startupFailure } : {}),
|
|
3379
|
+
};
|
|
3380
|
+
const terminalResponse: BrokerResponse =
|
|
3381
|
+
!response.ok &&
|
|
3382
|
+
entry?.effectMarker &&
|
|
3383
|
+
(operation === "session.create" || operation === "session.fork" || operation === "session.resume")
|
|
3384
|
+
? entry.effectIntent?.childOwnershipEstablished === false
|
|
3385
|
+
? response
|
|
3386
|
+
: startupFailure && cleanupProof
|
|
3387
|
+
? {
|
|
3388
|
+
ok: false,
|
|
3389
|
+
error: {
|
|
3390
|
+
code: "spawn_failed",
|
|
3391
|
+
message: "No ready SDK endpoint remains available.",
|
|
3392
|
+
endpoint: "unavailable",
|
|
3393
|
+
},
|
|
3394
|
+
...(durableEffects ? { durableEffects } : {}),
|
|
3395
|
+
startupFailure,
|
|
3396
|
+
}
|
|
3397
|
+
: {
|
|
3398
|
+
ok: false,
|
|
3399
|
+
error: {
|
|
3400
|
+
code: "terminal_uncertain",
|
|
3401
|
+
message:
|
|
3402
|
+
"Lifecycle startup cleanup could not be proven; retained artifacts require reconciliation.",
|
|
3403
|
+
},
|
|
3404
|
+
...(durableEffects ? { durableEffects } : {}),
|
|
3405
|
+
...(startupFailure ? { startupFailure } : {}),
|
|
3406
|
+
}
|
|
3407
|
+
: response;
|
|
3408
|
+
return {
|
|
3409
|
+
response: terminalResponse,
|
|
3410
|
+
...(durableEffects ? { durableEffects } : {}),
|
|
3411
|
+
...(startupFailure ? { startupFailure } : {}),
|
|
3412
|
+
};
|
|
3413
|
+
}
|