@sayknow-cli/coding-agent 0.3.11 → 0.3.13
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 +0 -21
- package/package.json +116 -117
- package/scripts/build-binary.ts +2 -43
- package/scripts/compile-args.ts +106 -0
- package/scripts/g004-tmux-smoke.ts +196 -36
- package/scripts/generate-docs-index.ts +3 -0
- package/src/build-metadata.ts +104 -0
- package/src/capability/mcp.ts +3 -1
- package/src/cli/daemon-cli.ts +19 -29
- package/src/cli/malloc-env-guard.test.ts +104 -0
- package/src/cli/malloc-env-guard.ts +149 -0
- package/src/cli/mcp-cli.ts +108 -26
- package/src/cli/notify-cli.ts +78 -14
- package/src/cli/session-picker.ts +23 -40
- package/src/cli/update-cli.ts +56 -37
- package/src/cli.ts +71 -10
- package/src/commands/daemon.ts +19 -5
- package/src/commands/harness.ts +515 -47
- package/src/commands/launch.ts +11 -3
- package/src/commands/mcp.ts +8 -8
- package/src/commands/notify.ts +5 -1
- package/src/commands/session.ts +6 -1
- package/src/commands/stats.ts +6 -2
- package/src/config/keybindings.ts +7 -2
- package/src/config/mcp-schema.json +21 -17
- package/src/config/model-profile-activation.ts +1 -1
- package/src/config/model-profiles.ts +23 -23
- package/src/config/settings-schema.ts +41 -1
- package/src/coordinator-mcp/model-preset.ts +100 -0
- package/src/coordinator-mcp/server.ts +2735 -435
- package/src/daemon/control-types.ts +11 -0
- package/src/daemon/operator-contract.ts +121 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
- package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
- package/src/discovery/builtin.ts +14 -0
- package/src/discovery/mcp-json.ts +14 -0
- package/src/eval/js/executor.ts +7 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/internal-urls/local-protocol.ts +29 -10
- package/src/internal-urls/skc-protocol.ts +14 -1
- package/src/main.ts +298 -75
- package/src/modes/acp/acp-agent.ts +800 -114
- package/src/modes/acp/acp-client-bridge.ts +27 -5
- package/src/modes/acp/acp-event-mapper.ts +39 -1
- package/src/modes/acp/acp-mode.ts +29 -2
- package/src/modes/components/assistant-message.ts +33 -4
- package/src/modes/components/bash-execution.ts +43 -13
- package/src/modes/components/custom-editor.ts +5 -0
- package/src/modes/components/hook-selector.ts +16 -0
- package/src/modes/components/irc-sidebar.ts +206 -0
- package/src/modes/components/model-selector.ts +17 -6
- package/src/modes/components/pet-selector.ts +55 -0
- package/src/modes/components/sayknow-pet-widget.ts +360 -0
- package/src/modes/components/session-selector.ts +223 -222
- package/src/modes/components/settings-selector.ts +10 -0
- package/src/modes/components/status-line/presets.ts +5 -19
- package/src/modes/components/status-line/segments.ts +14 -0
- package/src/modes/components/status-line/types.ts +6 -0
- package/src/modes/components/status-line.ts +13 -6
- package/src/modes/components/tool-execution.ts +48 -7
- package/src/modes/components/user-message.ts +26 -9
- package/src/modes/components/welcome.ts +4 -1
- package/src/modes/controllers/command-controller.ts +29 -3
- package/src/modes/controllers/event-controller.ts +168 -45
- package/src/modes/controllers/extension-ui-controller.ts +36 -17
- package/src/modes/controllers/input-controller.ts +16 -4
- package/src/modes/controllers/selector-controller.ts +69 -18
- package/src/modes/interactive-mode.ts +194 -10
- package/src/modes/irc-observation-ledger.ts +127 -0
- package/src/modes/rpc/rpc-client.ts +30 -6
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
- package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
- package/src/modes/types.ts +34 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/irc-message.ts +138 -0
- package/src/modes/utils/ui-helpers.ts +368 -80
- package/src/notifications/config.ts +44 -0
- package/src/notifications/daemon-paths.ts +8 -0
- package/src/notifications/index.ts +535 -75
- package/src/notifications/lifecycle-control-runtime.ts +930 -77
- package/src/notifications/notification-service.ts +710 -0
- package/src/notifications/rate-limit-pool.ts +46 -12
- package/src/notifications/telegram-daemon-control.ts +27 -2
- package/src/notifications/telegram-daemon.ts +663 -96
- package/src/notifications/telegram-reference.ts +73 -13
- package/src/notifications/topic-registry.ts +86 -9
- package/src/prompts/system/irc-peer-roster.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/skill.md +2 -2
- package/src/registry/agent-registry.ts +3 -0
- package/src/runtime-mcp/config-writer.ts +4 -3
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/types.ts +4 -3
- package/src/sdk.ts +52 -1
- package/src/secrets/obfuscator.ts +4 -2
- package/src/session/agent-session.ts +513 -148
- package/src/session/cache-economics.ts +275 -0
- package/src/session/provider-safety-stop.ts +9 -0
- package/src/session/session-dump-format.ts +6 -0
- package/src/session/session-manager.ts +877 -79
- package/src/session/session-storage.ts +582 -38
- package/src/skc-runtime/session-state-sidecar.ts +756 -135
- package/src/skc-runtime/state-runtime.ts +105 -12
- package/src/skc-runtime/tmux-common.ts +26 -7
- package/src/skc-runtime/tmux-gc.ts +52 -9
- package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
- package/src/skc-runtime/tmux-sessions.ts +871 -68
- package/src/skc-runtime/workflow-manifest.generated.json +4 -28
- package/src/skc-runtime/workflow-manifest.ts +1 -7
- package/src/slash-commands/builtin-registry.ts +152 -13
- package/src/task/executor.ts +12 -3
- package/src/task/index.ts +8 -0
- package/src/task/receipt.ts +7 -2
- package/src/task/types.ts +26 -0
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/bash-interactive.ts +9 -4
- package/src/tools/browser/readable.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +63 -6
- package/src/tools/browser.ts +8 -3
- package/src/tools/fetch.ts +5 -5
- package/src/tools/index.ts +81 -6
- package/src/tools/skill.ts +14 -11
- package/src/tools/sqlite-reader.ts +8 -4
- package/src/tui/output-block.ts +18 -6
- package/src/utils/external-editor.ts +6 -1
- package/src/utils/linkedom.ts +14 -0
- package/src/utils/pasted-image-path.ts +23 -9
- package/src/web/scrapers/arxiv.ts +2 -2
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/iacr.ts +2 -2
- package/src/web/scrapers/readthedocs.ts +2 -2
- package/src/web/scrapers/twitter.ts +3 -2
- package/src/web/scrapers/wikipedia.ts +2 -2
- 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/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 -47
- 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 -18
- 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 -25
- 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/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 -37
- 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 -3
- 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 -89
- 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 -135
- 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 -41
- 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 -130
- 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 -41
- 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 -359
- package/dist/types/config/model-equivalence.d.ts +0 -24
- package/dist/types/config/model-profile-activation.d.ts +0 -64
- 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 -4183
- package/dist/types/config/settings.d.ts +0 -156
- 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/policy.d.ts +0 -24
- package/dist/types/coordinator-mcp/safety.d.ts +0 -26
- package/dist/types/coordinator-mcp/server.d.ts +0 -66
- 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 -57
- 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 -25
- 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 -5
- 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 -918
- 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 -39
- 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 -75
- 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 -61
- package/dist/types/modes/acp/acp-client-bridge.d.ts +0 -9
- package/dist/types/modes/acp/acp-event-mapper.d.ts +0 -34
- package/dist/types/modes/acp/acp-mode.d.ts +0 -5
- 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 -67
- 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 -53
- package/dist/types/modes/components/index.d.ts +0 -35
- 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/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/session-observer-overlay.d.ts +0 -11
- package/dist/types/modes/components/session-selector.d.ts +0 -30
- package/dist/types/modes/components/settings-defs.d.ts +0 -50
- package/dist/types/modes/components/settings-selector.d.ts +0 -62
- 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 -77
- package/dist/types/modes/components/status-line.d.ts +0 -103
- 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 -54
- 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 -7
- package/dist/types/modes/components/visual-truncate.d.ts +0 -19
- package/dist/types/modes/components/welcome.d.ts +0 -45
- 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 -17
- 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 -67
- 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 -275
- 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 -45
- 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 -288
- package/dist/types/modes/rpc/rpc-mode.d.ts +0 -85
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +0 -10
- package/dist/types/modes/rpc/rpc-types.d.ts +0 -794
- 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 -94
- 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 -161
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +0 -82
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +0 -114
- 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 -292
- package/dist/types/modes/utils/abort-message.d.ts +0 -4
- package/dist/types/modes/utils/context-usage.d.ts +0 -51
- 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/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 -54
- 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 -98
- package/dist/types/notifications/daemon-paths.d.ts +0 -10
- 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 -192
- package/dist/types/notifications/lifecycle-commands.d.ts +0 -69
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +0 -99
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +0 -144
- package/dist/types/notifications/managed-daemon.d.ts +0 -48
- package/dist/types/notifications/operator-runtime.d.ts +0 -52
- package/dist/types/notifications/rate-limit-pool.d.ts +0 -95
- 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 -64
- package/dist/types/notifications/telegram-daemon.d.ts +0 -429
- package/dist/types/notifications/telegram-reference.d.ts +0 -119
- 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 -69
- 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 -62
- 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 -88
- 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 -348
- package/dist/types/sdk.d.ts +0 -254
- 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 -1185
- 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/client-bridge.d.ts +0 -88
- 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/session-dump-format.d.ts +0 -22
- package/dist/types/session/session-manager.d.ts +0 -660
- package/dist/types/session/session-storage.d.ts +0 -88
- 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 -39
- 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 -105
- package/dist/types/skc-runtime/tmux-gc.d.ts +0 -7
- package/dist/types/skc-runtime/tmux-sessions.d.ts +0 -62
- 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 -124
- 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 -87
- 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 -508
- 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 -13
- 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 -103
- 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 -319
- 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 -47
- 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 -32
- 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 -17
- 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/markit.d.ts +0 -7
- package/dist/types/utils/open.d.ts +0 -2
- package/dist/types/utils/pasted-image-path.d.ts +0 -30
- 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
|
@@ -11,8 +11,31 @@ import {
|
|
|
11
11
|
} from "../coordinator/contract";
|
|
12
12
|
import {
|
|
13
13
|
SKC_COORDINATOR_SESSION_ID_ENV,
|
|
14
|
+
SKC_COORDINATOR_SESSION_LAUNCH_ID_ENV,
|
|
15
|
+
SKC_COORDINATOR_SESSION_READINESS_FILE_ENV,
|
|
14
16
|
SKC_COORDINATOR_SESSION_STATE_FILE_ENV,
|
|
17
|
+
SKC_TMUX_OWNER_GENERATION_ENV,
|
|
18
|
+
SKC_TMUX_OWNER_SERVER_KEY_ENV,
|
|
19
|
+
SKC_TMUX_OWNER_STATE_DIR_ENV,
|
|
15
20
|
} from "../skc-runtime/session-state-sidecar";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
captureOwnerGenerationBaselineSync,
|
|
24
|
+
classifyCgroup,
|
|
25
|
+
isExactScopedBootstrapSuccessReceipt,
|
|
26
|
+
type OwnerIsolationProbe,
|
|
27
|
+
ownerProcessStartTime,
|
|
28
|
+
planTmuxOwnerIsolation,
|
|
29
|
+
replaceOwnerGenerationSync,
|
|
30
|
+
type TmuxServerProof,
|
|
31
|
+
} from "../skc-runtime/tmux-owner-isolation";
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
type CoordinatorModelProfileLoader,
|
|
35
|
+
loadCoordinatorModelProfiles,
|
|
36
|
+
resolveCoordinatorMpreset,
|
|
37
|
+
} from "./model-preset";
|
|
38
|
+
|
|
16
39
|
import {
|
|
17
40
|
assertCoordinatorArtifactPath,
|
|
18
41
|
assertCoordinatorWorkdir,
|
|
@@ -44,8 +67,15 @@ interface JsonRpcResponse {
|
|
|
44
67
|
interface SessionStartInput {
|
|
45
68
|
cwd: string;
|
|
46
69
|
prompt?: string;
|
|
70
|
+
mpreset?: string | null;
|
|
47
71
|
namespace: { profile: string | null; repo: string | null };
|
|
48
72
|
worktree: true;
|
|
73
|
+
sessionId: string;
|
|
74
|
+
launchId: string;
|
|
75
|
+
/** Canonical, coordinator-owned immutable readiness marker path. */
|
|
76
|
+
readinessMarkerFile: string;
|
|
77
|
+
/** @deprecated Compatibility alias for injected launch providers. */
|
|
78
|
+
readinessFile: string;
|
|
49
79
|
}
|
|
50
80
|
|
|
51
81
|
interface SessionRegisterInput {
|
|
@@ -79,10 +109,20 @@ interface RuntimeSessionStatePayload extends CoordinatorSessionState {
|
|
|
79
109
|
error?: { code: string; message: string; recoverable: boolean } | null;
|
|
80
110
|
}
|
|
81
111
|
|
|
112
|
+
interface CommandResult {
|
|
113
|
+
exitCode: number;
|
|
114
|
+
stdout: string;
|
|
115
|
+
stderr: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type CommandRunner = (command: string[], stdinLine?: string) => Promise<CommandResult>;
|
|
119
|
+
|
|
82
120
|
interface CoordinatorServices {
|
|
83
121
|
listSessions?: () => unknown[] | Promise<unknown[]>;
|
|
84
122
|
startSession?: (input: SessionStartInput) => unknown | Promise<unknown>;
|
|
85
|
-
commandRunner?:
|
|
123
|
+
commandRunner?: CommandRunner;
|
|
124
|
+
ownerIsolationProbe?: OwnerIsolationProbe;
|
|
125
|
+
resolveModelProfiles?: CoordinatorModelProfileLoader;
|
|
86
126
|
}
|
|
87
127
|
|
|
88
128
|
interface CoordinatorMcpServerOptions {
|
|
@@ -92,7 +132,7 @@ interface CoordinatorMcpServerOptions {
|
|
|
92
132
|
|
|
93
133
|
interface LegacyHandlerOptions {
|
|
94
134
|
env?: NodeJS.ProcessEnv;
|
|
95
|
-
createSession?: () => unknown;
|
|
135
|
+
createSession?: (input: SessionStartInput) => unknown;
|
|
96
136
|
}
|
|
97
137
|
|
|
98
138
|
type TurnStatus =
|
|
@@ -112,16 +152,28 @@ interface TurnRecord {
|
|
|
112
152
|
session_id: string;
|
|
113
153
|
namespace: { profile: string | null; repo: string | null };
|
|
114
154
|
status: TurnStatus;
|
|
115
|
-
prompt: {
|
|
155
|
+
prompt: {
|
|
156
|
+
text: string;
|
|
157
|
+
created_at: string;
|
|
158
|
+
source: "mcp" | "question_answer";
|
|
159
|
+
};
|
|
116
160
|
delivery: {
|
|
117
161
|
delivered: boolean;
|
|
118
162
|
queued: boolean;
|
|
119
163
|
target: string | null;
|
|
120
164
|
tmux_keys_sent?: boolean;
|
|
121
165
|
prompt_acknowledged?: boolean;
|
|
122
|
-
state?:
|
|
166
|
+
state?:
|
|
167
|
+
| "queued"
|
|
168
|
+
| "awaiting_runtime_readiness"
|
|
169
|
+
| "delivering"
|
|
170
|
+
| "tmux_keys_sent"
|
|
171
|
+
| "acknowledged"
|
|
172
|
+
| "unavailable"
|
|
173
|
+
| "unacknowledged";
|
|
123
174
|
attempts: Array<{
|
|
124
175
|
delivered: boolean;
|
|
176
|
+
attempt_id?: string;
|
|
125
177
|
created_at: string;
|
|
126
178
|
reason: string | null;
|
|
127
179
|
channel?: "tmux_keys" | "runtime_ack";
|
|
@@ -138,7 +190,11 @@ interface TurnRecord {
|
|
|
138
190
|
};
|
|
139
191
|
evidence: Array<Record<string, unknown>>;
|
|
140
192
|
error: { code: string; message: string; recoverable: boolean } | null;
|
|
141
|
-
liveness: {
|
|
193
|
+
liveness: {
|
|
194
|
+
checked_at: string | null;
|
|
195
|
+
live: boolean | null;
|
|
196
|
+
reason: string | null;
|
|
197
|
+
};
|
|
142
198
|
created_at: string;
|
|
143
199
|
updated_at: string;
|
|
144
200
|
started_at: string | null;
|
|
@@ -163,9 +219,22 @@ interface CoordinatorSessionState {
|
|
|
163
219
|
current_turn_id: string | null;
|
|
164
220
|
last_turn_id: string | null;
|
|
165
221
|
updated_at: string;
|
|
166
|
-
source: "coordinator" | "agent_session_event";
|
|
222
|
+
source: "coordinator" | "agent_session_event" | "process_postmortem";
|
|
223
|
+
|
|
167
224
|
live: boolean | null;
|
|
168
225
|
reason: string | null;
|
|
226
|
+
cwd?: string;
|
|
227
|
+
workdir?: string;
|
|
228
|
+
session_file?: string | null;
|
|
229
|
+
owner_generation?: string;
|
|
230
|
+
event?: string;
|
|
231
|
+
owner_terminal?: {
|
|
232
|
+
generation: string;
|
|
233
|
+
socket_key: string;
|
|
234
|
+
classification: string;
|
|
235
|
+
observer: string;
|
|
236
|
+
observed_at: string;
|
|
237
|
+
};
|
|
169
238
|
}
|
|
170
239
|
|
|
171
240
|
type CoordinatorEventKind =
|
|
@@ -175,6 +244,7 @@ type CoordinatorEventKind =
|
|
|
175
244
|
| "turn.queued"
|
|
176
245
|
| "turn.delivering"
|
|
177
246
|
| "turn.active"
|
|
247
|
+
| "turn.acknowledged"
|
|
178
248
|
| "turn.waiting_for_answer"
|
|
179
249
|
| "turn.completed"
|
|
180
250
|
| "turn.failed"
|
|
@@ -217,6 +287,10 @@ const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
|
|
|
217
287
|
const PROMPT_ACK_TIMEOUT_REASON = "runtime_prompt_ack_timeout";
|
|
218
288
|
const DEFAULT_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 10_000;
|
|
219
289
|
const MAX_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
290
|
+
const RUNTIME_READINESS_TIMEOUT_REASON = "runtime_readiness_timeout";
|
|
291
|
+
const DEFAULT_RUNTIME_READINESS_TIMEOUT_MS = 10_000;
|
|
292
|
+
const MAX_RUNTIME_READINESS_TIMEOUT_MS = 5 * 60 * 1000;
|
|
293
|
+
|
|
220
294
|
const ACTIVE_TURN_STATUSES = new Set<TurnStatus>(["delivering", "active", "waiting_for_answer", "completing"]);
|
|
221
295
|
const TERMINAL_TURN_STATUSES = new Set<TurnStatus>(["completed", "failed", "cancelled", "superseded"]);
|
|
222
296
|
const TURN_ID_PATTERN = /^turn-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -232,7 +306,12 @@ function textResult(
|
|
|
232
306
|
isError = false,
|
|
233
307
|
): { content: Array<{ type: "text"; text: string }>; isError: boolean } {
|
|
234
308
|
return {
|
|
235
|
-
content: [
|
|
309
|
+
content: [
|
|
310
|
+
{
|
|
311
|
+
type: "text",
|
|
312
|
+
text: typeof payload === "string" ? payload : JSON.stringify(payload),
|
|
313
|
+
},
|
|
314
|
+
],
|
|
236
315
|
isError,
|
|
237
316
|
};
|
|
238
317
|
}
|
|
@@ -242,13 +321,27 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
242
321
|
description: string;
|
|
243
322
|
inputSchema: Record<string, unknown>;
|
|
244
323
|
} {
|
|
245
|
-
const allowMutation = {
|
|
324
|
+
const allowMutation = {
|
|
325
|
+
type: "boolean",
|
|
326
|
+
description: "Required and must be true for mutating tools.",
|
|
327
|
+
};
|
|
246
328
|
const cwd = {
|
|
247
329
|
type: "string",
|
|
248
330
|
description: "Canonicalized SKC worktree or project directory inside configured roots.",
|
|
249
331
|
};
|
|
250
|
-
const sessionId = {
|
|
251
|
-
|
|
332
|
+
const sessionId = {
|
|
333
|
+
type: "string",
|
|
334
|
+
description: "SKC coordinator bridge session id.",
|
|
335
|
+
};
|
|
336
|
+
const pathField = {
|
|
337
|
+
type: "string",
|
|
338
|
+
description: "Artifact path inside configured safe roots.",
|
|
339
|
+
};
|
|
340
|
+
const mpreset = {
|
|
341
|
+
type: "string",
|
|
342
|
+
description:
|
|
343
|
+
"Optional SKC model profile (`skc --mpreset <profile>`) to authoritatively activate for a fresh session; resolved through the merged built-in/custom profile registry and applied from the first turn. Unknown names are rejected with the available-profile listing.",
|
|
344
|
+
};
|
|
252
345
|
const common = { type: "object", properties: {} as Record<string, unknown> };
|
|
253
346
|
if (name === "skc_coordinator_register_session") {
|
|
254
347
|
return {
|
|
@@ -277,7 +370,12 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
277
370
|
description: "Start a SKC worktree/tmux oriented session through the coordinator bridge.",
|
|
278
371
|
inputSchema: {
|
|
279
372
|
type: "object",
|
|
280
|
-
properties: {
|
|
373
|
+
properties: {
|
|
374
|
+
cwd,
|
|
375
|
+
prompt: { type: "string" },
|
|
376
|
+
mpreset,
|
|
377
|
+
allow_mutation: allowMutation,
|
|
378
|
+
},
|
|
281
379
|
required: ["cwd", "allow_mutation"],
|
|
282
380
|
},
|
|
283
381
|
};
|
|
@@ -306,7 +404,11 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
306
404
|
description: "Read authoritative durable turn state plus bounded advisory tmux status.",
|
|
307
405
|
inputSchema: {
|
|
308
406
|
type: "object",
|
|
309
|
-
properties: {
|
|
407
|
+
properties: {
|
|
408
|
+
session_id: sessionId,
|
|
409
|
+
turn_id: { type: "string" },
|
|
410
|
+
lines: { type: "number" },
|
|
411
|
+
},
|
|
310
412
|
required: ["turn_id"],
|
|
311
413
|
},
|
|
312
414
|
};
|
|
@@ -375,7 +477,11 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
375
477
|
return {
|
|
376
478
|
name,
|
|
377
479
|
description: "Read one bounded artifact from configured safe roots.",
|
|
378
|
-
inputSchema: {
|
|
480
|
+
inputSchema: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: { path: pathField },
|
|
483
|
+
required: ["path"],
|
|
484
|
+
},
|
|
379
485
|
};
|
|
380
486
|
}
|
|
381
487
|
if (name === "skc_coordinator_read_status") {
|
|
@@ -389,21 +495,35 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
389
495
|
return {
|
|
390
496
|
name,
|
|
391
497
|
description: "Read a bounded structured session tail, not tmux scrollback.",
|
|
392
|
-
inputSchema: {
|
|
498
|
+
inputSchema: {
|
|
499
|
+
type: "object",
|
|
500
|
+
properties: { session_id: sessionId, lines: { type: "number" } },
|
|
501
|
+
},
|
|
393
502
|
};
|
|
394
503
|
}
|
|
395
504
|
if (name === "skc_coordinator_list_questions") {
|
|
396
505
|
return {
|
|
397
506
|
name,
|
|
398
507
|
description: "List bounded structured questions for coordinator coordination.",
|
|
399
|
-
inputSchema: {
|
|
508
|
+
inputSchema: {
|
|
509
|
+
type: "object",
|
|
510
|
+
properties: { session_id: sessionId, status: { type: "string" } },
|
|
511
|
+
},
|
|
400
512
|
};
|
|
401
513
|
}
|
|
402
514
|
if (name === "skc_coordinator_list_artifacts") {
|
|
403
|
-
return {
|
|
515
|
+
return {
|
|
516
|
+
name,
|
|
517
|
+
description: "List known safe artifact roots for coordinator coordination.",
|
|
518
|
+
inputSchema: common,
|
|
519
|
+
};
|
|
404
520
|
}
|
|
405
521
|
if (name === "skc_coordinator_read_coordination_status") {
|
|
406
|
-
return {
|
|
522
|
+
return {
|
|
523
|
+
name,
|
|
524
|
+
description: "Read coordinator coordination reports.",
|
|
525
|
+
inputSchema: common,
|
|
526
|
+
};
|
|
407
527
|
}
|
|
408
528
|
if (name === "skc_coordinator_watch_events") {
|
|
409
529
|
return {
|
|
@@ -437,7 +557,10 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
437
557
|
type: "string",
|
|
438
558
|
description: "Delegated task or objective to run through the selected SKC workflow.",
|
|
439
559
|
},
|
|
440
|
-
prompt: {
|
|
560
|
+
prompt: {
|
|
561
|
+
type: "string",
|
|
562
|
+
description: "Alias for task; accepted when task is absent.",
|
|
563
|
+
},
|
|
441
564
|
allow_mutation: allowMutation,
|
|
442
565
|
session_id: {
|
|
443
566
|
type: "string",
|
|
@@ -452,24 +575,38 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
452
575
|
type: "boolean",
|
|
453
576
|
description: "When reusing a session with an active turn, supersede it before sending.",
|
|
454
577
|
},
|
|
578
|
+
mpreset,
|
|
455
579
|
model: {
|
|
456
580
|
type: "string",
|
|
457
581
|
description: "Optional model hint passed in prompt metadata; no provider default is implied.",
|
|
458
582
|
},
|
|
459
|
-
await_completion: {
|
|
583
|
+
await_completion: {
|
|
584
|
+
type: "boolean",
|
|
585
|
+
description: "If true, poll the turn until terminal or timeout.",
|
|
586
|
+
},
|
|
460
587
|
timeout_ms: {
|
|
461
588
|
type: "number",
|
|
462
589
|
description:
|
|
463
590
|
"Bounded await timeout in milliseconds, capped at 30 minutes like skc_coordinator_await_turn.",
|
|
464
591
|
},
|
|
465
|
-
poll_interval_ms: {
|
|
466
|
-
|
|
592
|
+
poll_interval_ms: {
|
|
593
|
+
type: "number",
|
|
594
|
+
description: "Bounded await polling interval.",
|
|
595
|
+
},
|
|
596
|
+
lines: {
|
|
597
|
+
type: "number",
|
|
598
|
+
description: "Bounded advisory tail lines returned with await/read payloads.",
|
|
599
|
+
},
|
|
467
600
|
},
|
|
468
601
|
required: ["cwd", "allow_mutation"],
|
|
469
602
|
},
|
|
470
603
|
};
|
|
471
604
|
}
|
|
472
|
-
return {
|
|
605
|
+
return {
|
|
606
|
+
name,
|
|
607
|
+
description: "List known scoped SKC coordinator bridge sessions.",
|
|
608
|
+
inputSchema: common,
|
|
609
|
+
};
|
|
473
610
|
}
|
|
474
611
|
|
|
475
612
|
type DelegateWorkflow = "plan" | "execute" | "team";
|
|
@@ -535,13 +672,80 @@ function workflowPrompt(
|
|
|
535
672
|
].join("\n");
|
|
536
673
|
}
|
|
537
674
|
|
|
675
|
+
const PRIVATE_SESSION_CONTROL_FIELDS = new Set([
|
|
676
|
+
"tmux_socket_key",
|
|
677
|
+
"tmuxSocketKey",
|
|
678
|
+
"tmux_owner_generation",
|
|
679
|
+
"tmuxOwnerGeneration",
|
|
680
|
+
"tmux_owner_state_dir",
|
|
681
|
+
"tmuxOwnerStateDir",
|
|
682
|
+
"tmux_owner_server_key",
|
|
683
|
+
"tmuxOwnerServerKey",
|
|
684
|
+
"tmux_owner_server_pid",
|
|
685
|
+
"tmuxOwnerServerPid",
|
|
686
|
+
"tmux_owner_server_start_time",
|
|
687
|
+
"tmuxOwnerServerStartTime",
|
|
688
|
+
"tmux_native_session_id",
|
|
689
|
+
"tmuxNativeSessionId",
|
|
690
|
+
"pane_id",
|
|
691
|
+
"paneId",
|
|
692
|
+
"socket_key",
|
|
693
|
+
"socketKey",
|
|
694
|
+
"owner_generation",
|
|
695
|
+
"ownerGeneration",
|
|
696
|
+
"state_dir",
|
|
697
|
+
"ownerStateDir",
|
|
698
|
+
"owner_server_key",
|
|
699
|
+
"ownerServerKey",
|
|
700
|
+
"owner_server_pid",
|
|
701
|
+
"ownerServerPid",
|
|
702
|
+
"owner_server_start_time",
|
|
703
|
+
"ownerServerStartTime",
|
|
704
|
+
"owner_terminal",
|
|
705
|
+
"ownerTerminal",
|
|
706
|
+
"generation",
|
|
707
|
+
"server_key",
|
|
708
|
+
"intent_id",
|
|
709
|
+
"dedupe_key",
|
|
710
|
+
"__coordinatorOwnerTransaction",
|
|
711
|
+
]);
|
|
712
|
+
|
|
713
|
+
function publicSessionProjection(session: Record<string, unknown>): Record<string, unknown> {
|
|
714
|
+
return Object.fromEntries(Object.entries(session).filter(([key]) => !PRIVATE_SESSION_CONTROL_FIELDS.has(key)));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function publicCoordinatorResponse(value: unknown): unknown {
|
|
718
|
+
if (Array.isArray(value)) return value.map(publicCoordinatorResponse);
|
|
719
|
+
const record = asRecord(value);
|
|
720
|
+
if (!record) return value;
|
|
721
|
+
return Object.fromEntries(
|
|
722
|
+
Object.entries(publicSessionProjection(record)).map(([key, item]) => [key, publicCoordinatorResponse(item)]),
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
|
|
538
726
|
function normalizeSession(session: Record<string, unknown>): Record<string, unknown> {
|
|
727
|
+
const suppliedIds = [session.session_id, session.sessionId, session.name].filter(
|
|
728
|
+
(value): value is string => typeof value === "string",
|
|
729
|
+
);
|
|
730
|
+
if (suppliedIds.length === 0) throw new Error("invalid_session_id");
|
|
731
|
+
const sessionId = safeExternalId("session", suppliedIds[0]);
|
|
732
|
+
if (suppliedIds.some(value => value !== sessionId)) throw new Error("session_id_conflict");
|
|
539
733
|
return {
|
|
540
|
-
session_id: session.sessionId ?? session.session_id ?? session.name ?? "unknown",
|
|
541
734
|
...(session.tmuxSession ? { tmux_session: session.tmuxSession } : {}),
|
|
735
|
+
...(session.tmuxTarget ? { tmux_target: session.tmuxTarget } : {}),
|
|
736
|
+
...(session.tmuxSocketKey ? { tmux_socket_key: session.tmuxSocketKey } : {}),
|
|
737
|
+
...(session.tmuxOwnerGeneration ? { tmux_owner_generation: session.tmuxOwnerGeneration } : {}),
|
|
738
|
+
...(session.tmuxOwnerStateDir ? { tmux_owner_state_dir: session.tmuxOwnerStateDir } : {}),
|
|
739
|
+
...(session.tmuxOwnerServerKey ? { tmux_owner_server_key: session.tmuxOwnerServerKey } : {}),
|
|
740
|
+
...(session.tmuxOwnerServerPid ? { tmux_owner_server_pid: session.tmuxOwnerServerPid } : {}),
|
|
741
|
+
...(session.tmuxOwnerServerStartTime ? { tmux_owner_server_start_time: session.tmuxOwnerServerStartTime } : {}),
|
|
742
|
+
...(session.tmuxNativeSessionId ? { tmux_native_session_id: session.tmuxNativeSessionId } : {}),
|
|
743
|
+
...(session.paneId ? { pane_id: session.paneId } : {}),
|
|
542
744
|
...(session.cwd ? { cwd: session.cwd } : {}),
|
|
543
745
|
...(session.createdAt ? { created_at: session.createdAt } : {}),
|
|
544
746
|
...session,
|
|
747
|
+
session_id: sessionId,
|
|
748
|
+
sessionId,
|
|
545
749
|
};
|
|
546
750
|
}
|
|
547
751
|
|
|
@@ -553,15 +757,379 @@ async function canonicalizePath(value: string): Promise<string> {
|
|
|
553
757
|
}
|
|
554
758
|
}
|
|
555
759
|
|
|
760
|
+
interface RuntimeReadinessMarker {
|
|
761
|
+
schema_version: 1;
|
|
762
|
+
session_id: string;
|
|
763
|
+
launch_id: string;
|
|
764
|
+
state: "ready_for_input";
|
|
765
|
+
event: "interactive_input_ready";
|
|
766
|
+
source: "skc_interactive_runtime";
|
|
767
|
+
ready_for_input: true;
|
|
768
|
+
created_at: string;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async function readRuntimeReadinessMarker(
|
|
772
|
+
markerFile: string,
|
|
773
|
+
sessionId: string,
|
|
774
|
+
launchId: string,
|
|
775
|
+
readinessRoot?: string,
|
|
776
|
+
): Promise<RuntimeReadinessMarker | null> {
|
|
777
|
+
try {
|
|
778
|
+
const expectedFile = path.resolve(markerFile);
|
|
779
|
+
if (readinessRoot) {
|
|
780
|
+
const canonicalRoot = await fs.realpath(readinessRoot);
|
|
781
|
+
const canonicalParent = await fs.realpath(path.dirname(expectedFile));
|
|
782
|
+
if (
|
|
783
|
+
!expectedFile.startsWith(`${canonicalRoot}${path.sep}`) ||
|
|
784
|
+
!canonicalParent.startsWith(`${canonicalRoot}${path.sep}`)
|
|
785
|
+
)
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
const stat = await fs.lstat(expectedFile);
|
|
789
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return null;
|
|
790
|
+
const marker = JSON.parse(await fs.readFile(expectedFile, "utf8")) as RuntimeReadinessMarker;
|
|
791
|
+
return marker.schema_version === 1 &&
|
|
792
|
+
marker.session_id === sessionId &&
|
|
793
|
+
marker.launch_id === launchId &&
|
|
794
|
+
marker.state === "ready_for_input" &&
|
|
795
|
+
marker.event === "interactive_input_ready" &&
|
|
796
|
+
marker.source === "skc_interactive_runtime" &&
|
|
797
|
+
marker.ready_for_input === true &&
|
|
798
|
+
typeof marker.created_at === "string" &&
|
|
799
|
+
marker.created_at.length > 0 &&
|
|
800
|
+
Number.isFinite(Date.parse(marker.created_at))
|
|
801
|
+
? marker
|
|
802
|
+
: null;
|
|
803
|
+
} catch {
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function waitForRuntimeReadinessMarker(
|
|
809
|
+
markerFile: string,
|
|
810
|
+
sessionId: string,
|
|
811
|
+
launchId: string,
|
|
812
|
+
readinessRoot: string,
|
|
813
|
+
timeoutMs: number,
|
|
814
|
+
): Promise<boolean> {
|
|
815
|
+
const ready = async (): Promise<boolean> =>
|
|
816
|
+
(await readRuntimeReadinessMarker(markerFile, sessionId, launchId, readinessRoot)) !== null;
|
|
817
|
+
if (await ready()) return true;
|
|
818
|
+
|
|
819
|
+
const markerDir = path.dirname(markerFile);
|
|
820
|
+
await ensureDir(markerDir);
|
|
821
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
822
|
+
let wake = Promise.withResolvers<void>();
|
|
823
|
+
let watcher: nodeFs.FSWatcher | null = null;
|
|
824
|
+
try {
|
|
825
|
+
watcher = nodeFs.watch(markerDir, (_eventType, filename) => {
|
|
826
|
+
if (filename === path.basename(markerFile)) wake.resolve();
|
|
827
|
+
});
|
|
828
|
+
// A marker may be installed between the first read and watch creation.
|
|
829
|
+
if (await ready()) return true;
|
|
830
|
+
while (Date.now() < deadline) {
|
|
831
|
+
const remainingMs = Math.max(1, deadline - Date.now());
|
|
832
|
+
let timer: NodeJS.Timeout | null = null;
|
|
833
|
+
const fallback = new Promise<void>(resolve => {
|
|
834
|
+
timer = setTimeout(resolve, Math.min(50, remainingMs));
|
|
835
|
+
timer.unref?.();
|
|
836
|
+
});
|
|
837
|
+
await Promise.race([wake.promise, fallback]);
|
|
838
|
+
if (timer) clearTimeout(timer);
|
|
839
|
+
wake = Promise.withResolvers<void>();
|
|
840
|
+
if (await ready()) return true;
|
|
841
|
+
}
|
|
842
|
+
return false;
|
|
843
|
+
} finally {
|
|
844
|
+
watcher?.close();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
556
848
|
async function ensureDir(dir: string): Promise<void> {
|
|
557
849
|
await fs.mkdir(dir, { recursive: true });
|
|
558
850
|
}
|
|
559
851
|
|
|
852
|
+
class CoordinatorStateError extends Error {
|
|
853
|
+
constructor(readonly reason: "invalid" | "unreadable") {
|
|
854
|
+
super(`coordinator_state_${reason}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const COORDINATOR_SESSION_STATES = new Set<CoordinatorSessionStateValue>([
|
|
859
|
+
"booting",
|
|
860
|
+
"ready_for_input",
|
|
861
|
+
"running",
|
|
862
|
+
"needs_user_input",
|
|
863
|
+
"completed",
|
|
864
|
+
"errored",
|
|
865
|
+
"stale",
|
|
866
|
+
"unknown",
|
|
867
|
+
]);
|
|
868
|
+
const COORDINATOR_EVENT_KINDS = new Set<CoordinatorEventKind>([
|
|
869
|
+
"session.registered",
|
|
870
|
+
"session.started",
|
|
871
|
+
"session.state_changed",
|
|
872
|
+
"turn.queued",
|
|
873
|
+
"turn.delivering",
|
|
874
|
+
"turn.active",
|
|
875
|
+
"turn.acknowledged",
|
|
876
|
+
"turn.waiting_for_answer",
|
|
877
|
+
"turn.completed",
|
|
878
|
+
"turn.failed",
|
|
879
|
+
"turn.cancelled",
|
|
880
|
+
"turn.superseded",
|
|
881
|
+
"question.opened",
|
|
882
|
+
"question.answered",
|
|
883
|
+
"report.written",
|
|
884
|
+
"tmux.delivery_succeeded",
|
|
885
|
+
"tmux.delivery_failed",
|
|
886
|
+
"delegation.started",
|
|
887
|
+
]);
|
|
888
|
+
|
|
889
|
+
function coordinatorStateError(reason: "invalid" | "unreadable"): CoordinatorStateError {
|
|
890
|
+
return new CoordinatorStateError(reason);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function isValidCoordinatorSessionState(value: unknown, sessionId: string): value is CoordinatorSessionState {
|
|
894
|
+
const record = asRecord(value);
|
|
895
|
+
return (
|
|
896
|
+
record?.schema_version === 1 &&
|
|
897
|
+
record.session_id === sessionId &&
|
|
898
|
+
typeof record.state === "string" &&
|
|
899
|
+
COORDINATOR_SESSION_STATES.has(record.state as CoordinatorSessionStateValue) &&
|
|
900
|
+
typeof record.ready_for_input === "boolean" &&
|
|
901
|
+
record.ready_for_input === (record.state === "ready_for_input" || record.state === "completed") &&
|
|
902
|
+
(record.current_turn_id === null ||
|
|
903
|
+
(typeof record.current_turn_id === "string" && TURN_ID_PATTERN.test(record.current_turn_id))) &&
|
|
904
|
+
(record.last_turn_id === null ||
|
|
905
|
+
(typeof record.last_turn_id === "string" && TURN_ID_PATTERN.test(record.last_turn_id))) &&
|
|
906
|
+
typeof record.updated_at === "string" &&
|
|
907
|
+
Number.isFinite(Date.parse(record.updated_at)) &&
|
|
908
|
+
(record.source === "coordinator" ||
|
|
909
|
+
record.source === "agent_session_event" ||
|
|
910
|
+
record.source === "process_postmortem") &&
|
|
911
|
+
(record.live === null || typeof record.live === "boolean") &&
|
|
912
|
+
(record.reason === null || typeof record.reason === "string")
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const MAX_RUNTIME_FINAL_RESPONSE_TEXT_LENGTH = 16_384;
|
|
917
|
+
const MAX_RUNTIME_FINAL_RESPONSE_SOURCE_LENGTH = 256;
|
|
918
|
+
const MAX_RUNTIME_FINAL_RESPONSE_ARTIFACT_PATH_LENGTH = 1_024;
|
|
919
|
+
const MAX_RUNTIME_ERROR_CODE_LENGTH = 128;
|
|
920
|
+
const MAX_RUNTIME_ERROR_MESSAGE_LENGTH = 4_096;
|
|
921
|
+
|
|
922
|
+
function isBoundedNullableString(value: unknown, maxLength: number): value is string | null {
|
|
923
|
+
return value === null || (typeof value === "string" && value.length <= maxLength);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function isValidRuntimeFinalResponse(value: unknown): value is CoordinatorFinalResponse {
|
|
927
|
+
const record = asRecord(value);
|
|
928
|
+
return (
|
|
929
|
+
record !== null &&
|
|
930
|
+
Object.keys(record).every(key => ["text", "format", "source", "artifact_path", "truncated"].includes(key)) &&
|
|
931
|
+
isBoundedNullableString(record.text, MAX_RUNTIME_FINAL_RESPONSE_TEXT_LENGTH) &&
|
|
932
|
+
record.format === "markdown" &&
|
|
933
|
+
(record.source === null ||
|
|
934
|
+
(typeof record.source === "string" &&
|
|
935
|
+
record.source.length > 0 &&
|
|
936
|
+
record.source.length <= MAX_RUNTIME_FINAL_RESPONSE_SOURCE_LENGTH)) &&
|
|
937
|
+
(record.artifact_path === null ||
|
|
938
|
+
(typeof record.artifact_path === "string" &&
|
|
939
|
+
record.artifact_path.length > 0 &&
|
|
940
|
+
record.artifact_path.length <= MAX_RUNTIME_FINAL_RESPONSE_ARTIFACT_PATH_LENGTH &&
|
|
941
|
+
!path.isAbsolute(record.artifact_path) &&
|
|
942
|
+
!record.artifact_path.split(/[\\/]/).includes(".."))) &&
|
|
943
|
+
typeof record.truncated === "boolean"
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function isValidRuntimeError(value: unknown): value is NonNullable<RuntimeSessionStatePayload["error"]> {
|
|
948
|
+
const record = asRecord(value);
|
|
949
|
+
return (
|
|
950
|
+
record !== null &&
|
|
951
|
+
Object.keys(record).every(key => ["code", "message", "recoverable"].includes(key)) &&
|
|
952
|
+
typeof record.code === "string" &&
|
|
953
|
+
record.code.length > 0 &&
|
|
954
|
+
record.code.length <= MAX_RUNTIME_ERROR_CODE_LENGTH &&
|
|
955
|
+
typeof record.message === "string" &&
|
|
956
|
+
record.message.length > 0 &&
|
|
957
|
+
record.message.length <= MAX_RUNTIME_ERROR_MESSAGE_LENGTH &&
|
|
958
|
+
typeof record.recoverable === "boolean"
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function isValidOwnerTerminalRuntimeReceipt(value: unknown, sessionId: string): boolean {
|
|
963
|
+
const record = asRecord(value);
|
|
964
|
+
if (!record) return false;
|
|
965
|
+
const baseKeys = [
|
|
966
|
+
"generation",
|
|
967
|
+
"socket_key",
|
|
968
|
+
"signal",
|
|
969
|
+
"result",
|
|
970
|
+
"classification",
|
|
971
|
+
"observer",
|
|
972
|
+
"observed_at",
|
|
973
|
+
"dedupe_key",
|
|
974
|
+
];
|
|
975
|
+
const keys = Object.keys(record);
|
|
976
|
+
if (!keys.every(key => [...baseKeys, "intent_id"].includes(key)) || baseKeys.some(key => !keys.includes(key)))
|
|
977
|
+
return false;
|
|
978
|
+
if (
|
|
979
|
+
!validRuntimeOwnerGeneration(record.generation) ||
|
|
980
|
+
!validRuntimeOwnerGeneration(record.socket_key) ||
|
|
981
|
+
typeof record.signal !== "string" ||
|
|
982
|
+
!["SIGTERM", "SIGHUP", "SIGINT", "SIGKILL", "EXIT", "MANUAL", "UNKNOWN"].includes(record.signal) ||
|
|
983
|
+
typeof record.result !== "string" ||
|
|
984
|
+
!record.result ||
|
|
985
|
+
record.result.length > 64 ||
|
|
986
|
+
!["expected_operator_shutdown", "unexpected_owner_loss", "non_operator_cleanup"].includes(
|
|
987
|
+
String(record.classification),
|
|
988
|
+
) ||
|
|
989
|
+
!["sidecar", "raw_monitor"].includes(String(record.observer)) ||
|
|
990
|
+
typeof record.observed_at !== "string" ||
|
|
991
|
+
!Number.isFinite(Date.parse(record.observed_at)) ||
|
|
992
|
+
record.dedupe_key !== `owner-loss:${sessionId}:${record.generation}`
|
|
993
|
+
)
|
|
994
|
+
return false;
|
|
995
|
+
if (record.classification === "expected_operator_shutdown")
|
|
996
|
+
return (
|
|
997
|
+
record.signal === "SIGTERM" &&
|
|
998
|
+
record.result === "owner_term_then_session_cleanup" &&
|
|
999
|
+
typeof record.intent_id === "string" &&
|
|
1000
|
+
record.intent_id.length > 0
|
|
1001
|
+
);
|
|
1002
|
+
if (record.intent_id !== undefined) return false;
|
|
1003
|
+
if (record.classification === "non_operator_cleanup") return record.result === "cleanup";
|
|
1004
|
+
return ["owner_lost", "cleanup", "process_postmortem", "exit", "unknown_terminal"].includes(record.result);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function isValidRuntimeSessionState(value: unknown, sessionId: string): value is RuntimeSessionStatePayload {
|
|
1008
|
+
if (!isValidCoordinatorSessionState(value, sessionId)) return false;
|
|
1009
|
+
const record = asRecord(value);
|
|
1010
|
+
if (!record) return false;
|
|
1011
|
+
const finalResponse = record.final_response;
|
|
1012
|
+
const error = record.error;
|
|
1013
|
+
if (finalResponse !== undefined && !isValidRuntimeFinalResponse(finalResponse)) return false;
|
|
1014
|
+
if (record.source !== "coordinator" && record.live !== (record.state === "running")) return false;
|
|
1015
|
+
if (error !== undefined && error !== null && !isValidRuntimeError(error)) return false;
|
|
1016
|
+
if (record.owner_generation !== undefined && !validRuntimeOwnerGeneration(record.owner_generation)) return false;
|
|
1017
|
+
if (record.source === "coordinator") return finalResponse === undefined && (error === undefined || error === null);
|
|
1018
|
+
if (record.source === "process_postmortem") {
|
|
1019
|
+
if (record.state !== "completed" && record.state !== "errored") return false;
|
|
1020
|
+
if (record.event === "owner_terminal") {
|
|
1021
|
+
if (!isValidOwnerTerminalRuntimeReceipt(record.owner_terminal, sessionId)) return false;
|
|
1022
|
+
const ownerTerminal = asRecord(record.owner_terminal)!;
|
|
1023
|
+
const expected = ownerTerminal.classification === "expected_operator_shutdown";
|
|
1024
|
+
if (
|
|
1025
|
+
record.reason !== ownerTerminal.classification ||
|
|
1026
|
+
record.state !== (expected ? "completed" : "errored") ||
|
|
1027
|
+
(expected
|
|
1028
|
+
? error !== undefined && error !== null
|
|
1029
|
+
: error === undefined ||
|
|
1030
|
+
error === null ||
|
|
1031
|
+
error.code !== ownerTerminal.classification ||
|
|
1032
|
+
error.recoverable !== true)
|
|
1033
|
+
)
|
|
1034
|
+
return false;
|
|
1035
|
+
} else if (record.event === "process_exit") {
|
|
1036
|
+
if (
|
|
1037
|
+
record.owner_terminal !== undefined ||
|
|
1038
|
+
typeof record.exit_kind !== "string" ||
|
|
1039
|
+
record.exit_kind.length === 0 ||
|
|
1040
|
+
record.exit_kind.length > 128 ||
|
|
1041
|
+
(record.exit_code !== null &&
|
|
1042
|
+
(typeof record.exit_code !== "number" || !Number.isSafeInteger(record.exit_code))) ||
|
|
1043
|
+
(record.signal !== null &&
|
|
1044
|
+
record.signal !== "SIGINT" &&
|
|
1045
|
+
record.signal !== "SIGTERM" &&
|
|
1046
|
+
record.signal !== "SIGHUP") ||
|
|
1047
|
+
typeof record.prompt_accepted !== "boolean" ||
|
|
1048
|
+
typeof record.observed_recoverable_worktree_changes !== "boolean" ||
|
|
1049
|
+
(record.worktree_baseline_dirty !== null && typeof record.worktree_baseline_dirty !== "boolean") ||
|
|
1050
|
+
typeof record.worktree_changed_since_baseline !== "boolean"
|
|
1051
|
+
)
|
|
1052
|
+
return false;
|
|
1053
|
+
} else {
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (record.state === "completed") return error === undefined || error === null;
|
|
1058
|
+
if (record.state === "errored") return error !== undefined && error !== null;
|
|
1059
|
+
return finalResponse === undefined && (error === undefined || error === null);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const PUBLIC_COORDINATOR_ERRORS = new Set([
|
|
1063
|
+
"coordinator_workdir_required",
|
|
1064
|
+
"coordinator_workdir_roots_required",
|
|
1065
|
+
"coordinator_workdir_outside_allowed_roots",
|
|
1066
|
+
"coordinator_artifact_path_required",
|
|
1067
|
+
"coordinator_artifact_roots_required",
|
|
1068
|
+
"coordinator_artifact_outside_allowed_roots",
|
|
1069
|
+
"coordinator_mutation_class_disabled",
|
|
1070
|
+
"coordinator_mutation_call_not_allowed",
|
|
1071
|
+
"invalid_session_id",
|
|
1072
|
+
"invalid_tmux_session",
|
|
1073
|
+
"invalid_tmux_target",
|
|
1074
|
+
"invalid_turn_id",
|
|
1075
|
+
"invalid_question_id",
|
|
1076
|
+
"session_id_conflict",
|
|
1077
|
+
"tmux_session_unavailable",
|
|
1078
|
+
"tmux_target_unavailable",
|
|
1079
|
+
"coordinator_session_command_required",
|
|
1080
|
+
"coordinator_launch_attestation_missing",
|
|
1081
|
+
"coordinator_launch_attestation_mismatch",
|
|
1082
|
+
"coordinator_launch_metadata_missing",
|
|
1083
|
+
"coordinator_readiness_path_invalid",
|
|
1084
|
+
"session_origin_unverified",
|
|
1085
|
+
"coordinator_prompt_buffer_privacy_unverified",
|
|
1086
|
+
"coordinator_tmux_start_failed",
|
|
1087
|
+
"coordinator_tmux_owner_server_unsafe",
|
|
1088
|
+
"coordinator_tmux_owner_server_unverifiable",
|
|
1089
|
+
"coordinator_tmux_owner_server_race",
|
|
1090
|
+
"coordinator_evidence_paths_must_be_array",
|
|
1091
|
+
]);
|
|
1092
|
+
|
|
1093
|
+
class CoordinatorStartError extends Error {
|
|
1094
|
+
constructor(
|
|
1095
|
+
message: string,
|
|
1096
|
+
readonly cleanupStatus: "cleaned" | "failed" | "unverifiable",
|
|
1097
|
+
) {
|
|
1098
|
+
super(message);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function publicCleanupStatus(error: unknown): "cleaned" | "failed" | "unverifiable" | undefined {
|
|
1103
|
+
return error instanceof CoordinatorStartError ? error.cleanupStatus : undefined;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function publicCoordinatorError(error: unknown): string {
|
|
1107
|
+
if (error instanceof CoordinatorStateError) return `coordinator_state_${error.reason}`;
|
|
1108
|
+
const message = error instanceof Error ? error.message : "";
|
|
1109
|
+
const normalized = message
|
|
1110
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
1111
|
+
.replace(/\s+/g, " ")
|
|
1112
|
+
.trim();
|
|
1113
|
+
const code = normalized.split(":", 1)[0];
|
|
1114
|
+
return PUBLIC_COORDINATOR_ERRORS.has(code) ? code : "coordinator_request_failed";
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function isExpectedStateAbsence(error: unknown): boolean {
|
|
1118
|
+
return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
|
1119
|
+
}
|
|
1120
|
+
|
|
560
1121
|
async function readJsonFile(file: string): Promise<unknown | null> {
|
|
1122
|
+
let content: string;
|
|
1123
|
+
try {
|
|
1124
|
+
content = await fs.readFile(file, "utf8");
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
if (isExpectedStateAbsence(error)) return null;
|
|
1127
|
+
throw coordinatorStateError("unreadable");
|
|
1128
|
+
}
|
|
561
1129
|
try {
|
|
562
|
-
return JSON.parse(
|
|
1130
|
+
return JSON.parse(content);
|
|
563
1131
|
} catch {
|
|
564
|
-
|
|
1132
|
+
throw coordinatorStateError("invalid");
|
|
565
1133
|
}
|
|
566
1134
|
}
|
|
567
1135
|
|
|
@@ -571,15 +1139,16 @@ async function writeJsonFile(file: string, value: unknown): Promise<void> {
|
|
|
571
1139
|
}
|
|
572
1140
|
|
|
573
1141
|
async function listJsonFiles(dir: string): Promise<unknown[]> {
|
|
1142
|
+
let entries: string[];
|
|
574
1143
|
try {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
);
|
|
579
|
-
return values.filter(value => value !== null);
|
|
580
|
-
} catch {
|
|
581
|
-
return [];
|
|
1144
|
+
entries = await fs.readdir(dir);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (isExpectedStateAbsence(error)) return [];
|
|
1147
|
+
throw coordinatorStateError("unreadable");
|
|
582
1148
|
}
|
|
1149
|
+
return await Promise.all(
|
|
1150
|
+
entries.filter(entry => entry.endsWith(".json")).map(entry => readJsonFile(path.join(dir, entry))),
|
|
1151
|
+
);
|
|
583
1152
|
}
|
|
584
1153
|
|
|
585
1154
|
const COORDINATOR_STATUS_EVENT_LIMIT = 100;
|
|
@@ -667,22 +1236,39 @@ function boundSummary(value: string): string {
|
|
|
667
1236
|
}
|
|
668
1237
|
|
|
669
1238
|
async function readLatestEventSeq(namespaceDir: string): Promise<number> {
|
|
670
|
-
const
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1239
|
+
const rawSequence = await readJsonFile(eventSequenceFile(namespaceDir));
|
|
1240
|
+
const events = await readCoordinatorEvents(namespaceDir);
|
|
1241
|
+
const journalLatestSeq = events.at(-1)?.seq ?? 0;
|
|
1242
|
+
if (rawSequence === null) return journalLatestSeq;
|
|
1243
|
+
const sequence = asRecord(rawSequence);
|
|
1244
|
+
if (
|
|
1245
|
+
!sequence ||
|
|
1246
|
+
Object.keys(sequence).some(key => key !== "seq" && key !== "updated_at") ||
|
|
1247
|
+
typeof sequence.seq !== "number" ||
|
|
1248
|
+
!Number.isSafeInteger(sequence.seq) ||
|
|
1249
|
+
sequence.seq < 0 ||
|
|
1250
|
+
typeof sequence.updated_at !== "string" ||
|
|
1251
|
+
!Number.isFinite(Date.parse(sequence.updated_at)) ||
|
|
1252
|
+
sequence.seq > journalLatestSeq
|
|
1253
|
+
)
|
|
1254
|
+
throw coordinatorStateError("invalid");
|
|
1255
|
+
return journalLatestSeq;
|
|
676
1256
|
}
|
|
677
1257
|
|
|
678
1258
|
const eventAppendQueues = new Map<string, Promise<unknown>>();
|
|
679
1259
|
|
|
1260
|
+
/** Lock order: namespace transaction lock, then event journal lock. */
|
|
1261
|
+
function coordinatorTransactionLockFile(namespaceDir: string): string {
|
|
1262
|
+
return path.join(namespaceDir, "locks", "mutation.lock");
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
async function withCoordinatorTransaction<T>(namespaceDir: string, operation: () => Promise<T>): Promise<T> {
|
|
1266
|
+
return await withSessionStateLock(coordinatorTransactionLockFile(namespaceDir), operation);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
680
1269
|
async function appendCoordinatorEvent(namespaceDir: string, input: CoordinatorEventInput): Promise<CoordinatorEvent> {
|
|
681
1270
|
const previous = eventAppendQueues.get(namespaceDir) ?? Promise.resolve();
|
|
682
|
-
|
|
683
|
-
const current = new Promise<void>(resolve => {
|
|
684
|
-
release = resolve;
|
|
685
|
-
});
|
|
1271
|
+
const { promise: current, resolve: release } = Promise.withResolvers<void>();
|
|
686
1272
|
const queued = previous.then(
|
|
687
1273
|
() => current,
|
|
688
1274
|
() => current,
|
|
@@ -691,57 +1277,148 @@ async function appendCoordinatorEvent(namespaceDir: string, input: CoordinatorEv
|
|
|
691
1277
|
|
|
692
1278
|
await previous.catch(() => undefined);
|
|
693
1279
|
try {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1280
|
+
return await withSessionStateLock(eventJournalFile(namespaceDir), async () => {
|
|
1281
|
+
const latestSeq = await readLatestEventSeq(namespaceDir);
|
|
1282
|
+
const seq = latestSeq + 1;
|
|
1283
|
+
const timestamp = new Date().toISOString();
|
|
1284
|
+
const event: CoordinatorEvent = {
|
|
1285
|
+
schema_version: 1,
|
|
1286
|
+
seq,
|
|
1287
|
+
id: `event-${seq.toString().padStart(12, "0")}`,
|
|
1288
|
+
timestamp,
|
|
1289
|
+
kind: input.kind,
|
|
1290
|
+
summary: boundSummary(input.summary),
|
|
1291
|
+
...(input.sessionId ? { session_id: input.sessionId } : {}),
|
|
1292
|
+
...(input.turnId ? { turn_id: input.turnId } : {}),
|
|
1293
|
+
...(input.questionId ? { question_id: input.questionId } : {}),
|
|
1294
|
+
...(input.reportId ? { report_id: input.reportId } : {}),
|
|
1295
|
+
...(input.payloadRef ? { payload_ref: input.payloadRef } : {}),
|
|
1296
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
1297
|
+
};
|
|
1298
|
+
await ensureDir(eventsDir(namespaceDir));
|
|
1299
|
+
await fs.appendFile(eventJournalFile(namespaceDir), `${JSON.stringify(event)}\n`);
|
|
1300
|
+
await writeJsonFile(eventSequenceFile(namespaceDir), {
|
|
1301
|
+
seq,
|
|
1302
|
+
updated_at: timestamp,
|
|
1303
|
+
});
|
|
1304
|
+
return event;
|
|
1305
|
+
});
|
|
715
1306
|
} finally {
|
|
716
1307
|
release();
|
|
717
1308
|
if (eventAppendQueues.get(namespaceDir) === queued) eventAppendQueues.delete(namespaceDir);
|
|
718
1309
|
}
|
|
719
1310
|
}
|
|
720
1311
|
|
|
721
|
-
|
|
1312
|
+
// Per-session mutation lock. Concurrent stdio dispatch (see pumpCoordinatorMcpStream) makes
|
|
1313
|
+
// same-session read-modify-write tool calls (notably send_prompt: read active turn → decide →
|
|
1314
|
+
// write new turn) interleave, which could persist two "active" turns while only one active-turn
|
|
1315
|
+
// pointer survives. Serializing per session — same promise-chain shape as eventAppendQueues —
|
|
1316
|
+
// restores the atomicity the former serial read loop provided, without serializing across
|
|
1317
|
+
// sessions or blocking long read-only polls (await_turn/watch_events). This is an in-process
|
|
1318
|
+
// lock distinct from the file-based withSessionStateLock/withCoordinatorTransaction, so it can
|
|
1319
|
+
// never deadlock by nesting inside them.
|
|
1320
|
+
const sessionMutationQueues = new Map<string, Promise<unknown>>();
|
|
1321
|
+
|
|
1322
|
+
async function withSessionMutation<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
1323
|
+
const previous = sessionMutationQueues.get(key) ?? Promise.resolve();
|
|
1324
|
+
const { promise: current, resolve: release } = Promise.withResolvers<void>();
|
|
1325
|
+
const queued = previous.then(
|
|
1326
|
+
() => current,
|
|
1327
|
+
() => current,
|
|
1328
|
+
);
|
|
1329
|
+
sessionMutationQueues.set(key, queued);
|
|
1330
|
+
await previous.catch(() => undefined);
|
|
1331
|
+
try {
|
|
1332
|
+
return await fn();
|
|
1333
|
+
} finally {
|
|
1334
|
+
release();
|
|
1335
|
+
if (sessionMutationQueues.get(key) === queued) sessionMutationQueues.delete(key);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function parseCoordinatorEvent(line: string): CoordinatorEvent {
|
|
1340
|
+
let event: unknown;
|
|
722
1341
|
try {
|
|
723
|
-
|
|
724
|
-
if (typeof event.seq !== "number" || typeof event.kind !== "string") return null;
|
|
725
|
-
return event;
|
|
1342
|
+
event = JSON.parse(line);
|
|
726
1343
|
} catch {
|
|
727
|
-
|
|
1344
|
+
throw coordinatorStateError("invalid");
|
|
1345
|
+
}
|
|
1346
|
+
const record = asRecord(event);
|
|
1347
|
+
const allowed = new Set([
|
|
1348
|
+
"schema_version",
|
|
1349
|
+
"seq",
|
|
1350
|
+
"id",
|
|
1351
|
+
"timestamp",
|
|
1352
|
+
"kind",
|
|
1353
|
+
"session_id",
|
|
1354
|
+
"turn_id",
|
|
1355
|
+
"question_id",
|
|
1356
|
+
"report_id",
|
|
1357
|
+
"summary",
|
|
1358
|
+
"payload_ref",
|
|
1359
|
+
"metadata",
|
|
1360
|
+
]);
|
|
1361
|
+
const metadata = asRecord(record?.metadata);
|
|
1362
|
+
const validMetadata =
|
|
1363
|
+
record?.metadata === undefined && metadata === null
|
|
1364
|
+
? true
|
|
1365
|
+
: metadata !== null &&
|
|
1366
|
+
Object.values(metadata).every(
|
|
1367
|
+
value => value === null || ["string", "number", "boolean"].includes(typeof value),
|
|
1368
|
+
);
|
|
1369
|
+
const validOptionalExternalId = (value: unknown) =>
|
|
1370
|
+
value === undefined || (typeof value === "string" && SAFE_EXTERNAL_ID_PATTERN.test(value));
|
|
1371
|
+
const validPayloadRef =
|
|
1372
|
+
record?.payload_ref === undefined ||
|
|
1373
|
+
(typeof record.payload_ref === "string" &&
|
|
1374
|
+
record.payload_ref.length > 0 &&
|
|
1375
|
+
!path.isAbsolute(record.payload_ref) &&
|
|
1376
|
+
!record.payload_ref.split(/[\\/]/).includes(".."));
|
|
1377
|
+
if (
|
|
1378
|
+
!record ||
|
|
1379
|
+
Object.keys(record).some(key => !allowed.has(key)) ||
|
|
1380
|
+
record.schema_version !== 1 ||
|
|
1381
|
+
typeof record.seq !== "number" ||
|
|
1382
|
+
!Number.isSafeInteger(record.seq) ||
|
|
1383
|
+
record.seq <= 0 ||
|
|
1384
|
+
record.id !== `event-${record.seq.toString().padStart(12, "0")}` ||
|
|
1385
|
+
typeof record.timestamp !== "string" ||
|
|
1386
|
+
!Number.isFinite(Date.parse(record.timestamp)) ||
|
|
1387
|
+
typeof record.kind !== "string" ||
|
|
1388
|
+
!COORDINATOR_EVENT_KINDS.has(record.kind as CoordinatorEventKind) ||
|
|
1389
|
+
typeof record.summary !== "string" ||
|
|
1390
|
+
record.summary.length > 240 ||
|
|
1391
|
+
!validOptionalExternalId(record.session_id) ||
|
|
1392
|
+
(record.turn_id !== undefined && (typeof record.turn_id !== "string" || !TURN_ID_PATTERN.test(record.turn_id))) ||
|
|
1393
|
+
!validOptionalExternalId(record.question_id) ||
|
|
1394
|
+
!validOptionalExternalId(record.report_id) ||
|
|
1395
|
+
!validPayloadRef ||
|
|
1396
|
+
!validMetadata
|
|
1397
|
+
) {
|
|
1398
|
+
throw coordinatorStateError("invalid");
|
|
728
1399
|
}
|
|
1400
|
+
return record as unknown as CoordinatorEvent;
|
|
729
1401
|
}
|
|
730
1402
|
|
|
731
1403
|
async function readCoordinatorEvents(namespaceDir: string): Promise<CoordinatorEvent[]> {
|
|
1404
|
+
const journalFile = eventJournalFile(namespaceDir);
|
|
1405
|
+
let content: string;
|
|
732
1406
|
try {
|
|
733
|
-
|
|
734
|
-
return content
|
|
735
|
-
.split("\n")
|
|
736
|
-
.map(line => line.trim())
|
|
737
|
-
.filter(Boolean)
|
|
738
|
-
.map(parseCoordinatorEvent)
|
|
739
|
-
.filter((event): event is CoordinatorEvent => event !== null)
|
|
740
|
-
.sort((left, right) => left.seq - right.seq);
|
|
1407
|
+
content = await fs.readFile(journalFile, "utf8");
|
|
741
1408
|
} catch (error) {
|
|
742
|
-
if ((error
|
|
743
|
-
throw
|
|
1409
|
+
if (isExpectedStateAbsence(error)) return [];
|
|
1410
|
+
throw coordinatorStateError("unreadable");
|
|
1411
|
+
}
|
|
1412
|
+
const events = content
|
|
1413
|
+
.split("\n")
|
|
1414
|
+
.map(line => line.trim())
|
|
1415
|
+
.filter(Boolean)
|
|
1416
|
+
.map(parseCoordinatorEvent);
|
|
1417
|
+
for (let index = 1; index < events.length; index++) {
|
|
1418
|
+
if (events[index].seq !== events[index - 1].seq + 1) throw coordinatorStateError("invalid");
|
|
744
1419
|
}
|
|
1420
|
+
|
|
1421
|
+
return events;
|
|
745
1422
|
}
|
|
746
1423
|
|
|
747
1424
|
function boundedEventLimit(value: unknown): number {
|
|
@@ -818,6 +1495,33 @@ function safeTmuxTarget(value: unknown): string {
|
|
|
818
1495
|
return value;
|
|
819
1496
|
}
|
|
820
1497
|
|
|
1498
|
+
function safeTmuxRuntimeIdentity(value: string): boolean {
|
|
1499
|
+
return value.length > 0 && value.length <= 160 && !/[\s\u0000-\u001f\u007f]/.test(value);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function coordinatorCleanupPredicate(expectedPid: number, nativeSessionId: string, attemptSession: string): string {
|
|
1503
|
+
return `#{&&:#{==:#{pid},${expectedPid}},#{&&:#{==:#{session_id},${nativeSessionId}},#{==:#{session_name},${attemptSession}}}}`;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function parseTmuxNewSessionRecord(
|
|
1507
|
+
stdout: string,
|
|
1508
|
+
sessionName: string,
|
|
1509
|
+
): { tmuxTarget: string; paneId: string; nativeSessionId: string } | undefined {
|
|
1510
|
+
const match = /^(\S+) (%\d+) (\$\d+)\n?$/.exec(stdout);
|
|
1511
|
+
if (!match) return undefined;
|
|
1512
|
+
const [tmuxTarget, paneId, nativeSessionId] = match.slice(1);
|
|
1513
|
+
const targetSuffix = tmuxTarget!.slice(`${sessionName}:`.length);
|
|
1514
|
+
if (
|
|
1515
|
+
!tmuxTarget!.startsWith(`${sessionName}:`) ||
|
|
1516
|
+
!/^\d+\.\d+$/.test(targetSuffix) ||
|
|
1517
|
+
!safeTmuxRuntimeIdentity(tmuxTarget!) ||
|
|
1518
|
+
!safeTmuxRuntimeIdentity(paneId!) ||
|
|
1519
|
+
!safeTmuxRuntimeIdentity(nativeSessionId!)
|
|
1520
|
+
)
|
|
1521
|
+
return undefined;
|
|
1522
|
+
return { tmuxTarget: tmuxTarget!, paneId: paneId!, nativeSessionId: nativeSessionId! };
|
|
1523
|
+
}
|
|
1524
|
+
|
|
821
1525
|
function optionalString(value: unknown): string | null {
|
|
822
1526
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
823
1527
|
}
|
|
@@ -846,8 +1550,150 @@ function sessionStateFile(namespaceDir: string, sessionId: string): string {
|
|
|
846
1550
|
return path.join(namespaceDir, "session-states", `${safeExternalId("session", sessionId)}.json`);
|
|
847
1551
|
}
|
|
848
1552
|
|
|
1553
|
+
function isTimestamp(value: unknown): value is string {
|
|
1554
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function isValidTurnRecord(value: unknown): value is TurnRecord {
|
|
1558
|
+
const turn = asRecord(value);
|
|
1559
|
+
const delivery = asRecord(turn?.delivery);
|
|
1560
|
+
const prompt = asRecord(turn?.prompt);
|
|
1561
|
+
const response = asRecord(turn?.final_response);
|
|
1562
|
+
const liveness = asRecord(turn?.liveness);
|
|
1563
|
+
const namespace = asRecord(turn?.namespace);
|
|
1564
|
+
const statuses: readonly TurnStatus[] = [
|
|
1565
|
+
"queued",
|
|
1566
|
+
"delivering",
|
|
1567
|
+
"active",
|
|
1568
|
+
"waiting_for_answer",
|
|
1569
|
+
"completing",
|
|
1570
|
+
"completed",
|
|
1571
|
+
"failed",
|
|
1572
|
+
"cancelled",
|
|
1573
|
+
"superseded",
|
|
1574
|
+
];
|
|
1575
|
+
if (
|
|
1576
|
+
turn?.schema_version !== 1 ||
|
|
1577
|
+
typeof turn.turn_id !== "string" ||
|
|
1578
|
+
!TURN_ID_PATTERN.test(turn.turn_id) ||
|
|
1579
|
+
typeof turn.session_id !== "string" ||
|
|
1580
|
+
!SAFE_EXTERNAL_ID_PATTERN.test(turn.session_id) ||
|
|
1581
|
+
!namespace ||
|
|
1582
|
+
(namespace.profile !== null && typeof namespace.profile !== "string") ||
|
|
1583
|
+
(namespace.repo !== null && typeof namespace.repo !== "string") ||
|
|
1584
|
+
!statuses.includes(turn.status as TurnStatus)
|
|
1585
|
+
)
|
|
1586
|
+
return false;
|
|
1587
|
+
if (
|
|
1588
|
+
!prompt ||
|
|
1589
|
+
typeof prompt.text !== "string" ||
|
|
1590
|
+
!isTimestamp(prompt.created_at) ||
|
|
1591
|
+
(prompt.source !== "mcp" && prompt.source !== "question_answer")
|
|
1592
|
+
)
|
|
1593
|
+
return false;
|
|
1594
|
+
if (
|
|
1595
|
+
!delivery ||
|
|
1596
|
+
typeof delivery.delivered !== "boolean" ||
|
|
1597
|
+
typeof delivery.queued !== "boolean" ||
|
|
1598
|
+
(delivery.target !== null && typeof delivery.target !== "string") ||
|
|
1599
|
+
(delivery.tmux_keys_sent !== undefined && typeof delivery.tmux_keys_sent !== "boolean") ||
|
|
1600
|
+
(delivery.prompt_acknowledged !== undefined && typeof delivery.prompt_acknowledged !== "boolean") ||
|
|
1601
|
+
(delivery.state !== undefined &&
|
|
1602
|
+
![
|
|
1603
|
+
"queued",
|
|
1604
|
+
"awaiting_runtime_readiness",
|
|
1605
|
+
"delivering",
|
|
1606
|
+
"tmux_keys_sent",
|
|
1607
|
+
"acknowledged",
|
|
1608
|
+
"unavailable",
|
|
1609
|
+
"unacknowledged",
|
|
1610
|
+
].includes(delivery.state as string)) ||
|
|
1611
|
+
!Array.isArray(delivery.attempts) ||
|
|
1612
|
+
!delivery.attempts.every(attempt => {
|
|
1613
|
+
const item = asRecord(attempt);
|
|
1614
|
+
return (
|
|
1615
|
+
item !== null &&
|
|
1616
|
+
typeof item.delivered === "boolean" &&
|
|
1617
|
+
isTimestamp(item.created_at) &&
|
|
1618
|
+
(item.reason === null || typeof item.reason === "string") &&
|
|
1619
|
+
(item.channel === undefined || item.channel === "tmux_keys" || item.channel === "runtime_ack") &&
|
|
1620
|
+
(item.tmux_keys_sent === undefined || typeof item.tmux_keys_sent === "boolean")
|
|
1621
|
+
);
|
|
1622
|
+
})
|
|
1623
|
+
)
|
|
1624
|
+
return false;
|
|
1625
|
+
if (
|
|
1626
|
+
!Array.isArray(turn.question_ids) ||
|
|
1627
|
+
!turn.question_ids.every(id => typeof id === "string" && SAFE_EXTERNAL_ID_PATTERN.test(id)) ||
|
|
1628
|
+
!isValidRuntimeFinalResponse(response) ||
|
|
1629
|
+
!Array.isArray(turn.evidence) ||
|
|
1630
|
+
!turn.evidence.every(item => asRecord(item) !== null) ||
|
|
1631
|
+
(turn.error !== null && !isValidRuntimeError(turn.error))
|
|
1632
|
+
)
|
|
1633
|
+
return false;
|
|
1634
|
+
if (
|
|
1635
|
+
!liveness ||
|
|
1636
|
+
(liveness.checked_at !== null && !isTimestamp(liveness.checked_at)) ||
|
|
1637
|
+
(liveness.live !== null && typeof liveness.live !== "boolean") ||
|
|
1638
|
+
(liveness.reason !== null && typeof liveness.reason !== "string") ||
|
|
1639
|
+
!isTimestamp(turn.created_at) ||
|
|
1640
|
+
!isTimestamp(turn.updated_at) ||
|
|
1641
|
+
(turn.started_at !== null && !isTimestamp(turn.started_at)) ||
|
|
1642
|
+
(turn.completed_at !== null && !isTimestamp(turn.completed_at))
|
|
1643
|
+
)
|
|
1644
|
+
return false;
|
|
1645
|
+
return TERMINAL_TURN_STATUSES.has(turn.status as TurnStatus)
|
|
1646
|
+
? turn.completed_at !== null && (turn.status === "failed" ? turn.error !== null : turn.error === null)
|
|
1647
|
+
: turn.completed_at === null && turn.error === null;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function isValidActiveTurnRecord(
|
|
1651
|
+
value: unknown,
|
|
1652
|
+
sessionId: string,
|
|
1653
|
+
): value is {
|
|
1654
|
+
session_id: string;
|
|
1655
|
+
turn_id: string;
|
|
1656
|
+
status: TurnStatus;
|
|
1657
|
+
updated_at: string;
|
|
1658
|
+
} {
|
|
1659
|
+
const active = asRecord(value);
|
|
1660
|
+
return (
|
|
1661
|
+
active !== null &&
|
|
1662
|
+
Object.keys(active).every(key => ["session_id", "turn_id", "status", "updated_at"].includes(key)) &&
|
|
1663
|
+
active.session_id === sessionId &&
|
|
1664
|
+
typeof active.turn_id === "string" &&
|
|
1665
|
+
TURN_ID_PATTERN.test(active.turn_id) &&
|
|
1666
|
+
ACTIVE_TURN_STATUSES.has(active.status as TurnStatus) &&
|
|
1667
|
+
isTimestamp(active.updated_at)
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
849
1671
|
async function readTurnRecord(namespaceDir: string, turnId: unknown): Promise<TurnRecord | null> {
|
|
850
|
-
|
|
1672
|
+
const requestedTurnId = safeTurnId(turnId);
|
|
1673
|
+
const value = await readJsonFile(turnFile(namespaceDir, requestedTurnId));
|
|
1674
|
+
if (value === null) return null;
|
|
1675
|
+
if (!isValidTurnRecord(value) || value.turn_id !== requestedTurnId) throw coordinatorStateError("invalid");
|
|
1676
|
+
return value;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
async function listTurnRecords(namespaceDir: string): Promise<TurnRecord[]> {
|
|
1680
|
+
let entries: string[];
|
|
1681
|
+
try {
|
|
1682
|
+
entries = await fs.readdir(turnsDir(namespaceDir));
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
if (isExpectedStateAbsence(error)) return [];
|
|
1685
|
+
throw coordinatorStateError("unreadable");
|
|
1686
|
+
}
|
|
1687
|
+
const records = await Promise.all(
|
|
1688
|
+
entries
|
|
1689
|
+
.filter(entry => entry.endsWith(".json"))
|
|
1690
|
+
.map(async entry => {
|
|
1691
|
+
const turnId = entry.slice(0, -".json".length);
|
|
1692
|
+
if (!TURN_ID_PATTERN.test(turnId)) throw coordinatorStateError("invalid");
|
|
1693
|
+
return await readTurnRecord(namespaceDir, turnId);
|
|
1694
|
+
}),
|
|
1695
|
+
);
|
|
1696
|
+
return records.filter((record): record is TurnRecord => record !== null);
|
|
851
1697
|
}
|
|
852
1698
|
|
|
853
1699
|
function turnEventKind(status: TurnStatus): CoordinatorEventKind | null {
|
|
@@ -863,7 +1709,8 @@ function turnEventKind(status: TurnStatus): CoordinatorEventKind | null {
|
|
|
863
1709
|
}
|
|
864
1710
|
|
|
865
1711
|
async function writeTurnRecord(namespaceDir: string, turn: TurnRecord): Promise<void> {
|
|
866
|
-
|
|
1712
|
+
if (!isValidTurnRecord(turn)) throw coordinatorStateError("invalid");
|
|
1713
|
+
const previous = await readTurnRecord(namespaceDir, turn.turn_id);
|
|
867
1714
|
await writeJsonFile(turnFile(namespaceDir, turn.turn_id), turn);
|
|
868
1715
|
const kind = previous?.status === turn.status ? null : turnEventKind(turn.status);
|
|
869
1716
|
if (kind) {
|
|
@@ -883,14 +1730,17 @@ async function writeTurnRecord(namespaceDir: string, turn: TurnRecord): Promise<
|
|
|
883
1730
|
}
|
|
884
1731
|
|
|
885
1732
|
async function readActiveTurn(namespaceDir: string, sessionId: string): Promise<TurnRecord | null> {
|
|
886
|
-
const
|
|
887
|
-
if (
|
|
888
|
-
|
|
889
|
-
|
|
1733
|
+
const value = await readJsonFile(activeTurnFile(namespaceDir, sessionId));
|
|
1734
|
+
if (value === null) return null;
|
|
1735
|
+
if (!isValidActiveTurnRecord(value, sessionId)) throw coordinatorStateError("invalid");
|
|
1736
|
+
const turn = await readTurnRecord(namespaceDir, value.turn_id);
|
|
1737
|
+
if (!turn || turn.session_id !== sessionId || turn.status !== value.status || !ACTIVE_TURN_STATUSES.has(turn.status))
|
|
1738
|
+
throw coordinatorStateError("invalid");
|
|
890
1739
|
return turn;
|
|
891
1740
|
}
|
|
892
1741
|
|
|
893
1742
|
async function writeActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<void> {
|
|
1743
|
+
if (!ACTIVE_TURN_STATUSES.has(turn.status)) throw coordinatorStateError("invalid");
|
|
894
1744
|
await writeJsonFile(activeTurnFile(namespaceDir, turn.session_id), {
|
|
895
1745
|
session_id: turn.session_id,
|
|
896
1746
|
turn_id: turn.turn_id,
|
|
@@ -900,15 +1750,84 @@ async function writeActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<
|
|
|
900
1750
|
}
|
|
901
1751
|
|
|
902
1752
|
async function clearActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<void> {
|
|
903
|
-
const
|
|
904
|
-
if (
|
|
1753
|
+
const value = await readJsonFile(activeTurnFile(namespaceDir, turn.session_id));
|
|
1754
|
+
if (value === null) return;
|
|
1755
|
+
if (!isValidActiveTurnRecord(value, turn.session_id)) throw coordinatorStateError("invalid");
|
|
1756
|
+
if (value.turn_id === turn.turn_id) await fs.rm(activeTurnFile(namespaceDir, turn.session_id), { force: true });
|
|
905
1757
|
}
|
|
906
1758
|
|
|
907
1759
|
async function readSessionState(namespaceDir: string, sessionId: string): Promise<CoordinatorSessionState | null> {
|
|
908
|
-
|
|
1760
|
+
const value = await readJsonFile(sessionStateFile(namespaceDir, sessionId));
|
|
1761
|
+
if (value === null) return null;
|
|
1762
|
+
if (!isValidRuntimeSessionState(value, sessionId)) throw coordinatorStateError("invalid");
|
|
1763
|
+
return value;
|
|
909
1764
|
}
|
|
910
1765
|
|
|
911
|
-
|
|
1766
|
+
function validRuntimeOwnerGeneration(value: unknown): value is string {
|
|
1767
|
+
return (
|
|
1768
|
+
typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\s\u0000-\u001f\u007f]/.test(value)
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function runtimeStateMatchesPrivateOwnerGeneration(
|
|
1773
|
+
sessionState: CoordinatorSessionState,
|
|
1774
|
+
session: Record<string, unknown> | null,
|
|
1775
|
+
): boolean {
|
|
1776
|
+
const expectedGeneration = session?.tmux_owner_generation ?? session?.tmuxOwnerGeneration;
|
|
1777
|
+
if (expectedGeneration === undefined)
|
|
1778
|
+
return !(sessionState.source === "process_postmortem" && sessionState.event === "owner_terminal");
|
|
1779
|
+
if (!validRuntimeOwnerGeneration(expectedGeneration) || sessionState.owner_generation !== expectedGeneration)
|
|
1780
|
+
return false;
|
|
1781
|
+
if (sessionState.source !== "process_postmortem") return true;
|
|
1782
|
+
if (sessionState.event !== "owner_terminal") return false;
|
|
1783
|
+
const expectedSocketKey = session?.tmux_owner_server_key ?? session?.tmuxOwnerServerKey;
|
|
1784
|
+
return (
|
|
1785
|
+
typeof expectedSocketKey === "string" &&
|
|
1786
|
+
sessionState.owner_terminal?.generation === expectedGeneration &&
|
|
1787
|
+
sessionState.owner_terminal.socket_key === expectedSocketKey
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
async function reconcileTerminalRuntimeEvidence(namespaceDir: string, sessionId: string): Promise<boolean> {
|
|
1792
|
+
const sessionState = await readSessionState(namespaceDir, sessionId);
|
|
1793
|
+
if (
|
|
1794
|
+
!sessionState ||
|
|
1795
|
+
sessionState.source === "coordinator" ||
|
|
1796
|
+
(sessionState.state !== "completed" && sessionState.state !== "errored")
|
|
1797
|
+
)
|
|
1798
|
+
return false;
|
|
1799
|
+
const session = asRecord(await readJsonFile(path.join(namespaceDir, "sessions", `${sessionId}.json`)));
|
|
1800
|
+
if (!runtimeStateMatchesPrivateOwnerGeneration(sessionState, session)) return false;
|
|
1801
|
+
|
|
1802
|
+
const activeTurn = await readActiveTurn(namespaceDir, sessionId);
|
|
1803
|
+
if (activeTurn && (sessionState.current_turn_id === null || sessionState.current_turn_id === activeTurn.turn_id)) {
|
|
1804
|
+
await markTurnTerminalFromSessionState(namespaceDir, activeTurn, sessionState);
|
|
1805
|
+
}
|
|
1806
|
+
return true;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
async function preflightCoordinatorMutation(
|
|
1810
|
+
namespaceDir: string,
|
|
1811
|
+
sessionId?: string,
|
|
1812
|
+
options: { rejectTerminalRuntimeEvidence?: boolean } = {},
|
|
1813
|
+
): Promise<void> {
|
|
1814
|
+
await readLatestEventSeq(namespaceDir);
|
|
1815
|
+
if (!sessionId) return;
|
|
1816
|
+
const [sessionState, session] = await Promise.all([
|
|
1817
|
+
readSessionState(namespaceDir, sessionId),
|
|
1818
|
+
readJsonFile(path.join(namespaceDir, "sessions", `${sessionId}.json`)),
|
|
1819
|
+
]);
|
|
1820
|
+
const sessionRecord = asRecord(session);
|
|
1821
|
+
if (sessionRecord && normalizeSession(sessionRecord).session_id !== sessionId)
|
|
1822
|
+
throw coordinatorStateError("invalid");
|
|
1823
|
+
if (sessionState && sessionState.session_id !== sessionId) throw coordinatorStateError("invalid");
|
|
1824
|
+
await readActiveTurn(namespaceDir, sessionId);
|
|
1825
|
+
const terminalRuntimeEvidence = await reconcileTerminalRuntimeEvidence(namespaceDir, sessionId);
|
|
1826
|
+
if (terminalRuntimeEvidence && options.rejectTerminalRuntimeEvidence !== false)
|
|
1827
|
+
throw coordinatorStateError("invalid");
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
async function writeSessionStateUnlocked(
|
|
912
1831
|
namespaceDir: string,
|
|
913
1832
|
sessionId: string,
|
|
914
1833
|
state: CoordinatorSessionStateValue,
|
|
@@ -921,6 +1840,20 @@ async function writeSessionState(
|
|
|
921
1840
|
} = {},
|
|
922
1841
|
): Promise<CoordinatorSessionState> {
|
|
923
1842
|
const previous = await readSessionState(namespaceDir, sessionId);
|
|
1843
|
+
if (previous && previous.source !== "coordinator") return previous;
|
|
1844
|
+
const session = asRecord(await readJsonFile(path.join(namespaceDir, "sessions", `${sessionId}.json`)));
|
|
1845
|
+
const previousCwd = typeof previous?.cwd === "string" && previous.cwd.trim() ? previous.cwd : null;
|
|
1846
|
+
const cwd =
|
|
1847
|
+
typeof session?.cwd === "string" && session.cwd.trim()
|
|
1848
|
+
? path.resolve(session.cwd)
|
|
1849
|
+
: previousCwd
|
|
1850
|
+
? path.resolve(previousCwd)
|
|
1851
|
+
: null;
|
|
1852
|
+
if (!cwd) throw coordinatorStateError("invalid");
|
|
1853
|
+
const sessionFile =
|
|
1854
|
+
typeof session?.session_file === "string" && session.session_file.trim()
|
|
1855
|
+
? path.resolve(session.session_file)
|
|
1856
|
+
: null;
|
|
924
1857
|
const payload: CoordinatorSessionState = {
|
|
925
1858
|
schema_version: 1,
|
|
926
1859
|
session_id: sessionId,
|
|
@@ -932,6 +1865,9 @@ async function writeSessionState(
|
|
|
932
1865
|
source: options.source ?? "coordinator",
|
|
933
1866
|
live: options.live ?? previous?.live ?? null,
|
|
934
1867
|
reason: options.reason ?? null,
|
|
1868
|
+
cwd,
|
|
1869
|
+
workdir: cwd,
|
|
1870
|
+
session_file: sessionFile,
|
|
935
1871
|
};
|
|
936
1872
|
await writeJsonFile(sessionStateFile(namespaceDir, sessionId), payload);
|
|
937
1873
|
if (
|
|
@@ -959,31 +1895,196 @@ async function writeSessionState(
|
|
|
959
1895
|
return payload;
|
|
960
1896
|
}
|
|
961
1897
|
|
|
962
|
-
|
|
1898
|
+
interface SessionStateLockOwner {
|
|
1899
|
+
pid: number;
|
|
1900
|
+
start_time: string;
|
|
1901
|
+
token: string;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function processStartTime(pid: number): string | null {
|
|
1905
|
+
try {
|
|
1906
|
+
const stat = nodeFs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
1907
|
+
const close = stat.lastIndexOf(")");
|
|
1908
|
+
const fields = stat
|
|
1909
|
+
.slice(close + 1)
|
|
1910
|
+
.trim()
|
|
1911
|
+
.split(/\s+/);
|
|
1912
|
+
return fields[19] ?? null;
|
|
1913
|
+
} catch {
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
function validLockOwner(value: unknown): value is SessionStateLockOwner {
|
|
1919
|
+
if (!value || typeof value !== "object") return false;
|
|
1920
|
+
const owner = value as Partial<SessionStateLockOwner>;
|
|
963
1921
|
return (
|
|
964
|
-
|
|
965
|
-
(
|
|
1922
|
+
typeof owner.pid === "number" &&
|
|
1923
|
+
Number.isSafeInteger(owner.pid) &&
|
|
1924
|
+
owner.pid > 0 &&
|
|
1925
|
+
typeof owner.start_time === "string" &&
|
|
1926
|
+
typeof owner.token === "string" &&
|
|
1927
|
+
owner.token.length > 0
|
|
966
1928
|
);
|
|
967
1929
|
}
|
|
968
1930
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
)
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1931
|
+
function lockOwnerIsAlive(value: unknown): boolean {
|
|
1932
|
+
if (!validLockOwner(value)) return false;
|
|
1933
|
+
const owner = value;
|
|
1934
|
+
try {
|
|
1935
|
+
process.kill(owner.pid, 0);
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
if ((error as NodeJS.ErrnoException).code === "ESRCH") return false;
|
|
1938
|
+
return true;
|
|
1939
|
+
}
|
|
1940
|
+
const currentStartTime = processStartTime(owner.pid);
|
|
1941
|
+
return currentStartTime === null || currentStartTime === owner.start_time;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
async function reclaimStaleSessionStateLock(lockFile: string): Promise<void> {
|
|
1945
|
+
let raw: string;
|
|
1946
|
+
try {
|
|
1947
|
+
raw = await fs.readFile(lockFile, "utf8");
|
|
1948
|
+
} catch (error) {
|
|
1949
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
1950
|
+
throw error;
|
|
1951
|
+
}
|
|
1952
|
+
let owner: unknown;
|
|
1953
|
+
try {
|
|
1954
|
+
owner = JSON.parse(raw);
|
|
1955
|
+
} catch {
|
|
1956
|
+
owner = null;
|
|
1957
|
+
}
|
|
1958
|
+
if (!validLockOwner(owner)) {
|
|
1959
|
+
const stat = await fs.stat(lockFile);
|
|
1960
|
+
if (Date.now() - stat.mtimeMs < 30_000) return;
|
|
1961
|
+
} else if (lockOwnerIsAlive(owner)) return;
|
|
1962
|
+
try {
|
|
1963
|
+
if ((await fs.readFile(lockFile, "utf8")) === raw) await fs.rm(lockFile);
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
async function withSessionStateLock<T>(stateFile: string, operation: () => Promise<T>): Promise<T> {
|
|
1970
|
+
const lockFile = `${stateFile}.lock`;
|
|
1971
|
+
const owner: SessionStateLockOwner = {
|
|
1972
|
+
pid: process.pid,
|
|
1973
|
+
start_time: processStartTime(process.pid) ?? "unknown",
|
|
1974
|
+
token: randomUUID(),
|
|
1975
|
+
};
|
|
1976
|
+
await ensureDir(path.dirname(stateFile));
|
|
1977
|
+
for (let attempt = 0; attempt < 12_000; attempt++) {
|
|
1978
|
+
let handle: fs.FileHandle | undefined;
|
|
1979
|
+
try {
|
|
1980
|
+
handle = await fs.open(lockFile, "wx");
|
|
1981
|
+
try {
|
|
1982
|
+
await handle.writeFile(JSON.stringify(owner));
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
await handle.close().catch(() => undefined);
|
|
1985
|
+
handle = undefined;
|
|
1986
|
+
await fs.rm(lockFile, { force: true }).catch(() => undefined);
|
|
1987
|
+
throw error;
|
|
1988
|
+
}
|
|
1989
|
+
const outcome = await operation().then(
|
|
1990
|
+
value => ({ ok: true as const, value }),
|
|
1991
|
+
error => ({ ok: false as const, error }),
|
|
1992
|
+
);
|
|
1993
|
+
await handle.close();
|
|
1994
|
+
try {
|
|
1995
|
+
if ((await fs.readFile(lockFile, "utf8")) === JSON.stringify(owner)) await fs.rm(lockFile);
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1998
|
+
}
|
|
1999
|
+
if (!outcome.ok) throw outcome.error;
|
|
2000
|
+
return outcome.value;
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
if (handle) throw error;
|
|
2003
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw coordinatorStateError("unreadable");
|
|
2004
|
+
await reclaimStaleSessionStateLock(lockFile);
|
|
2005
|
+
await Bun.sleep(5);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
throw coordinatorStateError("unreadable");
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
async function writeSessionState(
|
|
2012
|
+
namespaceDir: string,
|
|
2013
|
+
sessionId: string,
|
|
2014
|
+
state: CoordinatorSessionStateValue,
|
|
2015
|
+
options: {
|
|
2016
|
+
currentTurnId?: string | null;
|
|
2017
|
+
lastTurnId?: string | null;
|
|
2018
|
+
live?: boolean | null;
|
|
2019
|
+
reason?: string | null;
|
|
2020
|
+
source?: CoordinatorSessionState["source"];
|
|
2021
|
+
} = {},
|
|
2022
|
+
): Promise<CoordinatorSessionState> {
|
|
2023
|
+
const file = sessionStateFile(namespaceDir, sessionId);
|
|
2024
|
+
return await withSessionStateLock(
|
|
2025
|
+
file,
|
|
2026
|
+
async () => await writeSessionStateUnlocked(namespaceDir, sessionId, state, options),
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
function hasTmuxIdentity(session: Record<string, unknown>): boolean {
|
|
2031
|
+
return (
|
|
2032
|
+
(typeof session.tmux_session === "string" && session.tmux_session.length > 0) ||
|
|
2033
|
+
(typeof session.tmuxSession === "string" && session.tmuxSession.length > 0)
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function unavailableSessionReason(turn: TurnRecord, reason: string): string {
|
|
2038
|
+
if (
|
|
2039
|
+
reason === "tmux_session_missing" &&
|
|
2040
|
+
turn.delivery.tmux_keys_sent === true &&
|
|
2041
|
+
turn.delivery.prompt_acknowledged === true
|
|
2042
|
+
) {
|
|
2043
|
+
return "tmux_session_missing_after_prompt_acknowledgement";
|
|
2044
|
+
}
|
|
2045
|
+
return reason;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
function unavailableSessionEvidence(turn: TurnRecord, reason: string, timestamp: string): Record<string, unknown>[] {
|
|
2049
|
+
if (reason !== "tmux_session_missing_after_prompt_acknowledgement") return turn.evidence;
|
|
2050
|
+
return [
|
|
2051
|
+
...turn.evidence,
|
|
2052
|
+
{
|
|
2053
|
+
type: reason,
|
|
2054
|
+
message:
|
|
2055
|
+
"The tmux session disappeared after SKC runtime acknowledged the prompt, before any terminal final_response or error was recorded. Treat this as an in-flight vanished session and inspect/restart with recovery evidence rather than resubmitting blindly.",
|
|
2056
|
+
tmux_keys_sent: true,
|
|
2057
|
+
prompt_acknowledged: true,
|
|
2058
|
+
prior_status: turn.status,
|
|
2059
|
+
created_at: timestamp,
|
|
2060
|
+
},
|
|
2061
|
+
];
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
async function markTurnFailedForUnavailableSession(
|
|
2065
|
+
namespaceDir: string,
|
|
2066
|
+
turn: TurnRecord,
|
|
2067
|
+
reason: string,
|
|
2068
|
+
): Promise<TurnRecord> {
|
|
2069
|
+
const timestamp = new Date().toISOString();
|
|
2070
|
+
const durableReason = unavailableSessionReason(turn, reason);
|
|
2071
|
+
const failed: TurnRecord = {
|
|
2072
|
+
...turn,
|
|
2073
|
+
status: "failed",
|
|
2074
|
+
final_response: {
|
|
2075
|
+
text: `Coordinator session unavailable: ${durableReason}`,
|
|
2076
|
+
format: "markdown",
|
|
981
2077
|
source: "coordinator_liveness",
|
|
982
2078
|
artifact_path: null,
|
|
983
2079
|
truncated: false,
|
|
984
2080
|
},
|
|
985
|
-
|
|
986
|
-
|
|
2081
|
+
evidence: unavailableSessionEvidence(turn, durableReason, timestamp),
|
|
2082
|
+
error: {
|
|
2083
|
+
code: "session_unavailable",
|
|
2084
|
+
message: durableReason,
|
|
2085
|
+
recoverable: true,
|
|
2086
|
+
},
|
|
2087
|
+
liveness: { checked_at: timestamp, live: false, reason: durableReason },
|
|
987
2088
|
updated_at: timestamp,
|
|
988
2089
|
completed_at: timestamp,
|
|
989
2090
|
};
|
|
@@ -992,7 +2093,7 @@ async function markTurnFailedForUnavailableSession(
|
|
|
992
2093
|
await writeSessionState(namespaceDir, failed.session_id, "stale", {
|
|
993
2094
|
lastTurnId: failed.turn_id,
|
|
994
2095
|
live: false,
|
|
995
|
-
reason,
|
|
2096
|
+
reason: durableReason,
|
|
996
2097
|
});
|
|
997
2098
|
return failed;
|
|
998
2099
|
}
|
|
@@ -1092,6 +2193,18 @@ async function markTurnAcknowledgedFromRuntimeState(
|
|
|
1092
2193
|
};
|
|
1093
2194
|
await writeTurnRecord(namespaceDir, acknowledged);
|
|
1094
2195
|
await writeActiveTurn(namespaceDir, acknowledged);
|
|
2196
|
+
await appendCoordinatorEvent(namespaceDir, {
|
|
2197
|
+
kind: "turn.acknowledged",
|
|
2198
|
+
sessionId: acknowledged.session_id,
|
|
2199
|
+
turnId: acknowledged.turn_id,
|
|
2200
|
+
summary: `Turn ${acknowledged.turn_id} was acknowledged by the SKC runtime`,
|
|
2201
|
+
payloadRef: path.relative(namespaceDir, turnFile(namespaceDir, acknowledged.turn_id)),
|
|
2202
|
+
metadata: {
|
|
2203
|
+
status: acknowledged.status,
|
|
2204
|
+
tmux_keys_sent: acknowledged.delivery.tmux_keys_sent ?? null,
|
|
2205
|
+
prompt_acknowledged: true,
|
|
2206
|
+
},
|
|
2207
|
+
});
|
|
1095
2208
|
return acknowledged;
|
|
1096
2209
|
}
|
|
1097
2210
|
|
|
@@ -1151,7 +2264,11 @@ async function markTurnFailedForUnacknowledgedDelivery(
|
|
|
1151
2264
|
created_at: timestamp,
|
|
1152
2265
|
},
|
|
1153
2266
|
],
|
|
1154
|
-
liveness: {
|
|
2267
|
+
liveness: {
|
|
2268
|
+
checked_at: timestamp,
|
|
2269
|
+
live: turn.liveness.live,
|
|
2270
|
+
reason: PROMPT_ACK_TIMEOUT_REASON,
|
|
2271
|
+
},
|
|
1155
2272
|
updated_at: timestamp,
|
|
1156
2273
|
completed_at: timestamp,
|
|
1157
2274
|
};
|
|
@@ -1172,6 +2289,16 @@ async function reconcileRuntimeAcknowledgement(
|
|
|
1172
2289
|
ackTimeoutMs: number,
|
|
1173
2290
|
options: { failOnTimeout: boolean } = { failOnTimeout: true },
|
|
1174
2291
|
): Promise<TurnRecord> {
|
|
2292
|
+
const session = asRecord(await readJsonFile(path.join(namespaceDir, "sessions", `${turn.session_id}.json`)));
|
|
2293
|
+
if (
|
|
2294
|
+
sessionState &&
|
|
2295
|
+
runtimeStateMatchesPrivateOwnerGeneration(sessionState, session) &&
|
|
2296
|
+
sessionState.source !== "coordinator" &&
|
|
2297
|
+
(sessionState.state === "completed" || sessionState.state === "errored") &&
|
|
2298
|
+
(sessionState.current_turn_id === null || sessionState.current_turn_id === turn.turn_id)
|
|
2299
|
+
) {
|
|
2300
|
+
return await markTurnTerminalFromSessionState(namespaceDir, turn, sessionState);
|
|
2301
|
+
}
|
|
1175
2302
|
if (sessionState && runtimeStateAcknowledgesTurn(turn, sessionState)) {
|
|
1176
2303
|
return await markTurnAcknowledgedFromRuntimeState(namespaceDir, turn, sessionState);
|
|
1177
2304
|
}
|
|
@@ -1184,6 +2311,27 @@ async function reconcileRuntimeAcknowledgement(
|
|
|
1184
2311
|
function shellQuote(value: string): string {
|
|
1185
2312
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1186
2313
|
}
|
|
2314
|
+
|
|
2315
|
+
/**
|
|
2316
|
+
* Append an authoritative `--mpreset <profile>` to the coordinator child launch
|
|
2317
|
+
* command so the spawned SKC session activates the selected profile from its
|
|
2318
|
+
* first turn. The profile name is validated against the merged registry before
|
|
2319
|
+
* reaching here; it is still shell-quoted as defense in depth.
|
|
2320
|
+
*
|
|
2321
|
+
* Precedence note: this matches `skc --mpreset <profile>` semantics exactly, so
|
|
2322
|
+
* an explicit model-affecting flag already present in the operator-configured
|
|
2323
|
+
* `sessionCommand` (e.g. a `--model`/`--thinking` baked into the launch command)
|
|
2324
|
+
* follows the same CLI precedence as a direct `skc --mpreset ... --model ...`
|
|
2325
|
+
* invocation. The effective profile recorded on the session is the
|
|
2326
|
+
* coordinator-resolved request, not a child-attested activation receipt; a
|
|
2327
|
+
* cross-process activation receipt is a possible future enhancement and is out
|
|
2328
|
+
* of scope for the per-session selection contract here.
|
|
2329
|
+
*/
|
|
2330
|
+
export function buildCoordinatorSessionCommand(sessionCommand: string, mpreset: string | null | undefined): string {
|
|
2331
|
+
if (!mpreset) return sessionCommand;
|
|
2332
|
+
return `${sessionCommand} --mpreset ${shellQuote(mpreset)}`;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
1187
2335
|
function makeTurnRecord(
|
|
1188
2336
|
config: CoordinatorMcpConfig,
|
|
1189
2337
|
sessionId: string,
|
|
@@ -1208,7 +2356,13 @@ function makeTurnRecord(
|
|
|
1208
2356
|
attempts: [],
|
|
1209
2357
|
},
|
|
1210
2358
|
question_ids: [],
|
|
1211
|
-
final_response: {
|
|
2359
|
+
final_response: {
|
|
2360
|
+
text: null,
|
|
2361
|
+
format: "markdown",
|
|
2362
|
+
source: null,
|
|
2363
|
+
artifact_path: null,
|
|
2364
|
+
truncated: false,
|
|
2365
|
+
},
|
|
1212
2366
|
evidence: [],
|
|
1213
2367
|
error: null,
|
|
1214
2368
|
liveness: { checked_at: null, live: null, reason: null },
|
|
@@ -1230,6 +2384,8 @@ function asTerminalTurnStatus(status: unknown): TurnStatus | null {
|
|
|
1230
2384
|
|
|
1231
2385
|
export const COORDINATOR_AWAIT_TURN_TIMEOUT_MAX_MS = 30 * 60 * 1000;
|
|
1232
2386
|
export const COORDINATOR_RUNTIME_PROMPT_ACK_TIMEOUT_MAX_MS = MAX_RUNTIME_PROMPT_ACK_TIMEOUT_MS;
|
|
2387
|
+
export const COORDINATOR_RUNTIME_READINESS_TIMEOUT_MAX_MS = MAX_RUNTIME_READINESS_TIMEOUT_MS;
|
|
2388
|
+
|
|
1233
2389
|
export const COORDINATOR_EVENT_WATCH_TIMEOUT_MAX_MS = 30_000;
|
|
1234
2390
|
export const COORDINATOR_POLL_INTERVAL_MAX_MS = 10_000;
|
|
1235
2391
|
|
|
@@ -1248,6 +2404,12 @@ export function boundedRuntimePromptAckTimeoutMs(value: unknown): number {
|
|
|
1248
2404
|
COORDINATOR_RUNTIME_PROMPT_ACK_TIMEOUT_MAX_MS,
|
|
1249
2405
|
);
|
|
1250
2406
|
}
|
|
2407
|
+
export function boundedRuntimeReadinessTimeoutMs(value: unknown): number {
|
|
2408
|
+
return Math.min(
|
|
2409
|
+
parsePositiveIntegerMs(value, DEFAULT_RUNTIME_READINESS_TIMEOUT_MS),
|
|
2410
|
+
COORDINATOR_RUNTIME_READINESS_TIMEOUT_MAX_MS,
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
1251
2413
|
|
|
1252
2414
|
export function boundedEventWatchTimeoutMs(value: unknown): number {
|
|
1253
2415
|
return Math.min(parsePositiveIntegerMs(value, 1000), COORDINATOR_EVENT_WATCH_TIMEOUT_MAX_MS);
|
|
@@ -1256,8 +2418,21 @@ export function boundedEventWatchTimeoutMs(value: unknown): number {
|
|
|
1256
2418
|
export function boundedPollIntervalMs(value: unknown): number {
|
|
1257
2419
|
return Math.min(Math.max(parsePositiveIntegerMs(value, 100), 10), COORDINATOR_POLL_INTERVAL_MAX_MS);
|
|
1258
2420
|
}
|
|
1259
|
-
async function runCommand(
|
|
1260
|
-
|
|
2421
|
+
async function runCommand(
|
|
2422
|
+
command: string[],
|
|
2423
|
+
stdinLine?: string,
|
|
2424
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
2425
|
+
const proc = Bun.spawn(command, {
|
|
2426
|
+
stdin: stdinLine === undefined ? "ignore" : "pipe",
|
|
2427
|
+
stdout: "pipe",
|
|
2428
|
+
stderr: "pipe",
|
|
2429
|
+
});
|
|
2430
|
+
if (stdinLine !== undefined) {
|
|
2431
|
+
const stdin = proc.stdin;
|
|
2432
|
+
if (!stdin) throw new Error("coordinator_command_stdin_unavailable");
|
|
2433
|
+
stdin.write(`${stdinLine}\n`);
|
|
2434
|
+
stdin.end();
|
|
2435
|
+
}
|
|
1261
2436
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1262
2437
|
new Response(proc.stdout).text(),
|
|
1263
2438
|
new Response(proc.stderr).text(),
|
|
@@ -1266,15 +2441,39 @@ async function runCommand(command: string[]): Promise<{ exitCode: number; stdout
|
|
|
1266
2441
|
return { exitCode, stdout, stderr };
|
|
1267
2442
|
}
|
|
1268
2443
|
|
|
1269
|
-
type CommandRunner = (command: string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>;
|
|
1270
|
-
|
|
1271
2444
|
async function sendTmuxPromptKeys(
|
|
1272
2445
|
target: string,
|
|
1273
2446
|
prompt: string,
|
|
1274
|
-
runner: CommandRunner
|
|
2447
|
+
runner: CommandRunner,
|
|
2448
|
+
socketKey: string,
|
|
2449
|
+
proveCurrentServer: () => Promise<boolean>,
|
|
1275
2450
|
): Promise<boolean> {
|
|
1276
|
-
const
|
|
1277
|
-
|
|
2451
|
+
const runMutation = async (argv: string[]): Promise<CommandResult | null> => {
|
|
2452
|
+
if (!(await proveCurrentServer())) return null;
|
|
2453
|
+
return await runner(argv);
|
|
2454
|
+
};
|
|
2455
|
+
const bufferName = `skc-coordinator-prompt-${randomUUID()}`;
|
|
2456
|
+
const tmux = ["tmux", "-L", socketKey];
|
|
2457
|
+
const zeroize = async (): Promise<boolean> => {
|
|
2458
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2459
|
+
const cleared = await runMutation([...tmux, "set-buffer", "-b", bufferName, "--", ""]);
|
|
2460
|
+
const deleted =
|
|
2461
|
+
cleared?.exitCode === 0 ? await runMutation([...tmux, "delete-buffer", "-b", bufferName]) : null;
|
|
2462
|
+
if (cleared?.exitCode === 0 && deleted?.exitCode === 0) return true;
|
|
2463
|
+
}
|
|
2464
|
+
return false;
|
|
2465
|
+
};
|
|
2466
|
+
const buffered = await runMutation([...tmux, "set-buffer", "-b", bufferName, "--", prompt]);
|
|
2467
|
+
if (buffered?.exitCode !== 0) return false;
|
|
2468
|
+
const pasted = await runMutation([...tmux, "paste-buffer", "-d", "-b", bufferName, "-t", target]);
|
|
2469
|
+
if (pasted?.exitCode !== 0) {
|
|
2470
|
+
if (!(await zeroize())) throw new Error("coordinator_prompt_buffer_privacy_unverified");
|
|
2471
|
+
return false;
|
|
2472
|
+
}
|
|
2473
|
+
const dismissedAutocomplete = await runMutation([...tmux, "send-keys", "-t", target, "Escape"]);
|
|
2474
|
+
if (dismissedAutocomplete?.exitCode !== 0) return false;
|
|
2475
|
+
const submitted = await runMutation([...tmux, "send-keys", "-t", target, "Enter"]);
|
|
2476
|
+
return submitted?.exitCode === 0;
|
|
1278
2477
|
}
|
|
1279
2478
|
|
|
1280
2479
|
function boundedLineCount(value: unknown): number {
|
|
@@ -1299,7 +2498,10 @@ async function registerExistingTmuxSession(
|
|
|
1299
2498
|
namespaceDir: string,
|
|
1300
2499
|
sessionFilePath: string,
|
|
1301
2500
|
runner: CommandRunner = runCommand,
|
|
1302
|
-
): Promise<{
|
|
2501
|
+
): Promise<{
|
|
2502
|
+
session: Record<string, unknown>;
|
|
2503
|
+
sessionState: CoordinatorSessionState;
|
|
2504
|
+
}> {
|
|
1303
2505
|
await assertTmuxTargetAvailable(input.tmuxSession, input.tmuxTarget, runner);
|
|
1304
2506
|
const existing = asRecord(await readJsonFile(sessionFilePath));
|
|
1305
2507
|
if (existing) {
|
|
@@ -1325,6 +2527,7 @@ async function registerExistingTmuxSession(
|
|
|
1325
2527
|
authoritative: true,
|
|
1326
2528
|
warp_attached: input.warpAttached,
|
|
1327
2529
|
source: input.source,
|
|
2530
|
+
origin: "registered",
|
|
1328
2531
|
model: input.model,
|
|
1329
2532
|
};
|
|
1330
2533
|
await writeJsonFile(sessionFilePath, session);
|
|
@@ -1335,52 +2538,406 @@ async function registerExistingTmuxSession(
|
|
|
1335
2538
|
return { session, sessionState: state };
|
|
1336
2539
|
}
|
|
1337
2540
|
|
|
2541
|
+
async function portableProcessStartTime(pid: number, runner: CommandRunner): Promise<string | null> {
|
|
2542
|
+
const result = await runner(["ps", "-o", "lstart=", "-p", String(pid)]);
|
|
2543
|
+
const startTime = result.stdout.trim();
|
|
2544
|
+
return result.exitCode === 0 &&
|
|
2545
|
+
startTime.length > 0 &&
|
|
2546
|
+
startTime.length <= 160 &&
|
|
2547
|
+
!/[\r\n\u0000-\u001f\u007f]/.test(startTime)
|
|
2548
|
+
? startTime
|
|
2549
|
+
: null;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
export async function coordinatorOwnerIsolationProbe(runner: CommandRunner): Promise<OwnerIsolationProbe> {
|
|
2553
|
+
return {
|
|
2554
|
+
readCallerCgroup: async () => await fs.readFile("/proc/self/cgroup", "utf8").catch(() => null),
|
|
2555
|
+
probeServer: async (socketKey): Promise<TmuxServerProof> => {
|
|
2556
|
+
try {
|
|
2557
|
+
const result = await runner(["tmux", "-L", socketKey, "list-sessions", "-F", "#{pid} #{session_name}"]);
|
|
2558
|
+
if (result.exitCode !== 0) {
|
|
2559
|
+
const diagnostic = `${result.stdout}\n${result.stderr}`.slice(0, 512);
|
|
2560
|
+
return /(?:no server running|failed to connect to server|no sessions|error connecting to)/i.test(
|
|
2561
|
+
diagnostic,
|
|
2562
|
+
)
|
|
2563
|
+
? { state: "absent" }
|
|
2564
|
+
: { state: "unverifiable" };
|
|
2565
|
+
}
|
|
2566
|
+
const rows = result.stdout
|
|
2567
|
+
.split("\n")
|
|
2568
|
+
.map(line => line.trim().split(/\s+/, 2))
|
|
2569
|
+
.filter(([pid, name]) => Boolean(pid && name));
|
|
2570
|
+
const pid = Number(rows[0]?.[0]);
|
|
2571
|
+
const sessionNames = rows.map(([, name]) => name as string);
|
|
2572
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || rows.some(([rowPid]) => Number(rowPid) !== pid))
|
|
2573
|
+
return { state: "unverifiable" };
|
|
2574
|
+
const stat =
|
|
2575
|
+
process.platform === "linux" ? await fs.readFile(`/proc/${pid}/stat`, "utf8").catch(() => null) : null;
|
|
2576
|
+
const startTime =
|
|
2577
|
+
process.platform === "linux"
|
|
2578
|
+
? ownerProcessStartTime(process.platform, stat)
|
|
2579
|
+
: await portableProcessStartTime(pid, runner);
|
|
2580
|
+
if (!startTime) return { state: "unverifiable", pid, sessionNames };
|
|
2581
|
+
if (process.platform !== "linux")
|
|
2582
|
+
return {
|
|
2583
|
+
state: "safe",
|
|
2584
|
+
pid,
|
|
2585
|
+
startTime,
|
|
2586
|
+
sessionNames,
|
|
2587
|
+
cgroup: { classification: "not_applicable" },
|
|
2588
|
+
};
|
|
2589
|
+
const cgroupText = await fs.readFile(`/proc/${pid}/cgroup`, "utf8").catch(() => null);
|
|
2590
|
+
const cgroup = classifyCgroup({ platform: process.platform, cgroupText });
|
|
2591
|
+
return {
|
|
2592
|
+
state:
|
|
2593
|
+
cgroup.classification === "safe"
|
|
2594
|
+
? "safe"
|
|
2595
|
+
: cgroup.classification === "unsafe_service"
|
|
2596
|
+
? "unsafe"
|
|
2597
|
+
: "unverifiable",
|
|
2598
|
+
pid,
|
|
2599
|
+
startTime,
|
|
2600
|
+
sessionNames,
|
|
2601
|
+
cgroup,
|
|
2602
|
+
};
|
|
2603
|
+
} catch {
|
|
2604
|
+
// A transient spawn failure (e.g. EAGAIN under load, or a momentary tmux/ps hiccup)
|
|
2605
|
+
// must not fatally abort the owner-server proof. Surfacing it as an unverifiable state
|
|
2606
|
+
// lets proveCoordinatorOwnerServer's bounded retry loop re-probe once the transient
|
|
2607
|
+
// clears, instead of propagating the throw and failing the entire delegate outright.
|
|
2608
|
+
return { state: "unverifiable" };
|
|
2609
|
+
}
|
|
2610
|
+
},
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
async function proveCoordinatorOwnerServer(probe: OwnerIsolationProbe, socketKey: string): Promise<TmuxServerProof> {
|
|
2615
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
2616
|
+
const proof = await probe.probeServer(socketKey);
|
|
2617
|
+
if (proof.state === "unsafe" || (proof.state === "safe" && proof.pid && proof.startTime)) return proof;
|
|
2618
|
+
if (attempt < 49) await Bun.sleep(10);
|
|
2619
|
+
}
|
|
2620
|
+
return { state: "unverifiable" };
|
|
2621
|
+
}
|
|
2622
|
+
|
|
1338
2623
|
async function startTmuxSession(
|
|
1339
2624
|
config: CoordinatorMcpConfig,
|
|
1340
2625
|
input: SessionStartInput,
|
|
1341
2626
|
namespaceDir: string,
|
|
1342
2627
|
runner: CommandRunner = runCommand,
|
|
2628
|
+
ownerIsolationProbe?: OwnerIsolationProbe,
|
|
1343
2629
|
): Promise<Record<string, unknown>> {
|
|
1344
2630
|
if (!config.sessionCommand) throw new Error("coordinator_session_command_required");
|
|
1345
|
-
const sessionName =
|
|
2631
|
+
const sessionName = input.sessionId;
|
|
2632
|
+
const socketKey = `skc-coordinator-${randomUUID().slice(0, 8)}`;
|
|
2633
|
+
const ownerStateDir = namespaceDir;
|
|
2634
|
+
const ownerGeneration = randomUUID();
|
|
2635
|
+
const generationBaseline = captureOwnerGenerationBaselineSync(ownerStateDir, sessionName);
|
|
1346
2636
|
const runtimeStateFile = sessionStateFile(namespaceDir, sessionName);
|
|
1347
2637
|
const sessionCommand = [
|
|
1348
2638
|
"exec env",
|
|
1349
2639
|
`${SKC_COORDINATOR_SESSION_STATE_FILE_ENV}=${shellQuote(runtimeStateFile)}`,
|
|
1350
2640
|
`${SKC_COORDINATOR_SESSION_ID_ENV}=${shellQuote(sessionName)}`,
|
|
1351
|
-
|
|
2641
|
+
`${SKC_TMUX_OWNER_GENERATION_ENV}=${shellQuote(ownerGeneration)}`,
|
|
2642
|
+
`${SKC_TMUX_OWNER_STATE_DIR_ENV}=${shellQuote(ownerStateDir)}`,
|
|
2643
|
+
`${SKC_TMUX_OWNER_SERVER_KEY_ENV}=${shellQuote(socketKey)}`,
|
|
2644
|
+
`${SKC_COORDINATOR_SESSION_LAUNCH_ID_ENV}=${shellQuote(input.launchId)}`,
|
|
2645
|
+
`${SKC_COORDINATOR_SESSION_READINESS_FILE_ENV}=${shellQuote(input.readinessMarkerFile)}`,
|
|
2646
|
+
buildCoordinatorSessionCommand(config.sessionCommand, input.mpreset),
|
|
1352
2647
|
].join(" ");
|
|
1353
|
-
const
|
|
2648
|
+
const tmuxArgv = [
|
|
1354
2649
|
"tmux",
|
|
2650
|
+
"-L",
|
|
2651
|
+
socketKey,
|
|
1355
2652
|
"new-session",
|
|
1356
2653
|
"-d",
|
|
1357
2654
|
"-P",
|
|
1358
2655
|
"-F",
|
|
1359
|
-
"#{session_name}:#{window_index}.#{pane_index} #{pane_id}",
|
|
2656
|
+
"#{session_name}:#{window_index}.#{pane_index} #{pane_id} #{session_id}",
|
|
1360
2657
|
"-s",
|
|
1361
2658
|
sessionName,
|
|
1362
2659
|
"-c",
|
|
1363
2660
|
input.cwd,
|
|
1364
2661
|
sessionCommand,
|
|
1365
|
-
]
|
|
1366
|
-
|
|
1367
|
-
|
|
2662
|
+
];
|
|
2663
|
+
const probe = ownerIsolationProbe ?? (await coordinatorOwnerIsolationProbe(runner));
|
|
2664
|
+
let plan = await planTmuxOwnerIsolation(
|
|
2665
|
+
{
|
|
2666
|
+
schema_version: 1,
|
|
2667
|
+
op: "plan",
|
|
2668
|
+
platform: process.platform,
|
|
2669
|
+
session_id: sessionName,
|
|
2670
|
+
owner_generation: ownerGeneration,
|
|
2671
|
+
baseline: generationBaseline,
|
|
2672
|
+
cwd: input.cwd,
|
|
2673
|
+
state_dir: ownerStateDir,
|
|
2674
|
+
socket_key: socketKey,
|
|
2675
|
+
tmux_argv: tmuxArgv,
|
|
2676
|
+
},
|
|
2677
|
+
probe,
|
|
2678
|
+
);
|
|
2679
|
+
if (!plan.ok) throw new Error(`coordinator_tmux_owner_${plan.code}`);
|
|
2680
|
+
if (plan.execution.mode === "direct" && plan.execution.server_absent_before) {
|
|
2681
|
+
plan = await planTmuxOwnerIsolation(
|
|
2682
|
+
{
|
|
2683
|
+
schema_version: 1,
|
|
2684
|
+
op: "plan",
|
|
2685
|
+
platform: process.platform,
|
|
2686
|
+
session_id: sessionName,
|
|
2687
|
+
owner_generation: ownerGeneration,
|
|
2688
|
+
baseline: generationBaseline,
|
|
2689
|
+
cwd: input.cwd,
|
|
2690
|
+
state_dir: ownerStateDir,
|
|
2691
|
+
socket_key: socketKey,
|
|
2692
|
+
tmux_argv: tmuxArgv,
|
|
2693
|
+
},
|
|
2694
|
+
{
|
|
2695
|
+
...probe,
|
|
2696
|
+
readCallerCgroup: async () => "0::/skc-coordinator-bootstrap.service",
|
|
2697
|
+
},
|
|
2698
|
+
);
|
|
2699
|
+
if (!plan.ok) throw new Error(`coordinator_tmux_owner_${plan.code}`);
|
|
2700
|
+
}
|
|
2701
|
+
const preSpawnProof =
|
|
2702
|
+
plan.execution.mode === "direct" && plan.server_state === "safe"
|
|
2703
|
+
? await proveCoordinatorOwnerServer(probe, socketKey)
|
|
2704
|
+
: null;
|
|
2705
|
+
if (
|
|
2706
|
+
preSpawnProof &&
|
|
2707
|
+
(preSpawnProof.state !== "safe" ||
|
|
2708
|
+
!preSpawnProof.pid ||
|
|
2709
|
+
!preSpawnProof.startTime ||
|
|
2710
|
+
(process.platform === "linux" && preSpawnProof.cgroup?.classification !== "safe"))
|
|
2711
|
+
)
|
|
2712
|
+
throw new Error("coordinator_tmux_owner_server_unverifiable");
|
|
2713
|
+
const started = await runner(
|
|
2714
|
+
plan.execution.argv,
|
|
2715
|
+
plan.execution.mode === "scoped" ? plan.execution.stdin_line : undefined,
|
|
2716
|
+
);
|
|
2717
|
+
|
|
2718
|
+
const rawScopedReceipt =
|
|
2719
|
+
plan.execution.mode === "scoped" && isExactScopedBootstrapSuccessReceipt(started.stdout)
|
|
2720
|
+
? (JSON.parse(started.stdout.trim()) as {
|
|
2721
|
+
native_session_id: string;
|
|
2722
|
+
server_pid: number;
|
|
2723
|
+
server_start_time: string;
|
|
2724
|
+
session_name: string;
|
|
2725
|
+
})
|
|
2726
|
+
: undefined;
|
|
2727
|
+
const scopedReceipt =
|
|
2728
|
+
rawScopedReceipt?.session_name === plan.execution.attempt_session ? rawScopedReceipt : undefined;
|
|
2729
|
+
const directReceipt =
|
|
2730
|
+
plan.execution.mode === "direct"
|
|
2731
|
+
? parseTmuxNewSessionRecord(started.stdout, plan.execution.attempt_session)
|
|
2732
|
+
: undefined;
|
|
2733
|
+
const directNativeSessionId = directReceipt?.nativeSessionId;
|
|
2734
|
+
const spawnedNativeSessionId = scopedReceipt?.native_session_id ?? directNativeSessionId;
|
|
2735
|
+
let expectedServer = scopedReceipt
|
|
2736
|
+
? {
|
|
2737
|
+
pid: scopedReceipt.server_pid,
|
|
2738
|
+
startTime: scopedReceipt.server_start_time,
|
|
2739
|
+
}
|
|
2740
|
+
: preSpawnProof && preSpawnProof.state === "safe" && preSpawnProof.pid && preSpawnProof.startTime
|
|
2741
|
+
? { pid: preSpawnProof.pid, startTime: preSpawnProof.startTime }
|
|
2742
|
+
: undefined;
|
|
2743
|
+
const cleanupFailedAttempt = async (): Promise<"cleaned" | "failed" | "unverifiable"> => {
|
|
2744
|
+
if (!spawnedNativeSessionId || !expectedServer) return "unverifiable";
|
|
2745
|
+
try {
|
|
2746
|
+
const result = await runner([
|
|
2747
|
+
"tmux",
|
|
2748
|
+
"-L",
|
|
2749
|
+
socketKey,
|
|
2750
|
+
"if-shell",
|
|
2751
|
+
"-t",
|
|
2752
|
+
spawnedNativeSessionId,
|
|
2753
|
+
"-F",
|
|
2754
|
+
coordinatorCleanupPredicate(expectedServer.pid, spawnedNativeSessionId, plan.execution.attempt_session),
|
|
2755
|
+
`kill-session -t '${spawnedNativeSessionId}' ; display-message -p __skc_coordinator_cleanup_ok__`,
|
|
2756
|
+
"display-message -p __skc_coordinator_cleanup_refused__",
|
|
2757
|
+
]);
|
|
2758
|
+
if (result.exitCode !== 0) return "failed";
|
|
2759
|
+
return result.stdout.trim() === "__skc_coordinator_cleanup_ok__" ? "cleaned" : "unverifiable";
|
|
2760
|
+
} catch {
|
|
2761
|
+
return "unverifiable";
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
const failAfterCleanup = async (reason: string): Promise<never> => {
|
|
2765
|
+
throw new CoordinatorStartError(reason, await cleanupFailedAttempt());
|
|
2766
|
+
};
|
|
2767
|
+
if (started.exitCode !== 0) return await failAfterCleanup("coordinator_tmux_start_failed");
|
|
2768
|
+
const bootstrapReceiptValid = plan.execution.mode !== "scoped" || scopedReceipt !== undefined;
|
|
2769
|
+
const initialProof = await proveCoordinatorOwnerServer(probe, socketKey).catch(error =>
|
|
2770
|
+
failAfterCleanup(error instanceof Error ? error.message : "coordinator_tmux_owner_server_unverifiable"),
|
|
2771
|
+
);
|
|
2772
|
+
if (
|
|
2773
|
+
initialProof.state !== "safe" ||
|
|
2774
|
+
!initialProof.pid ||
|
|
2775
|
+
!initialProof.startTime ||
|
|
2776
|
+
(process.platform === "linux" && initialProof.cgroup?.classification !== "safe")
|
|
2777
|
+
) {
|
|
2778
|
+
return await failAfterCleanup("coordinator_tmux_owner_server_unverifiable");
|
|
2779
|
+
}
|
|
2780
|
+
if (
|
|
2781
|
+
scopedReceipt &&
|
|
2782
|
+
(initialProof.pid !== scopedReceipt.server_pid || initialProof.startTime !== scopedReceipt.server_start_time)
|
|
2783
|
+
) {
|
|
2784
|
+
return await failAfterCleanup("coordinator_tmux_owner_server_race");
|
|
2785
|
+
}
|
|
2786
|
+
if (
|
|
2787
|
+
preSpawnProof &&
|
|
2788
|
+
(initialProof.pid !== preSpawnProof.pid || initialProof.startTime !== preSpawnProof.startTime)
|
|
2789
|
+
) {
|
|
2790
|
+
return await failAfterCleanup("coordinator_tmux_owner_server_race");
|
|
2791
|
+
}
|
|
2792
|
+
if (!expectedServer)
|
|
2793
|
+
expectedServer = {
|
|
2794
|
+
pid: initialProof.pid,
|
|
2795
|
+
startTime: initialProof.startTime,
|
|
2796
|
+
};
|
|
2797
|
+
if (!bootstrapReceiptValid) {
|
|
2798
|
+
return await failAfterCleanup("coordinator_tmux_start_failed");
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
const startRecord = await (async () => {
|
|
2802
|
+
const queried = await runner([
|
|
2803
|
+
"tmux",
|
|
2804
|
+
"-L",
|
|
2805
|
+
socketKey,
|
|
2806
|
+
"display-message",
|
|
2807
|
+
"-p",
|
|
2808
|
+
"-t",
|
|
2809
|
+
sessionName,
|
|
2810
|
+
"#{session_name}:#{window_index}.#{pane_index} #{pane_id} #{session_id}",
|
|
2811
|
+
]);
|
|
2812
|
+
if (queried.exitCode !== 0) throw new Error("coordinator_tmux_start_failed");
|
|
2813
|
+
return queried.stdout.trim();
|
|
2814
|
+
})().catch(error => failAfterCleanup(error instanceof Error ? error.message : "coordinator_tmux_start_failed"));
|
|
2815
|
+
const postLaunchProof = await probe.probeServer(socketKey);
|
|
2816
|
+
if (
|
|
2817
|
+
postLaunchProof.state !== "safe" ||
|
|
2818
|
+
postLaunchProof.pid !== initialProof.pid ||
|
|
2819
|
+
postLaunchProof.startTime !== initialProof.startTime
|
|
2820
|
+
) {
|
|
2821
|
+
return await failAfterCleanup("coordinator_tmux_owner_server_race");
|
|
2822
|
+
}
|
|
2823
|
+
const parsedStartRecord = parseTmuxNewSessionRecord(startRecord, plan.execution.attempt_session);
|
|
2824
|
+
if (!parsedStartRecord) {
|
|
2825
|
+
return await failAfterCleanup("coordinator_tmux_start_failed");
|
|
2826
|
+
}
|
|
2827
|
+
const { tmuxTarget, paneId, nativeSessionId } = parsedStartRecord;
|
|
2828
|
+
if (
|
|
2829
|
+
(scopedReceipt && nativeSessionId !== scopedReceipt.native_session_id) ||
|
|
2830
|
+
(plan.execution.mode === "direct" && nativeSessionId !== directNativeSessionId)
|
|
2831
|
+
) {
|
|
2832
|
+
return await failAfterCleanup("coordinator_tmux_owner_server_race");
|
|
2833
|
+
}
|
|
2834
|
+
const ownerTransaction = {
|
|
2835
|
+
commit: () => replaceOwnerGenerationSync(ownerStateDir, sessionName, ownerGeneration, generationBaseline),
|
|
2836
|
+
rollback: cleanupFailedAttempt,
|
|
2837
|
+
};
|
|
1368
2838
|
return {
|
|
1369
2839
|
sessionId: sessionName,
|
|
1370
2840
|
tmuxSession: sessionName,
|
|
1371
|
-
tmuxTarget
|
|
2841
|
+
tmuxTarget,
|
|
1372
2842
|
paneId,
|
|
2843
|
+
tmuxNativeSessionId: nativeSessionId,
|
|
2844
|
+
tmuxSocketKey: socketKey,
|
|
2845
|
+
tmuxOwnerGeneration: ownerGeneration,
|
|
2846
|
+
tmuxOwnerStateDir: ownerStateDir,
|
|
2847
|
+
tmuxOwnerServerKey: socketKey,
|
|
2848
|
+
tmuxOwnerServerPid: initialProof.pid,
|
|
2849
|
+
tmuxOwnerServerStartTime: initialProof.startTime,
|
|
2850
|
+
|
|
1373
2851
|
cwd: input.cwd,
|
|
1374
2852
|
createdAt: new Date().toISOString(),
|
|
1375
2853
|
sessionCommand: config.sessionCommand,
|
|
2854
|
+
...(input.mpreset ? { mpreset: input.mpreset } : {}),
|
|
1376
2855
|
runtimeStateFile,
|
|
2856
|
+
__coordinatorOwnerTransaction: ownerTransaction,
|
|
2857
|
+
launchId: input.launchId,
|
|
2858
|
+
readinessMarkerFile: input.readinessMarkerFile,
|
|
2859
|
+
readinessFile: input.readinessMarkerFile,
|
|
2860
|
+
origin: "coordinator_created",
|
|
2861
|
+
runtime_readiness_latched: false,
|
|
1377
2862
|
};
|
|
1378
2863
|
}
|
|
1379
2864
|
|
|
1380
|
-
|
|
2865
|
+
function hasPrivateTmuxOwnerIdentity(session: Record<string, unknown>): boolean {
|
|
2866
|
+
const socketKey = typeof session.tmux_socket_key === "string" ? session.tmux_socket_key : session.tmuxSocketKey;
|
|
2867
|
+
const serverKey =
|
|
2868
|
+
typeof session.tmux_owner_server_key === "string" ? session.tmux_owner_server_key : session.tmuxOwnerServerKey;
|
|
2869
|
+
const pid = session.tmux_owner_server_pid ?? session.tmuxOwnerServerPid;
|
|
2870
|
+
const startTime = session.tmux_owner_server_start_time ?? session.tmuxOwnerServerStartTime;
|
|
2871
|
+
const nativeSessionId = session.tmux_native_session_id ?? session.tmuxNativeSessionId;
|
|
2872
|
+
const paneId = session.pane_id ?? session.paneId;
|
|
2873
|
+
return (
|
|
2874
|
+
typeof socketKey === "string" &&
|
|
2875
|
+
socketKey.length > 0 &&
|
|
2876
|
+
socketKey === serverKey &&
|
|
2877
|
+
Number.isSafeInteger(pid) &&
|
|
2878
|
+
typeof startTime === "string" &&
|
|
2879
|
+
startTime.length > 0 &&
|
|
2880
|
+
typeof nativeSessionId === "string" &&
|
|
2881
|
+
nativeSessionId.length > 0 &&
|
|
2882
|
+
typeof paneId === "string" &&
|
|
2883
|
+
paneId.length > 0
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
async function proveImmutableTmuxTarget(session: Record<string, unknown>, runner: CommandRunner): Promise<boolean> {
|
|
2888
|
+
const target = typeof session.tmux_target === "string" ? session.tmux_target : session.tmuxTarget;
|
|
2889
|
+
const socketKey = typeof session.tmux_socket_key === "string" ? session.tmux_socket_key : session.tmuxSocketKey;
|
|
2890
|
+
const expectedSessionId = session.tmux_native_session_id ?? session.tmuxNativeSessionId;
|
|
2891
|
+
const expectedPaneId = session.pane_id ?? session.paneId;
|
|
2892
|
+
if (
|
|
2893
|
+
typeof target !== "string" ||
|
|
2894
|
+
typeof socketKey !== "string" ||
|
|
2895
|
+
typeof expectedSessionId !== "string" ||
|
|
2896
|
+
typeof expectedPaneId !== "string"
|
|
2897
|
+
)
|
|
2898
|
+
return false;
|
|
2899
|
+
const identity = await runner([
|
|
2900
|
+
"tmux",
|
|
2901
|
+
"-L",
|
|
2902
|
+
socketKey,
|
|
2903
|
+
"display-message",
|
|
2904
|
+
"-p",
|
|
2905
|
+
"-t",
|
|
2906
|
+
target,
|
|
2907
|
+
"#{session_id} #{pane_id}",
|
|
2908
|
+
]);
|
|
2909
|
+
const parts = identity.stdout.trim().split(/\s+/);
|
|
2910
|
+
if (identity.exitCode !== 0) return false;
|
|
2911
|
+
return parts.length === 2 && parts[0] === expectedSessionId && parts[1] === expectedPaneId;
|
|
2912
|
+
}
|
|
2913
|
+
async function captureTmuxTail(
|
|
2914
|
+
session: Record<string, unknown>,
|
|
2915
|
+
lines: number,
|
|
2916
|
+
runner: CommandRunner = runCommand,
|
|
2917
|
+
): Promise<string[]> {
|
|
1381
2918
|
const target = typeof session.tmux_target === "string" ? session.tmux_target : session.tmuxTarget;
|
|
1382
|
-
|
|
1383
|
-
|
|
2919
|
+
const socketKey = typeof session.tmux_socket_key === "string" ? session.tmux_socket_key : session.tmuxSocketKey;
|
|
2920
|
+
if (
|
|
2921
|
+
!hasPrivateTmuxOwnerIdentity(session) ||
|
|
2922
|
+
typeof socketKey !== "string" ||
|
|
2923
|
+
socketKey.length === 0 ||
|
|
2924
|
+
typeof target !== "string" ||
|
|
2925
|
+
target.length === 0
|
|
2926
|
+
)
|
|
2927
|
+
return [];
|
|
2928
|
+
const pid = session.tmux_owner_server_pid ?? session.tmuxOwnerServerPid;
|
|
2929
|
+
const startTime = session.tmux_owner_server_start_time ?? session.tmuxOwnerServerStartTime;
|
|
2930
|
+
const probe = await coordinatorOwnerIsolationProbe(runner);
|
|
2931
|
+
const proof = await probe.probeServer(socketKey);
|
|
2932
|
+
if (
|
|
2933
|
+
proof.state !== "safe" ||
|
|
2934
|
+
proof.pid !== pid ||
|
|
2935
|
+
proof.startTime !== startTime ||
|
|
2936
|
+
(process.platform === "linux" && proof.cgroup?.classification !== "safe") ||
|
|
2937
|
+
!(await proveImmutableTmuxTarget(session, runner))
|
|
2938
|
+
)
|
|
2939
|
+
return [];
|
|
2940
|
+
const captured = await runner(["tmux", "-L", socketKey, "capture-pane", "-t", target, "-p", "-S", `-${lines}`]);
|
|
1384
2941
|
if (captured.exitCode !== 0) return [];
|
|
1385
2942
|
return captured.stdout.split("\n").slice(-lines);
|
|
1386
2943
|
}
|
|
@@ -1389,10 +2946,38 @@ async function sendTmuxPrompt(
|
|
|
1389
2946
|
session: Record<string, unknown>,
|
|
1390
2947
|
prompt: string,
|
|
1391
2948
|
runner: CommandRunner = runCommand,
|
|
2949
|
+
ownerIsolationProbe?: OwnerIsolationProbe,
|
|
1392
2950
|
): Promise<boolean> {
|
|
1393
2951
|
const target = typeof session.tmux_target === "string" ? session.tmux_target : session.tmuxTarget;
|
|
1394
|
-
|
|
1395
|
-
|
|
2952
|
+
const socketKey = typeof session.tmux_socket_key === "string" ? session.tmux_socket_key : session.tmuxSocketKey;
|
|
2953
|
+
const serverKey =
|
|
2954
|
+
typeof session.tmux_owner_server_key === "string" ? session.tmux_owner_server_key : session.tmuxOwnerServerKey;
|
|
2955
|
+
const pid = session.tmux_owner_server_pid ?? session.tmuxOwnerServerPid;
|
|
2956
|
+
const startTime = session.tmux_owner_server_start_time ?? session.tmuxOwnerServerStartTime;
|
|
2957
|
+
if (
|
|
2958
|
+
typeof target !== "string" ||
|
|
2959
|
+
target.length === 0 ||
|
|
2960
|
+
typeof socketKey !== "string" ||
|
|
2961
|
+
socketKey.length === 0 ||
|
|
2962
|
+
socketKey !== serverKey ||
|
|
2963
|
+
!Number.isSafeInteger(pid) ||
|
|
2964
|
+
typeof startTime !== "string" ||
|
|
2965
|
+
startTime.length === 0
|
|
2966
|
+
)
|
|
2967
|
+
return false;
|
|
2968
|
+
const paneId = session.pane_id ?? session.paneId;
|
|
2969
|
+
if (typeof paneId !== "string" || !(await proveImmutableTmuxTarget(session, runner))) return false;
|
|
2970
|
+
const probe = ownerIsolationProbe ?? (await coordinatorOwnerIsolationProbe(runner));
|
|
2971
|
+
const proveCurrentServer = async (): Promise<boolean> => {
|
|
2972
|
+
const proof = await probe.probeServer(socketKey);
|
|
2973
|
+
return (
|
|
2974
|
+
proof.state === "safe" &&
|
|
2975
|
+
proof.pid === pid &&
|
|
2976
|
+
proof.startTime === startTime &&
|
|
2977
|
+
(process.platform !== "linux" || proof.cgroup?.classification === "safe")
|
|
2978
|
+
);
|
|
2979
|
+
};
|
|
2980
|
+
return await sendTmuxPromptKeys(paneId, prompt, runner, socketKey, proveCurrentServer);
|
|
1396
2981
|
}
|
|
1397
2982
|
|
|
1398
2983
|
async function hasTmuxSession(
|
|
@@ -1400,8 +2985,16 @@ async function hasTmuxSession(
|
|
|
1400
2985
|
runner: CommandRunner = runCommand,
|
|
1401
2986
|
): Promise<boolean | null> {
|
|
1402
2987
|
const tmuxSession = typeof session.tmux_session === "string" ? session.tmux_session : session.tmuxSession;
|
|
1403
|
-
|
|
1404
|
-
|
|
2988
|
+
const socketKey = typeof session.tmux_socket_key === "string" ? session.tmux_socket_key : session.tmuxSocketKey;
|
|
2989
|
+
if (
|
|
2990
|
+
!hasPrivateTmuxOwnerIdentity(session) ||
|
|
2991
|
+
typeof socketKey !== "string" ||
|
|
2992
|
+
socketKey.length === 0 ||
|
|
2993
|
+
typeof tmuxSession !== "string" ||
|
|
2994
|
+
tmuxSession.length === 0
|
|
2995
|
+
)
|
|
2996
|
+
return null;
|
|
2997
|
+
const checked = await runner(["tmux", "-L", socketKey, "has-session", "-t", tmuxSession]);
|
|
1405
2998
|
return checked.exitCode === 0;
|
|
1406
2999
|
}
|
|
1407
3000
|
|
|
@@ -1436,7 +3029,8 @@ async function inspectTmuxSession(
|
|
|
1436
3029
|
runner: CommandRunner = runCommand,
|
|
1437
3030
|
): Promise<Record<string, unknown>> {
|
|
1438
3031
|
const live = await hasTmuxSession(session, runner);
|
|
1439
|
-
const tail = live ? await captureTmuxTail(session, lines) : [];
|
|
3032
|
+
const tail = live ? await captureTmuxTail(session, lines, runner) : [];
|
|
3033
|
+
|
|
1440
3034
|
return {
|
|
1441
3035
|
live,
|
|
1442
3036
|
...summarizePaneTail(tail),
|
|
@@ -1490,14 +3084,27 @@ async function waitForCoordinatorEvents(namespaceDir: string, timeoutMs: number)
|
|
|
1490
3084
|
};
|
|
1491
3085
|
const timer = setTimeout(finish, Math.max(timeoutMs, 0));
|
|
1492
3086
|
timer.unref?.();
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
3087
|
+
const eventDir = eventsDir(namespaceDir);
|
|
3088
|
+
const watchedDirs = [
|
|
3089
|
+
eventDir,
|
|
3090
|
+
turnsDir(namespaceDir),
|
|
3091
|
+
path.join(namespaceDir, "active-turns"),
|
|
3092
|
+
path.join(namespaceDir, "session-states"),
|
|
3093
|
+
];
|
|
3094
|
+
for (const dir of watchedDirs) {
|
|
3095
|
+
await ensureDir(dir);
|
|
3096
|
+
try {
|
|
3097
|
+
const watcher = nodeFs.watch(dir, (_eventType, filename) => {
|
|
3098
|
+
if (dir === eventDir) {
|
|
3099
|
+
if (filename === "event-journal.jsonl" || filename === "latest-seq.json") finish();
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
if (typeof filename === "string" && filename.endsWith(".json")) finish();
|
|
3103
|
+
});
|
|
3104
|
+
watchers.push(watcher);
|
|
3105
|
+
} catch {
|
|
3106
|
+
// Directory may not be watchable on this platform; the timeout remains a bounded fallback.
|
|
3107
|
+
}
|
|
1501
3108
|
}
|
|
1502
3109
|
return deferred.promise;
|
|
1503
3110
|
}
|
|
@@ -1538,7 +3145,7 @@ export async function readCoordinatorArtifact(
|
|
|
1538
3145
|
} catch (error) {
|
|
1539
3146
|
return {
|
|
1540
3147
|
ok: false,
|
|
1541
|
-
reason: (error
|
|
3148
|
+
reason: publicCoordinatorError(error),
|
|
1542
3149
|
};
|
|
1543
3150
|
} finally {
|
|
1544
3151
|
await handle?.close();
|
|
@@ -1552,6 +3159,122 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1552
3159
|
const services = options.services ?? {};
|
|
1553
3160
|
const namespaceDir = coordinatorNamespacePath(config);
|
|
1554
3161
|
const commandRunner = services.commandRunner ?? runCommand;
|
|
3162
|
+
const loadModelProfiles = services.resolveModelProfiles ?? loadCoordinatorModelProfiles;
|
|
3163
|
+
const readinessTimeoutMs = boundedRuntimeReadinessTimeoutMs(env.SKC_COORDINATOR_MCP_RUNTIME_READINESS_TIMEOUT_MS);
|
|
3164
|
+
async function withSessionTransition<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
|
|
3165
|
+
return await withSessionMutation(`${namespaceDir}::${sessionId}`, operation);
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
async function startCoordinatorSession(
|
|
3169
|
+
cwd: string,
|
|
3170
|
+
prompt?: string,
|
|
3171
|
+
mpreset?: string | null,
|
|
3172
|
+
): Promise<Record<string, unknown>> {
|
|
3173
|
+
const sessionId = `skc-coordinator-${randomUUID().slice(0, 8)}`;
|
|
3174
|
+
const launchId = randomUUID();
|
|
3175
|
+
const readinessRoot = path.join(namespaceDir, "launch-readiness");
|
|
3176
|
+
const readinessDir = path.join(readinessRoot, sessionId);
|
|
3177
|
+
await ensureDir(readinessDir);
|
|
3178
|
+
const canonicalRoot = await fs.realpath(readinessRoot);
|
|
3179
|
+
const canonicalDir = await fs.realpath(readinessDir);
|
|
3180
|
+
if (!canonicalDir.startsWith(`${canonicalRoot}${path.sep}`))
|
|
3181
|
+
throw new Error("coordinator_readiness_path_invalid");
|
|
3182
|
+
const readinessMarkerFile = path.join(canonicalDir, `${launchId}.json`);
|
|
3183
|
+
const input: SessionStartInput = {
|
|
3184
|
+
cwd,
|
|
3185
|
+
prompt,
|
|
3186
|
+
namespace: config.namespace,
|
|
3187
|
+
mpreset,
|
|
3188
|
+
worktree: true,
|
|
3189
|
+
sessionId,
|
|
3190
|
+
launchId,
|
|
3191
|
+
readinessMarkerFile,
|
|
3192
|
+
readinessFile: readinessMarkerFile,
|
|
3193
|
+
};
|
|
3194
|
+
const started = services.startSession
|
|
3195
|
+
? await services.startSession(input)
|
|
3196
|
+
: await startTmuxSession(config, input, namespaceDir, commandRunner, services.ownerIsolationProbe);
|
|
3197
|
+
const record = asRecord(started);
|
|
3198
|
+
if (!record) throw new Error("coordinator_session_command_required");
|
|
3199
|
+
const attestedSessionId = optionalString(record.sessionId ?? record.session_id);
|
|
3200
|
+
const attestedLaunchId = optionalString(record.launchId ?? record.launch_id);
|
|
3201
|
+
const attestedMarkerFile = optionalString(
|
|
3202
|
+
record.readinessMarkerFile ?? record.readiness_marker_file ?? record.readinessFile ?? record.readiness_file,
|
|
3203
|
+
);
|
|
3204
|
+
if (!attestedSessionId || !attestedLaunchId || !attestedMarkerFile)
|
|
3205
|
+
throw new Error("coordinator_launch_attestation_missing");
|
|
3206
|
+
if (
|
|
3207
|
+
attestedSessionId !== sessionId ||
|
|
3208
|
+
attestedLaunchId !== launchId ||
|
|
3209
|
+
(await canonicalizePath(attestedMarkerFile)) !== readinessMarkerFile
|
|
3210
|
+
)
|
|
3211
|
+
throw new Error("coordinator_launch_attestation_mismatch");
|
|
3212
|
+
return normalizeSession({
|
|
3213
|
+
...record,
|
|
3214
|
+
session_id: sessionId,
|
|
3215
|
+
launch_id: launchId,
|
|
3216
|
+
readiness_marker_file: readinessMarkerFile,
|
|
3217
|
+
readiness_marker_path: readinessMarkerFile,
|
|
3218
|
+
origin: "coordinator_created",
|
|
3219
|
+
runtime_ready_launch_id: null,
|
|
3220
|
+
runtime_ready_at: null,
|
|
3221
|
+
runtime_readiness_latched: false,
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
function sessionReadinessAuthorityError(
|
|
3226
|
+
reason: "coordinator_launch_metadata_missing" | "session_origin_unverified",
|
|
3227
|
+
session: Record<string, unknown>,
|
|
3228
|
+
): Error {
|
|
3229
|
+
const error = new Error(reason);
|
|
3230
|
+
Object.assign(error, {
|
|
3231
|
+
sessionId: optionalString(session.session_id),
|
|
3232
|
+
action: "Start a new coordinator session or explicitly register the live external tmux session.",
|
|
3233
|
+
});
|
|
3234
|
+
return error;
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
function assertSessionReadinessAuthority(session: Record<string, unknown>): void {
|
|
3238
|
+
if (session.origin === "registered") return;
|
|
3239
|
+
const tmuxTarget = session.tmux_target ?? session.tmuxTarget;
|
|
3240
|
+
if (
|
|
3241
|
+
session.origin == null &&
|
|
3242
|
+
typeof session.registered_at === "string" &&
|
|
3243
|
+
session.registered_at.length > 0 &&
|
|
3244
|
+
session.authoritative === true &&
|
|
3245
|
+
hasTmuxIdentity(session) &&
|
|
3246
|
+
typeof tmuxTarget === "string" &&
|
|
3247
|
+
tmuxTarget.length > 0
|
|
3248
|
+
)
|
|
3249
|
+
return;
|
|
3250
|
+
if (session.origin === "coordinator_created") {
|
|
3251
|
+
const sessionId = optionalString(session.session_id);
|
|
3252
|
+
const launchId = optionalString(session.launch_id);
|
|
3253
|
+
const markerFile = optionalString(session.readiness_marker_file ?? session.readiness_marker_path);
|
|
3254
|
+
if (!sessionId || !launchId || !markerFile) {
|
|
3255
|
+
throw sessionReadinessAuthorityError("coordinator_launch_metadata_missing", session);
|
|
3256
|
+
}
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
throw sessionReadinessAuthorityError("session_origin_unverified", session);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
async function awaitRuntimeReadiness(session: Record<string, unknown>): Promise<boolean> {
|
|
3263
|
+
assertSessionReadinessAuthority(session);
|
|
3264
|
+
if (session.origin === "registered" || session.origin == null) return true;
|
|
3265
|
+
const sessionId = optionalString(session.session_id);
|
|
3266
|
+
const launchId = optionalString(session.launch_id);
|
|
3267
|
+
const markerFile = optionalString(session.readiness_marker_file ?? session.readiness_marker_path);
|
|
3268
|
+
if (!sessionId || !launchId || !markerFile) throw new Error("coordinator_launch_metadata_missing");
|
|
3269
|
+
if (session.runtime_ready_launch_id === launchId) return true;
|
|
3270
|
+
return await waitForRuntimeReadinessMarker(
|
|
3271
|
+
markerFile,
|
|
3272
|
+
sessionId,
|
|
3273
|
+
launchId,
|
|
3274
|
+
path.join(namespaceDir, "launch-readiness"),
|
|
3275
|
+
readinessTimeoutMs,
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
1555
3278
|
|
|
1556
3279
|
async function listSessions(): Promise<unknown[]> {
|
|
1557
3280
|
if (!config.namespace.profile || !config.namespace.repo) return [];
|
|
@@ -1561,6 +3284,57 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1561
3284
|
function sessionFile(sessionId: unknown): string {
|
|
1562
3285
|
return path.join(namespaceDir, "sessions", `${safeExternalId("session", sessionId)}.json`);
|
|
1563
3286
|
}
|
|
3287
|
+
async function compensateFailedOwnerStart(
|
|
3288
|
+
session: Record<string, unknown>,
|
|
3289
|
+
ownerTransaction: {
|
|
3290
|
+
rollback?: () => Promise<"cleaned" | "failed" | "unverifiable">;
|
|
3291
|
+
} | null,
|
|
3292
|
+
): Promise<"cleaned" | "failed" | "unverifiable"> {
|
|
3293
|
+
let cleanupStatus: "cleaned" | "failed" | "unverifiable" = "unverifiable";
|
|
3294
|
+
try {
|
|
3295
|
+
cleanupStatus = (await ownerTransaction?.rollback?.()) ?? "unverifiable";
|
|
3296
|
+
} catch {
|
|
3297
|
+
cleanupStatus = "unverifiable";
|
|
3298
|
+
}
|
|
3299
|
+
try {
|
|
3300
|
+
const sessionId = String(session.session_id);
|
|
3301
|
+
const cwd = typeof session.cwd === "string" ? path.resolve(session.cwd) : null;
|
|
3302
|
+
if (!cwd) throw coordinatorStateError("invalid");
|
|
3303
|
+
const state: CoordinatorSessionState = {
|
|
3304
|
+
schema_version: 1,
|
|
3305
|
+
session_id: sessionId,
|
|
3306
|
+
state: "errored",
|
|
3307
|
+
ready_for_input: false,
|
|
3308
|
+
current_turn_id: null,
|
|
3309
|
+
last_turn_id: null,
|
|
3310
|
+
updated_at: new Date().toISOString(),
|
|
3311
|
+
source: "coordinator",
|
|
3312
|
+
live: false,
|
|
3313
|
+
reason: "coordinator_start_rolled_back",
|
|
3314
|
+
cwd,
|
|
3315
|
+
workdir: cwd,
|
|
3316
|
+
session_file: null,
|
|
3317
|
+
};
|
|
3318
|
+
await writeJsonFile(sessionStateFile(namespaceDir, sessionId), state);
|
|
3319
|
+
await fs.rm(sessionFile(sessionId), { force: true });
|
|
3320
|
+
await appendCoordinatorEvent(namespaceDir, {
|
|
3321
|
+
kind: "session.state_changed",
|
|
3322
|
+
sessionId,
|
|
3323
|
+
summary: `Session ${sessionId} start rolled back`,
|
|
3324
|
+
payloadRef: path.relative(namespaceDir, sessionStateFile(namespaceDir, sessionId)),
|
|
3325
|
+
metadata: {
|
|
3326
|
+
state: "errored",
|
|
3327
|
+
ready_for_input: false,
|
|
3328
|
+
live: false,
|
|
3329
|
+
reason: "coordinator_start_rolled_back",
|
|
3330
|
+
},
|
|
3331
|
+
});
|
|
3332
|
+
} catch {
|
|
3333
|
+
return "failed";
|
|
3334
|
+
}
|
|
3335
|
+
return cleanupStatus;
|
|
3336
|
+
}
|
|
3337
|
+
|
|
1564
3338
|
async function listQuestions(args: Record<string, unknown>): Promise<unknown[]> {
|
|
1565
3339
|
const sessionId = args.session_id == null ? null : safeExternalId("session", args.session_id);
|
|
1566
3340
|
const status = typeof args.status === "string" && args.status.length > 0 ? args.status : null;
|
|
@@ -1584,11 +3358,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1584
3358
|
return evidence;
|
|
1585
3359
|
}
|
|
1586
3360
|
|
|
1587
|
-
async function
|
|
3361
|
+
async function prepareActiveTurnUnlocked(session: Record<string, unknown>, turn: TurnRecord): Promise<TurnRecord> {
|
|
3362
|
+
const active = await readActiveTurn(namespaceDir, turn.session_id);
|
|
3363
|
+
if (active && active.turn_id !== turn.turn_id) return active;
|
|
1588
3364
|
const timestamp = new Date().toISOString();
|
|
1589
3365
|
const target = typeof session.tmux_target === "string" ? session.tmux_target : session.tmuxTarget;
|
|
1590
|
-
const
|
|
1591
|
-
const pendingTurn: TurnRecord = {
|
|
3366
|
+
const pending: TurnRecord = {
|
|
1592
3367
|
...turn,
|
|
1593
3368
|
status: "active",
|
|
1594
3369
|
delivery: {
|
|
@@ -1597,90 +3372,312 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1597
3372
|
target: typeof target === "string" ? target : null,
|
|
1598
3373
|
tmux_keys_sent: false,
|
|
1599
3374
|
prompt_acknowledged: false,
|
|
1600
|
-
state: "
|
|
3375
|
+
state: "awaiting_runtime_readiness",
|
|
1601
3376
|
attempts: [
|
|
1602
3377
|
{
|
|
1603
3378
|
delivered: false,
|
|
1604
3379
|
tmux_keys_sent: false,
|
|
1605
3380
|
channel: "tmux_keys",
|
|
1606
3381
|
created_at: timestamp,
|
|
1607
|
-
reason: "
|
|
3382
|
+
reason: "awaiting_runtime_readiness",
|
|
1608
3383
|
},
|
|
1609
3384
|
],
|
|
1610
3385
|
},
|
|
1611
|
-
liveness: { checked_at:
|
|
3386
|
+
liveness: { checked_at: null, live: null, reason: null },
|
|
1612
3387
|
started_at: turn.started_at ?? timestamp,
|
|
1613
3388
|
updated_at: timestamp,
|
|
1614
3389
|
};
|
|
1615
|
-
await writeTurnRecord(namespaceDir,
|
|
1616
|
-
await writeActiveTurn(namespaceDir,
|
|
1617
|
-
await writeSessionState(namespaceDir,
|
|
1618
|
-
currentTurnId:
|
|
1619
|
-
live,
|
|
1620
|
-
reason:
|
|
3390
|
+
await writeTurnRecord(namespaceDir, pending);
|
|
3391
|
+
await writeActiveTurn(namespaceDir, pending);
|
|
3392
|
+
await writeSessionState(namespaceDir, pending.session_id, "booting", {
|
|
3393
|
+
currentTurnId: pending.turn_id,
|
|
3394
|
+
live: null,
|
|
3395
|
+
reason: "awaiting_runtime_readiness",
|
|
1621
3396
|
});
|
|
3397
|
+
return pending;
|
|
3398
|
+
}
|
|
1622
3399
|
|
|
1623
|
-
|
|
1624
|
-
const
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
3400
|
+
async function completePreparedTurn(session: Record<string, unknown>, pendingTurn: TurnRecord): Promise<TurnRecord> {
|
|
3401
|
+
const ready = await awaitRuntimeReadiness(session);
|
|
3402
|
+
const observedLive = hasTmuxIdentity(session) ? await hasTmuxSession(session, commandRunner) : null;
|
|
3403
|
+
return await withSessionTransition(pendingTurn.session_id, async () => {
|
|
3404
|
+
const current = await readTurnRecord(namespaceDir, pendingTurn.turn_id);
|
|
3405
|
+
const active = await readActiveTurn(namespaceDir, pendingTurn.session_id);
|
|
3406
|
+
if (
|
|
3407
|
+
!current ||
|
|
3408
|
+
!active ||
|
|
3409
|
+
active.turn_id !== pendingTurn.turn_id ||
|
|
3410
|
+
TERMINAL_TURN_STATUSES.has(current.status) ||
|
|
3411
|
+
current.delivery.state !== "awaiting_runtime_readiness" ||
|
|
3412
|
+
current.delivery.tmux_keys_sent === true
|
|
3413
|
+
)
|
|
3414
|
+
return current ?? pendingTurn;
|
|
3415
|
+
if (!ready) {
|
|
3416
|
+
const completedAt = new Date().toISOString();
|
|
3417
|
+
const message = `SKC runtime did not attest interactive input readiness within ${readinessTimeoutMs}ms.`;
|
|
3418
|
+
const failed: TurnRecord = {
|
|
3419
|
+
...current,
|
|
3420
|
+
status: "failed",
|
|
3421
|
+
delivery: {
|
|
3422
|
+
...current.delivery,
|
|
1636
3423
|
delivered: false,
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
3424
|
+
queued: false,
|
|
3425
|
+
tmux_keys_sent: false,
|
|
3426
|
+
prompt_acknowledged: false,
|
|
3427
|
+
state: "awaiting_runtime_readiness",
|
|
1641
3428
|
},
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
3429
|
+
final_response: {
|
|
3430
|
+
text: message,
|
|
3431
|
+
format: "markdown",
|
|
3432
|
+
source: "coordinator_runtime_readiness",
|
|
3433
|
+
artifact_path: null,
|
|
3434
|
+
truncated: false,
|
|
3435
|
+
},
|
|
3436
|
+
error: { code: RUNTIME_READINESS_TIMEOUT_REASON, message, recoverable: true },
|
|
3437
|
+
evidence: [
|
|
3438
|
+
...current.evidence,
|
|
3439
|
+
{
|
|
3440
|
+
type: RUNTIME_READINESS_TIMEOUT_REASON,
|
|
3441
|
+
created_at: completedAt,
|
|
3442
|
+
tmux_keys_sent: false,
|
|
3443
|
+
prompt_acknowledged: false,
|
|
3444
|
+
},
|
|
3445
|
+
],
|
|
3446
|
+
updated_at: completedAt,
|
|
3447
|
+
completed_at: completedAt,
|
|
3448
|
+
};
|
|
3449
|
+
await writeTurnRecord(namespaceDir, failed);
|
|
3450
|
+
await clearActiveTurn(namespaceDir, failed);
|
|
3451
|
+
await writeSessionState(namespaceDir, failed.session_id, "booting", {
|
|
3452
|
+
lastTurnId: failed.turn_id,
|
|
3453
|
+
live: null,
|
|
3454
|
+
reason: RUNTIME_READINESS_TIMEOUT_REASON,
|
|
3455
|
+
});
|
|
3456
|
+
return failed;
|
|
3457
|
+
}
|
|
3458
|
+
const currentSession = asRecord(await readJsonFile(sessionFile(pendingTurn.session_id))) ?? session;
|
|
3459
|
+
if (
|
|
3460
|
+
currentSession.origin === "coordinator_created" &&
|
|
3461
|
+
(currentSession.session_id !== session.session_id ||
|
|
3462
|
+
currentSession.launch_id !== session.launch_id ||
|
|
3463
|
+
currentSession.readiness_marker_file !== session.readiness_marker_file)
|
|
3464
|
+
)
|
|
3465
|
+
return current;
|
|
3466
|
+
const latched =
|
|
3467
|
+
currentSession.origin === "coordinator_created"
|
|
3468
|
+
? {
|
|
3469
|
+
...currentSession,
|
|
3470
|
+
runtime_ready_launch_id: currentSession.launch_id,
|
|
3471
|
+
runtime_ready_at: new Date().toISOString(),
|
|
3472
|
+
runtime_readiness_latched: true,
|
|
3473
|
+
}
|
|
3474
|
+
: currentSession;
|
|
3475
|
+
if (latched !== currentSession) await writeJsonFile(sessionFile(pendingTurn.session_id), latched);
|
|
3476
|
+
const target = typeof latched.tmux_target === "string" ? latched.tmux_target : latched.tmuxTarget;
|
|
3477
|
+
const attemptId = `delivery-${randomUUID()}`;
|
|
3478
|
+
const attemptedAt = new Date().toISOString();
|
|
3479
|
+
const delivering: TurnRecord = {
|
|
3480
|
+
...current,
|
|
3481
|
+
status: "delivering",
|
|
3482
|
+
updated_at: attemptedAt,
|
|
3483
|
+
delivery: {
|
|
3484
|
+
...current.delivery,
|
|
3485
|
+
queued: false,
|
|
3486
|
+
state: "delivering",
|
|
3487
|
+
attempts: [
|
|
3488
|
+
...current.delivery.attempts,
|
|
3489
|
+
{
|
|
3490
|
+
attempt_id: attemptId,
|
|
3491
|
+
delivered: false,
|
|
3492
|
+
tmux_keys_sent: false,
|
|
3493
|
+
channel: "tmux_keys",
|
|
3494
|
+
created_at: attemptedAt,
|
|
3495
|
+
reason: "delivery_attempt",
|
|
3496
|
+
},
|
|
3497
|
+
],
|
|
3498
|
+
},
|
|
3499
|
+
};
|
|
3500
|
+
await writeTurnRecord(namespaceDir, delivering);
|
|
3501
|
+
await writeActiveTurn(namespaceDir, delivering);
|
|
3502
|
+
await writeSessionState(namespaceDir, delivering.session_id, "running", {
|
|
3503
|
+
currentTurnId: delivering.turn_id,
|
|
3504
|
+
live: observedLive,
|
|
3505
|
+
reason: null,
|
|
1660
3506
|
});
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
3507
|
+
const tmuxKeysSent = await sendTmuxPrompt(
|
|
3508
|
+
latched,
|
|
3509
|
+
delivering.prompt.text,
|
|
3510
|
+
commandRunner,
|
|
3511
|
+
services.ownerIsolationProbe,
|
|
3512
|
+
);
|
|
3513
|
+
const deliveredAt = new Date().toISOString();
|
|
3514
|
+
const delivered: TurnRecord = {
|
|
3515
|
+
...delivering,
|
|
3516
|
+
status: "active",
|
|
3517
|
+
updated_at: deliveredAt,
|
|
3518
|
+
delivery: {
|
|
3519
|
+
...delivering.delivery,
|
|
3520
|
+
delivered: false,
|
|
3521
|
+
queued: !tmuxKeysSent,
|
|
3522
|
+
tmux_keys_sent: tmuxKeysSent,
|
|
3523
|
+
prompt_acknowledged: false,
|
|
3524
|
+
state: tmuxKeysSent ? "tmux_keys_sent" : "unavailable",
|
|
3525
|
+
attempts: [
|
|
3526
|
+
...delivering.delivery.attempts.slice(0, -1),
|
|
3527
|
+
{
|
|
3528
|
+
attempt_id: attemptId,
|
|
3529
|
+
delivered: false,
|
|
3530
|
+
tmux_keys_sent: tmuxKeysSent,
|
|
3531
|
+
channel: "tmux_keys",
|
|
3532
|
+
created_at: deliveredAt,
|
|
3533
|
+
reason: tmuxKeysSent ? "awaiting_runtime_ack" : "tmux_delivery_unavailable",
|
|
3534
|
+
},
|
|
3535
|
+
],
|
|
3536
|
+
},
|
|
3537
|
+
};
|
|
3538
|
+
await writeTurnRecord(namespaceDir, delivered);
|
|
3539
|
+
await writeActiveTurn(namespaceDir, delivered);
|
|
3540
|
+
await appendCoordinatorEvent(namespaceDir, {
|
|
3541
|
+
kind: tmuxKeysSent ? "tmux.delivery_succeeded" : "tmux.delivery_failed",
|
|
3542
|
+
sessionId: delivered.session_id,
|
|
3543
|
+
turnId: delivered.turn_id,
|
|
3544
|
+
summary: tmuxKeysSent
|
|
3545
|
+
? `Tmux delivery succeeded for turn ${delivered.turn_id}`
|
|
3546
|
+
: `Tmux delivery failed for turn ${delivered.turn_id}`,
|
|
3547
|
+
payloadRef: path.relative(namespaceDir, turnFile(namespaceDir, delivered.turn_id)),
|
|
3548
|
+
metadata: {
|
|
3549
|
+
target: typeof target === "string" ? target : null,
|
|
3550
|
+
live: observedLive,
|
|
3551
|
+
attempt_id: attemptId,
|
|
3552
|
+
},
|
|
3553
|
+
});
|
|
3554
|
+
return delivered;
|
|
1671
3555
|
});
|
|
1672
|
-
return resolvedTurn;
|
|
1673
3556
|
}
|
|
1674
3557
|
|
|
1675
|
-
async function
|
|
3558
|
+
async function activateTurn(session: Record<string, unknown>, turn: TurnRecord): Promise<TurnRecord> {
|
|
3559
|
+
const pendingTurn = await withSessionTransition(
|
|
3560
|
+
turn.session_id,
|
|
3561
|
+
async () => await prepareActiveTurnUnlocked(session, turn),
|
|
3562
|
+
);
|
|
3563
|
+
if (pendingTurn.turn_id !== turn.turn_id) return pendingTurn;
|
|
3564
|
+
return await completePreparedTurn(session, pendingTurn);
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
async function prepareNextQueuedTurnUnlocked(sessionId: string): Promise<{
|
|
3568
|
+
session: Record<string, unknown>;
|
|
3569
|
+
turn: TurnRecord;
|
|
3570
|
+
} | null> {
|
|
1676
3571
|
const session = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
1677
|
-
if (!session) return null;
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
3572
|
+
if (!session || (await readActiveTurn(namespaceDir, sessionId))) return null;
|
|
3573
|
+
try {
|
|
3574
|
+
assertSessionReadinessAuthority(session);
|
|
3575
|
+
} catch {
|
|
3576
|
+
return null;
|
|
3577
|
+
}
|
|
3578
|
+
const queuedTurns = (await listTurnRecords(namespaceDir))
|
|
3579
|
+
.filter(turn => turn.session_id === sessionId && turn.status === "queued")
|
|
1681
3580
|
.sort((left, right) => left.created_at.localeCompare(right.created_at));
|
|
1682
3581
|
const nextTurn = queuedTurns[0];
|
|
1683
|
-
|
|
3582
|
+
if (!nextTurn) return null;
|
|
3583
|
+
return { session, turn: await prepareActiveTurnUnlocked(session, nextTurn) };
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
async function reconcileRuntimeAcknowledgementFenced(
|
|
3587
|
+
turnId: string,
|
|
3588
|
+
sessionState: CoordinatorSessionState | null,
|
|
3589
|
+
failOnTimeout: boolean,
|
|
3590
|
+
): Promise<TurnRecord | null> {
|
|
3591
|
+
const observed = await readTurnRecord(namespaceDir, turnId);
|
|
3592
|
+
if (!observed) return null;
|
|
3593
|
+
const observedTurnId = sessionState?.current_turn_id;
|
|
3594
|
+
return await withSessionTransition(observed.session_id, async () => {
|
|
3595
|
+
const current = await readTurnRecord(namespaceDir, turnId);
|
|
3596
|
+
if (!current || !ACTIVE_TURN_STATUSES.has(current.status)) return current;
|
|
3597
|
+
const currentState = await readSessionState(namespaceDir, current.session_id);
|
|
3598
|
+
const state =
|
|
3599
|
+
sessionState &&
|
|
3600
|
+
sessionState.updated_at === currentState?.updated_at &&
|
|
3601
|
+
observedTurnId === currentState.current_turn_id
|
|
3602
|
+
? sessionState
|
|
3603
|
+
: currentState;
|
|
3604
|
+
return await reconcileRuntimeAcknowledgement(namespaceDir, current, state, promptAckTimeoutMs, {
|
|
3605
|
+
failOnTimeout,
|
|
3606
|
+
});
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
async function markTurnTerminalFromSessionStateFenced(
|
|
3611
|
+
turnId: string,
|
|
3612
|
+
observedState: CoordinatorSessionState,
|
|
3613
|
+
): Promise<TurnRecord | null> {
|
|
3614
|
+
const observed = await readTurnRecord(namespaceDir, turnId);
|
|
3615
|
+
if (!observed) return null;
|
|
3616
|
+
return await withSessionTransition(observed.session_id, async () => {
|
|
3617
|
+
const current = await readTurnRecord(namespaceDir, turnId);
|
|
3618
|
+
if (!current || !ACTIVE_TURN_STATUSES.has(current.status)) return current;
|
|
3619
|
+
const currentState = await readSessionState(namespaceDir, current.session_id);
|
|
3620
|
+
if (!currentState || currentState.updated_at !== observedState.updated_at) return current;
|
|
3621
|
+
const matchesTurn =
|
|
3622
|
+
currentState.current_turn_id === current.turn_id ||
|
|
3623
|
+
(currentState.state === "errored" &&
|
|
3624
|
+
currentState.source === "agent_session_event" &&
|
|
3625
|
+
currentState.current_turn_id == null);
|
|
3626
|
+
if (!matchesTurn || (currentState.state !== "completed" && currentState.state !== "errored")) return current;
|
|
3627
|
+
return await markTurnTerminalFromSessionState(namespaceDir, current, currentState);
|
|
3628
|
+
});
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
async function markTurnFailedForUnavailableSessionFenced(
|
|
3632
|
+
turnId: string,
|
|
3633
|
+
reason: string,
|
|
3634
|
+
): Promise<TurnRecord | null> {
|
|
3635
|
+
const observed = await readTurnRecord(namespaceDir, turnId);
|
|
3636
|
+
if (!observed) return null;
|
|
3637
|
+
return await withSessionTransition(observed.session_id, async () => {
|
|
3638
|
+
const current = await readTurnRecord(namespaceDir, turnId);
|
|
3639
|
+
if (!current || !ACTIVE_TURN_STATUSES.has(current.status)) return current;
|
|
3640
|
+
return await markTurnFailedForUnavailableSession(namespaceDir, current, reason);
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
async function preparePromptTurn(
|
|
3645
|
+
session: Record<string, unknown>,
|
|
3646
|
+
prompt: string,
|
|
3647
|
+
options: { force: boolean; queue: boolean; queueWithoutActive?: boolean },
|
|
3648
|
+
): Promise<
|
|
3649
|
+
| { ok: false; activeTurn: TurnRecord }
|
|
3650
|
+
| { ok: true; turn: TurnRecord; priorActiveTurn: TurnRecord | null; queued: boolean }
|
|
3651
|
+
> {
|
|
3652
|
+
const sessionId = safeExternalId("session", session.session_id);
|
|
3653
|
+
const prepared = await withSessionTransition(sessionId, async () => {
|
|
3654
|
+
const activeTurn = await readActiveTurn(namespaceDir, sessionId);
|
|
3655
|
+
if (activeTurn && !options.force && !options.queue) {
|
|
3656
|
+
return { ok: false as const, activeTurn };
|
|
3657
|
+
}
|
|
3658
|
+
if (activeTurn && options.force) {
|
|
3659
|
+
const timestamp = new Date().toISOString();
|
|
3660
|
+
const superseded: TurnRecord = {
|
|
3661
|
+
...activeTurn,
|
|
3662
|
+
status: "superseded",
|
|
3663
|
+
updated_at: timestamp,
|
|
3664
|
+
completed_at: timestamp,
|
|
3665
|
+
};
|
|
3666
|
+
await writeTurnRecord(namespaceDir, superseded);
|
|
3667
|
+
await clearActiveTurn(namespaceDir, superseded);
|
|
3668
|
+
}
|
|
3669
|
+
const shouldQueue =
|
|
3670
|
+
options.queue && !options.force && (activeTurn !== null || options.queueWithoutActive === true);
|
|
3671
|
+
const turn = makeTurnRecord(config, sessionId, prompt, shouldQueue ? "queued" : "active");
|
|
3672
|
+
if (shouldQueue) {
|
|
3673
|
+
await writeTurnRecord(namespaceDir, turn);
|
|
3674
|
+
return { ok: true as const, turn, priorActiveTurn: activeTurn, queued: true };
|
|
3675
|
+
}
|
|
3676
|
+
const pending = await prepareActiveTurnUnlocked(session, turn);
|
|
3677
|
+
return { ok: true as const, turn: pending, priorActiveTurn: activeTurn, queued: false };
|
|
3678
|
+
});
|
|
3679
|
+
if (!prepared.ok || prepared.queued) return prepared;
|
|
3680
|
+
return { ...prepared, turn: await completePreparedTurn(session, prepared.turn) };
|
|
1684
3681
|
}
|
|
1685
3682
|
|
|
1686
3683
|
async function readTurnPayload(
|
|
@@ -1697,24 +3694,21 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1697
3694
|
let resolvedTurn = turn;
|
|
1698
3695
|
let advisoryStatus: Record<string, unknown> = { live: false };
|
|
1699
3696
|
let sessionState = await readSessionState(namespaceDir, turn.session_id);
|
|
1700
|
-
resolvedTurn =
|
|
1701
|
-
|
|
1702
|
-
resolvedTurn,
|
|
1703
|
-
sessionState,
|
|
1704
|
-
promptAckTimeoutMs,
|
|
1705
|
-
{ failOnTimeout: false },
|
|
1706
|
-
);
|
|
3697
|
+
resolvedTurn =
|
|
3698
|
+
(await reconcileRuntimeAcknowledgementFenced(resolvedTurn.turn_id, sessionState, false)) ?? resolvedTurn;
|
|
1707
3699
|
if (resolvedTurn !== turn) sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1708
3700
|
if (
|
|
1709
3701
|
sessionState &&
|
|
1710
3702
|
ACTIVE_TURN_STATUSES.has(resolvedTurn.status) &&
|
|
3703
|
+
runtimeStateMatchesPrivateOwnerGeneration(sessionState, session) &&
|
|
1711
3704
|
(sessionState.current_turn_id === resolvedTurn.turn_id ||
|
|
1712
3705
|
(sessionState.state === "errored" &&
|
|
1713
3706
|
sessionState.source === "agent_session_event" &&
|
|
1714
3707
|
sessionState.current_turn_id == null)) &&
|
|
1715
3708
|
(sessionState.state === "completed" || sessionState.state === "errored")
|
|
1716
3709
|
) {
|
|
1717
|
-
resolvedTurn =
|
|
3710
|
+
resolvedTurn =
|
|
3711
|
+
(await markTurnTerminalFromSessionStateFenced(resolvedTurn.turn_id, sessionState)) ?? resolvedTurn;
|
|
1718
3712
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1719
3713
|
} else if (
|
|
1720
3714
|
sessionState &&
|
|
@@ -1726,14 +3720,14 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1726
3720
|
session &&
|
|
1727
3721
|
hasTmuxIdentity(session)
|
|
1728
3722
|
) {
|
|
1729
|
-
resolvedTurn =
|
|
1730
|
-
|
|
1731
|
-
resolvedTurn
|
|
1732
|
-
"tmux_delivery_unavailable",
|
|
1733
|
-
);
|
|
3723
|
+
resolvedTurn =
|
|
3724
|
+
(await markTurnFailedForUnavailableSessionFenced(resolvedTurn.turn_id, "tmux_delivery_unavailable")) ??
|
|
3725
|
+
resolvedTurn;
|
|
1734
3726
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1735
3727
|
} else if (!session && ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
1736
|
-
resolvedTurn =
|
|
3728
|
+
resolvedTurn =
|
|
3729
|
+
(await markTurnFailedForUnavailableSessionFenced(resolvedTurn.turn_id, "session_record_missing")) ??
|
|
3730
|
+
resolvedTurn;
|
|
1737
3731
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1738
3732
|
} else if (session) {
|
|
1739
3733
|
advisoryStatus = await inspectTmuxSession(session, boundedLineCount(lines), commandRunner);
|
|
@@ -1742,21 +3736,15 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1742
3736
|
hasTmuxIdentity(session) &&
|
|
1743
3737
|
advisoryStatus.live === false
|
|
1744
3738
|
) {
|
|
1745
|
-
resolvedTurn =
|
|
1746
|
-
|
|
1747
|
-
resolvedTurn
|
|
1748
|
-
"tmux_session_missing",
|
|
1749
|
-
);
|
|
3739
|
+
resolvedTurn =
|
|
3740
|
+
(await markTurnFailedForUnavailableSessionFenced(resolvedTurn.turn_id, "tmux_session_missing")) ??
|
|
3741
|
+
resolvedTurn;
|
|
1750
3742
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1751
3743
|
}
|
|
1752
3744
|
}
|
|
1753
3745
|
if (ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
1754
|
-
resolvedTurn =
|
|
1755
|
-
|
|
1756
|
-
resolvedTurn,
|
|
1757
|
-
sessionState,
|
|
1758
|
-
promptAckTimeoutMs,
|
|
1759
|
-
);
|
|
3746
|
+
resolvedTurn =
|
|
3747
|
+
(await reconcileRuntimeAcknowledgementFenced(resolvedTurn.turn_id, sessionState, true)) ?? resolvedTurn;
|
|
1760
3748
|
if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
1761
3749
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1762
3750
|
}
|
|
@@ -1778,51 +3766,59 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1778
3766
|
}
|
|
1779
3767
|
|
|
1780
3768
|
async function reconcileActiveTurnAcknowledgements(): Promise<void> {
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
3769
|
+
await preflightCoordinatorMutation(namespaceDir, undefined, {
|
|
3770
|
+
rejectTerminalRuntimeEvidence: false,
|
|
3771
|
+
});
|
|
3772
|
+
|
|
3773
|
+
const turns = (await listTurnRecords(namespaceDir)).filter(turn => ACTIVE_TURN_STATUSES.has(turn.status));
|
|
1784
3774
|
for (const turn of turns) {
|
|
1785
3775
|
let sessionState = await readSessionState(namespaceDir, turn.session_id);
|
|
1786
|
-
const resolvedTurn = await reconcileRuntimeAcknowledgement(
|
|
1787
|
-
namespaceDir,
|
|
1788
|
-
turn,
|
|
1789
|
-
sessionState,
|
|
1790
|
-
promptAckTimeoutMs,
|
|
1791
|
-
{ failOnTimeout: false },
|
|
1792
|
-
);
|
|
1793
|
-
if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) continue;
|
|
1794
|
-
if (resolvedTurn !== turn) sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1795
|
-
const session = asRecord(await readJsonFile(sessionFile(resolvedTurn.session_id)));
|
|
1796
3776
|
if (
|
|
1797
3777
|
sessionState &&
|
|
1798
|
-
sessionState.
|
|
3778
|
+
(sessionState.state === "completed" || sessionState.state === "errored") &&
|
|
3779
|
+
(sessionState.current_turn_id === turn.turn_id ||
|
|
3780
|
+
(sessionState.state === "errored" &&
|
|
3781
|
+
sessionState.source === "agent_session_event" &&
|
|
3782
|
+
sessionState.current_turn_id == null))
|
|
3783
|
+
) {
|
|
3784
|
+
const terminal = await markTurnTerminalFromSessionStateFenced(turn.turn_id, sessionState);
|
|
3785
|
+
if (!terminal || !ACTIVE_TURN_STATUSES.has(terminal.status)) continue;
|
|
3786
|
+
}
|
|
3787
|
+
const acknowledged = (await reconcileRuntimeAcknowledgementFenced(turn.turn_id, sessionState, false)) ?? turn;
|
|
3788
|
+
if (!ACTIVE_TURN_STATUSES.has(acknowledged.status)) continue;
|
|
3789
|
+
if (acknowledged !== turn) sessionState = await readSessionState(namespaceDir, acknowledged.session_id);
|
|
3790
|
+
const session = asRecord(await readJsonFile(sessionFile(acknowledged.session_id)));
|
|
3791
|
+
if (
|
|
3792
|
+
sessionState &&
|
|
3793
|
+
sessionState.current_turn_id === acknowledged.turn_id &&
|
|
1799
3794
|
sessionState.state === "stale" &&
|
|
1800
3795
|
sessionState.reason === "tmux_delivery_unavailable" &&
|
|
1801
|
-
|
|
3796
|
+
acknowledged.delivery.state === "unavailable" &&
|
|
1802
3797
|
session &&
|
|
1803
3798
|
hasTmuxIdentity(session)
|
|
1804
3799
|
) {
|
|
1805
|
-
await
|
|
3800
|
+
await markTurnFailedForUnavailableSessionFenced(acknowledged.turn_id, "tmux_delivery_unavailable");
|
|
1806
3801
|
continue;
|
|
1807
3802
|
}
|
|
1808
3803
|
if (!session) {
|
|
1809
|
-
await
|
|
3804
|
+
await markTurnFailedForUnavailableSessionFenced(acknowledged.turn_id, "session_record_missing");
|
|
1810
3805
|
continue;
|
|
1811
3806
|
}
|
|
1812
3807
|
if (hasTmuxIdentity(session) && (await hasTmuxSession(session, commandRunner)) === false) {
|
|
1813
|
-
await
|
|
3808
|
+
await markTurnFailedForUnavailableSessionFenced(acknowledged.turn_id, "tmux_session_missing");
|
|
1814
3809
|
continue;
|
|
1815
3810
|
}
|
|
1816
|
-
await
|
|
3811
|
+
await reconcileRuntimeAcknowledgementFenced(acknowledged.turn_id, sessionState, true);
|
|
1817
3812
|
}
|
|
1818
3813
|
}
|
|
1819
3814
|
|
|
1820
|
-
async function
|
|
3815
|
+
async function callToolUnlocked(name: string, args: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
|
|
1821
3816
|
try {
|
|
1822
3817
|
if (name === "skc_coordinator_list_sessions") return { ok: true, sessions: await listSessions() };
|
|
1823
3818
|
if (name === "skc_coordinator_register_session") {
|
|
1824
3819
|
requireCoordinatorMutation(config, "sessions", args);
|
|
1825
3820
|
const sessionId = safeExternalId("session", args.session_id);
|
|
3821
|
+
await preflightCoordinatorMutation(namespaceDir, sessionId);
|
|
1826
3822
|
const cwd = await assertCoordinatorWorkdir(config, args.cwd);
|
|
1827
3823
|
const tmuxSession = safeTmuxSessionName(args.tmux_session);
|
|
1828
3824
|
const tmuxTarget = safeTmuxTarget(args.tmux_target);
|
|
@@ -1846,7 +3842,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1846
3842
|
sessionId,
|
|
1847
3843
|
summary: `Session ${sessionId} registered for coordinator control`,
|
|
1848
3844
|
payloadRef: path.relative(namespaceDir, sessionFile(sessionId)),
|
|
1849
|
-
metadata: {
|
|
3845
|
+
metadata: {
|
|
3846
|
+
source: optionalString(args.source) ?? "register_session",
|
|
3847
|
+
visible: args.visible !== false,
|
|
3848
|
+
},
|
|
1850
3849
|
});
|
|
1851
3850
|
|
|
1852
3851
|
return {
|
|
@@ -1883,7 +3882,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1883
3882
|
}
|
|
1884
3883
|
if (name === "skc_coordinator_read_tail") {
|
|
1885
3884
|
const session = asRecord(await readJsonFile(sessionFile(args.session_id)));
|
|
1886
|
-
return {
|
|
3885
|
+
return {
|
|
3886
|
+
ok: true,
|
|
3887
|
+
lines: session ? await captureTmuxTail(session, boundedLineCount(args.lines), commandRunner) : [],
|
|
3888
|
+
};
|
|
1887
3889
|
}
|
|
1888
3890
|
if (name === "skc_coordinator_list_questions") return { ok: true, questions: await listQuestions(args) };
|
|
1889
3891
|
if (name === "skc_coordinator_list_artifacts") return { ok: true, roots: config.allowedRoots };
|
|
@@ -1918,23 +3920,31 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1918
3920
|
turns,
|
|
1919
3921
|
questions,
|
|
1920
3922
|
reports,
|
|
1921
|
-
events: buildCanonicalCoordinatorEvents({
|
|
3923
|
+
events: buildCanonicalCoordinatorEvents({
|
|
3924
|
+
sessionStates,
|
|
3925
|
+
turns,
|
|
3926
|
+
questions,
|
|
3927
|
+
reports,
|
|
3928
|
+
}),
|
|
1922
3929
|
latest_event_seq: await readLatestEventSeq(namespaceDir),
|
|
1923
3930
|
recent_events: eventSummaries(events.slice(-10)),
|
|
1924
3931
|
};
|
|
1925
3932
|
}
|
|
1926
3933
|
if (name === "skc_coordinator_watch_events") {
|
|
1927
|
-
await reconcileActiveTurnAcknowledgements
|
|
3934
|
+
await withCoordinatorTransaction(namespaceDir, reconcileActiveTurnAcknowledgements);
|
|
1928
3935
|
const limit = boundedEventLimit(args.limit);
|
|
1929
3936
|
const timeoutMs = boundedEventWatchTimeoutMs(args.timeout_ms);
|
|
1930
3937
|
let events = await readCoordinatorEvents(namespaceDir);
|
|
1931
3938
|
let matched = filterCoordinatorEvents(events, args, limit);
|
|
1932
3939
|
let timedOut = false;
|
|
1933
3940
|
if (matched.length === 0 && timeoutMs > 0) {
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
3941
|
+
const deadline = Date.now() + timeoutMs;
|
|
3942
|
+
while (matched.length === 0 && Date.now() < deadline) {
|
|
3943
|
+
await waitForCoordinatorEvents(namespaceDir, Math.min(50, Math.max(1, deadline - Date.now())));
|
|
3944
|
+
await withCoordinatorTransaction(namespaceDir, reconcileActiveTurnAcknowledgements);
|
|
3945
|
+
events = await readCoordinatorEvents(namespaceDir);
|
|
3946
|
+
matched = filterCoordinatorEvents(events, args, limit);
|
|
3947
|
+
}
|
|
1938
3948
|
timedOut = matched.length === 0;
|
|
1939
3949
|
}
|
|
1940
3950
|
return {
|
|
@@ -1953,6 +3963,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1953
3963
|
const hasPrompt = typeof args.prompt === "string" && args.prompt.trim().length > 0;
|
|
1954
3964
|
const task = hasTask ? String(args.task) : hasPrompt ? String(args.prompt) : null;
|
|
1955
3965
|
if (!task) return { ok: false, reason: "task_required" };
|
|
3966
|
+
await preflightCoordinatorMutation(namespaceDir);
|
|
3967
|
+
const mpresetResolution = await resolveCoordinatorMpreset(args.mpreset, loadModelProfiles);
|
|
3968
|
+
if (!mpresetResolution.ok) {
|
|
3969
|
+
return {
|
|
3970
|
+
ok: false,
|
|
3971
|
+
reason: mpresetResolution.reason,
|
|
3972
|
+
mpreset: mpresetResolution.mpreset,
|
|
3973
|
+
available_profiles: mpresetResolution.available_profiles,
|
|
3974
|
+
};
|
|
3975
|
+
}
|
|
3976
|
+
|
|
1956
3977
|
const promptAliasIgnored = hasTask && hasPrompt;
|
|
1957
3978
|
const mutationRequested = args.allow_mutation === true;
|
|
1958
3979
|
const taggedPrompt = workflowPrompt(delegateWorkflow, name, canonicalCwd, task, {
|
|
@@ -1964,70 +3985,85 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1964
3985
|
let reusedSession = false;
|
|
1965
3986
|
if (args.session_id != null) {
|
|
1966
3987
|
const sessionId = safeExternalId("session", args.session_id);
|
|
3988
|
+
await preflightCoordinatorMutation(namespaceDir, sessionId);
|
|
1967
3989
|
const existing = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
1968
|
-
if (!existing)
|
|
3990
|
+
if (!existing)
|
|
3991
|
+
return {
|
|
3992
|
+
ok: false,
|
|
3993
|
+
reason: "unknown_session",
|
|
3994
|
+
session_id: sessionId,
|
|
3995
|
+
};
|
|
1969
3996
|
const storedCwd = typeof existing.cwd === "string" ? existing.cwd : null;
|
|
1970
3997
|
const canonicalStored = storedCwd ? await canonicalizePath(storedCwd) : null;
|
|
1971
3998
|
const canonicalRequested = await canonicalizePath(canonicalCwd);
|
|
1972
3999
|
if (!canonicalStored || canonicalStored !== canonicalRequested) {
|
|
1973
|
-
return {
|
|
4000
|
+
return {
|
|
4001
|
+
ok: false,
|
|
4002
|
+
reason: "session_cwd_mismatch",
|
|
4003
|
+
session_id: sessionId,
|
|
4004
|
+
};
|
|
4005
|
+
}
|
|
4006
|
+
if (mpresetResolution.mpreset !== null) {
|
|
4007
|
+
const sessionMpreset = typeof existing.mpreset === "string" ? existing.mpreset : null;
|
|
4008
|
+
if (sessionMpreset !== mpresetResolution.mpreset) {
|
|
4009
|
+
return {
|
|
4010
|
+
ok: false,
|
|
4011
|
+
reason: "mpreset_conflict",
|
|
4012
|
+
session_id: sessionId,
|
|
4013
|
+
session_mpreset: sessionMpreset,
|
|
4014
|
+
requested_mpreset: mpresetResolution.mpreset,
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
1974
4017
|
}
|
|
4018
|
+
|
|
1975
4019
|
session = existing;
|
|
1976
4020
|
reusedSession = true;
|
|
1977
4021
|
} else {
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
4022
|
+
session = await startCoordinatorSession(canonicalCwd, undefined, mpresetResolution.mpreset);
|
|
4023
|
+
const ownerTransaction = asRecord(session.__coordinatorOwnerTransaction) as {
|
|
4024
|
+
commit?: () => void;
|
|
4025
|
+
rollback?: () => Promise<"cleaned" | "failed" | "unverifiable">;
|
|
4026
|
+
} | null;
|
|
4027
|
+
try {
|
|
4028
|
+
if (mpresetResolution.mpreset) session.mpreset = mpresetResolution.mpreset;
|
|
4029
|
+
await writeJsonFile(sessionFile(session.session_id), session);
|
|
4030
|
+
await appendCoordinatorEvent(namespaceDir, {
|
|
4031
|
+
kind: "session.started",
|
|
4032
|
+
sessionId: String(session.session_id),
|
|
4033
|
+
summary: `Session ${String(session.session_id)} started by coordinator delegate`,
|
|
4034
|
+
payloadRef: path.relative(namespaceDir, sessionFile(session.session_id)),
|
|
4035
|
+
metadata: { delegate: true, workflow: delegateWorkflow },
|
|
4036
|
+
});
|
|
4037
|
+
await writeSessionState(namespaceDir, String(session.session_id), "booting", {
|
|
4038
|
+
live: null,
|
|
4039
|
+
reason: "awaiting_runtime_readiness",
|
|
4040
|
+
});
|
|
4041
|
+
ownerTransaction?.commit?.();
|
|
4042
|
+
} catch (error) {
|
|
4043
|
+
if (ownerTransaction)
|
|
4044
|
+
throw new CoordinatorStartError(
|
|
4045
|
+
"coordinator_tmux_start_failed",
|
|
4046
|
+
await compensateFailedOwnerStart(session, ownerTransaction),
|
|
4047
|
+
);
|
|
4048
|
+
throw error;
|
|
4049
|
+
}
|
|
2003
4050
|
}
|
|
2004
4051
|
|
|
2005
4052
|
const sessionId = String(session.session_id);
|
|
2006
|
-
|
|
2007
|
-
|
|
4053
|
+
assertSessionReadinessAuthority(session);
|
|
4054
|
+
const prepared = await preparePromptTurn(session, taggedPrompt, {
|
|
4055
|
+
force: args.force === true,
|
|
4056
|
+
queue: args.queue === true,
|
|
4057
|
+
});
|
|
4058
|
+
if (!prepared.ok) {
|
|
2008
4059
|
return {
|
|
2009
4060
|
ok: false,
|
|
2010
4061
|
reason: "active_turn_exists",
|
|
2011
4062
|
session_id: sessionId,
|
|
2012
|
-
active_turn_id: activeTurn.turn_id,
|
|
4063
|
+
active_turn_id: prepared.activeTurn.turn_id,
|
|
2013
4064
|
};
|
|
2014
4065
|
}
|
|
2015
|
-
|
|
2016
|
-
const timestamp = new Date().toISOString();
|
|
2017
|
-
const superseded = {
|
|
2018
|
-
...activeTurn,
|
|
2019
|
-
status: "superseded" as const,
|
|
2020
|
-
updated_at: timestamp,
|
|
2021
|
-
completed_at: timestamp,
|
|
2022
|
-
};
|
|
2023
|
-
await writeTurnRecord(namespaceDir, superseded);
|
|
2024
|
-
await clearActiveTurn(namespaceDir, superseded);
|
|
2025
|
-
}
|
|
2026
|
-
const shouldQueue = args.queue === true && args.force !== true && !!activeTurn;
|
|
2027
|
-
const turn = shouldQueue
|
|
2028
|
-
? makeTurnRecord(config, sessionId, taggedPrompt, "queued")
|
|
2029
|
-
: await activateTurn(session, makeTurnRecord(config, sessionId, taggedPrompt, "active"));
|
|
2030
|
-
if (shouldQueue) await writeTurnRecord(namespaceDir, turn);
|
|
4066
|
+
const { turn, priorActiveTurn: activeTurn, queued: shouldQueue } = prepared;
|
|
2031
4067
|
await appendCoordinatorEvent(namespaceDir, {
|
|
2032
4068
|
kind: "delegation.started",
|
|
2033
4069
|
sessionId,
|
|
@@ -2101,31 +4137,50 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2101
4137
|
if (name === "skc_coordinator_start_session") {
|
|
2102
4138
|
requireCoordinatorMutation(config, "sessions", args);
|
|
2103
4139
|
const cwd = await assertCoordinatorWorkdir(config, args.cwd);
|
|
2104
|
-
|
|
4140
|
+
await preflightCoordinatorMutation(namespaceDir);
|
|
4141
|
+
const mpresetResolution = await resolveCoordinatorMpreset(args.mpreset, loadModelProfiles);
|
|
4142
|
+
if (!mpresetResolution.ok) {
|
|
4143
|
+
return {
|
|
4144
|
+
ok: false,
|
|
4145
|
+
reason: mpresetResolution.reason,
|
|
4146
|
+
mpreset: mpresetResolution.mpreset,
|
|
4147
|
+
available_profiles: mpresetResolution.available_profiles,
|
|
4148
|
+
};
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
const session = await startCoordinatorSession(
|
|
2105
4152
|
cwd,
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
4153
|
+
typeof args.prompt === "string" ? args.prompt : undefined,
|
|
4154
|
+
mpresetResolution.mpreset,
|
|
4155
|
+
);
|
|
4156
|
+
const ownerTransaction = asRecord(session.__coordinatorOwnerTransaction) as {
|
|
4157
|
+
commit?: () => void;
|
|
4158
|
+
rollback?: () => Promise<"cleaned" | "failed" | "unverifiable">;
|
|
4159
|
+
} | null;
|
|
4160
|
+
let sessionState: CoordinatorSessionState;
|
|
4161
|
+
try {
|
|
4162
|
+
if (mpresetResolution.mpreset) session.mpreset = mpresetResolution.mpreset;
|
|
4163
|
+
await writeJsonFile(sessionFile(session.session_id), session);
|
|
4164
|
+
await appendCoordinatorEvent(namespaceDir, {
|
|
4165
|
+
kind: "session.started",
|
|
4166
|
+
sessionId: String(session.session_id),
|
|
4167
|
+
summary: `Session ${String(session.session_id)} started by coordinator`,
|
|
4168
|
+
payloadRef: path.relative(namespaceDir, sessionFile(session.session_id)),
|
|
4169
|
+
metadata: { prompted: typeof args.prompt === "string" && args.prompt.length > 0 },
|
|
4170
|
+
});
|
|
4171
|
+
sessionState = await writeSessionState(namespaceDir, String(session.session_id), "booting", {
|
|
4172
|
+
live: null,
|
|
4173
|
+
reason: "awaiting_runtime_readiness",
|
|
4174
|
+
});
|
|
4175
|
+
ownerTransaction?.commit?.();
|
|
4176
|
+
} catch (error) {
|
|
4177
|
+
if (ownerTransaction)
|
|
4178
|
+
throw new CoordinatorStartError(
|
|
4179
|
+
"coordinator_tmux_start_failed",
|
|
4180
|
+
await compensateFailedOwnerStart(session, ownerTransaction),
|
|
4181
|
+
);
|
|
4182
|
+
throw error;
|
|
4183
|
+
}
|
|
2129
4184
|
if (typeof args.prompt === "string" && args.prompt.length > 0) {
|
|
2130
4185
|
const turn = await activateTurn(
|
|
2131
4186
|
session,
|
|
@@ -2161,36 +4216,28 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2161
4216
|
if (name === "skc_coordinator_send_prompt") {
|
|
2162
4217
|
requireCoordinatorMutation(config, "sessions", args);
|
|
2163
4218
|
const sessionId = safeExternalId("session", args.session_id);
|
|
4219
|
+
await preflightCoordinatorMutation(namespaceDir, sessionId);
|
|
2164
4220
|
const session = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
2165
4221
|
if (!session) return { ok: false, reason: "unknown_session", session_id: sessionId };
|
|
2166
4222
|
if (typeof args.prompt !== "string" || args.prompt.length === 0)
|
|
2167
4223
|
return { ok: false, reason: "prompt_required" };
|
|
2168
|
-
|
|
2169
|
-
|
|
4224
|
+
await readSessionState(namespaceDir, sessionId);
|
|
4225
|
+
await readLatestEventSeq(namespaceDir);
|
|
4226
|
+
assertSessionReadinessAuthority(session);
|
|
4227
|
+
const prepared = await preparePromptTurn(session, args.prompt, {
|
|
4228
|
+
force: args.force === true,
|
|
4229
|
+
queue: args.queue === true,
|
|
4230
|
+
queueWithoutActive: true,
|
|
4231
|
+
});
|
|
4232
|
+
if (!prepared.ok) {
|
|
2170
4233
|
return {
|
|
2171
4234
|
ok: false,
|
|
2172
4235
|
reason: "active_turn_exists",
|
|
2173
4236
|
session_id: sessionId,
|
|
2174
|
-
active_turn_id: activeTurn.turn_id,
|
|
4237
|
+
active_turn_id: prepared.activeTurn.turn_id,
|
|
2175
4238
|
};
|
|
2176
4239
|
}
|
|
2177
|
-
|
|
2178
|
-
const timestamp = new Date().toISOString();
|
|
2179
|
-
const superseded = {
|
|
2180
|
-
...activeTurn,
|
|
2181
|
-
status: "superseded" as const,
|
|
2182
|
-
updated_at: timestamp,
|
|
2183
|
-
completed_at: timestamp,
|
|
2184
|
-
};
|
|
2185
|
-
await writeTurnRecord(namespaceDir, superseded);
|
|
2186
|
-
await clearActiveTurn(namespaceDir, superseded);
|
|
2187
|
-
}
|
|
2188
|
-
const shouldQueue = args.queue === true && args.force !== true;
|
|
2189
|
-
const turn = shouldQueue
|
|
2190
|
-
? makeTurnRecord(config, sessionId, args.prompt, "queued")
|
|
2191
|
-
: await activateTurn(session, makeTurnRecord(config, sessionId, args.prompt, "active"));
|
|
2192
|
-
if (shouldQueue) await writeTurnRecord(namespaceDir, turn);
|
|
2193
|
-
const recordedTurn = turn;
|
|
4240
|
+
const { turn: recordedTurn, priorActiveTurn: activeTurn, queued: shouldQueue } = prepared;
|
|
2194
4241
|
const prompt = {
|
|
2195
4242
|
session_id: sessionId,
|
|
2196
4243
|
turn_id: recordedTurn.turn_id,
|
|
@@ -2224,7 +4271,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2224
4271
|
const timeoutMs = boundedAwaitTurnTimeoutMs(args.timeout_ms);
|
|
2225
4272
|
const pollIntervalMs = boundedPollIntervalMs(args.poll_interval_ms);
|
|
2226
4273
|
const deadline = Date.now() + timeoutMs;
|
|
2227
|
-
let payload = await
|
|
4274
|
+
let payload = await withCoordinatorTransaction(
|
|
4275
|
+
namespaceDir,
|
|
4276
|
+
async () => await readTurnPayload(args.turn_id, args.session_id, args.lines),
|
|
4277
|
+
);
|
|
2228
4278
|
while (
|
|
2229
4279
|
payload.ok === true &&
|
|
2230
4280
|
!TERMINAL_TURN_STATUSES.has((payload.turn as TurnRecord).status) &&
|
|
@@ -2236,7 +4286,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2236
4286
|
payload.turn as TurnRecord,
|
|
2237
4287
|
Math.min(pollIntervalMs, remainingMs),
|
|
2238
4288
|
);
|
|
2239
|
-
payload = await
|
|
4289
|
+
payload = await withCoordinatorTransaction(
|
|
4290
|
+
namespaceDir,
|
|
4291
|
+
async () => await readTurnPayload(args.turn_id, args.session_id, args.lines),
|
|
4292
|
+
);
|
|
2240
4293
|
}
|
|
2241
4294
|
if (payload.ok === true && !TERMINAL_TURN_STATUSES.has((payload.turn as TurnRecord).status)) {
|
|
2242
4295
|
return {
|
|
@@ -2261,6 +4314,16 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2261
4314
|
if (args.turn_id != null && question.turn_id !== safeTurnId(args.turn_id)) {
|
|
2262
4315
|
return { ok: false, reason: "question_turn_mismatch" };
|
|
2263
4316
|
}
|
|
4317
|
+
await preflightCoordinatorMutation(namespaceDir);
|
|
4318
|
+
const questionSessionId =
|
|
4319
|
+
typeof question.session_id === "string" ? safeExternalId("session", question.session_id) : null;
|
|
4320
|
+
if (!questionSessionId) throw coordinatorStateError("invalid");
|
|
4321
|
+
await preflightCoordinatorMutation(namespaceDir, questionSessionId);
|
|
4322
|
+
if (typeof question.turn_id === "string") {
|
|
4323
|
+
const referencedTurn = await readTurnRecord(namespaceDir, question.turn_id);
|
|
4324
|
+
if (!referencedTurn || referencedTurn.session_id !== questionSessionId)
|
|
4325
|
+
throw coordinatorStateError("invalid");
|
|
4326
|
+
}
|
|
2264
4327
|
const answeredTurnId = typeof question.turn_id === "string" ? question.turn_id : null;
|
|
2265
4328
|
const answered = {
|
|
2266
4329
|
...question,
|
|
@@ -2290,33 +4353,43 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2290
4353
|
|
|
2291
4354
|
let turn: TurnRecord | null = null;
|
|
2292
4355
|
if (answeredTurnId) {
|
|
2293
|
-
|
|
2294
|
-
if (
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
4356
|
+
const observedTurn = await readTurnRecord(namespaceDir, answeredTurnId);
|
|
4357
|
+
if (observedTurn) {
|
|
4358
|
+
turn = await withSessionTransition(observedTurn.session_id, async () => {
|
|
4359
|
+
const current = await readTurnRecord(namespaceDir, answeredTurnId);
|
|
4360
|
+
if (!current || !ACTIVE_TURN_STATUSES.has(current.status)) return current;
|
|
4361
|
+
const timestamp = new Date().toISOString();
|
|
4362
|
+
const answeredTurn: TurnRecord = {
|
|
4363
|
+
...current,
|
|
4364
|
+
status: "active",
|
|
4365
|
+
question_ids: [...new Set([...current.question_ids, questionId])],
|
|
4366
|
+
updated_at: timestamp,
|
|
4367
|
+
};
|
|
4368
|
+
await writeTurnRecord(namespaceDir, answeredTurn);
|
|
4369
|
+
await writeActiveTurn(namespaceDir, answeredTurn);
|
|
4370
|
+
await writeSessionState(namespaceDir, answeredTurn.session_id, "running", {
|
|
4371
|
+
currentTurnId: answeredTurn.turn_id,
|
|
4372
|
+
live: null,
|
|
4373
|
+
reason: null,
|
|
4374
|
+
});
|
|
4375
|
+
const session = asRecord(await readJsonFile(sessionFile(answeredTurn.session_id)));
|
|
4376
|
+
if (session && typeof args.answer === "string") {
|
|
4377
|
+
await sendTmuxPrompt(session, args.answer, commandRunner, services.ownerIsolationProbe);
|
|
4378
|
+
}
|
|
4379
|
+
return answeredTurn;
|
|
2308
4380
|
});
|
|
2309
|
-
const session = asRecord(await readJsonFile(sessionFile(turn.session_id)));
|
|
2310
|
-
if (session && typeof args.answer === "string")
|
|
2311
|
-
await sendTmuxPrompt(session, args.answer, commandRunner);
|
|
2312
4381
|
}
|
|
2313
4382
|
}
|
|
2314
4383
|
return { ok: true, question: answered, ...(turn ? { turn } : {}) };
|
|
2315
4384
|
}
|
|
2316
4385
|
if (name === "skc_coordinator_report_status") {
|
|
2317
4386
|
requireCoordinatorMutation(config, "reports", args);
|
|
4387
|
+
await preflightCoordinatorMutation(namespaceDir);
|
|
4388
|
+
|
|
2318
4389
|
const evidence = await validateEvidencePaths(args.evidence_paths);
|
|
2319
4390
|
const sessionId = args.session_id == null ? null : safeExternalId("session", args.session_id);
|
|
4391
|
+
if (sessionId) await preflightCoordinatorMutation(namespaceDir, sessionId);
|
|
4392
|
+
|
|
2320
4393
|
const report = {
|
|
2321
4394
|
session_id: sessionId,
|
|
2322
4395
|
turn_id: args.turn_id,
|
|
@@ -2329,61 +4402,79 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2329
4402
|
};
|
|
2330
4403
|
let turn: TurnRecord | null = null;
|
|
2331
4404
|
let promotedTurn: TurnRecord | null = null;
|
|
4405
|
+
let promotedPreparation: { session: Record<string, unknown>; turn: TurnRecord } | null = null;
|
|
2332
4406
|
if (args.turn_id != null) {
|
|
2333
4407
|
turn = await readTurnRecord(namespaceDir, args.turn_id);
|
|
2334
4408
|
if (!turn) return { ok: false, reason: "unknown_turn" };
|
|
2335
4409
|
if (sessionId != null && turn.session_id !== sessionId) {
|
|
2336
4410
|
return { ok: false, reason: "turn_session_mismatch" };
|
|
2337
4411
|
}
|
|
4412
|
+
await preflightCoordinatorMutation(namespaceDir, turn.session_id);
|
|
2338
4413
|
const terminalStatus = asTerminalTurnStatus(args.status);
|
|
2339
4414
|
if (terminalStatus) {
|
|
2340
|
-
const
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
4415
|
+
const transition = await withSessionTransition(turn.session_id, async () => {
|
|
4416
|
+
const current = await readTurnRecord(namespaceDir, turn?.turn_id);
|
|
4417
|
+
if (!current || TERMINAL_TURN_STATUSES.has(current.status)) {
|
|
4418
|
+
return { turn: current, promotion: null };
|
|
4419
|
+
}
|
|
4420
|
+
const timestamp = new Date().toISOString();
|
|
4421
|
+
const terminalTurn: TurnRecord = {
|
|
4422
|
+
...current,
|
|
4423
|
+
status: terminalStatus,
|
|
4424
|
+
delivery: {
|
|
4425
|
+
...current.delivery,
|
|
4426
|
+
prompt_acknowledged: true,
|
|
4427
|
+
state: "acknowledged",
|
|
4428
|
+
},
|
|
4429
|
+
final_response: {
|
|
4430
|
+
text:
|
|
4431
|
+
typeof args.summary === "string"
|
|
4432
|
+
? args.summary
|
|
4433
|
+
: typeof args.blocker === "string"
|
|
4434
|
+
? args.blocker
|
|
4435
|
+
: null,
|
|
4436
|
+
format: "markdown",
|
|
4437
|
+
source: "report_status",
|
|
4438
|
+
artifact_path: null,
|
|
4439
|
+
truncated: false,
|
|
4440
|
+
},
|
|
4441
|
+
evidence,
|
|
4442
|
+
error:
|
|
4443
|
+
terminalStatus === "failed"
|
|
4444
|
+
? {
|
|
4445
|
+
code: "reported_failure",
|
|
4446
|
+
message:
|
|
4447
|
+
typeof args.blocker === "string"
|
|
4448
|
+
? args.blocker
|
|
4449
|
+
: String(args.summary ?? "failed"),
|
|
4450
|
+
recoverable: true,
|
|
4451
|
+
}
|
|
4452
|
+
: null,
|
|
4453
|
+
updated_at: timestamp,
|
|
4454
|
+
completed_at: timestamp,
|
|
4455
|
+
};
|
|
4456
|
+
await writeTurnRecord(namespaceDir, terminalTurn);
|
|
4457
|
+
await clearActiveTurn(namespaceDir, terminalTurn);
|
|
4458
|
+
await writeSessionState(
|
|
4459
|
+
namespaceDir,
|
|
4460
|
+
terminalTurn.session_id,
|
|
4461
|
+
terminalStatus === "failed" ? "errored" : "completed",
|
|
4462
|
+
{
|
|
4463
|
+
lastTurnId: terminalTurn.turn_id,
|
|
4464
|
+
live: null,
|
|
4465
|
+
reason: terminalStatus === "failed" ? "reported_failure" : null,
|
|
4466
|
+
},
|
|
4467
|
+
);
|
|
4468
|
+
return {
|
|
4469
|
+
turn: terminalTurn,
|
|
4470
|
+
promotion: await prepareNextQueuedTurnUnlocked(terminalTurn.session_id),
|
|
4471
|
+
};
|
|
4472
|
+
});
|
|
4473
|
+
turn = transition.turn;
|
|
4474
|
+
promotedPreparation = transition.promotion;
|
|
4475
|
+
if (promotedPreparation) {
|
|
4476
|
+
promotedTurn = await completePreparedTurn(promotedPreparation.session, promotedPreparation.turn);
|
|
4477
|
+
}
|
|
2387
4478
|
}
|
|
2388
4479
|
}
|
|
2389
4480
|
const reportId = `report-${Date.now()}`;
|
|
@@ -2399,21 +4490,60 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2399
4490
|
? args.summary
|
|
2400
4491
|
: `Report ${String(args.status ?? "unknown")} written`,
|
|
2401
4492
|
payloadRef: path.relative(namespaceDir, reportPath),
|
|
2402
|
-
metadata: {
|
|
4493
|
+
metadata: {
|
|
4494
|
+
status: typeof args.status === "string" ? args.status : null,
|
|
4495
|
+
},
|
|
2403
4496
|
});
|
|
2404
4497
|
return {
|
|
2405
4498
|
ok: true,
|
|
2406
4499
|
report,
|
|
2407
|
-
...(turn
|
|
4500
|
+
...(turn
|
|
4501
|
+
? {
|
|
4502
|
+
turn,
|
|
4503
|
+
session_state: await readSessionState(namespaceDir, turn.session_id),
|
|
4504
|
+
}
|
|
4505
|
+
: {}),
|
|
2408
4506
|
...(promotedTurn ? { promoted_turn: promotedTurn } : {}),
|
|
2409
4507
|
};
|
|
2410
4508
|
}
|
|
2411
4509
|
return { ok: false, reason: "unknown_tool", tool: name };
|
|
2412
4510
|
} catch (error) {
|
|
2413
|
-
|
|
4511
|
+
const failure = error as { sessionId?: unknown; action?: unknown };
|
|
4512
|
+
return {
|
|
4513
|
+
ok: false,
|
|
4514
|
+
reason: publicCoordinatorError(error),
|
|
4515
|
+
...(publicCleanupStatus(error) ? { cleanup_status: publicCleanupStatus(error) } : {}),
|
|
4516
|
+
...(typeof failure.sessionId === "string" ? { session_id: failure.sessionId } : {}),
|
|
4517
|
+
...(typeof failure.action === "string" ? { action: failure.action } : {}),
|
|
4518
|
+
};
|
|
2414
4519
|
}
|
|
2415
4520
|
}
|
|
2416
4521
|
|
|
4522
|
+
async function callToolInternal(name: string, args: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
|
|
4523
|
+
try {
|
|
4524
|
+
if (
|
|
4525
|
+
name === "skc_coordinator_await_turn" ||
|
|
4526
|
+
name === "skc_coordinator_watch_events" ||
|
|
4527
|
+
(name === "skc_coordinator_start_session" && typeof args.prompt === "string" && args.prompt.length > 0) ||
|
|
4528
|
+
name === "skc_coordinator_send_prompt" ||
|
|
4529
|
+
workflowForDelegateTool(name) !== null
|
|
4530
|
+
) {
|
|
4531
|
+
return await callToolUnlocked(name, args);
|
|
4532
|
+
}
|
|
4533
|
+
return await withCoordinatorTransaction(namespaceDir, async () => await callToolUnlocked(name, args));
|
|
4534
|
+
} catch (error) {
|
|
4535
|
+
return {
|
|
4536
|
+
ok: false,
|
|
4537
|
+
reason: publicCoordinatorError(error),
|
|
4538
|
+
...(publicCleanupStatus(error) ? { cleanup_status: publicCleanupStatus(error) } : {}),
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
async function callTool(name: string, args: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
|
|
4544
|
+
return publicCoordinatorResponse(await callToolInternal(name, args)) as Record<string, unknown>;
|
|
4545
|
+
}
|
|
4546
|
+
|
|
2417
4547
|
async function handleJsonRpc(request: JsonRpcRequest): Promise<JsonRpcResponse> {
|
|
2418
4548
|
const id = request.id ?? null;
|
|
2419
4549
|
if (request.method === "initialize") {
|
|
@@ -2427,8 +4557,15 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2427
4557
|
},
|
|
2428
4558
|
};
|
|
2429
4559
|
}
|
|
4560
|
+
if (request.method === "ping") {
|
|
4561
|
+
return { jsonrpc: "2.0", id, result: {} };
|
|
4562
|
+
}
|
|
2430
4563
|
if (request.method === "tools/list") {
|
|
2431
|
-
return {
|
|
4564
|
+
return {
|
|
4565
|
+
jsonrpc: "2.0",
|
|
4566
|
+
id,
|
|
4567
|
+
result: { tools: COORDINATOR_MCP_TOOL_NAMES.map(toolSchema) },
|
|
4568
|
+
};
|
|
2432
4569
|
}
|
|
2433
4570
|
if (request.method === "prompts/list") {
|
|
2434
4571
|
return { jsonrpc: "2.0", id, result: { prompts: [] } };
|
|
@@ -2437,17 +4574,31 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2437
4574
|
return { jsonrpc: "2.0", id, result: { resources: [] } };
|
|
2438
4575
|
}
|
|
2439
4576
|
if (request.method === "tools/call") {
|
|
2440
|
-
const params = (request.params ?? {}) as {
|
|
4577
|
+
const params = (request.params ?? {}) as {
|
|
4578
|
+
name?: string;
|
|
4579
|
+
arguments?: Record<string, unknown>;
|
|
4580
|
+
};
|
|
2441
4581
|
const payload = await callTool(params.name ?? "", params.arguments ?? {});
|
|
2442
|
-
return {
|
|
4582
|
+
return {
|
|
4583
|
+
jsonrpc: "2.0",
|
|
4584
|
+
id,
|
|
4585
|
+
result: textResult(payload, payload.ok === false),
|
|
4586
|
+
};
|
|
2443
4587
|
}
|
|
2444
|
-
return {
|
|
4588
|
+
return {
|
|
4589
|
+
jsonrpc: "2.0",
|
|
4590
|
+
id,
|
|
4591
|
+
error: { code: -32601, message: `unknown_method:${request.method}` },
|
|
4592
|
+
};
|
|
2445
4593
|
}
|
|
2446
4594
|
|
|
2447
4595
|
return { config, callTool, handleJsonRpc, handle: handleJsonRpc };
|
|
2448
4596
|
}
|
|
2449
4597
|
|
|
2450
|
-
function legacyToolResult(payload: unknown): {
|
|
4598
|
+
function legacyToolResult(payload: unknown): {
|
|
4599
|
+
content: Array<{ type: "text"; text: string }>;
|
|
4600
|
+
isError: boolean;
|
|
4601
|
+
} {
|
|
2451
4602
|
const failed = typeof payload === "object" && payload !== null && (payload as { ok?: unknown }).ok === false;
|
|
2452
4603
|
return textResult(payload, failed);
|
|
2453
4604
|
}
|
|
@@ -2468,13 +4619,21 @@ export async function handleCoordinatorMcpRequest(
|
|
|
2468
4619
|
};
|
|
2469
4620
|
}
|
|
2470
4621
|
if (request.method === "tools/list") {
|
|
2471
|
-
return {
|
|
4622
|
+
return {
|
|
4623
|
+
jsonrpc: "2.0",
|
|
4624
|
+
id: request.id ?? null,
|
|
4625
|
+
result: { tools: COORDINATOR_MCP_TOOL_NAMES.map(toolSchema) },
|
|
4626
|
+
};
|
|
2472
4627
|
}
|
|
2473
4628
|
if (request.method === "prompts/list") {
|
|
2474
4629
|
return { jsonrpc: "2.0", id: request.id ?? null, result: { prompts: [] } };
|
|
2475
4630
|
}
|
|
2476
4631
|
if (request.method === "resources/list") {
|
|
2477
|
-
return {
|
|
4632
|
+
return {
|
|
4633
|
+
jsonrpc: "2.0",
|
|
4634
|
+
id: request.id ?? null,
|
|
4635
|
+
result: { resources: [] },
|
|
4636
|
+
};
|
|
2478
4637
|
}
|
|
2479
4638
|
if (request.method !== "tools/call")
|
|
2480
4639
|
return {
|
|
@@ -2482,11 +4641,14 @@ export async function handleCoordinatorMcpRequest(
|
|
|
2482
4641
|
id: request.id ?? null,
|
|
2483
4642
|
error: { code: -32601, message: `unknown_method:${request.method}` },
|
|
2484
4643
|
};
|
|
2485
|
-
const params = (request.params ?? {}) as {
|
|
4644
|
+
const params = (request.params ?? {}) as {
|
|
4645
|
+
name?: string;
|
|
4646
|
+
arguments?: Record<string, unknown>;
|
|
4647
|
+
};
|
|
2486
4648
|
const args = params.arguments ?? {};
|
|
2487
4649
|
const server = createCoordinatorMcpServer({
|
|
2488
4650
|
env: options.env ?? process.env,
|
|
2489
|
-
services: options.createSession ? { startSession:
|
|
4651
|
+
services: options.createSession ? { startSession: input => options.createSession?.(input) } : undefined,
|
|
2490
4652
|
});
|
|
2491
4653
|
return {
|
|
2492
4654
|
jsonrpc: "2.0",
|
|
@@ -2495,23 +4657,161 @@ export async function handleCoordinatorMcpRequest(
|
|
|
2495
4657
|
};
|
|
2496
4658
|
}
|
|
2497
4659
|
|
|
2498
|
-
export
|
|
2499
|
-
|
|
4660
|
+
export interface PumpCoordinatorOptions {
|
|
4661
|
+
/** Max concurrent in-flight *data* (non-control) handlers. Control frames (ping) bypass this. */
|
|
4662
|
+
maxDataConcurrency?: number;
|
|
4663
|
+
/** Max data requests queued waiting for a slot before overflow is rejected as server_busy. */
|
|
4664
|
+
maxQueueDepth?: number;
|
|
4665
|
+
/** Bounded wait for in-flight handlers/writes to settle after input ends. */
|
|
4666
|
+
drainTimeoutMs?: number;
|
|
4667
|
+
}
|
|
4668
|
+
|
|
4669
|
+
/**
|
|
4670
|
+
* Pump a newline-delimited JSON-RPC stream with BOUNDED concurrent dispatch.
|
|
4671
|
+
*
|
|
4672
|
+
* A long-running tool call (e.g. skc_coordinator_await_turn, which polls for
|
|
4673
|
+
* minutes) must not block the read loop from answering keepalive pings on the
|
|
4674
|
+
* same stdio channel. But naive unbounded concurrency reintroduces its own
|
|
4675
|
+
* hazards, so this pump enforces the safety envelope the coordinator needs:
|
|
4676
|
+
*
|
|
4677
|
+
* - Control frames (ping) bypass the data-concurrency cap → keepalive is always
|
|
4678
|
+
* answerable even while data handlers saturate.
|
|
4679
|
+
* - Data handlers are capped at `maxDataConcurrency`; excess is queued up to
|
|
4680
|
+
* `maxQueueDepth`, then rejected as `server_busy` (bounded memory / fanout).
|
|
4681
|
+
* - `writeLine` failures move the writer to a terminal closed state instead of
|
|
4682
|
+
* poisoning the serialized write chain or escaping as an unhandled rejection;
|
|
4683
|
+
* no writes happen after close.
|
|
4684
|
+
* - On EOF the pump drains in-flight handlers (bounded by `drainTimeoutMs`) and
|
|
4685
|
+
* flushes queued writes before returning, so shutdown never races live work.
|
|
4686
|
+
* - Byte chunks are decoded with a streaming decoder so multibyte characters
|
|
4687
|
+
* split across chunks are not corrupted.
|
|
4688
|
+
*/
|
|
4689
|
+
export async function pumpCoordinatorMcpStream(
|
|
4690
|
+
handleJsonRpc: (request: JsonRpcRequest) => Promise<JsonRpcResponse>,
|
|
4691
|
+
input: AsyncIterable<string | Uint8Array>,
|
|
4692
|
+
writeLine: (line: string) => void | Promise<void>,
|
|
4693
|
+
options: PumpCoordinatorOptions = {},
|
|
4694
|
+
): Promise<void> {
|
|
4695
|
+
const maxDataConcurrency = Math.max(1, options.maxDataConcurrency ?? 32);
|
|
4696
|
+
const maxQueueDepth = Math.max(0, options.maxQueueDepth ?? 256);
|
|
4697
|
+
const drainTimeoutMs = Math.max(0, options.drainTimeoutMs ?? 30_000);
|
|
4698
|
+
|
|
4699
|
+
let writeClosed = false;
|
|
4700
|
+
let draining = false;
|
|
4701
|
+
let writeChain: Promise<void> = Promise.resolve();
|
|
4702
|
+
const inFlight = new Set<Promise<void>>();
|
|
4703
|
+
let activeData = 0;
|
|
4704
|
+
const dataQueue: JsonRpcRequest[] = [];
|
|
4705
|
+
|
|
4706
|
+
const emit = (response: JsonRpcResponse): void => {
|
|
4707
|
+
writeChain = writeChain.then(async () => {
|
|
4708
|
+
if (writeClosed) return;
|
|
4709
|
+
try {
|
|
4710
|
+
await writeLine(`${JSON.stringify(response)}\n`);
|
|
4711
|
+
} catch {
|
|
4712
|
+
writeClosed = true; // terminal writer error: stop, but never poison the chain
|
|
4713
|
+
}
|
|
4714
|
+
});
|
|
4715
|
+
};
|
|
4716
|
+
|
|
4717
|
+
const launch = (request: JsonRpcRequest, control: boolean): void => {
|
|
4718
|
+
const task = (async () => {
|
|
4719
|
+
try {
|
|
4720
|
+
emit(await handleJsonRpc(request));
|
|
4721
|
+
} catch (err) {
|
|
4722
|
+
emit({
|
|
4723
|
+
jsonrpc: "2.0",
|
|
4724
|
+
id: request.id ?? null,
|
|
4725
|
+
error: { code: -32603, message: publicCoordinatorError(err) },
|
|
4726
|
+
});
|
|
4727
|
+
} finally {
|
|
4728
|
+
if (!control) {
|
|
4729
|
+
activeData -= 1;
|
|
4730
|
+
if (!draining) {
|
|
4731
|
+
const next = dataQueue.shift();
|
|
4732
|
+
if (next) {
|
|
4733
|
+
activeData += 1;
|
|
4734
|
+
launch(next, false);
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
})();
|
|
4740
|
+
inFlight.add(task);
|
|
4741
|
+
void task.finally(() => inFlight.delete(task));
|
|
4742
|
+
};
|
|
4743
|
+
|
|
4744
|
+
const dispatch = (request: JsonRpcRequest): void => {
|
|
4745
|
+
// Notifications (no id) get no response; the coordinator has no side-effecting ones.
|
|
4746
|
+
if (request.id === undefined || request.id === null) return;
|
|
4747
|
+
if (request.method === "ping") {
|
|
4748
|
+
launch(request, true); // control frame: bypass the data cap
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
if (activeData < maxDataConcurrency) {
|
|
4752
|
+
activeData += 1;
|
|
4753
|
+
launch(request, false);
|
|
4754
|
+
return;
|
|
4755
|
+
}
|
|
4756
|
+
if (dataQueue.length < maxQueueDepth) {
|
|
4757
|
+
dataQueue.push(request);
|
|
4758
|
+
return;
|
|
4759
|
+
}
|
|
4760
|
+
emit({
|
|
4761
|
+
jsonrpc: "2.0",
|
|
4762
|
+
id: request.id,
|
|
4763
|
+
error: { code: -32000, message: "server_busy: coordinator request queue is full" },
|
|
4764
|
+
});
|
|
4765
|
+
};
|
|
4766
|
+
|
|
4767
|
+
const decoder = new TextDecoder();
|
|
2500
4768
|
let buffer = "";
|
|
2501
|
-
for await (const chunk of
|
|
2502
|
-
buffer += chunk.
|
|
4769
|
+
for await (const chunk of input) {
|
|
4770
|
+
buffer += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
2503
4771
|
let newline = buffer.indexOf("\n");
|
|
2504
4772
|
while (newline >= 0) {
|
|
2505
4773
|
const line = buffer.slice(0, newline).trim();
|
|
2506
4774
|
buffer = buffer.slice(newline + 1);
|
|
2507
4775
|
if (line.length > 0) {
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
4776
|
+
let request: JsonRpcRequest | null = null;
|
|
4777
|
+
try {
|
|
4778
|
+
request = JSON.parse(line) as JsonRpcRequest;
|
|
4779
|
+
} catch {
|
|
4780
|
+
request = null; // ignore malformed frames rather than crashing the loop
|
|
2512
4781
|
}
|
|
4782
|
+
if (request) dispatch(request);
|
|
2513
4783
|
}
|
|
2514
4784
|
newline = buffer.indexOf("\n");
|
|
2515
4785
|
}
|
|
2516
4786
|
}
|
|
4787
|
+
|
|
4788
|
+
// EOF: stop promoting queued work, then drain in-flight handlers under a bound.
|
|
4789
|
+
draining = true;
|
|
4790
|
+
if (inFlight.size > 0) {
|
|
4791
|
+
const drain = Promise.allSettled(Array.from(inFlight)).then(() => undefined);
|
|
4792
|
+
if (drainTimeoutMs > 0) {
|
|
4793
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
4794
|
+
const timeout = new Promise<void>(resolve => {
|
|
4795
|
+
timer = setTimeout(resolve, drainTimeoutMs);
|
|
4796
|
+
(timer as { unref?: () => void }).unref?.();
|
|
4797
|
+
});
|
|
4798
|
+
await Promise.race([drain, timeout]);
|
|
4799
|
+
if (timer) clearTimeout(timer);
|
|
4800
|
+
} else {
|
|
4801
|
+
await drain;
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
await writeChain;
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4807
|
+
export async function runCoordinatorMcpStdio(options: CoordinatorMcpServerOptions = {}): Promise<void> {
|
|
4808
|
+
const server = createCoordinatorMcpServer(options);
|
|
4809
|
+
await pumpCoordinatorMcpStream(
|
|
4810
|
+
request => server.handleJsonRpc(request),
|
|
4811
|
+
process.stdin,
|
|
4812
|
+
line =>
|
|
4813
|
+
new Promise<void>((resolve, reject) => {
|
|
4814
|
+
process.stdout.write(line, err => (err ? reject(err) : resolve()));
|
|
4815
|
+
}),
|
|
4816
|
+
);
|
|
2517
4817
|
}
|