@gajae-code/coding-agent 0.11.0 → 0.11.2
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 +63 -1
- package/dist/types/async/job-manager.d.ts +46 -7
- package/dist/types/cli/config-cli.d.ts +11 -1
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/cli/session-picker.d.ts +2 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/mcp-serve.d.ts +33 -0
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/commands/notify.d.ts +0 -2
- package/dist/types/commands/sdk.d.ts +1 -1
- package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
- package/dist/types/config/file-lock.d.ts +14 -4
- package/dist/types/config/keybindings.d.ts +42 -1
- package/dist/types/config/model-auth.d.ts +3 -0
- package/dist/types/config/model-bindings-applier.d.ts +13 -0
- package/dist/types/config/model-discovery-manager.d.ts +49 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-registry.d.ts +16 -15
- package/dist/types/config/model-resolver.d.ts +28 -18
- package/dist/types/config/models-config-schema.d.ts +11 -0
- package/dist/types/config/settings-schema.d.ts +56 -1
- package/dist/types/config/settings.d.ts +14 -11
- package/dist/types/coordinator-mcp/policy.d.ts +5 -0
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +0 -6
- package/dist/types/edit/index.d.ts +3 -0
- package/dist/types/eval/py/env.d.ts +9 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -3
- package/dist/types/extensibility/hooks/runner.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
- package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
- package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
- package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
- package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
- package/dist/types/gjc-runtime/team-store.d.ts +160 -0
- package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +11 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
- package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
- package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/goals/state.d.ts +8 -0
- package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
- package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/state.d.ts +11 -4
- package/dist/types/lsp/lspmux.d.ts +5 -3
- package/dist/types/lsp/path-trust.d.ts +1 -0
- package/dist/types/main.d.ts +3 -2
- package/dist/types/memories/storage.d.ts +1 -0
- package/dist/types/modes/action-registry.d.ts +29 -0
- package/dist/types/modes/components/chrome.d.ts +14 -0
- package/dist/types/modes/components/command-palette.d.ts +27 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
- package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
- package/dist/types/modes/components/queue-pane.d.ts +12 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
- package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/show-images-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +1 -94
- package/dist/types/modes/components/tasks-pane.d.ts +15 -0
- package/dist/types/modes/components/theme-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/tool-status-header.d.ts +111 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -1
- package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
- package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
- package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
- package/dist/types/modes/interactive-mode.d.ts +21 -19
- package/dist/types/modes/jobs-observer.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +21 -2
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
- package/dist/types/modes/tasks-aggregator.d.ts +39 -0
- package/dist/types/modes/transcript-item-registry.d.ts +60 -0
- package/dist/types/modes/types.d.ts +22 -15
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +78 -12
- package/dist/types/sdk/broker/ensure.d.ts +9 -0
- package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
- package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
- package/dist/types/sdk/broker/session-index.d.ts +0 -1
- package/dist/types/sdk/broker/state-version.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
- package/dist/types/sdk/bus/config-commands.d.ts +1 -1
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +5 -0
- package/dist/types/sdk/bus/html-format.d.ts +3 -2
- package/dist/types/sdk/bus/index.d.ts +27 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/notification-service.d.ts +20 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
- package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
- package/dist/types/sdk/bus/rich-document.d.ts +7 -0
- package/dist/types/sdk/bus/rich-render.d.ts +18 -41
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +69 -3
- package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/session-directory.d.ts +56 -0
- package/dist/types/session/agent-session.d.ts +82 -47
- package/dist/types/session/agent-storage.d.ts +7 -0
- package/dist/types/session/fallback-chain-controller.d.ts +10 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
- package/dist/types/session/messages.d.ts +16 -0
- package/dist/types/session/session-manager-internal.d.ts +14 -0
- package/dist/types/session/session-manager.d.ts +82 -12
- package/dist/types/session/session-storage.d.ts +43 -3
- package/dist/types/session/volatile-context-pruning.d.ts +15 -0
- package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
- package/dist/types/system-prompt.d.ts +2 -11
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/thinking.d.ts +7 -0
- package/dist/types/tool-discovery/tool-index.d.ts +1 -50
- package/dist/types/tools/ask.d.ts +64 -2
- package/dist/types/tools/bash.d.ts +3 -0
- package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
- package/dist/types/tools/cron.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -0
- package/dist/types/tools/index.d.ts +30 -15
- package/dist/types/tools/output-meta.d.ts +1 -0
- package/dist/types/tools/read.d.ts +3 -0
- package/dist/types/tools/search-tool-bm25.d.ts +3 -4
- package/dist/types/tools/subagent-render.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +6 -1
- package/dist/types/utils/event-bus.d.ts +1 -1
- package/dist/types/web/search/index.d.ts +3 -1
- package/package.json +21 -9
- package/scripts/build-sdk-package-smoke.ts +39 -1
- package/scripts/generate-hotkeys-docs.ts +74 -0
- package/scripts/generate-sdk-operation-inventory.ts +64 -20
- package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
- package/src/async/job-manager.ts +455 -69
- package/src/cli/config-cli.ts +75 -2
- package/src/cli/notify-cli.ts +92 -26
- package/src/cli/session-picker.ts +20 -3
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/harness.ts +34 -106
- package/src/commands/launch.ts +6 -4
- package/src/commands/mcp-serve.ts +85 -8
- package/src/commands/mcp.ts +2 -2
- package/src/commands/notify.ts +8 -4
- package/src/commands/sdk.ts +61 -40
- package/src/commands/ultragoal.ts +4 -1
- package/src/config/atomic-yaml-patch.ts +80 -13
- package/src/config/file-lock.ts +105 -43
- package/src/config/keybindings.ts +64 -20
- package/src/config/model-auth.ts +9 -0
- package/src/config/model-bindings-applier.ts +139 -0
- package/src/config/model-discovery-manager.ts +224 -0
- package/src/config/model-equivalence.ts +66 -0
- package/src/config/model-registry.ts +169 -302
- package/src/config/model-resolver.ts +361 -369
- package/src/config/models-config-schema.ts +5 -4
- package/src/config/settings-schema.ts +159 -2
- package/src/config/settings.ts +458 -482
- package/src/coordinator-mcp/policy.ts +76 -11
- package/src/coordinator-mcp/question-gate-codec.ts +274 -0
- package/src/coordinator-mcp/question-state.ts +778 -0
- package/src/coordinator-mcp/server.ts +1702 -238
- package/src/debug/index.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
- package/src/defaults/gjc/skills/team/SKILL.md +16 -0
- package/src/discovery/helpers.ts +1 -50
- package/src/edit/index.ts +64 -0
- package/src/eval/py/env.ts +26 -0
- package/src/eval/py/kernel.ts +5 -3
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +12 -3
- package/src/extensibility/extensions/types.ts +39 -1
- package/src/extensibility/gjc-plugins/installer.ts +16 -16
- package/src/extensibility/hooks/runner.ts +2 -2
- package/src/extensibility/runtime-skill-discovery.ts +33 -13
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
- package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
- package/src/gjc-runtime/deep-interview-state.ts +317 -5
- package/src/gjc-runtime/goal-mode-request.ts +55 -8
- package/src/gjc-runtime/launch-tmux.ts +2 -5
- package/src/gjc-runtime/launch-worktree.ts +5 -5
- package/src/gjc-runtime/linux-proc.ts +71 -0
- package/src/gjc-runtime/ralplan-runtime.ts +74 -90
- package/src/gjc-runtime/session-resolution.ts +18 -3
- package/src/gjc-runtime/session-state-sidecar.ts +6 -107
- package/src/gjc-runtime/state-migrations.ts +1 -1
- package/src/gjc-runtime/state-runtime.ts +320 -249
- package/src/gjc-runtime/state-writer.ts +36 -40
- package/src/gjc-runtime/team-launch.ts +319 -0
- package/src/gjc-runtime/team-notify.ts +326 -0
- package/src/gjc-runtime/team-runtime.ts +1099 -1482
- package/src/gjc-runtime/team-store.ts +828 -0
- package/src/gjc-runtime/team-workers.ts +962 -0
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
- package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
- package/src/gjc-runtime/tmux-sessions.ts +4 -21
- package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
- package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
- package/src/gjc-runtime/ultragoal-guard.ts +83 -12
- package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +156 -1319
- package/src/gjc-runtime/workflow-cli-common.ts +30 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +18 -0
- package/src/goals/continuation-timeout-guard.ts +79 -0
- package/src/goals/runtime.ts +7 -5
- package/src/goals/state.ts +18 -0
- package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
- package/src/harness-control-plane/phase-rollup.ts +4 -7
- package/src/harness-control-plane/receipt-ingest.ts +5 -0
- package/src/harness-control-plane/receipt-spool.ts +51 -7
- package/src/harness-control-plane/session-lease.ts +12 -130
- package/src/hindsight/backend.ts +26 -27
- package/src/hindsight/state.ts +101 -32
- package/src/hooks/skill-state.ts +17 -11
- package/src/internal-urls/docs-index.generated.ts +21 -18
- package/src/lsp/client.ts +1 -1
- package/src/lsp/config.ts +145 -26
- package/src/lsp/index.ts +1 -1
- package/src/lsp/lspmux.ts +39 -10
- package/src/lsp/path-trust.ts +81 -0
- package/src/main.ts +28 -8
- package/src/memories/index.ts +1 -0
- package/src/memories/storage.ts +6 -2
- package/src/modes/action-registry.ts +165 -0
- package/src/modes/components/chrome.ts +40 -0
- package/src/modes/components/command-palette.ts +168 -0
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/custom-provider-wizard.ts +51 -14
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/pet-selector.ts +11 -17
- package/src/modes/components/plan-preview-overlay.ts +295 -0
- package/src/modes/components/queue-mode-selector.ts +10 -27
- package/src/modes/components/queue-pane.ts +79 -0
- package/src/modes/components/read-tool-group.ts +13 -0
- package/src/modes/components/session-observer-overlay.ts +207 -772
- package/src/modes/components/sessions-dashboard.ts +181 -0
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/show-images-selector.ts +10 -24
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +1 -969
- package/src/modes/components/tasks-pane.ts +73 -0
- package/src/modes/components/theme-selector.ts +11 -31
- package/src/modes/components/tool-execution.ts +22 -0
- package/src/modes/components/tool-status-header.ts +1056 -0
- package/src/modes/components/transcript-viewer-overlay.ts +384 -0
- package/src/modes/controllers/btw-controller.ts +1 -0
- package/src/modes/controllers/command-controller.ts +9 -15
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +72 -25
- package/src/modes/controllers/goal-mode-controller.ts +474 -0
- package/src/modes/controllers/input-controller.ts +764 -229
- package/src/modes/controllers/mode-gate.ts +24 -0
- package/src/modes/controllers/plan-mode-controller.ts +517 -0
- package/src/modes/controllers/selector-controller.ts +225 -10
- package/src/modes/controllers/stt-controller.ts +125 -0
- package/src/modes/interactive-mode.ts +302 -1151
- package/src/modes/jobs-observer.ts +3 -0
- package/src/modes/runtime-init.ts +8 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +78 -19
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
- package/src/modes/tasks-aggregator.ts +199 -0
- package/src/modes/transcript-item-registry.ts +229 -0
- package/src/modes/types.ts +47 -20
- package/src/modes/utils/ui-helpers.ts +17 -9
- package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -0
- package/src/prompts/agents/architect.md +2 -8
- package/src/prompts/agents/critic.md +2 -8
- package/src/prompts/agents/executor.md +3 -0
- package/src/prompts/agents/planner.md +4 -12
- package/src/prompts/system/custom-system-prompt.md +1 -23
- package/src/prompts/system/plan-mode-approved.md +8 -0
- package/src/prompts/system/system-prompt.md +17 -96
- package/src/prompts/tools/bash.md +4 -4
- package/src/prompts/tools/browser.md +15 -52
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/hashline.md +13 -55
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/search-tool-bm25.md +0 -2
- package/src/prompts/tools/search.md +2 -4
- package/src/prompts/tools/skill-discovery.md +1 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-mcp/manager.ts +1 -1
- package/src/sdk/broker/broker.ts +206 -78
- package/src/sdk/broker/discovery.ts +21 -2
- package/src/sdk/broker/ensure.ts +85 -25
- package/src/sdk/broker/lifecycle-codec.ts +11 -0
- package/src/sdk/broker/lifecycle-ledger.ts +371 -74
- package/src/sdk/broker/lifecycle.ts +1399 -142
- package/src/sdk/broker/process-incarnation.ts +5 -13
- package/src/sdk/broker/session-index.ts +145 -56
- package/src/sdk/broker/state-version.ts +21 -0
- package/src/sdk/broker/transport.ts +1 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -28
- package/src/sdk/bus/chat-effect-journal.ts +14 -4
- package/src/sdk/bus/config-commands.ts +5 -4
- package/src/sdk/bus/config.ts +98 -0
- package/src/sdk/bus/conversation-store.ts +5 -29
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/discord-daemon.ts +25 -8
- package/src/sdk/bus/html-format.ts +20 -3
- package/src/sdk/bus/index.ts +834 -113
- package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
- package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
- package/src/sdk/bus/natives-augment.d.ts +26 -1
- package/src/sdk/bus/notification-service.ts +168 -20
- package/src/sdk/bus/rate-limit-pool.ts +58 -14
- package/src/sdk/bus/recent-activity.ts +188 -74
- package/src/sdk/bus/rich-document.ts +258 -0
- package/src/sdk/bus/rich-render.ts +62 -70
- package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon.ts +1491 -247
- package/src/sdk/bus/threaded-inbound.ts +8 -3
- package/src/sdk/bus/threaded-render.ts +57 -1
- package/src/sdk/bus/topic-registry.ts +120 -13
- package/src/sdk/host/control/dispatch.ts +14 -2
- package/src/sdk/host/control/operations.ts +6 -1
- package/src/sdk/host/host.ts +31 -1
- package/src/sdk/host/query/handlers.ts +20 -22
- package/src/sdk/host/query/revision-store.ts +80 -57
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
- package/src/sdk/session-directory.ts +141 -0
- package/src/sdk/session.ts +458 -479
- package/src/session/agent-session.ts +1901 -637
- package/src/session/agent-storage.ts +18 -0
- package/src/session/cache-economics.ts +25 -8
- package/src/session/fallback-chain-controller.ts +53 -3
- package/src/session/internal/managed-session-scope.ts +2226 -0
- package/src/session/internal/managed-session-storage.ts +453 -0
- package/src/session/messages.ts +91 -2
- package/src/session/session-manager-internal.ts +33 -0
- package/src/session/session-manager.ts +1112 -333
- package/src/session/session-storage.ts +444 -39
- package/src/session/volatile-context-pruning.ts +78 -0
- package/src/setup/credential-auto-import.ts +2 -1
- package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
- package/src/setup/provider-onboarding.ts +36 -61
- package/src/setup/provider-presets.json +5 -0
- package/src/skill-state/active-state.ts +3 -13
- package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
- package/src/skill-state/workflow-state-contract.ts +6 -41
- package/src/slash-commands/builtin-registry.ts +46 -11
- package/src/system-prompt.ts +37 -44
- package/src/task/agents.ts +19 -1
- package/src/task/executor.ts +90 -6
- package/src/task/index.ts +62 -37
- package/src/thinking.ts +21 -6
- package/src/tool-discovery/tool-index.ts +2 -134
- package/src/tools/ask.ts +157 -21
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/bash.ts +38 -0
- package/src/tools/browser/dead-tab-recovery.ts +78 -0
- package/src/tools/browser/tab-supervisor.ts +282 -36
- package/src/tools/computer.ts +10 -3
- package/src/tools/cron.ts +30 -16
- package/src/tools/eval.ts +1 -2
- package/src/tools/fetch.ts +11 -4
- package/src/tools/index.ts +76 -24
- package/src/tools/output-meta.ts +13 -5
- package/src/tools/read.ts +37 -1
- package/src/tools/resource-gc.ts +2 -2
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/tools/subagent-render.ts +19 -8
- package/src/tools/subagent.ts +84 -32
- package/src/utils/event-bus.ts +9 -4
- package/src/web/search/index.ts +3 -3
- package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
- package/src/discovery/plugin-dir-roots.ts +0 -28
- package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
COORDINATOR_MCP_TOOL_NAMES,
|
|
12
12
|
type CoordinatorToolName,
|
|
13
13
|
} from "../coordinator/contract";
|
|
14
|
+
import { readLinuxProcStartTimeSync } from "../gjc-runtime/linux-proc";
|
|
15
|
+
import type { WorkflowGate, WorkflowGateQueryRecord } from "../modes/shared/agent-wire/workflow-gate-types";
|
|
16
|
+
import type { BrokerDiscovery } from "../sdk/broker/discovery";
|
|
17
|
+
import { type EnsureBrokerSettings, ensureBroker } from "../sdk/broker/ensure";
|
|
18
|
+
import { UnsupportedStateVersionError } from "../sdk/broker/state-version";
|
|
14
19
|
import { SdkClient, SdkClientError } from "../sdk/client/client";
|
|
15
20
|
import { readSdkBrokerDiscovery } from "../sdk/client/discovery";
|
|
16
21
|
import {
|
|
@@ -23,9 +28,42 @@ import {
|
|
|
23
28
|
assertCoordinatorWorkdir,
|
|
24
29
|
buildCoordinatorMcpConfig,
|
|
25
30
|
type CoordinatorMcpConfig,
|
|
26
|
-
coordinatorNamespacePath,
|
|
27
31
|
requireCoordinatorMutation,
|
|
32
|
+
safeOpenCoordinatorArtifact,
|
|
28
33
|
} from "./policy";
|
|
34
|
+
import {
|
|
35
|
+
answerBindingMatches,
|
|
36
|
+
type CoordinatorQuestionDiagnosticPublicV1,
|
|
37
|
+
type CoordinatorQuestionPublicV1,
|
|
38
|
+
createAnswerBinding,
|
|
39
|
+
decodeAskGateV1,
|
|
40
|
+
type ListQuestionsSuccessV1,
|
|
41
|
+
type PublicReason,
|
|
42
|
+
projectAskGateQuestion,
|
|
43
|
+
translateCoordinatorAskAnswer,
|
|
44
|
+
validateCoordinatorAskAnswer,
|
|
45
|
+
} from "./question-gate-codec";
|
|
46
|
+
import {
|
|
47
|
+
advanceCreationReceipt,
|
|
48
|
+
advanceDeletion,
|
|
49
|
+
bindCreationRequest,
|
|
50
|
+
type CanonicalCreateIntentV1,
|
|
51
|
+
type CanonicalReportSnapshotV1,
|
|
52
|
+
type CanonicalSessionSnapshotV1,
|
|
53
|
+
type CoordinatorSessionTransactionV1,
|
|
54
|
+
claimCreationRequest,
|
|
55
|
+
commitCreationWal,
|
|
56
|
+
coordinatorStatePaths,
|
|
57
|
+
createSessionTransaction,
|
|
58
|
+
deterministicOutboxId,
|
|
59
|
+
initializeCoordinatorNamespace,
|
|
60
|
+
recordDeletionIntent,
|
|
61
|
+
repairProjections,
|
|
62
|
+
withAdmittedSessionTransaction,
|
|
63
|
+
withNamespaceRegistry,
|
|
64
|
+
withSessionTransaction,
|
|
65
|
+
} from "./question-state";
|
|
66
|
+
|
|
29
67
|
import { createSessionReaper, type ReapableSession, type SessionReaper } from "./session-reaper";
|
|
30
68
|
|
|
31
69
|
export type { CoordinatorToolName };
|
|
@@ -56,6 +94,41 @@ function sinkErrorCode(error: unknown): string | undefined {
|
|
|
56
94
|
return undefined;
|
|
57
95
|
}
|
|
58
96
|
}
|
|
97
|
+
|
|
98
|
+
type CoordinatorBrokerStage = "ensure" | "read" | "connect" | "request" | "close";
|
|
99
|
+
|
|
100
|
+
function toCoordinatorBrokerError(stage: CoordinatorBrokerStage, error: unknown): SdkClientError {
|
|
101
|
+
if (stage === "request" && error instanceof SdkClientError) return error;
|
|
102
|
+
if (stage === "ensure") {
|
|
103
|
+
if (error instanceof AggregateError)
|
|
104
|
+
return new SdkClientError(
|
|
105
|
+
"broker_cleanup_unverified",
|
|
106
|
+
"SDK broker bootstrap failed and cleanup was not verified.",
|
|
107
|
+
);
|
|
108
|
+
if (error instanceof UnsupportedStateVersionError)
|
|
109
|
+
return new SdkClientError(
|
|
110
|
+
"broker_discovery_unsupported",
|
|
111
|
+
"SDK broker discovery state version is unsupported.",
|
|
112
|
+
);
|
|
113
|
+
if (sinkErrorCode(error) === "EACCES" || sinkErrorCode(error) === "EPERM")
|
|
114
|
+
return new SdkClientError("broker_discovery_access_denied", "SDK broker discovery cannot be accessed.");
|
|
115
|
+
return new SdkClientError("broker_bootstrap_failed", "SDK broker bootstrap failed.");
|
|
116
|
+
}
|
|
117
|
+
if (stage === "read") {
|
|
118
|
+
if (error instanceof UnsupportedStateVersionError)
|
|
119
|
+
return new SdkClientError(
|
|
120
|
+
"broker_discovery_unsupported",
|
|
121
|
+
"SDK broker discovery state version is unsupported.",
|
|
122
|
+
);
|
|
123
|
+
if (sinkErrorCode(error) === "EACCES" || sinkErrorCode(error) === "EPERM")
|
|
124
|
+
return new SdkClientError("broker_discovery_access_denied", "SDK broker discovery cannot be accessed.");
|
|
125
|
+
return new SdkClientError("broker_discovery_unavailable", "SDK broker discovery cannot be read.");
|
|
126
|
+
}
|
|
127
|
+
return new SdkClientError(
|
|
128
|
+
stage === "request" ? "broker_request_unavailable" : "broker_transport_unavailable",
|
|
129
|
+
stage === "request" ? "SDK broker request is unavailable." : "SDK broker transport is unavailable.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
59
132
|
interface CoordinatorFinalResponse {
|
|
60
133
|
text: string | null;
|
|
61
134
|
format: "markdown";
|
|
@@ -78,6 +151,8 @@ interface RuntimeSessionStatePayload extends CoordinatorSessionState {
|
|
|
78
151
|
|
|
79
152
|
interface CoordinatorServices {
|
|
80
153
|
connectSdk?: (url: string, token: string) => Promise<SdkClient>;
|
|
154
|
+
ensureBroker?: (settings: EnsureBrokerSettings) => Promise<BrokerDiscovery>;
|
|
155
|
+
readSdkBrokerDiscovery?: (agentDir: string) => Promise<BrokerDiscovery | null>;
|
|
81
156
|
getAgentDir?: () => string;
|
|
82
157
|
resolveModelProfiles?: CoordinatorModelProfileLoader;
|
|
83
158
|
canonicalizePath?: (value: string) => Promise<string>;
|
|
@@ -385,11 +460,20 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
385
460
|
session_id: sessionId,
|
|
386
461
|
turn_id: { type: "string" },
|
|
387
462
|
question_id: { type: "string" },
|
|
463
|
+
answer_binding: { type: "string" },
|
|
388
464
|
answer: {},
|
|
389
465
|
idempotency_key: idempotencyKey,
|
|
390
466
|
allow_mutation: allowMutation,
|
|
391
467
|
},
|
|
392
|
-
required: [
|
|
468
|
+
required: [
|
|
469
|
+
"session_id",
|
|
470
|
+
"turn_id",
|
|
471
|
+
"question_id",
|
|
472
|
+
"answer_binding",
|
|
473
|
+
"answer",
|
|
474
|
+
"idempotency_key",
|
|
475
|
+
"allow_mutation",
|
|
476
|
+
],
|
|
393
477
|
},
|
|
394
478
|
};
|
|
395
479
|
}
|
|
@@ -439,7 +523,12 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
439
523
|
return {
|
|
440
524
|
name,
|
|
441
525
|
description: "List bounded structured questions for coordinator coordination.",
|
|
442
|
-
inputSchema: {
|
|
526
|
+
inputSchema: {
|
|
527
|
+
type: "object",
|
|
528
|
+
properties: { session_id: sessionId, turn_id: { type: "string" }, status: { type: "string" } },
|
|
529
|
+
|
|
530
|
+
required: ["session_id"],
|
|
531
|
+
},
|
|
443
532
|
};
|
|
444
533
|
}
|
|
445
534
|
if (name === "gjc_coordinator_list_artifacts") {
|
|
@@ -816,10 +905,6 @@ function publicSdkAcknowledgement(result: RuntimePromptAcknowledgement): Record<
|
|
|
816
905
|
};
|
|
817
906
|
}
|
|
818
907
|
|
|
819
|
-
function publicSdkAccepted(result: unknown): Record<string, unknown> {
|
|
820
|
-
return acknowledgementPayload(result)?.accepted === true ? { accepted: true } : {};
|
|
821
|
-
}
|
|
822
|
-
|
|
823
908
|
async function listJsonFiles(dir: string): Promise<unknown[]> {
|
|
824
909
|
try {
|
|
825
910
|
const entries = await fs.readdir(dir);
|
|
@@ -901,6 +986,22 @@ function publicCoordinatorSession(session: Record<string, unknown>): Record<stri
|
|
|
901
986
|
return result;
|
|
902
987
|
}
|
|
903
988
|
|
|
989
|
+
function publicCoordinatorStatusSession(session: Record<string, unknown>): Record<string, unknown> {
|
|
990
|
+
const { cwd: _cwd, ...safe } = publicCoordinatorSession(session);
|
|
991
|
+
return safe;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function capabilityFreeStatusValue(value: unknown): unknown {
|
|
995
|
+
if (Array.isArray(value)) return value.map(capabilityFreeStatusValue);
|
|
996
|
+
if (!value || typeof value !== "object") return value;
|
|
997
|
+
const result: Record<string, unknown> = {};
|
|
998
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
999
|
+
if (/^(?:answer_binding|cwd|path|payload_ref|evidence_paths|artifact_path|state_root)$/i.test(key)) continue;
|
|
1000
|
+
result[key] = capabilityFreeStatusValue(child);
|
|
1001
|
+
}
|
|
1002
|
+
return result;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
904
1005
|
function publicLifecycleReceipt(result: Record<string, unknown>, sessionId: string): Record<string, unknown> {
|
|
905
1006
|
const receipt: Record<string, unknown> = { session_id: sessionId };
|
|
906
1007
|
const worktree = asRecord(result.worktree);
|
|
@@ -1242,19 +1343,27 @@ async function writeSessionStateUnlocked(
|
|
|
1242
1343
|
live?: boolean | null;
|
|
1243
1344
|
reason?: string | null;
|
|
1244
1345
|
source?: CoordinatorSessionState["source"];
|
|
1346
|
+
overwrite?: boolean;
|
|
1245
1347
|
} = {},
|
|
1246
1348
|
): Promise<CoordinatorSessionState> {
|
|
1247
|
-
const previous = await readSessionState(namespaceDir, sessionId);
|
|
1349
|
+
const previous = options.overwrite ? null : await readSessionState(namespaceDir, sessionId);
|
|
1350
|
+
const hasCurrentTurn = Object.hasOwn(options, "currentTurnId");
|
|
1351
|
+
const hasLastTurn = Object.hasOwn(options, "lastTurnId");
|
|
1352
|
+
const hasLive = Object.hasOwn(options, "live");
|
|
1248
1353
|
const payload: CoordinatorSessionState = {
|
|
1249
1354
|
schema_version: 1,
|
|
1250
1355
|
session_id: sessionId,
|
|
1251
1356
|
state,
|
|
1252
1357
|
ready_for_input: state === "ready_for_input" || state === "completed",
|
|
1253
|
-
current_turn_id:
|
|
1254
|
-
|
|
1358
|
+
current_turn_id: hasCurrentTurn
|
|
1359
|
+
? (options.currentTurnId ?? null)
|
|
1360
|
+
: state === "running"
|
|
1361
|
+
? (previous?.current_turn_id ?? null)
|
|
1362
|
+
: null,
|
|
1363
|
+
last_turn_id: hasLastTurn ? (options.lastTurnId ?? null) : (previous?.last_turn_id ?? null),
|
|
1255
1364
|
updated_at: new Date().toISOString(),
|
|
1256
1365
|
source: options.source ?? "coordinator",
|
|
1257
|
-
live: options.live ?? previous?.live ?? null,
|
|
1366
|
+
live: hasLive ? (options.live ?? null) : (previous?.live ?? null),
|
|
1258
1367
|
reason: options.reason ?? null,
|
|
1259
1368
|
};
|
|
1260
1369
|
await writeJsonFile(sessionStateFile(namespaceDir, sessionId), payload);
|
|
@@ -1290,17 +1399,7 @@ interface SessionStateLockOwner {
|
|
|
1290
1399
|
}
|
|
1291
1400
|
|
|
1292
1401
|
function processStartTime(pid: number): string | null {
|
|
1293
|
-
|
|
1294
|
-
const stat = nodeFs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
1295
|
-
const close = stat.lastIndexOf(")");
|
|
1296
|
-
const fields = stat
|
|
1297
|
-
.slice(close + 1)
|
|
1298
|
-
.trim()
|
|
1299
|
-
.split(/\s+/);
|
|
1300
|
-
return fields[19] ?? null;
|
|
1301
|
-
} catch {
|
|
1302
|
-
return null;
|
|
1303
|
-
}
|
|
1402
|
+
return readLinuxProcStartTimeSync(pid);
|
|
1304
1403
|
}
|
|
1305
1404
|
|
|
1306
1405
|
function validLockOwner(value: unknown): value is SessionStateLockOwner {
|
|
@@ -1407,6 +1506,7 @@ async function writeSessionState(
|
|
|
1407
1506
|
live?: boolean | null;
|
|
1408
1507
|
reason?: string | null;
|
|
1409
1508
|
source?: CoordinatorSessionState["source"];
|
|
1509
|
+
overwrite?: boolean;
|
|
1410
1510
|
} = {},
|
|
1411
1511
|
): Promise<CoordinatorSessionState> {
|
|
1412
1512
|
const file = sessionStateFile(namespaceDir, sessionId);
|
|
@@ -1416,11 +1516,7 @@ async function writeSessionState(
|
|
|
1416
1516
|
);
|
|
1417
1517
|
}
|
|
1418
1518
|
|
|
1419
|
-
async function markTurnFailedForUnavailableSession(
|
|
1420
|
-
namespaceDir: string,
|
|
1421
|
-
turn: TurnRecord,
|
|
1422
|
-
reason: string,
|
|
1423
|
-
): Promise<TurnRecord> {
|
|
1519
|
+
async function markTurnFailedForUnavailableSession(turn: TurnRecord, reason: string): Promise<TurnRecord> {
|
|
1424
1520
|
const timestamp = new Date().toISOString();
|
|
1425
1521
|
const failed: TurnRecord = {
|
|
1426
1522
|
...turn,
|
|
@@ -1438,18 +1534,10 @@ async function markTurnFailedForUnavailableSession(
|
|
|
1438
1534
|
updated_at: timestamp,
|
|
1439
1535
|
completed_at: timestamp,
|
|
1440
1536
|
};
|
|
1441
|
-
await writeTurnRecord(namespaceDir, failed);
|
|
1442
|
-
await clearActiveTurn(namespaceDir, failed);
|
|
1443
|
-
await writeSessionState(namespaceDir, failed.session_id, "stale", {
|
|
1444
|
-
lastTurnId: failed.turn_id,
|
|
1445
|
-
live: false,
|
|
1446
|
-
reason,
|
|
1447
|
-
});
|
|
1448
1537
|
return failed;
|
|
1449
1538
|
}
|
|
1450
1539
|
|
|
1451
1540
|
async function markTurnTerminalFromSessionState(
|
|
1452
|
-
namespaceDir: string,
|
|
1453
1541
|
turn: TurnRecord,
|
|
1454
1542
|
sessionState: CoordinatorSessionState,
|
|
1455
1543
|
): Promise<TurnRecord> {
|
|
@@ -1493,13 +1581,6 @@ async function markTurnTerminalFromSessionState(
|
|
|
1493
1581
|
updated_at: timestamp,
|
|
1494
1582
|
completed_at: timestamp,
|
|
1495
1583
|
};
|
|
1496
|
-
await writeTurnRecord(namespaceDir, resolved);
|
|
1497
|
-
await clearActiveTurn(namespaceDir, resolved);
|
|
1498
|
-
await writeSessionState(namespaceDir, resolved.session_id, sessionState.state, {
|
|
1499
|
-
lastTurnId: resolved.turn_id,
|
|
1500
|
-
live: sessionState.live,
|
|
1501
|
-
reason: sessionState.reason,
|
|
1502
|
-
});
|
|
1503
1584
|
return resolved;
|
|
1504
1585
|
}
|
|
1505
1586
|
|
|
@@ -1569,11 +1650,7 @@ function turnAwaitingRuntimeAckExpired(turn: TurnRecord, nowMs: number, ackTimeo
|
|
|
1569
1650
|
return Number.isFinite(deliveredMs) && nowMs - deliveredMs >= ackTimeoutMs;
|
|
1570
1651
|
}
|
|
1571
1652
|
|
|
1572
|
-
async function markTurnFailedForUnacknowledgedDelivery(
|
|
1573
|
-
namespaceDir: string,
|
|
1574
|
-
turn: TurnRecord,
|
|
1575
|
-
ackTimeoutMs: number,
|
|
1576
|
-
): Promise<TurnRecord> {
|
|
1653
|
+
async function markTurnFailedForUnacknowledgedDelivery(turn: TurnRecord, ackTimeoutMs: number): Promise<TurnRecord> {
|
|
1577
1654
|
const timestamp = new Date().toISOString();
|
|
1578
1655
|
const message = `Tmux key delivery succeeded, but the GJC runtime did not acknowledge the prompt or emit turn_start within ${ackTimeoutMs}ms. The turn never started; stop waiting and inspect/retry the coordinator session.`;
|
|
1579
1656
|
const failed: TurnRecord = {
|
|
@@ -1618,13 +1695,6 @@ async function markTurnFailedForUnacknowledgedDelivery(
|
|
|
1618
1695
|
updated_at: timestamp,
|
|
1619
1696
|
completed_at: timestamp,
|
|
1620
1697
|
};
|
|
1621
|
-
await writeTurnRecord(namespaceDir, failed);
|
|
1622
|
-
await clearActiveTurn(namespaceDir, failed);
|
|
1623
|
-
await writeSessionState(namespaceDir, failed.session_id, "stale", {
|
|
1624
|
-
lastTurnId: failed.turn_id,
|
|
1625
|
-
live: failed.liveness.live,
|
|
1626
|
-
reason: PROMPT_ACK_TIMEOUT_REASON,
|
|
1627
|
-
});
|
|
1628
1698
|
return failed;
|
|
1629
1699
|
}
|
|
1630
1700
|
|
|
@@ -1639,7 +1709,7 @@ async function reconcileRuntimeAcknowledgement(
|
|
|
1639
1709
|
return await markTurnAcknowledgedFromRuntimeState(namespaceDir, turn, sessionState);
|
|
1640
1710
|
}
|
|
1641
1711
|
if (options.failOnTimeout && turnAwaitingRuntimeAckExpired(turn, Date.now(), ackTimeoutMs)) {
|
|
1642
|
-
return await markTurnFailedForUnacknowledgedDelivery(
|
|
1712
|
+
return await markTurnFailedForUnacknowledgedDelivery(turn, ackTimeoutMs);
|
|
1643
1713
|
}
|
|
1644
1714
|
return turn;
|
|
1645
1715
|
}
|
|
@@ -1813,19 +1883,18 @@ export async function readCoordinatorArtifact(
|
|
|
1813
1883
|
): Promise<Record<string, unknown>> {
|
|
1814
1884
|
let handle: fs.FileHandle | null = null;
|
|
1815
1885
|
try {
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
const readLimit = resolved.byteCap + 1;
|
|
1886
|
+
handle = await safeOpenCoordinatorArtifact(config, args.path);
|
|
1887
|
+
const readLimit = config.artifactByteCap + 1;
|
|
1819
1888
|
const buffer = Buffer.alloc(readLimit);
|
|
1820
1889
|
const { bytesRead } = await handle.read(buffer, 0, readLimit, 0);
|
|
1821
|
-
const boundedBytes = buffer.subarray(0, Math.min(bytesRead,
|
|
1822
|
-
const text = decodeUtf8WithinByteCap(boundedBytes,
|
|
1890
|
+
const boundedBytes = buffer.subarray(0, Math.min(bytesRead, config.artifactByteCap));
|
|
1891
|
+
const text = decodeUtf8WithinByteCap(boundedBytes, config.artifactByteCap);
|
|
1823
1892
|
return {
|
|
1824
1893
|
ok: true,
|
|
1825
|
-
path:
|
|
1894
|
+
path: typeof args.path === "string" ? path.resolve(args.path) : "",
|
|
1826
1895
|
text,
|
|
1827
1896
|
bytes: Buffer.byteLength(text),
|
|
1828
|
-
truncated: bytesRead >
|
|
1897
|
+
truncated: bytesRead > config.artifactByteCap,
|
|
1829
1898
|
};
|
|
1830
1899
|
} catch (error) {
|
|
1831
1900
|
return {
|
|
@@ -1844,7 +1913,436 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1844
1913
|
const services = options.services ?? {};
|
|
1845
1914
|
const platform = options.platform ?? process.platform;
|
|
1846
1915
|
const loadModelProfiles = services.resolveModelProfiles ?? loadCoordinatorModelProfiles;
|
|
1847
|
-
const namespaceDir =
|
|
1916
|
+
const namespaceDir = path.join(
|
|
1917
|
+
config.stateRoot,
|
|
1918
|
+
config.namespace.profile ?? "unscoped-profile",
|
|
1919
|
+
config.namespace.repo ?? "unscoped-repo",
|
|
1920
|
+
);
|
|
1921
|
+
const questionPaths = coordinatorStatePaths(config.stateRoot, config.namespace.identity);
|
|
1922
|
+
let questionStateReady: Promise<void> | null = null;
|
|
1923
|
+
|
|
1924
|
+
function ensureQuestionStateReady(): Promise<void> {
|
|
1925
|
+
questionStateReady ??= initializeCoordinatorNamespace(questionPaths);
|
|
1926
|
+
return questionStateReady;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function creationDigests(
|
|
1930
|
+
tool: string,
|
|
1931
|
+
idempotencyKey: string,
|
|
1932
|
+
canonicalArgs: Record<string, unknown>,
|
|
1933
|
+
): {
|
|
1934
|
+
keyDigest: string;
|
|
1935
|
+
requestDigest: string;
|
|
1936
|
+
} {
|
|
1937
|
+
return {
|
|
1938
|
+
keyDigest: createHash("sha256").update(`${tool}\0${idempotencyKey}`).digest("hex"),
|
|
1939
|
+
requestDigest: createHash("sha256")
|
|
1940
|
+
.update(canonicalJson({ tool, args: canonicalArgs }))
|
|
1941
|
+
.digest("hex"),
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
function canonicalCreationSnapshot(session: Record<string, unknown>): CanonicalSessionSnapshotV1 {
|
|
1946
|
+
const now = new Date().toISOString();
|
|
1947
|
+
const sessionId = safeExternalId("session", session.session_id ?? session.sessionId);
|
|
1948
|
+
const cwd = optionalString(session.cwd);
|
|
1949
|
+
const incarnation = optionalString(session.endpoint_incarnation);
|
|
1950
|
+
if (!cwd || !incarnation) throw new Error("state_corrupt");
|
|
1951
|
+
return {
|
|
1952
|
+
schema_version: 1,
|
|
1953
|
+
namespace_id: config.namespace.identity,
|
|
1954
|
+
session_id: sessionId,
|
|
1955
|
+
cwd: path.resolve(cwd),
|
|
1956
|
+
created_at: optionalString(session.created_at) ?? now,
|
|
1957
|
+
updated_at: now,
|
|
1958
|
+
mpreset: optionalString(session.mpreset),
|
|
1959
|
+
source: optionalString(session.source),
|
|
1960
|
+
model: optionalString(session.model),
|
|
1961
|
+
tmux: {
|
|
1962
|
+
session: optionalString(session.tmux_session),
|
|
1963
|
+
window: null,
|
|
1964
|
+
pane: optionalString(session.tmux_target),
|
|
1965
|
+
},
|
|
1966
|
+
broker: {
|
|
1967
|
+
workspace: optionalString(session.broker_workspace),
|
|
1968
|
+
endpoint_url: "",
|
|
1969
|
+
endpoint_generation: typeof session.endpoint_generation === "number" ? session.endpoint_generation : 0,
|
|
1970
|
+
endpoint_incarnation: incarnation,
|
|
1971
|
+
},
|
|
1972
|
+
ephemeral: session.ephemeral === true,
|
|
1973
|
+
visible: session.visible !== false,
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function sessionFromCreationSnapshot(snapshot: CanonicalSessionSnapshotV1): Record<string, unknown> {
|
|
1978
|
+
return normalizeSession({
|
|
1979
|
+
session_id: snapshot.session_id,
|
|
1980
|
+
cwd: snapshot.cwd,
|
|
1981
|
+
created_at: snapshot.created_at,
|
|
1982
|
+
...(snapshot.mpreset ? { mpreset: snapshot.mpreset } : {}),
|
|
1983
|
+
...(snapshot.source ? { source: snapshot.source } : {}),
|
|
1984
|
+
...(snapshot.model ? { model: snapshot.model } : {}),
|
|
1985
|
+
tmux_session: snapshot.tmux.session,
|
|
1986
|
+
tmux_target: snapshot.tmux.pane,
|
|
1987
|
+
ephemeral: snapshot.ephemeral,
|
|
1988
|
+
visible: snapshot.visible,
|
|
1989
|
+
broker_workspace: snapshot.broker.workspace,
|
|
1990
|
+
endpoint_generation: snapshot.broker.endpoint_generation,
|
|
1991
|
+
endpoint_incarnation: snapshot.broker.endpoint_incarnation,
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
async function claimProductionCreation(
|
|
1996
|
+
tool: string,
|
|
1997
|
+
idempotencyKey: string,
|
|
1998
|
+
canonicalArgs: Record<string, unknown>,
|
|
1999
|
+
) {
|
|
2000
|
+
await ensureQuestionStateReady();
|
|
2001
|
+
const { keyDigest, requestDigest } = creationDigests(tool, idempotencyKey, canonicalArgs);
|
|
2002
|
+
return {
|
|
2003
|
+
keyDigest,
|
|
2004
|
+
request: await claimCreationRequest(questionPaths, {
|
|
2005
|
+
key_digest: keyDigest,
|
|
2006
|
+
request_digest: requestDigest,
|
|
2007
|
+
tool,
|
|
2008
|
+
}),
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
async function ensureQuestionTransaction(sessionId: string): Promise<void> {
|
|
2013
|
+
await ensureQuestionStateReady();
|
|
2014
|
+
try {
|
|
2015
|
+
await withSessionTransaction(questionPaths, sessionId, async () => undefined);
|
|
2016
|
+
return;
|
|
2017
|
+
} catch (error) {
|
|
2018
|
+
if (!(error instanceof Error) || error.message !== "resource_gone") throw error;
|
|
2019
|
+
}
|
|
2020
|
+
const session = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
2021
|
+
if (!session) throw new Error("resource_gone");
|
|
2022
|
+
const incarnation = optionalString(session.endpoint_incarnation);
|
|
2023
|
+
const cwd = optionalString(session.cwd);
|
|
2024
|
+
if (!incarnation || !cwd) throw new Error("resource_gone");
|
|
2025
|
+
const now = new Date().toISOString();
|
|
2026
|
+
await createSessionTransaction(questionPaths, {
|
|
2027
|
+
kind: "register",
|
|
2028
|
+
session: {
|
|
2029
|
+
schema_version: 1,
|
|
2030
|
+
namespace_id: config.namespace.identity,
|
|
2031
|
+
session_id: sessionId,
|
|
2032
|
+
cwd: path.resolve(cwd),
|
|
2033
|
+
created_at: optionalString(session.created_at) ?? now,
|
|
2034
|
+
updated_at: now,
|
|
2035
|
+
mpreset: optionalString(session.mpreset),
|
|
2036
|
+
source: optionalString(session.source),
|
|
2037
|
+
model: optionalString(session.model),
|
|
2038
|
+
tmux: {
|
|
2039
|
+
session: optionalString(session.tmux_session),
|
|
2040
|
+
window: null,
|
|
2041
|
+
pane: optionalString(session.tmux_target),
|
|
2042
|
+
},
|
|
2043
|
+
broker: {
|
|
2044
|
+
workspace: optionalString(session.broker_workspace),
|
|
2045
|
+
endpoint_url: "",
|
|
2046
|
+
endpoint_generation: typeof session.endpoint_generation === "number" ? session.endpoint_generation : 0,
|
|
2047
|
+
endpoint_incarnation: incarnation,
|
|
2048
|
+
},
|
|
2049
|
+
ephemeral: session.ephemeral === true,
|
|
2050
|
+
visible: session.visible !== false,
|
|
2051
|
+
},
|
|
2052
|
+
initial_state: "ready_for_input",
|
|
2053
|
+
initial_events: [],
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
function publicQuestions(
|
|
2058
|
+
transaction: CoordinatorSessionTransactionV1,
|
|
2059
|
+
status: string | null,
|
|
2060
|
+
): CoordinatorQuestionPublicV1[] {
|
|
2061
|
+
return Object.values(transaction.canonical.questions)
|
|
2062
|
+
.filter(
|
|
2063
|
+
question => !status || question.status === status || (status === "open" && question.status === "pending"),
|
|
2064
|
+
)
|
|
2065
|
+
.map(question =>
|
|
2066
|
+
projectAskGateQuestion({
|
|
2067
|
+
question_id: question.question_id,
|
|
2068
|
+
session_id: question.session_id,
|
|
2069
|
+
turn_id: question.turn_id,
|
|
2070
|
+
status: question.status === "resolving" ? "pending" : question.status,
|
|
2071
|
+
stage: question.stage,
|
|
2072
|
+
kind: question.kind,
|
|
2073
|
+
prompt: question.prompt,
|
|
2074
|
+
codec: question.codec,
|
|
2075
|
+
created_at: question.created_at,
|
|
2076
|
+
updated_at: question.updated_at,
|
|
2077
|
+
answered_at: question.answered_at,
|
|
2078
|
+
reason: question.history.at(-1)?.reason ?? null,
|
|
2079
|
+
...(question.status === "pending" ? { answer_binding: question.binding_plaintext } : {}),
|
|
2080
|
+
}),
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
async function reconcileQuestions(sessionId: string): Promise<ListQuestionsSuccessV1> {
|
|
2085
|
+
const observedAt = new Date().toISOString();
|
|
2086
|
+
const diagnostics: CoordinatorQuestionDiagnosticPublicV1[] = [];
|
|
2087
|
+
const diagnostic = (
|
|
2088
|
+
reason: CoordinatorQuestionDiagnosticPublicV1["reason"],
|
|
2089
|
+
turnId: string | null = null,
|
|
2090
|
+
gateId: string | null = null,
|
|
2091
|
+
) => {
|
|
2092
|
+
if (diagnostics.length < 64)
|
|
2093
|
+
diagnostics.push({
|
|
2094
|
+
schema_version: 1,
|
|
2095
|
+
session_id: sessionId,
|
|
2096
|
+
turn_id: turnId,
|
|
2097
|
+
gate_id: gateId,
|
|
2098
|
+
reason,
|
|
2099
|
+
observed_at: observedAt,
|
|
2100
|
+
});
|
|
2101
|
+
};
|
|
2102
|
+
try {
|
|
2103
|
+
await ensureQuestionTransaction(sessionId);
|
|
2104
|
+
const session = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
2105
|
+
if (!session) throw new Error("resource_gone");
|
|
2106
|
+
const snapshot = await readCompleteQ12Snapshot(session);
|
|
2107
|
+
const { items, complete, revision } = snapshot;
|
|
2108
|
+
if (!complete) {
|
|
2109
|
+
diagnostic(snapshot.reason ?? "query_unavailable");
|
|
2110
|
+
return {
|
|
2111
|
+
ok: true,
|
|
2112
|
+
schema_version: 1,
|
|
2113
|
+
questions: await withSessionTransaction(questionPaths, sessionId, async tx => publicQuestions(tx, null)),
|
|
2114
|
+
diagnostics,
|
|
2115
|
+
reconciliation: {
|
|
2116
|
+
attempted: true,
|
|
2117
|
+
complete: false,
|
|
2118
|
+
revision,
|
|
2119
|
+
observed_at: observedAt,
|
|
2120
|
+
reason: snapshot.reason ?? "query_unavailable",
|
|
2121
|
+
},
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
const projectedTurnQuestions = new Map<string, string[]>();
|
|
2125
|
+
await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
|
|
2126
|
+
const seen = new Set<string>();
|
|
2127
|
+
const byRuntimeTurn = new Map<string, Array<(typeof transaction.canonical.turns)[string]>>();
|
|
2128
|
+
for (const turn of Object.values(transaction.canonical.turns)) {
|
|
2129
|
+
const runtimeTurnId = turn.delivery.runtime_turn_id;
|
|
2130
|
+
if (typeof runtimeTurnId !== "string") continue;
|
|
2131
|
+
const owners = byRuntimeTurn.get(runtimeTurnId) ?? [];
|
|
2132
|
+
owners.push(turn);
|
|
2133
|
+
byRuntimeTurn.set(runtimeTurnId, owners);
|
|
2134
|
+
}
|
|
2135
|
+
for (const row of items as WorkflowGateQueryRecord[]) {
|
|
2136
|
+
if (!row || typeof row !== "object" || row.tag !== "pending" || typeof row.gate_id !== "string") {
|
|
2137
|
+
diagnostic("invalid_gate_row");
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
const gate = row as WorkflowGateQueryRecord & WorkflowGate;
|
|
2141
|
+
const authorityId = createHash("sha256")
|
|
2142
|
+
.update(
|
|
2143
|
+
`${config.namespace.identity}\0${sessionId}\0${transaction.canonical.session.broker.endpoint_incarnation}\0${gate.gate_id}`,
|
|
2144
|
+
)
|
|
2145
|
+
.digest("hex");
|
|
2146
|
+
seen.add(authorityId);
|
|
2147
|
+
const runtimeTurnId = gate.runtime_turn_id;
|
|
2148
|
+
const existing = transaction.canonical.gate_authorities[authorityId];
|
|
2149
|
+
const authority = existing ?? {
|
|
2150
|
+
authority: {
|
|
2151
|
+
namespace_id: config.namespace.identity,
|
|
2152
|
+
session_id: sessionId,
|
|
2153
|
+
endpoint_incarnation: transaction.canonical.session.broker.endpoint_incarnation,
|
|
2154
|
+
gate_id: gate.gate_id,
|
|
2155
|
+
},
|
|
2156
|
+
observation:
|
|
2157
|
+
typeof runtimeTurnId === "string" && runtimeTurnId
|
|
2158
|
+
? {
|
|
2159
|
+
kind: "valid" as const,
|
|
2160
|
+
first_provenance: {
|
|
2161
|
+
runtime_turn_id: runtimeTurnId,
|
|
2162
|
+
gate_created_at: gate.created_at,
|
|
2163
|
+
schema_hash: gate.schema_hash,
|
|
2164
|
+
stage: gate.stage,
|
|
2165
|
+
kind: gate.kind,
|
|
2166
|
+
},
|
|
2167
|
+
}
|
|
2168
|
+
: {
|
|
2169
|
+
kind: "malformed" as const,
|
|
2170
|
+
immutable_observation_digest: createHash("sha256")
|
|
2171
|
+
.update(canonicalJson(row))
|
|
2172
|
+
.digest("hex"),
|
|
2173
|
+
malformed: "missing_runtime_turn" as const,
|
|
2174
|
+
},
|
|
2175
|
+
outcome: { state: "deferred_link" as const, first_seen_at: observedAt },
|
|
2176
|
+
first_seen_at: observedAt,
|
|
2177
|
+
updated_at: observedAt,
|
|
2178
|
+
};
|
|
2179
|
+
transaction.canonical.gate_authorities[authorityId] = authority;
|
|
2180
|
+
if (typeof runtimeTurnId !== "string" || !runtimeTurnId) {
|
|
2181
|
+
authority.outcome = { state: "stale", reason: "missing_runtime_turn" };
|
|
2182
|
+
authority.updated_at = observedAt;
|
|
2183
|
+
diagnostic("missing_runtime_turn", null, gate.gate_id);
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
if (
|
|
2187
|
+
existing?.observation.kind === "malformed" ||
|
|
2188
|
+
(existing?.observation.kind === "valid" &&
|
|
2189
|
+
(existing.observation.first_provenance.runtime_turn_id !== runtimeTurnId ||
|
|
2190
|
+
existing.observation.first_provenance.gate_created_at !== gate.created_at ||
|
|
2191
|
+
existing.observation.first_provenance.schema_hash !== gate.schema_hash ||
|
|
2192
|
+
existing.observation.first_provenance.stage !== gate.stage ||
|
|
2193
|
+
existing.observation.first_provenance.kind !== gate.kind))
|
|
2194
|
+
) {
|
|
2195
|
+
authority.outcome = { state: "stale", reason: "gate_provenance_changed" };
|
|
2196
|
+
authority.updated_at = observedAt;
|
|
2197
|
+
diagnostic("gate_provenance_changed", null, gate.gate_id);
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
const owners = byRuntimeTurn.get(runtimeTurnId) ?? [];
|
|
2201
|
+
if (owners.length !== 1) {
|
|
2202
|
+
const watermarkAt = transaction.recovery.prompt_watermark_at;
|
|
2203
|
+
const gateCreatedAt = Date.parse(gate.created_at);
|
|
2204
|
+
const watermarkBeyondGate =
|
|
2205
|
+
watermarkAt !== null && Number.isFinite(gateCreatedAt) && Date.parse(watermarkAt) > gateCreatedAt;
|
|
2206
|
+
const boundedAbsenceProved =
|
|
2207
|
+
watermarkBeyondGate &&
|
|
2208
|
+
Number.isFinite(gateCreatedAt) &&
|
|
2209
|
+
Date.now() - gateCreatedAt >= 5 * 60 * 1000;
|
|
2210
|
+
if (owners.length === 0 && !boundedAbsenceProved) {
|
|
2211
|
+
authority.outcome = { state: "deferred_link", first_seen_at: authority.first_seen_at };
|
|
2212
|
+
authority.updated_at = observedAt;
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
authority.outcome =
|
|
2216
|
+
owners.length === 0
|
|
2217
|
+
? { state: "ownership_unavailable", reason: "ownership_unavailable" }
|
|
2218
|
+
: { state: "ownership_conflict", reason: "ownership_conflict" };
|
|
2219
|
+
authority.updated_at = observedAt;
|
|
2220
|
+
diagnostic(owners.length === 0 ? "ownership_unavailable" : "ownership_conflict", null, gate.gate_id);
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
const turn = owners[0]!;
|
|
2224
|
+
if (TERMINAL_TURN_STATUSES.has(turn.status as TurnStatus)) {
|
|
2225
|
+
authority.outcome = { state: "stale", reason: "turn_terminal", turn_id: turn.turn_id };
|
|
2226
|
+
authority.updated_at = observedAt;
|
|
2227
|
+
diagnostic("turn_terminal", turn.turn_id, gate.gate_id);
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
const codec = decodeAskGateV1(gate);
|
|
2231
|
+
if (!codec) {
|
|
2232
|
+
authority.outcome = { state: "stale", reason: "unsupported_gate", turn_id: turn.turn_id };
|
|
2233
|
+
authority.updated_at = observedAt;
|
|
2234
|
+
diagnostic("unsupported_gate", turn.turn_id, gate.gate_id);
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
if (
|
|
2238
|
+
existing &&
|
|
2239
|
+
existing.outcome.state !== "pending" &&
|
|
2240
|
+
existing.outcome.state !== "answered" &&
|
|
2241
|
+
existing.outcome.state !== "deferred_link"
|
|
2242
|
+
)
|
|
2243
|
+
continue;
|
|
2244
|
+
const questionId =
|
|
2245
|
+
existing?.outcome.state === "pending" || existing?.outcome.state === "answered"
|
|
2246
|
+
? existing.outcome.question_id
|
|
2247
|
+
: gate.gate_id;
|
|
2248
|
+
if (!existing || authority.outcome.state === "deferred_link") {
|
|
2249
|
+
authority.observation = {
|
|
2250
|
+
kind: "valid",
|
|
2251
|
+
first_provenance: {
|
|
2252
|
+
runtime_turn_id: runtimeTurnId,
|
|
2253
|
+
gate_created_at: gate.created_at,
|
|
2254
|
+
schema_hash: gate.schema_hash,
|
|
2255
|
+
stage: gate.stage,
|
|
2256
|
+
kind: gate.kind,
|
|
2257
|
+
},
|
|
2258
|
+
};
|
|
2259
|
+
authority.outcome = { state: "pending", turn_id: turn.turn_id, question_id: questionId };
|
|
2260
|
+
}
|
|
2261
|
+
authority.updated_at = observedAt;
|
|
2262
|
+
transaction.canonical.gate_authorities[authorityId] = authority;
|
|
2263
|
+
if (!transaction.canonical.questions[questionId]) {
|
|
2264
|
+
const binding = createAnswerBinding();
|
|
2265
|
+
transaction.canonical.questions[questionId] = {
|
|
2266
|
+
question_id: questionId,
|
|
2267
|
+
authority_id: authorityId,
|
|
2268
|
+
session_id: sessionId,
|
|
2269
|
+
turn_id: turn.turn_id,
|
|
2270
|
+
endpoint_incarnation: transaction.canonical.session.broker.endpoint_incarnation,
|
|
2271
|
+
stage: gate.stage,
|
|
2272
|
+
kind: gate.kind,
|
|
2273
|
+
prompt: typeof gate.context.prompt === "string" ? gate.context.prompt : "",
|
|
2274
|
+
status: "pending",
|
|
2275
|
+
binding_plaintext: binding,
|
|
2276
|
+
binding_sha256: createHash("sha256").update(binding).digest("hex"),
|
|
2277
|
+
codec,
|
|
2278
|
+
claim_fence_epoch: null,
|
|
2279
|
+
answer_request_id: null,
|
|
2280
|
+
created_at: observedAt,
|
|
2281
|
+
updated_at: observedAt,
|
|
2282
|
+
answered_at: null,
|
|
2283
|
+
history: [{ at: observedAt, status: "pending", reason: null }],
|
|
2284
|
+
};
|
|
2285
|
+
turn.question_ids = [...new Set([...turn.question_ids, questionId])];
|
|
2286
|
+
projectedTurnQuestions.set(turn.turn_id, turn.question_ids);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
if (complete)
|
|
2290
|
+
for (const [authorityId, authority] of Object.entries(transaction.canonical.gate_authorities))
|
|
2291
|
+
if (!seen.has(authorityId) && authority.outcome.state === "pending") {
|
|
2292
|
+
const question = transaction.canonical.questions[authority.outcome.question_id];
|
|
2293
|
+
if (question?.status === "pending") {
|
|
2294
|
+
question.status = "stale";
|
|
2295
|
+
question.updated_at = observedAt;
|
|
2296
|
+
question.history.push({ at: observedAt, status: "stale", reason: "terminal_uncertain" });
|
|
2297
|
+
authority.outcome = {
|
|
2298
|
+
state: "stale",
|
|
2299
|
+
reason: "terminal_uncertain",
|
|
2300
|
+
turn_id: question.turn_id,
|
|
2301
|
+
question_id: question.question_id,
|
|
2302
|
+
};
|
|
2303
|
+
authority.updated_at = observedAt;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
});
|
|
2307
|
+
for (const [turnId, questionIds] of projectedTurnQuestions) {
|
|
2308
|
+
const legacyTurn = await readTurnRecord(namespaceDir, turnId);
|
|
2309
|
+
if (!legacyTurn) continue;
|
|
2310
|
+
legacyTurn.question_ids = questionIds;
|
|
2311
|
+
await writeTurnRecord(namespaceDir, legacyTurn);
|
|
2312
|
+
}
|
|
2313
|
+
return {
|
|
2314
|
+
ok: true,
|
|
2315
|
+
schema_version: 1,
|
|
2316
|
+
questions: await withSessionTransaction(questionPaths, sessionId, async tx => publicQuestions(tx, null)),
|
|
2317
|
+
diagnostics,
|
|
2318
|
+
reconciliation: {
|
|
2319
|
+
attempted: true,
|
|
2320
|
+
complete,
|
|
2321
|
+
revision,
|
|
2322
|
+
observed_at: observedAt,
|
|
2323
|
+
reason: complete ? null : "query_unavailable",
|
|
2324
|
+
},
|
|
2325
|
+
};
|
|
2326
|
+
} catch (error) {
|
|
2327
|
+
if (error instanceof Error && (error.message === "state_corrupt" || error.message === "resource_gone"))
|
|
2328
|
+
throw error;
|
|
2329
|
+
diagnostic("query_unavailable");
|
|
2330
|
+
await ensureQuestionTransaction(sessionId);
|
|
2331
|
+
return {
|
|
2332
|
+
ok: true,
|
|
2333
|
+
schema_version: 1,
|
|
2334
|
+
questions: await withSessionTransaction(questionPaths, sessionId, async tx => publicQuestions(tx, null)),
|
|
2335
|
+
diagnostics,
|
|
2336
|
+
reconciliation: {
|
|
2337
|
+
attempted: true,
|
|
2338
|
+
complete: false,
|
|
2339
|
+
revision: null,
|
|
2340
|
+
observed_at: observedAt,
|
|
2341
|
+
reason: "query_unavailable",
|
|
2342
|
+
},
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
1848
2346
|
const sessionTransitionTails = new Map<string, Promise<void>>();
|
|
1849
2347
|
|
|
1850
2348
|
async function withSessionTransition<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
|
|
@@ -1884,14 +2382,19 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1884
2382
|
}
|
|
1885
2383
|
}
|
|
1886
2384
|
|
|
1887
|
-
async function querySession(
|
|
2385
|
+
async function querySession(
|
|
2386
|
+
session: Record<string, unknown>,
|
|
2387
|
+
query: string,
|
|
2388
|
+
input: Record<string, unknown> = {},
|
|
2389
|
+
cursor?: string,
|
|
2390
|
+
): Promise<Record<string, unknown>> {
|
|
1888
2391
|
const endpoint = await resolveSessionEndpoint(session);
|
|
1889
2392
|
const client = await (services.connectSdk ?? ((url, token) => SdkClient.connect(url, token)))(
|
|
1890
2393
|
endpoint.url,
|
|
1891
2394
|
endpoint.token,
|
|
1892
2395
|
);
|
|
1893
2396
|
try {
|
|
1894
|
-
const response = asRecord(await client.query(query));
|
|
2397
|
+
const response = asRecord(await client.query(query, input, cursor));
|
|
1895
2398
|
if (response?.ok !== true) {
|
|
1896
2399
|
const error = asRecord(response?.error);
|
|
1897
2400
|
throw new SdkClientError(
|
|
@@ -1905,6 +2408,83 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1905
2408
|
}
|
|
1906
2409
|
}
|
|
1907
2410
|
|
|
2411
|
+
async function readCompleteQ12Snapshot(session: Record<string, unknown>): Promise<{
|
|
2412
|
+
items: unknown[];
|
|
2413
|
+
revision: string | null;
|
|
2414
|
+
complete: boolean;
|
|
2415
|
+
reason: "pagination_malformed" | "query_unavailable" | null;
|
|
2416
|
+
}> {
|
|
2417
|
+
const deadline = Date.now() + 5_000;
|
|
2418
|
+
const items: unknown[] = [];
|
|
2419
|
+
const cursors = new Set<string>();
|
|
2420
|
+
let cursor: string | undefined;
|
|
2421
|
+
let revision: string | null = null;
|
|
2422
|
+
let bytes = 0;
|
|
2423
|
+
let client: SdkClient;
|
|
2424
|
+
try {
|
|
2425
|
+
const endpoint = await resolveSessionEndpoint(session);
|
|
2426
|
+
client = await (services.connectSdk ?? ((url, token) => SdkClient.connect(url, token)))(
|
|
2427
|
+
endpoint.url,
|
|
2428
|
+
endpoint.token,
|
|
2429
|
+
);
|
|
2430
|
+
} catch {
|
|
2431
|
+
return { items: [], revision, complete: false, reason: "query_unavailable" };
|
|
2432
|
+
}
|
|
2433
|
+
try {
|
|
2434
|
+
for (let pageCount = 0; pageCount < 8 && Date.now() <= deadline; pageCount++) {
|
|
2435
|
+
const response = asRecord(await client.query("Q12", {}, cursor));
|
|
2436
|
+
if (response?.ok !== true) return { items: [], revision, complete: false, reason: "query_unavailable" };
|
|
2437
|
+
const page = asRecord(response.page);
|
|
2438
|
+
const pageItems = page?.items;
|
|
2439
|
+
const pageRevision = typeof page?.revision === "string" ? page.revision : null;
|
|
2440
|
+
const complete = page?.complete === true;
|
|
2441
|
+
const preview = page?.preview;
|
|
2442
|
+
const nextCursor = page?.continuationCursor;
|
|
2443
|
+
if (
|
|
2444
|
+
!Array.isArray(pageItems) ||
|
|
2445
|
+
typeof page?.complete !== "boolean" ||
|
|
2446
|
+
!pageRevision ||
|
|
2447
|
+
(complete && (preview === true || (nextCursor !== undefined && nextCursor !== null))) ||
|
|
2448
|
+
(!complete && (preview !== true || typeof nextCursor !== "string" || nextCursor.length === 0))
|
|
2449
|
+
)
|
|
2450
|
+
return { items: [], revision: pageRevision, complete: false, reason: "pagination_malformed" };
|
|
2451
|
+
const validatedCursor = typeof nextCursor === "string" ? nextCursor : undefined;
|
|
2452
|
+
if (revision !== null && revision !== pageRevision)
|
|
2453
|
+
return { items: [], revision: pageRevision, complete: false, reason: "pagination_malformed" };
|
|
2454
|
+
revision = pageRevision;
|
|
2455
|
+
bytes += Buffer.byteLength(JSON.stringify(pageItems));
|
|
2456
|
+
if (items.length + pageItems.length > 64 || bytes > 256 * 1024)
|
|
2457
|
+
return { items: [], revision, complete: false, reason: "pagination_malformed" };
|
|
2458
|
+
items.push(...pageItems);
|
|
2459
|
+
if (complete) {
|
|
2460
|
+
const valid = items.every(item => {
|
|
2461
|
+
if (!item || typeof item !== "object" || Buffer.byteLength(JSON.stringify(item)) > 16 * 1024)
|
|
2462
|
+
return false;
|
|
2463
|
+
const gate = item as WorkflowGateQueryRecord & WorkflowGate;
|
|
2464
|
+
return (
|
|
2465
|
+
gate.tag === "pending" &&
|
|
2466
|
+
typeof gate.gate_id === "string" &&
|
|
2467
|
+
gate.gate_id.length > 0 &&
|
|
2468
|
+
!!decodeAskGateV1(gate)
|
|
2469
|
+
);
|
|
2470
|
+
});
|
|
2471
|
+
return valid
|
|
2472
|
+
? { items, revision, complete: true, reason: null }
|
|
2473
|
+
: { items: [], revision, complete: false, reason: "pagination_malformed" };
|
|
2474
|
+
}
|
|
2475
|
+
if (!validatedCursor || cursors.has(validatedCursor))
|
|
2476
|
+
return { items: [], revision, complete: false, reason: "pagination_malformed" };
|
|
2477
|
+
cursors.add(validatedCursor);
|
|
2478
|
+
cursor = validatedCursor;
|
|
2479
|
+
}
|
|
2480
|
+
return { items: [], revision, complete: false, reason: "pagination_malformed" };
|
|
2481
|
+
} catch {
|
|
2482
|
+
return { items: [], revision, complete: false, reason: "query_unavailable" };
|
|
2483
|
+
} finally {
|
|
2484
|
+
await client.close().catch(() => undefined);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
|
|
1908
2488
|
function sdkQueryPageItem(response: Record<string, unknown>, query: string): unknown {
|
|
1909
2489
|
const items = asRecord(response.page)?.items;
|
|
1910
2490
|
if (!Array.isArray(items) || items.length !== 1)
|
|
@@ -1960,6 +2540,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1960
2540
|
idempotencyKey: string,
|
|
1961
2541
|
canonicalArgs: Record<string, unknown>,
|
|
1962
2542
|
operation: () => Promise<Record<string, unknown>>,
|
|
2543
|
+
recoverInProgress = false,
|
|
1963
2544
|
): Promise<Record<string, unknown>> {
|
|
1964
2545
|
const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
|
|
1965
2546
|
const requestDigest = createHash("sha256")
|
|
@@ -2000,7 +2581,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2000
2581
|
},
|
|
2001
2582
|
};
|
|
2002
2583
|
}
|
|
2003
|
-
if (existing.state === "in_progress")
|
|
2584
|
+
if (existing.state === "in_progress" && !recoverInProgress)
|
|
2004
2585
|
return {
|
|
2005
2586
|
ok: false,
|
|
2006
2587
|
error: {
|
|
@@ -2008,6 +2589,16 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2008
2589
|
message: "prior coordinator mutation outcome is not replayable",
|
|
2009
2590
|
},
|
|
2010
2591
|
};
|
|
2592
|
+
if (existing.state === "in_progress") {
|
|
2593
|
+
const response = boundedPublicResponse(await operation().catch(error => sdkError(error)));
|
|
2594
|
+
await writeCoordinatorIdempotencyFile(file, {
|
|
2595
|
+
...existing,
|
|
2596
|
+
state: "completed",
|
|
2597
|
+
response,
|
|
2598
|
+
completed_at: new Date().toISOString(),
|
|
2599
|
+
} as CoordinatorToolIdempotencyRecord);
|
|
2600
|
+
return response;
|
|
2601
|
+
}
|
|
2011
2602
|
return {
|
|
2012
2603
|
ok: false,
|
|
2013
2604
|
error: {
|
|
@@ -2042,17 +2633,45 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2042
2633
|
input: Record<string, unknown>,
|
|
2043
2634
|
idempotencyKey?: string,
|
|
2044
2635
|
): Promise<unknown> {
|
|
2045
|
-
const
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2636
|
+
const agentDir = services.getAgentDir?.() ?? getAgentDir();
|
|
2637
|
+
try {
|
|
2638
|
+
await (services.ensureBroker ?? ensureBroker)({ agentDir });
|
|
2639
|
+
} catch (error) {
|
|
2640
|
+
throw toCoordinatorBrokerError("ensure", error);
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
let discovery: BrokerDiscovery | null;
|
|
2644
|
+
try {
|
|
2645
|
+
discovery = await (services.readSdkBrokerDiscovery ?? readSdkBrokerDiscovery)(agentDir);
|
|
2646
|
+
} catch (error) {
|
|
2647
|
+
throw toCoordinatorBrokerError("read", error);
|
|
2648
|
+
}
|
|
2649
|
+
if (!discovery) throw new SdkClientError("broker_unavailable", "SDK broker is unavailable after bootstrap.");
|
|
2650
|
+
|
|
2651
|
+
let client: SdkClient;
|
|
2652
|
+
try {
|
|
2653
|
+
client = await (services.connectSdk ?? ((url, token) => SdkClient.connect(url, token)))(
|
|
2654
|
+
discovery.url,
|
|
2655
|
+
discovery.token,
|
|
2656
|
+
);
|
|
2657
|
+
} catch (error) {
|
|
2658
|
+
throw toCoordinatorBrokerError("connect", error);
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
let requestError: SdkClientError | undefined;
|
|
2662
|
+
let result: unknown;
|
|
2663
|
+
try {
|
|
2664
|
+
result = await client.global(operation, input, { ...(idempotencyKey ? { idempotencyKey } : {}) });
|
|
2665
|
+
} catch (error) {
|
|
2666
|
+
requestError = toCoordinatorBrokerError("request", error);
|
|
2667
|
+
}
|
|
2051
2668
|
try {
|
|
2052
|
-
return await client.global(operation, input, { ...(idempotencyKey ? { idempotencyKey } : {}) });
|
|
2053
|
-
} finally {
|
|
2054
2669
|
await client.close();
|
|
2670
|
+
} catch (error) {
|
|
2671
|
+
if (!requestError) throw toCoordinatorBrokerError("close", error);
|
|
2055
2672
|
}
|
|
2673
|
+
if (requestError) throw requestError;
|
|
2674
|
+
return result;
|
|
2056
2675
|
}
|
|
2057
2676
|
|
|
2058
2677
|
function brokerResult(value: unknown): Record<string, unknown> {
|
|
@@ -2211,7 +2830,19 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2211
2830
|
const id = safeExternalId("session", rawId);
|
|
2212
2831
|
return await withSessionTransition(id, async () => {
|
|
2213
2832
|
const session = asRecord(await readJsonFile(sessionFile(id)));
|
|
2214
|
-
if (!session)
|
|
2833
|
+
if (!session) {
|
|
2834
|
+
await ensureQuestionStateReady();
|
|
2835
|
+
const deletion = await withNamespaceRegistry(
|
|
2836
|
+
questionPaths,
|
|
2837
|
+
async registry =>
|
|
2838
|
+
Object.values(registry.deletions).find(
|
|
2839
|
+
entry => entry.session_id === id && entry.phase === "completed",
|
|
2840
|
+
) ?? null,
|
|
2841
|
+
);
|
|
2842
|
+
if (deletion?.safe_response) return deletion.safe_response as { ok: boolean; closed: boolean };
|
|
2843
|
+
return { ok: false, reason: "unknown_session", closed: false };
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2215
2846
|
if (session.ephemeral !== true && opts.force !== true)
|
|
2216
2847
|
return { ok: false, reason: "not_ephemeral", closed: false };
|
|
2217
2848
|
const activeTurn = await readActiveTurn(namespaceDir, id);
|
|
@@ -2227,6 +2858,39 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2227
2858
|
const persistedIncarnation = optionalString(session.endpoint_incarnation);
|
|
2228
2859
|
if (!cwd || !persistedWorkspace || persistedGeneration === null || !persistedIncarnation)
|
|
2229
2860
|
return { ok: false, reason: "endpoint_stale", closed: false };
|
|
2861
|
+
const deletionId = `delete:${id}:${persistedIncarnation}`;
|
|
2862
|
+
const deletionKey = createHash("sha256").update(deletionId).digest("hex");
|
|
2863
|
+
await ensureQuestionStateReady();
|
|
2864
|
+
await ensureQuestionTransaction(id);
|
|
2865
|
+
await withSessionTransaction(questionPaths, id, async transaction => {
|
|
2866
|
+
const now = new Date().toISOString();
|
|
2867
|
+
transaction.requests.operations[deletionId] = {
|
|
2868
|
+
operation_id: deletionId,
|
|
2869
|
+
tool: "gjc_coordinator_stop_session",
|
|
2870
|
+
key_digest: deletionKey,
|
|
2871
|
+
request_digest: deletionKey,
|
|
2872
|
+
local_id: id,
|
|
2873
|
+
phase: "remote_started",
|
|
2874
|
+
intent: { kind: "reap", endpoint_incarnation: persistedIncarnation },
|
|
2875
|
+
created_at: now,
|
|
2876
|
+
updated_at: now,
|
|
2877
|
+
};
|
|
2878
|
+
});
|
|
2879
|
+
await recordDeletionIntent(questionPaths, {
|
|
2880
|
+
deletion_id: deletionId,
|
|
2881
|
+
session_id: id,
|
|
2882
|
+
endpoint_incarnation: persistedIncarnation,
|
|
2883
|
+
operation_id: deletionId,
|
|
2884
|
+
key_digest: deletionKey,
|
|
2885
|
+
request_digest: deletionKey,
|
|
2886
|
+
close_key: deletionId,
|
|
2887
|
+
phase: "intent",
|
|
2888
|
+
cleanup: { wal: false, turns: false, reports: false, session: false, events: false },
|
|
2889
|
+
authority_digest: deletionKey,
|
|
2890
|
+
created_at: new Date().toISOString(),
|
|
2891
|
+
updated_at: new Date().toISOString(),
|
|
2892
|
+
});
|
|
2893
|
+
|
|
2230
2894
|
let workspace = "";
|
|
2231
2895
|
try {
|
|
2232
2896
|
workspace = await canonicalBrokerWorkspace(cwd);
|
|
@@ -2257,6 +2921,8 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2257
2921
|
closed: false,
|
|
2258
2922
|
};
|
|
2259
2923
|
}
|
|
2924
|
+
await advanceDeletion(questionPaths, deletionId, "broker_closed");
|
|
2925
|
+
|
|
2260
2926
|
try {
|
|
2261
2927
|
await exactBrokerSessionAuthority(id, workspace);
|
|
2262
2928
|
return { ok: false, reason: "endpoint_stale", closed: false };
|
|
@@ -2278,6 +2944,19 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2278
2944
|
summary: `Session ${id} closed and reaped${opts.reason ? ` (${opts.reason})` : ""}`,
|
|
2279
2945
|
metadata: { reason: opts.reason ?? null, force: opts.force === true, closed: true },
|
|
2280
2946
|
});
|
|
2947
|
+
await advanceDeletion(
|
|
2948
|
+
questionPaths,
|
|
2949
|
+
deletionId,
|
|
2950
|
+
"completed",
|
|
2951
|
+
{
|
|
2952
|
+
wal: true,
|
|
2953
|
+
turns: true,
|
|
2954
|
+
reports: false,
|
|
2955
|
+
session: true,
|
|
2956
|
+
events: true,
|
|
2957
|
+
},
|
|
2958
|
+
{ ok: true, closed: true },
|
|
2959
|
+
);
|
|
2281
2960
|
return { ok: true, closed: true };
|
|
2282
2961
|
});
|
|
2283
2962
|
}
|
|
@@ -2311,16 +2990,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2311
2990
|
},
|
|
2312
2991
|
{ idleTtlMs: config.sessionIdleTtlMs, sweepIntervalMs: config.sessionSweepIntervalMs },
|
|
2313
2992
|
);
|
|
2314
|
-
async function listQuestions(args: Record<string, unknown>): Promise<
|
|
2315
|
-
const sessionId =
|
|
2316
|
-
const
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2993
|
+
async function listQuestions(args: Record<string, unknown>): Promise<ListQuestionsSuccessV1> {
|
|
2994
|
+
const sessionId = safeExternalId("session", args.session_id);
|
|
2995
|
+
const reconciled = await reconcileQuestions(sessionId);
|
|
2996
|
+
const status = typeof args.status === "string" && args.status.length > 0 ? args.status : "pending";
|
|
2997
|
+
const turnId = args.turn_id == null ? null : safeTurnId(args.turn_id);
|
|
2998
|
+
return {
|
|
2999
|
+
...reconciled,
|
|
3000
|
+
questions: reconciled.questions
|
|
3001
|
+
.filter(question => question.status === status || (status === "open" && question.status === "pending"))
|
|
3002
|
+
.filter(question => turnId === null || question.turn_id === turnId),
|
|
3003
|
+
};
|
|
2324
3004
|
}
|
|
2325
3005
|
|
|
2326
3006
|
async function validateEvidencePaths(value: unknown): Promise<Array<{ path: string }>> {
|
|
@@ -2340,6 +3020,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2340
3020
|
if (sessionId != null && turn.session_id !== safeExternalId("session", sessionId)) {
|
|
2341
3021
|
return { ok: false, reason: "turn_session_mismatch" };
|
|
2342
3022
|
}
|
|
3023
|
+
await reconcileQuestions(turn.session_id);
|
|
2343
3024
|
const session = asRecord(await readJsonFile(sessionFile(turn.session_id)));
|
|
2344
3025
|
let resolvedTurn = turn;
|
|
2345
3026
|
let advisoryStatus: Record<string, unknown> = {
|
|
@@ -2389,10 +3070,20 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2389
3070
|
sessionState.current_turn_id == null)) &&
|
|
2390
3071
|
(sessionState.state === "completed" || sessionState.state === "errored")
|
|
2391
3072
|
) {
|
|
2392
|
-
resolvedTurn = await markTurnTerminalFromSessionState(
|
|
3073
|
+
resolvedTurn = await markTurnTerminalFromSessionState(resolvedTurn, sessionState);
|
|
3074
|
+
await projectTerminalTransition(resolvedTurn, {
|
|
3075
|
+
desiredState: sessionState.state,
|
|
3076
|
+
reason: sessionState.reason ? "terminal_uncertain" : null,
|
|
3077
|
+
live: sessionState.live,
|
|
3078
|
+
});
|
|
2393
3079
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
2394
3080
|
} else if (!session && ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
2395
|
-
resolvedTurn = await markTurnFailedForUnavailableSession(
|
|
3081
|
+
resolvedTurn = await markTurnFailedForUnavailableSession(resolvedTurn, "session_record_missing");
|
|
3082
|
+
await projectTerminalTransition(resolvedTurn, {
|
|
3083
|
+
desiredState: "stale",
|
|
3084
|
+
reason: "session_unavailable",
|
|
3085
|
+
live: false,
|
|
3086
|
+
});
|
|
2396
3087
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
2397
3088
|
}
|
|
2398
3089
|
if (ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
@@ -2403,6 +3094,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2403
3094
|
promptAckTimeoutMs,
|
|
2404
3095
|
);
|
|
2405
3096
|
if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
3097
|
+
await projectTerminalTransition(resolvedTurn, {
|
|
3098
|
+
desiredState: "stale",
|
|
3099
|
+
reason: "terminal_uncertain",
|
|
3100
|
+
live: resolvedTurn.liveness.live,
|
|
3101
|
+
});
|
|
2406
3102
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
2407
3103
|
}
|
|
2408
3104
|
}
|
|
@@ -2458,12 +3154,302 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2458
3154
|
return payload;
|
|
2459
3155
|
}
|
|
2460
3156
|
|
|
3157
|
+
async function claimCanonicalPrompt(
|
|
3158
|
+
sessionId: string,
|
|
3159
|
+
prompt: string,
|
|
3160
|
+
operation: "turn.prompt" | "turn.follow_up" | "turn.abort_and_prompt",
|
|
3161
|
+
idempotencyKey: string,
|
|
3162
|
+
): Promise<string> {
|
|
3163
|
+
await ensureQuestionTransaction(sessionId);
|
|
3164
|
+
const keyDigest = createHash("sha256").update(`${idempotencyKey}\0${operation}`).digest("hex");
|
|
3165
|
+
const requestDigest = createHash("sha256").update(`${operation}\0${prompt}`).digest("hex");
|
|
3166
|
+
await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
|
|
3167
|
+
const existing = transaction.requests.prompts[keyDigest];
|
|
3168
|
+
if (existing) {
|
|
3169
|
+
if (existing.request_digest !== requestDigest) throw new Error("idempotency_conflict");
|
|
3170
|
+
if (existing.phase === "remote_started" || existing.phase === "uncertain")
|
|
3171
|
+
throw new Error("terminal_uncertain");
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
const now = new Date().toISOString();
|
|
3175
|
+
transaction.requests.prompts[keyDigest] = {
|
|
3176
|
+
request_id: `prompt:${keyDigest}`,
|
|
3177
|
+
key_digest: keyDigest,
|
|
3178
|
+
request_digest: requestDigest,
|
|
3179
|
+
operation,
|
|
3180
|
+
canonical_prompt: { text: prompt },
|
|
3181
|
+
sdk_idempotency_key: idempotencyKey,
|
|
3182
|
+
phase: "remote_started",
|
|
3183
|
+
created_at: now,
|
|
3184
|
+
updated_at: now,
|
|
3185
|
+
};
|
|
3186
|
+
});
|
|
3187
|
+
return keyDigest;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
/** Commits terminal fencing, question revocation, report, and promotion together before legacy projection. */
|
|
3191
|
+
async function commitTerminalTransition(
|
|
3192
|
+
turn: TurnRecord,
|
|
3193
|
+
input: {
|
|
3194
|
+
desiredState: CoordinatorSessionStateValue;
|
|
3195
|
+
reason: PublicReason | null;
|
|
3196
|
+
report?: CanonicalReportSnapshotV1;
|
|
3197
|
+
promoteQueuedTurn?: boolean;
|
|
3198
|
+
},
|
|
3199
|
+
): Promise<{ promotedTurnId: string | null; desiredState: CoordinatorSessionStateValue }> {
|
|
3200
|
+
await ensureQuestionTransaction(turn.session_id);
|
|
3201
|
+
return await withAdmittedSessionTransaction(questionPaths, turn.session_id, async transaction => {
|
|
3202
|
+
const terminalEpoch = transaction.revision + 1;
|
|
3203
|
+
transaction.canonical.turns[turn.turn_id] = {
|
|
3204
|
+
schema_version: 1,
|
|
3205
|
+
turn_id: turn.turn_id,
|
|
3206
|
+
session_id: turn.session_id,
|
|
3207
|
+
namespace_id: config.namespace.identity,
|
|
3208
|
+
status: turn.status,
|
|
3209
|
+
prompt: turn.prompt,
|
|
3210
|
+
delivery: { ...turn.delivery },
|
|
3211
|
+
question_ids: Object.values(transaction.canonical.questions)
|
|
3212
|
+
.filter(question => question.turn_id === turn.turn_id)
|
|
3213
|
+
.map(question => question.question_id),
|
|
3214
|
+
final_response: { ...turn.final_response },
|
|
3215
|
+
evidence: turn.evidence,
|
|
3216
|
+
error: turn.error ? { ...turn.error } : null,
|
|
3217
|
+
liveness: { ...turn.liveness },
|
|
3218
|
+
created_at: turn.created_at,
|
|
3219
|
+
updated_at: turn.updated_at,
|
|
3220
|
+
started_at: turn.started_at,
|
|
3221
|
+
completed_at: turn.completed_at,
|
|
3222
|
+
terminal_fence: {
|
|
3223
|
+
epoch: terminalEpoch,
|
|
3224
|
+
status: turn.status,
|
|
3225
|
+
reason: input.reason,
|
|
3226
|
+
at: turn.completed_at ?? turn.updated_at,
|
|
3227
|
+
},
|
|
3228
|
+
};
|
|
3229
|
+
for (const question of Object.values(transaction.canonical.questions)) {
|
|
3230
|
+
if (question.turn_id !== turn.turn_id || question.status === "answered") continue;
|
|
3231
|
+
question.status = "stale";
|
|
3232
|
+
question.claim_fence_epoch = terminalEpoch;
|
|
3233
|
+
question.updated_at = turn.updated_at;
|
|
3234
|
+
question.history.push({
|
|
3235
|
+
at: turn.updated_at,
|
|
3236
|
+
status: "stale",
|
|
3237
|
+
reason: input.reason ?? "terminal_uncertain",
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
if (input.report) transaction.canonical.reports[input.report.report_id] = input.report;
|
|
3241
|
+
const next =
|
|
3242
|
+
input.promoteQueuedTurn === false
|
|
3243
|
+
? null
|
|
3244
|
+
: (Object.values(transaction.canonical.turns)
|
|
3245
|
+
.filter(candidate => candidate.status === "queued")
|
|
3246
|
+
.sort((left, right) => left.created_at.localeCompare(right.created_at))[0] ?? null);
|
|
3247
|
+
if (next) {
|
|
3248
|
+
const timestamp = new Date().toISOString();
|
|
3249
|
+
next.status = "active";
|
|
3250
|
+
next.started_at = timestamp;
|
|
3251
|
+
next.updated_at = timestamp;
|
|
3252
|
+
}
|
|
3253
|
+
transaction.canonical.queue.ordered_turn_ids = Object.values(transaction.canonical.turns)
|
|
3254
|
+
.filter(candidate => candidate.status === "queued")
|
|
3255
|
+
.map(candidate => candidate.turn_id);
|
|
3256
|
+
transaction.canonical.queue.active_turn_id = next?.turn_id ?? null;
|
|
3257
|
+
transaction.canonical.queue.selected_promotion = next
|
|
3258
|
+
? { from_turn_id: turn.turn_id, to_turn_id: next.turn_id, revision: terminalEpoch }
|
|
3259
|
+
: null;
|
|
3260
|
+
transaction.canonical.desired_session_state = next ? "running" : input.desiredState;
|
|
3261
|
+
const eventKind = turnEventKind(turn.status) ?? "turn.terminal";
|
|
3262
|
+
const eventId = deterministicOutboxId(turn.session_id, terminalEpoch, eventKind, "turn", turn.turn_id);
|
|
3263
|
+
transaction.outbox[eventId] ??= {
|
|
3264
|
+
id: eventId,
|
|
3265
|
+
transaction_revision: terminalEpoch,
|
|
3266
|
+
kind: eventKind,
|
|
3267
|
+
entity: "turn",
|
|
3268
|
+
entity_id: turn.turn_id,
|
|
3269
|
+
payload: {
|
|
3270
|
+
session_id: turn.session_id,
|
|
3271
|
+
turn_id: turn.turn_id,
|
|
3272
|
+
status: turn.status,
|
|
3273
|
+
created_at: turn.updated_at,
|
|
3274
|
+
},
|
|
3275
|
+
emitted: false,
|
|
3276
|
+
};
|
|
3277
|
+
if (input.report) {
|
|
3278
|
+
const reportEventId = deterministicOutboxId(
|
|
3279
|
+
turn.session_id,
|
|
3280
|
+
terminalEpoch,
|
|
3281
|
+
"report.written",
|
|
3282
|
+
"report",
|
|
3283
|
+
input.report.report_id,
|
|
3284
|
+
);
|
|
3285
|
+
transaction.outbox[reportEventId] ??= {
|
|
3286
|
+
id: reportEventId,
|
|
3287
|
+
transaction_revision: terminalEpoch,
|
|
3288
|
+
kind: "report.written",
|
|
3289
|
+
entity: "report",
|
|
3290
|
+
entity_id: input.report.report_id,
|
|
3291
|
+
payload: {
|
|
3292
|
+
session_id: turn.session_id,
|
|
3293
|
+
turn_id: turn.turn_id,
|
|
3294
|
+
report_id: input.report.report_id,
|
|
3295
|
+
status: input.report.status,
|
|
3296
|
+
created_at: input.report.created_at,
|
|
3297
|
+
},
|
|
3298
|
+
emitted: false,
|
|
3299
|
+
};
|
|
3300
|
+
}
|
|
3301
|
+
return { promotedTurnId: next?.turn_id ?? null, desiredState: next ? "running" : input.desiredState };
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
function turnFromCanonical(turn: CoordinatorSessionTransactionV1["canonical"]["turns"][string]): TurnRecord {
|
|
3306
|
+
return {
|
|
3307
|
+
schema_version: 1,
|
|
3308
|
+
turn_id: turn.turn_id,
|
|
3309
|
+
session_id: turn.session_id,
|
|
3310
|
+
namespace: config.namespace,
|
|
3311
|
+
status: turn.status as TurnStatus,
|
|
3312
|
+
prompt: turn.prompt as TurnRecord["prompt"],
|
|
3313
|
+
delivery: turn.delivery as TurnRecord["delivery"],
|
|
3314
|
+
question_ids: [...turn.question_ids],
|
|
3315
|
+
final_response: turn.final_response as TurnRecord["final_response"],
|
|
3316
|
+
evidence: [...turn.evidence],
|
|
3317
|
+
error: turn.error as TurnRecord["error"],
|
|
3318
|
+
liveness: turn.liveness as TurnRecord["liveness"],
|
|
3319
|
+
created_at: turn.created_at,
|
|
3320
|
+
updated_at: turn.updated_at,
|
|
3321
|
+
started_at: turn.started_at,
|
|
3322
|
+
completed_at: turn.completed_at,
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
/** Rebuild every legacy projection from the committed canonical snapshot. */
|
|
3327
|
+
async function repairCanonicalProjections(sessionId: string): Promise<void> {
|
|
3328
|
+
await repairProjections(questionPaths, sessionId, async canonical => {
|
|
3329
|
+
await writeJsonFile(sessionFile(sessionId), sessionFromCreationSnapshot(canonical.session));
|
|
3330
|
+
for (const turn of Object.values(canonical.turns))
|
|
3331
|
+
await writeTurnRecord(namespaceDir, turnFromCanonical(turn));
|
|
3332
|
+
for (const report of Object.values(canonical.reports))
|
|
3333
|
+
await writeJsonFile(path.join(namespaceDir, "reports", `${report.report_id}.json`), report);
|
|
3334
|
+
const canonicalTurnIds = new Set(Object.keys(canonical.turns));
|
|
3335
|
+
for (const entry of await fs.readdir(turnsDir(namespaceDir)).catch(() => [])) {
|
|
3336
|
+
if (!entry.endsWith(".json")) continue;
|
|
3337
|
+
const turnId = entry.slice(0, -5);
|
|
3338
|
+
if (canonicalTurnIds.has(turnId)) continue;
|
|
3339
|
+
const projected = asRecord(await readJsonFile(path.join(turnsDir(namespaceDir), entry)));
|
|
3340
|
+
if (projected?.session_id === sessionId)
|
|
3341
|
+
await fs.rm(path.join(turnsDir(namespaceDir), entry), { force: true });
|
|
3342
|
+
}
|
|
3343
|
+
const reportsDirectory = path.join(namespaceDir, "reports");
|
|
3344
|
+
const canonicalReportIds = new Set(Object.keys(canonical.reports));
|
|
3345
|
+
for (const entry of await fs.readdir(reportsDirectory).catch(() => [])) {
|
|
3346
|
+
if (!entry.endsWith(".json")) continue;
|
|
3347
|
+
const reportId = entry.slice(0, -5);
|
|
3348
|
+
if (canonicalReportIds.has(reportId)) continue;
|
|
3349
|
+
const projected = asRecord(await readJsonFile(path.join(reportsDirectory, entry)));
|
|
3350
|
+
if (projected?.session_id === sessionId) await fs.rm(path.join(reportsDirectory, entry), { force: true });
|
|
3351
|
+
}
|
|
3352
|
+
const activeId = canonical.queue.selected_promotion?.to_turn_id ?? canonical.queue.active_turn_id;
|
|
3353
|
+
const active = activeId ? canonical.turns[activeId] : null;
|
|
3354
|
+
if (active) await writeActiveTurn(namespaceDir, turnFromCanonical(active));
|
|
3355
|
+
else {
|
|
3356
|
+
const prior = await readActiveTurn(namespaceDir, sessionId);
|
|
3357
|
+
if (prior) await clearActiveTurn(namespaceDir, prior);
|
|
3358
|
+
}
|
|
3359
|
+
await writeSessionState(namespaceDir, sessionId, canonical.desired_session_state, {
|
|
3360
|
+
currentTurnId: active?.turn_id ?? null,
|
|
3361
|
+
lastTurnId: canonical.queue.selected_promotion?.from_turn_id ?? null,
|
|
3362
|
+
live: null,
|
|
3363
|
+
reason: null,
|
|
3364
|
+
overwrite: true,
|
|
3365
|
+
});
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
async function projectTerminalTransition(
|
|
3370
|
+
turn: TurnRecord,
|
|
3371
|
+
|
|
3372
|
+
input: Parameters<typeof commitTerminalTransition>[1] & { live?: boolean | null },
|
|
3373
|
+
): Promise<void> {
|
|
3374
|
+
await commitTerminalTransition(turn, input);
|
|
3375
|
+
await repairCanonicalProjections(turn.session_id);
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
async function commitCanonicalTurn(
|
|
3379
|
+
sessionId: string,
|
|
3380
|
+
turn: TurnRecord,
|
|
3381
|
+
promptKey: string | null = null,
|
|
3382
|
+
): Promise<void> {
|
|
3383
|
+
await ensureQuestionTransaction(sessionId);
|
|
3384
|
+
await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
|
|
3385
|
+
const terminal = TERMINAL_TURN_STATUSES.has(turn.status);
|
|
3386
|
+
transaction.canonical.turns[turn.turn_id] = {
|
|
3387
|
+
schema_version: 1,
|
|
3388
|
+
turn_id: turn.turn_id,
|
|
3389
|
+
session_id: sessionId,
|
|
3390
|
+
namespace_id: config.namespace.identity,
|
|
3391
|
+
status: turn.status,
|
|
3392
|
+
prompt: turn.prompt,
|
|
3393
|
+
delivery: { ...turn.delivery },
|
|
3394
|
+
question_ids: Object.values(transaction.canonical.questions)
|
|
3395
|
+
.filter(question => question.turn_id === turn.turn_id)
|
|
3396
|
+
.map(question => question.question_id),
|
|
3397
|
+
final_response: { ...turn.final_response },
|
|
3398
|
+
evidence: turn.evidence,
|
|
3399
|
+
error: turn.error ? { ...turn.error } : null,
|
|
3400
|
+
liveness: { ...turn.liveness },
|
|
3401
|
+
created_at: turn.created_at,
|
|
3402
|
+
updated_at: turn.updated_at,
|
|
3403
|
+
started_at: turn.started_at,
|
|
3404
|
+
completed_at: turn.completed_at,
|
|
3405
|
+
terminal_fence: terminal
|
|
3406
|
+
? {
|
|
3407
|
+
epoch: transaction.revision + 1,
|
|
3408
|
+
status: turn.status,
|
|
3409
|
+
reason: null,
|
|
3410
|
+
at: turn.completed_at ?? turn.updated_at,
|
|
3411
|
+
}
|
|
3412
|
+
: null,
|
|
3413
|
+
};
|
|
3414
|
+
transaction.canonical.queue.ordered_turn_ids = Object.values(transaction.canonical.turns)
|
|
3415
|
+
.filter(candidate => candidate.status === "queued")
|
|
3416
|
+
.map(candidate => candidate.turn_id);
|
|
3417
|
+
transaction.canonical.queue.active_turn_id = terminal
|
|
3418
|
+
? null
|
|
3419
|
+
: turn.delivery.queued
|
|
3420
|
+
? transaction.canonical.queue.active_turn_id
|
|
3421
|
+
: turn.turn_id;
|
|
3422
|
+
transaction.recovery.prompt_watermark_at = turn.updated_at;
|
|
3423
|
+
if (terminal)
|
|
3424
|
+
for (const question of Object.values(transaction.canonical.questions)) {
|
|
3425
|
+
if (question.turn_id !== turn.turn_id || question.status === "answered") continue;
|
|
3426
|
+
question.status = "stale";
|
|
3427
|
+
question.claim_fence_epoch = transaction.revision + 1;
|
|
3428
|
+
question.updated_at = turn.updated_at;
|
|
3429
|
+
question.history.push({ at: turn.updated_at, status: "stale", reason: "terminal_uncertain" });
|
|
3430
|
+
}
|
|
3431
|
+
if (promptKey) {
|
|
3432
|
+
const request = transaction.requests.prompts[promptKey];
|
|
3433
|
+
if (!request) throw new Error("state_corrupt");
|
|
3434
|
+
request.phase = "completed";
|
|
3435
|
+
request.coordinator_turn_id = turn.turn_id;
|
|
3436
|
+
request.runtime_receipt = {
|
|
3437
|
+
accepted: true,
|
|
3438
|
+
command_id: String(turn.delivery.runtime_command_id),
|
|
3439
|
+
turn_id: String(turn.delivery.runtime_turn_id),
|
|
3440
|
+
};
|
|
3441
|
+
request.updated_at = turn.updated_at;
|
|
3442
|
+
}
|
|
3443
|
+
});
|
|
3444
|
+
}
|
|
3445
|
+
|
|
2461
3446
|
async function recordAcceptedPrompt(
|
|
2462
3447
|
sessionId: string,
|
|
2463
3448
|
prompt: string,
|
|
2464
3449
|
operation: "turn.prompt" | "turn.follow_up" | "turn.abort_and_prompt",
|
|
2465
3450
|
previousActiveTurn: TurnRecord | null,
|
|
2466
3451
|
acknowledgement: RuntimePromptAcknowledgement,
|
|
3452
|
+
promptKey: string | null = null,
|
|
2467
3453
|
): Promise<TurnRecord> {
|
|
2468
3454
|
const timestamp = new Date().toISOString();
|
|
2469
3455
|
if (operation === "turn.abort_and_prompt" && previousActiveTurn) {
|
|
@@ -2473,8 +3459,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2473
3459
|
updated_at: timestamp,
|
|
2474
3460
|
completed_at: timestamp,
|
|
2475
3461
|
};
|
|
2476
|
-
await
|
|
2477
|
-
|
|
3462
|
+
await projectTerminalTransition(superseded, {
|
|
3463
|
+
desiredState: "running",
|
|
3464
|
+
reason: null,
|
|
3465
|
+
promoteQueuedTurn: false,
|
|
3466
|
+
});
|
|
2478
3467
|
}
|
|
2479
3468
|
const queued = operation === "turn.follow_up";
|
|
2480
3469
|
const turn = makeTurnRecord(config, sessionId, prompt, queued ? "queued" : "active");
|
|
@@ -2488,6 +3477,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2488
3477
|
state: "acknowledged",
|
|
2489
3478
|
attempts: [{ delivered: true, channel: "runtime_ack", created_at: timestamp, reason: null }],
|
|
2490
3479
|
};
|
|
3480
|
+
await commitCanonicalTurn(sessionId, turn, promptKey);
|
|
2491
3481
|
await writeTurnRecord(namespaceDir, turn);
|
|
2492
3482
|
if (!queued) {
|
|
2493
3483
|
await writeActiveTurn(namespaceDir, turn);
|
|
@@ -2500,48 +3490,50 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2500
3490
|
return turn;
|
|
2501
3491
|
}
|
|
2502
3492
|
|
|
2503
|
-
async function promoteNextQueuedTurn(sessionId: string): Promise<TurnRecord | null> {
|
|
2504
|
-
const queuedTurns = (await listJsonFiles(turnsDir(namespaceDir)))
|
|
2505
|
-
.map(turn => asRecord(turn) as TurnRecord | null)
|
|
2506
|
-
.filter(
|
|
2507
|
-
(turn): turn is TurnRecord => turn !== null && turn.session_id === sessionId && turn.status === "queued",
|
|
2508
|
-
)
|
|
2509
|
-
.sort((left, right) => left.created_at.localeCompare(right.created_at));
|
|
2510
|
-
const next = queuedTurns[0];
|
|
2511
|
-
if (!next) return null;
|
|
2512
|
-
const timestamp = new Date().toISOString();
|
|
2513
|
-
const active: TurnRecord = { ...next, status: "active", started_at: timestamp, updated_at: timestamp };
|
|
2514
|
-
await writeTurnRecord(namespaceDir, active);
|
|
2515
|
-
await writeActiveTurn(namespaceDir, active);
|
|
2516
|
-
await writeSessionState(namespaceDir, sessionId, "running", {
|
|
2517
|
-
currentTurnId: active.turn_id,
|
|
2518
|
-
live: null,
|
|
2519
|
-
reason: null,
|
|
2520
|
-
});
|
|
2521
|
-
return active;
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
3493
|
async function reconcileActiveTurnAcknowledgements(): Promise<void> {
|
|
2525
3494
|
const turns = (await listJsonFiles(turnsDir(namespaceDir)))
|
|
2526
3495
|
.map(turn => asRecord(turn) as TurnRecord | null)
|
|
2527
3496
|
.filter((turn): turn is TurnRecord => turn !== null && ACTIVE_TURN_STATUSES.has(turn.status));
|
|
2528
3497
|
for (const turn of turns) {
|
|
2529
3498
|
let sessionState = await readSessionState(namespaceDir, turn.session_id);
|
|
2530
|
-
|
|
3499
|
+
let resolvedTurn = await reconcileRuntimeAcknowledgement(
|
|
2531
3500
|
namespaceDir,
|
|
2532
3501
|
turn,
|
|
2533
3502
|
sessionState,
|
|
2534
3503
|
promptAckTimeoutMs,
|
|
2535
3504
|
{ failOnTimeout: false },
|
|
2536
3505
|
);
|
|
2537
|
-
if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status))
|
|
3506
|
+
if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
3507
|
+
await projectTerminalTransition(resolvedTurn, {
|
|
3508
|
+
desiredState: "stale",
|
|
3509
|
+
reason: "terminal_uncertain",
|
|
3510
|
+
live: resolvedTurn.liveness.live,
|
|
3511
|
+
});
|
|
3512
|
+
continue;
|
|
3513
|
+
}
|
|
2538
3514
|
if (resolvedTurn !== turn) sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
2539
3515
|
const session = asRecord(await readJsonFile(sessionFile(resolvedTurn.session_id)));
|
|
2540
3516
|
if (!session) {
|
|
2541
|
-
await markTurnFailedForUnavailableSession(
|
|
3517
|
+
resolvedTurn = await markTurnFailedForUnavailableSession(resolvedTurn, "session_record_missing");
|
|
3518
|
+
await projectTerminalTransition(resolvedTurn, {
|
|
3519
|
+
desiredState: "stale",
|
|
3520
|
+
reason: "session_unavailable",
|
|
3521
|
+
live: false,
|
|
3522
|
+
});
|
|
2542
3523
|
continue;
|
|
2543
3524
|
}
|
|
2544
|
-
await reconcileRuntimeAcknowledgement(
|
|
3525
|
+
resolvedTurn = await reconcileRuntimeAcknowledgement(
|
|
3526
|
+
namespaceDir,
|
|
3527
|
+
resolvedTurn,
|
|
3528
|
+
sessionState,
|
|
3529
|
+
promptAckTimeoutMs,
|
|
3530
|
+
);
|
|
3531
|
+
if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status))
|
|
3532
|
+
await projectTerminalTransition(resolvedTurn, {
|
|
3533
|
+
desiredState: "stale",
|
|
3534
|
+
reason: "terminal_uncertain",
|
|
3535
|
+
live: resolvedTurn.liveness.live,
|
|
3536
|
+
});
|
|
2545
3537
|
}
|
|
2546
3538
|
}
|
|
2547
3539
|
|
|
@@ -2556,20 +3548,24 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2556
3548
|
const cwd = await canonicalBrokerWorkspace(await assertCoordinatorWorkdir(config, args.cwd));
|
|
2557
3549
|
const tmuxSession = optionalString(args.tmux_session) ? safeTmuxSessionName(args.tmux_session) : undefined;
|
|
2558
3550
|
const tmuxTarget = optionalString(args.tmux_target) ? safeTmuxTarget(args.tmux_target) : undefined;
|
|
3551
|
+
const canonicalArgs = {
|
|
3552
|
+
session_id: sessionId,
|
|
3553
|
+
cwd,
|
|
3554
|
+
...(tmuxSession ? { tmux_session: tmuxSession } : {}),
|
|
3555
|
+
...(tmuxTarget ? { tmux_target: tmuxTarget } : {}),
|
|
3556
|
+
visible: args.visible !== false,
|
|
3557
|
+
source: optionalString(args.source) ?? "register_session",
|
|
3558
|
+
model: optionalString(args.model),
|
|
3559
|
+
allow_mutation: true,
|
|
3560
|
+
};
|
|
2559
3561
|
return await withToolIdempotency(
|
|
2560
3562
|
name,
|
|
2561
3563
|
idempotencyKey,
|
|
2562
|
-
|
|
2563
|
-
session_id: sessionId,
|
|
2564
|
-
cwd,
|
|
2565
|
-
...(tmuxSession ? { tmux_session: tmuxSession } : {}),
|
|
2566
|
-
...(tmuxTarget ? { tmux_target: tmuxTarget } : {}),
|
|
2567
|
-
visible: args.visible !== false,
|
|
2568
|
-
source: optionalString(args.source) ?? "register_session",
|
|
2569
|
-
model: optionalString(args.model),
|
|
2570
|
-
allow_mutation: true,
|
|
2571
|
-
},
|
|
3564
|
+
canonicalArgs,
|
|
2572
3565
|
async () => {
|
|
3566
|
+
const creation = await claimProductionCreation(name, idempotencyKey, canonicalArgs);
|
|
3567
|
+
if (creation.request.phase === "completed" && creation.request.safe_response)
|
|
3568
|
+
return creation.request.safe_response;
|
|
2573
3569
|
const binding = await exactBrokerSessionBinding(sessionId, cwd, idempotencyKey);
|
|
2574
3570
|
const session = normalizeSession({
|
|
2575
3571
|
session_id: sessionId,
|
|
@@ -2583,6 +3579,14 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2583
3579
|
endpoint_generation: binding.endpointGeneration,
|
|
2584
3580
|
endpoint_incarnation: binding.endpointIncarnation,
|
|
2585
3581
|
});
|
|
3582
|
+
const intent: CanonicalCreateIntentV1 = {
|
|
3583
|
+
kind: "register",
|
|
3584
|
+
session: canonicalCreationSnapshot(session),
|
|
3585
|
+
initial_state: "ready_for_input",
|
|
3586
|
+
initial_events: [],
|
|
3587
|
+
};
|
|
3588
|
+
await bindCreationRequest(questionPaths, creation.keyDigest, intent);
|
|
3589
|
+
await commitCreationWal(questionPaths, creation.keyDigest, intent);
|
|
2586
3590
|
await writeJsonFile(sessionFile(sessionId), session);
|
|
2587
3591
|
const sessionState = await writeSessionState(namespaceDir, sessionId, "ready_for_input", {
|
|
2588
3592
|
live: null,
|
|
@@ -2598,13 +3602,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2598
3602
|
visible: args.visible !== false,
|
|
2599
3603
|
},
|
|
2600
3604
|
});
|
|
2601
|
-
|
|
3605
|
+
const response = {
|
|
2602
3606
|
ok: true,
|
|
2603
3607
|
session: publicCoordinatorSession(session),
|
|
2604
3608
|
session_state: publicCoordinatorSessionState(sessionState),
|
|
2605
3609
|
registered: true,
|
|
2606
3610
|
};
|
|
3611
|
+
await advanceCreationReceipt(questionPaths, creation.keyDigest, "projected", response);
|
|
3612
|
+
await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
|
|
3613
|
+
return response;
|
|
2607
3614
|
},
|
|
3615
|
+
true,
|
|
2608
3616
|
);
|
|
2609
3617
|
}
|
|
2610
3618
|
if (name === "gjc_coordinator_read_status") {
|
|
@@ -2624,7 +3632,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2624
3632
|
);
|
|
2625
3633
|
return {
|
|
2626
3634
|
ok: true,
|
|
2627
|
-
session:
|
|
3635
|
+
session: publicCoordinatorStatusSession(session),
|
|
2628
3636
|
status: brokerLiveness(indexedSession ?? null),
|
|
2629
3637
|
session_state: publicCoordinatorSessionState(
|
|
2630
3638
|
await readSessionState(namespaceDir, canonicalSessionId),
|
|
@@ -2668,7 +3676,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2668
3676
|
return sdkError(error);
|
|
2669
3677
|
}
|
|
2670
3678
|
}
|
|
2671
|
-
if (name === "gjc_coordinator_list_questions") return
|
|
3679
|
+
if (name === "gjc_coordinator_list_questions") return await listQuestions(args);
|
|
2672
3680
|
if (name === "gjc_coordinator_list_artifacts") return { ok: true, roots: config.allowedRoots };
|
|
2673
3681
|
if (name === "gjc_coordinator_read_artifact")
|
|
2674
3682
|
return await readCoordinatorArtifact(config, { path: args.path });
|
|
@@ -2677,14 +3685,18 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2677
3685
|
const brokerSessions = await listSessions();
|
|
2678
3686
|
const sessionStates = jsonRecords(await listJsonFiles(path.join(namespaceDir, "session-states")));
|
|
2679
3687
|
const turns = jsonRecords(await listJsonFiles(turnsDir(namespaceDir)));
|
|
2680
|
-
const questions =
|
|
3688
|
+
const questions =
|
|
3689
|
+
args.session_id == null
|
|
3690
|
+
? []
|
|
3691
|
+
: (await listQuestions(args)).questions.map(
|
|
3692
|
+
({ answer_binding: _answerBinding, ...question }) => question,
|
|
3693
|
+
);
|
|
2681
3694
|
const reports = jsonRecords(await listJsonFiles(path.join(namespaceDir, "reports")));
|
|
2682
3695
|
const events = await readCoordinatorEvents(namespaceDir);
|
|
2683
|
-
return {
|
|
3696
|
+
return capabilityFreeStatusValue({
|
|
2684
3697
|
ok: true,
|
|
2685
3698
|
schema_version: 1,
|
|
2686
3699
|
namespace: config.namespace,
|
|
2687
|
-
state_root: namespaceDir,
|
|
2688
3700
|
transport: { mcp: "polling", push_subscriptions: false },
|
|
2689
3701
|
summary: {
|
|
2690
3702
|
sessions: brokerSessions.length,
|
|
@@ -2693,7 +3705,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2693
3705
|
active_turns: turns.filter(turn => ACTIVE_TURN_STATUSES.has(turn.status as TurnStatus)).length,
|
|
2694
3706
|
queued_turns: turns.filter(turn => turn.status === "queued").length,
|
|
2695
3707
|
terminal_turns: turns.filter(turn => TERMINAL_TURN_STATUSES.has(turn.status as TurnStatus)).length,
|
|
2696
|
-
open_questions: questions.filter(question => question.status === "
|
|
3708
|
+
open_questions: questions.filter(question => question.status === "pending").length,
|
|
2697
3709
|
reports: reports.length,
|
|
2698
3710
|
},
|
|
2699
3711
|
sessions: brokerSessions.map(publicBrokerSession),
|
|
@@ -2712,10 +3724,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2712
3724
|
events: buildCanonicalCoordinatorEvents({ sessionStates, turns, questions, reports }),
|
|
2713
3725
|
latest_event_seq: await readLatestEventSeq(namespaceDir),
|
|
2714
3726
|
recent_events: eventSummaries(events.slice(-10)),
|
|
2715
|
-
}
|
|
3727
|
+
}) as Record<string, unknown>;
|
|
2716
3728
|
}
|
|
2717
3729
|
if (name === "gjc_coordinator_watch_events") {
|
|
2718
3730
|
await reconcileActiveTurnAcknowledgements();
|
|
3731
|
+
if (args.session_id != null) await reconcileQuestions(safeExternalId("session", args.session_id));
|
|
2719
3732
|
const limit = boundedEventLimit(args.limit);
|
|
2720
3733
|
const timeoutMs = boundedEventWatchTimeoutMs(args.timeout_ms);
|
|
2721
3734
|
let events = await readCoordinatorEvents(namespaceDir);
|
|
@@ -2762,29 +3775,31 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2762
3775
|
model: typeof args.model === "string" ? args.model : null,
|
|
2763
3776
|
});
|
|
2764
3777
|
const reusedSessionId = args.session_id == null ? undefined : safeExternalId("session", args.session_id);
|
|
3778
|
+
const canonicalArgs = {
|
|
3779
|
+
cwd: canonicalCwd,
|
|
3780
|
+
task,
|
|
3781
|
+
...(reusedSessionId ? { session_id: reusedSessionId } : {}),
|
|
3782
|
+
queue: args.queue === true,
|
|
3783
|
+
force: args.force === true,
|
|
3784
|
+
mpreset: mpresetResolution.mpreset,
|
|
3785
|
+
model: typeof args.model === "string" ? args.model : null,
|
|
3786
|
+
await_completion: args.await_completion === true,
|
|
3787
|
+
...(args.await_completion === true
|
|
3788
|
+
? { timeout_ms: args.timeout_ms, poll_interval_ms: args.poll_interval_ms }
|
|
3789
|
+
: {}),
|
|
3790
|
+
prompt_alias_ignored: hasTask && hasPrompt,
|
|
3791
|
+
allow_mutation: true,
|
|
3792
|
+
};
|
|
2765
3793
|
return await withToolIdempotency(
|
|
2766
3794
|
name,
|
|
2767
3795
|
idempotencyKey,
|
|
2768
|
-
|
|
2769
|
-
cwd: canonicalCwd,
|
|
2770
|
-
task,
|
|
2771
|
-
...(reusedSessionId ? { session_id: reusedSessionId } : {}),
|
|
2772
|
-
queue: args.queue === true,
|
|
2773
|
-
force: args.force === true,
|
|
2774
|
-
mpreset: mpresetResolution.mpreset,
|
|
2775
|
-
model: typeof args.model === "string" ? args.model : null,
|
|
2776
|
-
await_completion: args.await_completion === true,
|
|
2777
|
-
...(args.await_completion === true
|
|
2778
|
-
? { timeout_ms: args.timeout_ms, poll_interval_ms: args.poll_interval_ms }
|
|
2779
|
-
: {}),
|
|
2780
|
-
prompt_alias_ignored: hasTask && hasPrompt,
|
|
2781
|
-
allow_mutation: true,
|
|
2782
|
-
},
|
|
3796
|
+
canonicalArgs,
|
|
2783
3797
|
async () => {
|
|
2784
3798
|
const delegate = async () => {
|
|
2785
3799
|
let sessionId: string;
|
|
2786
3800
|
let session: Record<string, unknown>;
|
|
2787
3801
|
let reusedSession = false;
|
|
3802
|
+
let creationKey: string | null = null;
|
|
2788
3803
|
if (reusedSessionId) {
|
|
2789
3804
|
sessionId = reusedSessionId;
|
|
2790
3805
|
const existing = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
@@ -2842,31 +3857,66 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2842
3857
|
});
|
|
2843
3858
|
reusedSession = true;
|
|
2844
3859
|
} else {
|
|
2845
|
-
const
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
3860
|
+
const creation = await claimProductionCreation(name, idempotencyKey, canonicalArgs);
|
|
3861
|
+
if (creation.request.phase === "completed" && creation.request.safe_response)
|
|
3862
|
+
return creation.request.safe_response;
|
|
3863
|
+
creationKey = creation.keyDigest;
|
|
3864
|
+
if (creation.request.canonical_create_intent) {
|
|
3865
|
+
session = sessionFromCreationSnapshot(creation.request.canonical_create_intent.session);
|
|
3866
|
+
await bindCreationRequest(
|
|
3867
|
+
questionPaths,
|
|
3868
|
+
creation.keyDigest,
|
|
3869
|
+
creation.request.canonical_create_intent,
|
|
3870
|
+
);
|
|
3871
|
+
await commitCreationWal(
|
|
3872
|
+
questionPaths,
|
|
3873
|
+
creation.keyDigest,
|
|
3874
|
+
creation.request.canonical_create_intent,
|
|
3875
|
+
);
|
|
3876
|
+
sessionId = creation.request.canonical_create_intent.session.session_id;
|
|
3877
|
+
} else {
|
|
3878
|
+
const created = brokerResult(
|
|
3879
|
+
await brokerSession(
|
|
3880
|
+
canonicalCwd,
|
|
3881
|
+
"session.create",
|
|
3882
|
+
{
|
|
3883
|
+
cwd: canonicalCwd,
|
|
3884
|
+
target: coordinatorLifecycleTarget(config.sessionCommand, canonicalCwd),
|
|
3885
|
+
...(mpresetResolution.mpreset ? { modelPreset: mpresetResolution.mpreset } : {}),
|
|
3886
|
+
},
|
|
3887
|
+
idempotencyKey,
|
|
3888
|
+
),
|
|
3889
|
+
);
|
|
3890
|
+
sessionId = safeExternalId("session", created.sessionId ?? created.session_id);
|
|
3891
|
+
const createdCwd = await canonicalBrokerWorkspace(
|
|
3892
|
+
optionalString(created.cwd) ?? canonicalCwd,
|
|
3893
|
+
);
|
|
3894
|
+
const binding = await exactBrokerSessionBinding(sessionId, createdCwd, idempotencyKey);
|
|
3895
|
+
session = normalizeSession({
|
|
3896
|
+
session_id: sessionId,
|
|
3897
|
+
cwd: createdCwd,
|
|
3898
|
+
ephemeral: true,
|
|
3899
|
+
created_at: new Date().toISOString(),
|
|
3900
|
+
...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
|
|
3901
|
+
broker_workspace: binding.workspace,
|
|
3902
|
+
endpoint_generation: binding.endpointGeneration,
|
|
3903
|
+
endpoint_incarnation: binding.endpointIncarnation,
|
|
3904
|
+
});
|
|
3905
|
+
const intent: CanonicalCreateIntentV1 = {
|
|
3906
|
+
kind: "delegate",
|
|
3907
|
+
workflow: delegateWorkflow,
|
|
3908
|
+
session: canonicalCreationSnapshot(session),
|
|
3909
|
+
remote_create_key: creation.request.remote_create_key,
|
|
3910
|
+
initial_state: "running",
|
|
3911
|
+
initial_prompt: {
|
|
3912
|
+
text: taggedPrompt,
|
|
3913
|
+
caller_key_digest: createHash("sha256").update(idempotencyKey).digest("hex"),
|
|
2853
3914
|
},
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
const binding = await exactBrokerSessionBinding(sessionId, createdCwd, idempotencyKey);
|
|
2860
|
-
session = normalizeSession({
|
|
2861
|
-
session_id: sessionId,
|
|
2862
|
-
cwd: createdCwd,
|
|
2863
|
-
ephemeral: true,
|
|
2864
|
-
created_at: new Date().toISOString(),
|
|
2865
|
-
...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
|
|
2866
|
-
broker_workspace: binding.workspace,
|
|
2867
|
-
endpoint_generation: binding.endpointGeneration,
|
|
2868
|
-
endpoint_incarnation: binding.endpointIncarnation,
|
|
2869
|
-
});
|
|
3915
|
+
initial_events: [],
|
|
3916
|
+
};
|
|
3917
|
+
await bindCreationRequest(questionPaths, creation.keyDigest, intent);
|
|
3918
|
+
await commitCreationWal(questionPaths, creation.keyDigest, intent);
|
|
3919
|
+
}
|
|
2870
3920
|
}
|
|
2871
3921
|
await writeJsonFile(sessionFile(sessionId), session);
|
|
2872
3922
|
const previousActiveTurn = await readActiveTurn(namespaceDir, sessionId);
|
|
@@ -2886,6 +3936,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2886
3936
|
: args.queue === true
|
|
2887
3937
|
? "turn.follow_up"
|
|
2888
3938
|
: "turn.prompt";
|
|
3939
|
+
const promptKey = await claimCanonicalPrompt(sessionId, taggedPrompt, operation, idempotencyKey);
|
|
2889
3940
|
const result = await controlSession(session, operation, { text: taggedPrompt }, idempotencyKey);
|
|
2890
3941
|
const acknowledgement = requirePromptAcknowledgement(result);
|
|
2891
3942
|
const turn = await recordAcceptedPrompt(
|
|
@@ -2894,6 +3945,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2894
3945
|
operation,
|
|
2895
3946
|
previousActiveTurn,
|
|
2896
3947
|
acknowledgement,
|
|
3948
|
+
promptKey,
|
|
2897
3949
|
);
|
|
2898
3950
|
await appendCoordinatorEvent(namespaceDir, {
|
|
2899
3951
|
kind: "delegation.started",
|
|
@@ -2924,6 +3976,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2924
3976
|
result: publicSdkAcknowledgement(acknowledgement),
|
|
2925
3977
|
...(hasTask && hasPrompt ? { prompt_alias_ignored: true } : {}),
|
|
2926
3978
|
};
|
|
3979
|
+
if (creationKey) {
|
|
3980
|
+
await advanceCreationReceipt(questionPaths, creationKey, "projected", response);
|
|
3981
|
+
await advanceCreationReceipt(questionPaths, creationKey, "completed", response);
|
|
3982
|
+
}
|
|
2927
3983
|
return args.await_completion === true
|
|
2928
3984
|
? {
|
|
2929
3985
|
...response,
|
|
@@ -2938,6 +3994,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2938
3994
|
};
|
|
2939
3995
|
return reusedSessionId ? await withSessionTransition(reusedSessionId, delegate) : await delegate();
|
|
2940
3996
|
},
|
|
3997
|
+
true,
|
|
2941
3998
|
);
|
|
2942
3999
|
}
|
|
2943
4000
|
if (name === "gjc_coordinator_stop_session") {
|
|
@@ -2976,46 +4033,79 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2976
4033
|
};
|
|
2977
4034
|
}
|
|
2978
4035
|
const prompt = typeof args.prompt === "string" && args.prompt.length > 0 ? args.prompt : null;
|
|
4036
|
+
const canonicalArgs = {
|
|
4037
|
+
cwd,
|
|
4038
|
+
mpreset: mpresetResolution.mpreset,
|
|
4039
|
+
...(prompt ? { prompt } : {}),
|
|
4040
|
+
allow_mutation: true,
|
|
4041
|
+
};
|
|
2979
4042
|
return await withToolIdempotency(
|
|
2980
4043
|
name,
|
|
2981
4044
|
idempotencyKey,
|
|
2982
|
-
|
|
2983
|
-
cwd,
|
|
2984
|
-
mpreset: mpresetResolution.mpreset,
|
|
2985
|
-
...(prompt ? { prompt } : {}),
|
|
2986
|
-
allow_mutation: true,
|
|
2987
|
-
},
|
|
4045
|
+
canonicalArgs,
|
|
2988
4046
|
async () => {
|
|
2989
|
-
const
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
4047
|
+
const creation = await claimProductionCreation(name, idempotencyKey, canonicalArgs);
|
|
4048
|
+
if (creation.request.phase === "completed" && creation.request.safe_response)
|
|
4049
|
+
return creation.request.safe_response;
|
|
4050
|
+
let created: Record<string, unknown>;
|
|
4051
|
+
let session: Record<string, unknown>;
|
|
4052
|
+
let sessionId: string;
|
|
4053
|
+
if (creation.request.canonical_create_intent) {
|
|
4054
|
+
session = sessionFromCreationSnapshot(creation.request.canonical_create_intent.session);
|
|
4055
|
+
sessionId = creation.request.canonical_create_intent.session.session_id;
|
|
4056
|
+
created = { session_id: sessionId };
|
|
4057
|
+
} else {
|
|
4058
|
+
created = brokerResult(
|
|
4059
|
+
await brokerSession(
|
|
2994
4060
|
cwd,
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
4061
|
+
"session.create",
|
|
4062
|
+
{
|
|
4063
|
+
cwd,
|
|
4064
|
+
target: coordinatorLifecycleTarget(config.sessionCommand, cwd),
|
|
4065
|
+
...(mpresetResolution.mpreset ? { modelPreset: mpresetResolution.mpreset } : {}),
|
|
4066
|
+
},
|
|
4067
|
+
idempotencyKey,
|
|
4068
|
+
),
|
|
4069
|
+
);
|
|
4070
|
+
sessionId = safeExternalId("session", created.sessionId ?? created.session_id);
|
|
4071
|
+
const sessionCwd = await canonicalBrokerWorkspace(optionalString(created.cwd) ?? cwd);
|
|
4072
|
+
const binding = await exactBrokerSessionBinding(sessionId, sessionCwd, idempotencyKey);
|
|
4073
|
+
session = normalizeSession({
|
|
4074
|
+
session_id: sessionId,
|
|
4075
|
+
cwd: sessionCwd,
|
|
4076
|
+
...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
|
|
4077
|
+
broker_workspace: binding.workspace,
|
|
4078
|
+
endpoint_generation: binding.endpointGeneration,
|
|
4079
|
+
endpoint_incarnation: binding.endpointIncarnation,
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
const intent: CanonicalCreateIntentV1 = {
|
|
4083
|
+
kind: "start",
|
|
4084
|
+
session: canonicalCreationSnapshot(session),
|
|
4085
|
+
remote_create_key: creation.request.remote_create_key,
|
|
4086
|
+
initial_state: prompt ? "running" : "ready_for_input",
|
|
4087
|
+
initial_prompt: prompt
|
|
4088
|
+
? { text: prompt, caller_key_digest: createHash("sha256").update(idempotencyKey).digest("hex") }
|
|
4089
|
+
: null,
|
|
4090
|
+
initial_events: [],
|
|
4091
|
+
};
|
|
4092
|
+
await bindCreationRequest(questionPaths, creation.keyDigest, intent);
|
|
4093
|
+
await commitCreationWal(questionPaths, creation.keyDigest, intent);
|
|
3012
4094
|
await writeJsonFile(sessionFile(sessionId), session);
|
|
3013
4095
|
const lifecycle = publicLifecycleReceipt(created, sessionId);
|
|
3014
4096
|
if (prompt) {
|
|
4097
|
+
const promptKey = await claimCanonicalPrompt(sessionId, prompt, "turn.prompt", idempotencyKey);
|
|
3015
4098
|
const result = await controlSession(session, "turn.prompt", { text: prompt }, idempotencyKey);
|
|
3016
4099
|
const acknowledgement = requirePromptAcknowledgement(result);
|
|
3017
|
-
const turn = await recordAcceptedPrompt(
|
|
3018
|
-
|
|
4100
|
+
const turn = await recordAcceptedPrompt(
|
|
4101
|
+
sessionId,
|
|
4102
|
+
prompt,
|
|
4103
|
+
"turn.prompt",
|
|
4104
|
+
null,
|
|
4105
|
+
acknowledgement,
|
|
4106
|
+
promptKey,
|
|
4107
|
+
);
|
|
4108
|
+
const response = {
|
|
3019
4109
|
ok: true,
|
|
3020
4110
|
session: publicCoordinatorSession(session),
|
|
3021
4111
|
session_id: sessionId,
|
|
@@ -3030,6 +4120,9 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3030
4120
|
result: publicSdkAcknowledgement(acknowledgement),
|
|
3031
4121
|
session_state: publicCoordinatorSessionState(await readSessionState(namespaceDir, sessionId)),
|
|
3032
4122
|
};
|
|
4123
|
+
await advanceCreationReceipt(questionPaths, creation.keyDigest, "projected", response);
|
|
4124
|
+
await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
|
|
4125
|
+
return response;
|
|
3033
4126
|
}
|
|
3034
4127
|
const sessionState = await writeSessionState(namespaceDir, sessionId, "ready_for_input", {
|
|
3035
4128
|
live: null,
|
|
@@ -3041,13 +4134,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3041
4134
|
summary: `Session ${sessionId} started through SDK lifecycle control`,
|
|
3042
4135
|
payloadRef: path.relative(namespaceDir, sessionFile(sessionId)),
|
|
3043
4136
|
});
|
|
3044
|
-
|
|
4137
|
+
const response = {
|
|
3045
4138
|
ok: true,
|
|
3046
4139
|
session: publicCoordinatorSession(session),
|
|
3047
4140
|
session_state: publicCoordinatorSessionState(sessionState),
|
|
3048
4141
|
lifecycle,
|
|
3049
4142
|
};
|
|
4143
|
+
await advanceCreationReceipt(questionPaths, creation.keyDigest, "projected", response);
|
|
4144
|
+
await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
|
|
4145
|
+
return response;
|
|
3050
4146
|
},
|
|
4147
|
+
true,
|
|
3051
4148
|
);
|
|
3052
4149
|
}
|
|
3053
4150
|
if (name === "gjc_coordinator_send_prompt") {
|
|
@@ -3093,6 +4190,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3093
4190
|
: args.queue === true
|
|
3094
4191
|
? "turn.follow_up"
|
|
3095
4192
|
: "turn.prompt";
|
|
4193
|
+
const promptKey = await claimCanonicalPrompt(sessionId, prompt, operation, idempotencyKey);
|
|
3096
4194
|
const result = await controlSession(currentSession, operation, { text: prompt }, idempotencyKey);
|
|
3097
4195
|
const acknowledgement = requirePromptAcknowledgement(result);
|
|
3098
4196
|
const turn = await recordAcceptedPrompt(
|
|
@@ -3101,6 +4199,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3101
4199
|
operation,
|
|
3102
4200
|
previousActiveTurn,
|
|
3103
4201
|
acknowledgement,
|
|
4202
|
+
promptKey,
|
|
3104
4203
|
);
|
|
3105
4204
|
return {
|
|
3106
4205
|
ok: true,
|
|
@@ -3128,37 +4227,383 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3128
4227
|
requireCoordinatorMutation(config, "questions", args);
|
|
3129
4228
|
const idempotencyKey = requiredIdempotencyKey(args);
|
|
3130
4229
|
const sessionId = safeExternalId("session", args.session_id);
|
|
3131
|
-
const
|
|
4230
|
+
const turnId = safeTurnId(args.turn_id);
|
|
4231
|
+
const questionId = typeof args.question_id === "string" ? args.question_id : "";
|
|
4232
|
+
const answerBinding = typeof args.answer_binding === "string" ? args.answer_binding : "";
|
|
3132
4233
|
return await withToolIdempotency(
|
|
3133
4234
|
name,
|
|
3134
4235
|
idempotencyKey,
|
|
3135
4236
|
{
|
|
3136
4237
|
session_id: sessionId,
|
|
4238
|
+
turn_id: turnId,
|
|
3137
4239
|
question_id: questionId,
|
|
4240
|
+
answer_binding: answerBinding,
|
|
3138
4241
|
answer: args.answer,
|
|
3139
|
-
...(args.turn_id == null ? {} : { turn_id: args.turn_id }),
|
|
3140
4242
|
allow_mutation: true,
|
|
3141
4243
|
},
|
|
3142
4244
|
async () => {
|
|
3143
|
-
const
|
|
3144
|
-
|
|
4245
|
+
const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
|
|
4246
|
+
const requestDigest = createHash("sha256")
|
|
4247
|
+
.update(
|
|
4248
|
+
canonicalJson({
|
|
4249
|
+
session_id: sessionId,
|
|
4250
|
+
turn_id: turnId,
|
|
4251
|
+
question_id: questionId,
|
|
4252
|
+
answer_binding: answerBinding,
|
|
4253
|
+
answer: args.answer,
|
|
4254
|
+
}),
|
|
4255
|
+
)
|
|
4256
|
+
.digest("hex");
|
|
4257
|
+
await ensureQuestionTransaction(sessionId);
|
|
4258
|
+
const replay = await withSessionTransaction(questionPaths, sessionId, async transaction => {
|
|
4259
|
+
const request = transaction.requests.answers[keyDigest];
|
|
4260
|
+
if (!request) return null;
|
|
4261
|
+
if (request.request_digest !== requestDigest)
|
|
4262
|
+
return {
|
|
4263
|
+
ok: false,
|
|
4264
|
+
error: {
|
|
4265
|
+
code: "idempotency_conflict",
|
|
4266
|
+
message: "Idempotency key was reused with a different answer request.",
|
|
4267
|
+
},
|
|
4268
|
+
};
|
|
4269
|
+
if (request.phase === "completed" && request.safe_receipt) {
|
|
4270
|
+
const receipt = request.safe_receipt;
|
|
4271
|
+
if (
|
|
4272
|
+
receipt.answer_hash !== request.answer_hash ||
|
|
4273
|
+
receipt.answer_binding_sha256 !== request.answer_binding_sha256 ||
|
|
4274
|
+
receipt.authority_id !== request.authority_id ||
|
|
4275
|
+
receipt.turn_id !== request.turn_id ||
|
|
4276
|
+
receipt.endpoint_incarnation !== request.endpoint_incarnation ||
|
|
4277
|
+
receipt.claim_fence_epoch !== request.claim_fence_epoch
|
|
4278
|
+
)
|
|
4279
|
+
return {
|
|
4280
|
+
ok: false,
|
|
4281
|
+
error: {
|
|
4282
|
+
code: "terminal_uncertain",
|
|
4283
|
+
message: "The stored answer receipt is incomplete.",
|
|
4284
|
+
},
|
|
4285
|
+
};
|
|
4286
|
+
return {
|
|
4287
|
+
ok: true,
|
|
4288
|
+
schema_version: 1,
|
|
4289
|
+
session_id: sessionId,
|
|
4290
|
+
turn_id: turnId,
|
|
4291
|
+
question_id: questionId,
|
|
4292
|
+
operation: "workflow.gate_answer",
|
|
4293
|
+
status: receipt.status,
|
|
4294
|
+
replayed: true,
|
|
4295
|
+
resolved_at: receipt.resolved_at,
|
|
4296
|
+
};
|
|
4297
|
+
}
|
|
4298
|
+
if (request.phase === "uncertain")
|
|
4299
|
+
return {
|
|
4300
|
+
ok: false,
|
|
4301
|
+
error: { code: "terminal_uncertain", message: "The previous answer outcome is uncertain." },
|
|
4302
|
+
};
|
|
4303
|
+
if (request.phase === "rejected" && request.safe_receipt)
|
|
4304
|
+
return {
|
|
4305
|
+
ok: false,
|
|
4306
|
+
schema_version: 1,
|
|
4307
|
+
session_id: sessionId,
|
|
4308
|
+
turn_id: turnId,
|
|
4309
|
+
question_id: questionId,
|
|
4310
|
+
error: { code: "validation_rejected", message: "Answer was rejected by the workflow gate." },
|
|
4311
|
+
question_status: "pending",
|
|
4312
|
+
};
|
|
4313
|
+
return null;
|
|
4314
|
+
});
|
|
4315
|
+
if (replay) return replay;
|
|
4316
|
+
|
|
4317
|
+
const reconciliation = await reconcileQuestions(sessionId);
|
|
4318
|
+
if (!reconciliation.reconciliation.complete)
|
|
3145
4319
|
return {
|
|
3146
4320
|
ok: false,
|
|
3147
|
-
error: { code: "
|
|
4321
|
+
error: { code: "terminal_uncertain", message: "Workflow gate snapshot is incomplete." },
|
|
3148
4322
|
};
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
4323
|
+
let translated: unknown;
|
|
4324
|
+
let session: Record<string, unknown> | null = null;
|
|
4325
|
+
const claimed = await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
|
|
4326
|
+
const question = transaction.canonical.questions[questionId];
|
|
4327
|
+
if (!question)
|
|
4328
|
+
return {
|
|
4329
|
+
response: { ok: false, error: { code: "not_found", message: "Question was not found." } },
|
|
4330
|
+
};
|
|
4331
|
+
if (
|
|
4332
|
+
question.session_id !== sessionId ||
|
|
4333
|
+
question.turn_id !== turnId ||
|
|
4334
|
+
question.endpoint_incarnation !== transaction.canonical.session.broker.endpoint_incarnation
|
|
4335
|
+
)
|
|
4336
|
+
return {
|
|
4337
|
+
response: {
|
|
4338
|
+
ok: false,
|
|
4339
|
+
error: {
|
|
4340
|
+
code: "ownership_mismatch",
|
|
4341
|
+
message: "Question ownership does not match this answer.",
|
|
4342
|
+
},
|
|
4343
|
+
},
|
|
4344
|
+
};
|
|
4345
|
+
const existingRequest = transaction.requests.answers[keyDigest];
|
|
4346
|
+
const recovering =
|
|
4347
|
+
(existingRequest?.phase === "remote_started" || existingRequest?.phase === "accepted") &&
|
|
4348
|
+
existingRequest.request_digest === requestDigest &&
|
|
4349
|
+
existingRequest.question_id === question.question_id &&
|
|
4350
|
+
existingRequest.authority_id === question.authority_id &&
|
|
4351
|
+
existingRequest.turn_id === question.turn_id &&
|
|
4352
|
+
existingRequest.endpoint_incarnation === question.endpoint_incarnation &&
|
|
4353
|
+
existingRequest.answer_binding_sha256 === question.binding_sha256 &&
|
|
4354
|
+
existingRequest.claim_fence_epoch === question.claim_fence_epoch &&
|
|
4355
|
+
question.answer_request_id === existingRequest.request_id;
|
|
4356
|
+
if (question.status === "answered" && !recovering)
|
|
4357
|
+
return {
|
|
4358
|
+
response: {
|
|
4359
|
+
ok: false,
|
|
4360
|
+
error: {
|
|
4361
|
+
code: "idempotency_conflict",
|
|
4362
|
+
message: "Question has already been answered by a different request.",
|
|
4363
|
+
},
|
|
4364
|
+
},
|
|
4365
|
+
};
|
|
4366
|
+
|
|
4367
|
+
if (question.status !== "pending" && !recovering)
|
|
4368
|
+
return {
|
|
4369
|
+
response: {
|
|
4370
|
+
ok: false,
|
|
4371
|
+
error: { code: "resource_gone", message: "Question is no longer answerable." },
|
|
4372
|
+
},
|
|
4373
|
+
};
|
|
4374
|
+
if (!answerBindingMatches(answerBinding, question.binding_plaintext))
|
|
4375
|
+
return {
|
|
4376
|
+
response: {
|
|
4377
|
+
ok: false,
|
|
4378
|
+
error: { code: "ownership_mismatch", message: "Question binding does not match." },
|
|
4379
|
+
},
|
|
4380
|
+
};
|
|
4381
|
+
const answer = validateCoordinatorAskAnswer(question.codec, args.answer);
|
|
4382
|
+
if (!answer)
|
|
4383
|
+
return {
|
|
4384
|
+
response: {
|
|
4385
|
+
ok: false,
|
|
4386
|
+
schema_version: 1,
|
|
4387
|
+
session_id: sessionId,
|
|
4388
|
+
turn_id: turnId,
|
|
4389
|
+
question_id: questionId,
|
|
4390
|
+
error: {
|
|
4391
|
+
code: "validation_rejected",
|
|
4392
|
+
message: "Answer does not match the question schema.",
|
|
4393
|
+
},
|
|
4394
|
+
question_status: "pending",
|
|
4395
|
+
},
|
|
4396
|
+
};
|
|
4397
|
+
const turn = await readTurnRecord(namespaceDir, turnId);
|
|
4398
|
+
const canonicalTurn = transaction.canonical.turns[turnId];
|
|
4399
|
+
const authority = transaction.canonical.gate_authorities[question.authority_id];
|
|
4400
|
+
const runtimeTurnId =
|
|
4401
|
+
authority?.observation.kind === "valid"
|
|
4402
|
+
? authority.observation.first_provenance.runtime_turn_id
|
|
4403
|
+
: null;
|
|
4404
|
+
if (
|
|
4405
|
+
!authority ||
|
|
4406
|
+
!turn ||
|
|
4407
|
+
canonicalTurn?.terminal_fence ||
|
|
4408
|
+
TERMINAL_TURN_STATUSES.has(canonicalTurn?.status as TurnStatus) ||
|
|
4409
|
+
TERMINAL_TURN_STATUSES.has(turn.status) ||
|
|
4410
|
+
turn.delivery.runtime_turn_id !== runtimeTurnId
|
|
4411
|
+
) {
|
|
4412
|
+
if (recovering && existingRequest) {
|
|
4413
|
+
existingRequest.phase = "uncertain";
|
|
4414
|
+
existingRequest.error_code = "terminal_uncertain";
|
|
4415
|
+
existingRequest.updated_at = new Date().toISOString();
|
|
4416
|
+
}
|
|
4417
|
+
return {
|
|
4418
|
+
response: {
|
|
4419
|
+
ok: false,
|
|
4420
|
+
error: {
|
|
4421
|
+
code: recovering ? "terminal_uncertain" : "resource_gone",
|
|
4422
|
+
message: recovering
|
|
4423
|
+
? "The previous answer outcome is uncertain."
|
|
4424
|
+
: "Turn is no longer answerable.",
|
|
4425
|
+
},
|
|
4426
|
+
},
|
|
4427
|
+
};
|
|
4428
|
+
}
|
|
4429
|
+
translated = translateCoordinatorAskAnswer(question.codec, answer);
|
|
4430
|
+
session = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
4431
|
+
if (!session)
|
|
4432
|
+
return {
|
|
4433
|
+
response: {
|
|
4434
|
+
ok: false,
|
|
4435
|
+
error: { code: "not_found", message: "Coordinator session was not found." },
|
|
4436
|
+
},
|
|
4437
|
+
};
|
|
4438
|
+
if (recovering) {
|
|
4439
|
+
return {
|
|
4440
|
+
response: null,
|
|
4441
|
+
fence: existingRequest.claim_fence_epoch,
|
|
4442
|
+
gateId: authority.authority.gate_id,
|
|
4443
|
+
requestId: existingRequest.sdk_idempotency_key,
|
|
4444
|
+
};
|
|
4445
|
+
}
|
|
4446
|
+
const requestId = `c07:${createHash("sha256")
|
|
4447
|
+
.update(`coordinator-question-v1${question.authority_id}${requestDigest}`)
|
|
4448
|
+
.digest("hex")}`;
|
|
4449
|
+
question.status = "resolving";
|
|
4450
|
+
question.claim_fence_epoch = transaction.revision + 1;
|
|
4451
|
+
question.answer_request_id = requestId;
|
|
4452
|
+
question.updated_at = new Date().toISOString();
|
|
4453
|
+
transaction.requests.answers[keyDigest] = {
|
|
4454
|
+
request_id: requestId,
|
|
4455
|
+
key_digest: keyDigest,
|
|
4456
|
+
request_digest: requestDigest,
|
|
4457
|
+
answer_hash: createHash("sha256").update(canonicalJson(args.answer)).digest("hex"),
|
|
4458
|
+
answer_binding_sha256: question.binding_sha256,
|
|
4459
|
+
authority_id: question.authority_id,
|
|
4460
|
+
question_id: question.question_id,
|
|
4461
|
+
turn_id: question.turn_id,
|
|
4462
|
+
endpoint_incarnation: question.endpoint_incarnation,
|
|
4463
|
+
sdk_idempotency_key: requestId,
|
|
4464
|
+
claim_fence_epoch: question.claim_fence_epoch,
|
|
4465
|
+
phase: "claimed",
|
|
4466
|
+
created_at: question.updated_at,
|
|
4467
|
+
updated_at: question.updated_at,
|
|
4468
|
+
};
|
|
4469
|
+
return {
|
|
4470
|
+
response: null,
|
|
4471
|
+
fence: question.claim_fence_epoch,
|
|
4472
|
+
gateId: authority.authority.gate_id,
|
|
4473
|
+
requestId,
|
|
4474
|
+
};
|
|
4475
|
+
});
|
|
4476
|
+
if (claimed.response) return claimed.response;
|
|
4477
|
+
const gateId = (claimed as { gateId: string }).gateId;
|
|
4478
|
+
await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
|
|
4479
|
+
const request = transaction.requests.answers[keyDigest];
|
|
4480
|
+
if (!request || request.claim_fence_epoch !== (claimed as { fence: number }).fence)
|
|
4481
|
+
throw new Error("terminal_uncertain");
|
|
4482
|
+
if (request.phase === "claimed") request.phase = "remote_started";
|
|
4483
|
+
if (request.phase !== "remote_started" && request.phase !== "accepted")
|
|
4484
|
+
throw new Error("terminal_uncertain");
|
|
4485
|
+
request.updated_at = new Date().toISOString();
|
|
4486
|
+
});
|
|
4487
|
+
|
|
4488
|
+
try {
|
|
4489
|
+
const result = await controlSession(
|
|
4490
|
+
session!,
|
|
4491
|
+
"workflow.gate_answer",
|
|
4492
|
+
{ id: gateId, response: translated, expectedSessionId: sessionId },
|
|
4493
|
+
(claimed as { requestId: string }).requestId,
|
|
4494
|
+
);
|
|
4495
|
+
const resolution = asRecord(result);
|
|
4496
|
+
const status = resolution?.status;
|
|
4497
|
+
if (status === "rejected") {
|
|
4498
|
+
await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
|
|
4499
|
+
const question = transaction.canonical.questions[questionId];
|
|
4500
|
+
if (
|
|
4501
|
+
question?.status === "resolving" &&
|
|
4502
|
+
question.claim_fence_epoch === (claimed as { fence: number }).fence
|
|
4503
|
+
) {
|
|
4504
|
+
question.status = "pending";
|
|
4505
|
+
question.claim_fence_epoch = null;
|
|
4506
|
+
question.updated_at = new Date().toISOString();
|
|
4507
|
+
question.history.push({
|
|
4508
|
+
at: question.updated_at,
|
|
4509
|
+
status: "pending",
|
|
4510
|
+
reason: "validation_rejected",
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
const request = transaction.requests.answers[keyDigest];
|
|
4514
|
+
if (request && question) {
|
|
4515
|
+
request.phase = "rejected";
|
|
4516
|
+
request.safe_receipt = {
|
|
4517
|
+
status: "rejected",
|
|
4518
|
+
answer_hash: request.answer_hash,
|
|
4519
|
+
answer_binding_sha256: request.answer_binding_sha256,
|
|
4520
|
+
authority_id: request.authority_id,
|
|
4521
|
+
turn_id: request.turn_id,
|
|
4522
|
+
endpoint_incarnation: request.endpoint_incarnation,
|
|
4523
|
+
claim_fence_epoch: request.claim_fence_epoch,
|
|
4524
|
+
resolved_at: question.updated_at,
|
|
4525
|
+
};
|
|
4526
|
+
request.updated_at = question.updated_at;
|
|
4527
|
+
}
|
|
4528
|
+
});
|
|
4529
|
+
return {
|
|
4530
|
+
ok: false,
|
|
4531
|
+
schema_version: 1,
|
|
4532
|
+
session_id: sessionId,
|
|
4533
|
+
turn_id: turnId,
|
|
4534
|
+
question_id: questionId,
|
|
4535
|
+
error: { code: "validation_rejected", message: "Answer was rejected by the workflow gate." },
|
|
4536
|
+
question_status: "pending",
|
|
4537
|
+
};
|
|
4538
|
+
}
|
|
4539
|
+
if (status !== "accepted")
|
|
4540
|
+
throw new SdkClientError("terminal_uncertain", "Workflow gate answer was not accepted.");
|
|
4541
|
+
const resolvedAt =
|
|
4542
|
+
typeof resolution?.resolved_at === "string" ? resolution.resolved_at : new Date().toISOString();
|
|
4543
|
+
await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
|
|
4544
|
+
const question = transaction.canonical.questions[questionId];
|
|
4545
|
+
if (!question || question.claim_fence_epoch !== (claimed as { fence: number }).fence)
|
|
4546
|
+
throw new Error("terminal_uncertain");
|
|
4547
|
+
question.status = "answered";
|
|
4548
|
+
question.answered_at = resolvedAt;
|
|
4549
|
+
question.updated_at = resolvedAt;
|
|
4550
|
+
question.history.push({ at: resolvedAt, status: "answered", reason: null });
|
|
4551
|
+
const authority = transaction.canonical.gate_authorities[question.authority_id];
|
|
4552
|
+
if (authority)
|
|
4553
|
+
authority.outcome = { state: "answered", turn_id: turnId, question_id: questionId };
|
|
4554
|
+
const request = transaction.requests.answers[keyDigest];
|
|
4555
|
+
if (!request) throw new Error("terminal_uncertain");
|
|
4556
|
+
request.phase = "accepted";
|
|
4557
|
+
request.safe_receipt = {
|
|
4558
|
+
status: "accepted",
|
|
4559
|
+
answer_hash: request.answer_hash,
|
|
4560
|
+
answer_binding_sha256: request.answer_binding_sha256,
|
|
4561
|
+
authority_id: request.authority_id,
|
|
4562
|
+
turn_id: request.turn_id,
|
|
4563
|
+
endpoint_incarnation: request.endpoint_incarnation,
|
|
4564
|
+
claim_fence_epoch: request.claim_fence_epoch,
|
|
4565
|
+
resolved_at: resolvedAt,
|
|
4566
|
+
};
|
|
4567
|
+
request.phase = "completed";
|
|
4568
|
+
request.updated_at = resolvedAt;
|
|
4569
|
+
});
|
|
4570
|
+
return {
|
|
4571
|
+
ok: true,
|
|
4572
|
+
schema_version: 1,
|
|
4573
|
+
session_id: sessionId,
|
|
4574
|
+
turn_id: turnId,
|
|
4575
|
+
question_id: questionId,
|
|
4576
|
+
operation: "workflow.gate_answer",
|
|
4577
|
+
status: "accepted",
|
|
4578
|
+
replayed: false,
|
|
4579
|
+
resolved_at: resolvedAt,
|
|
4580
|
+
};
|
|
4581
|
+
} catch (error) {
|
|
4582
|
+
await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
|
|
4583
|
+
const question = transaction.canonical.questions[questionId];
|
|
4584
|
+
if (
|
|
4585
|
+
question?.status === "resolving" &&
|
|
4586
|
+
question.claim_fence_epoch === (claimed as { fence: number }).fence
|
|
4587
|
+
) {
|
|
4588
|
+
question.status = "uncertain";
|
|
4589
|
+
question.updated_at = new Date().toISOString();
|
|
4590
|
+
question.history.push({
|
|
4591
|
+
at: question.updated_at,
|
|
4592
|
+
status: "uncertain",
|
|
4593
|
+
reason: "terminal_uncertain",
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4596
|
+
const request = transaction.requests.answers[keyDigest];
|
|
4597
|
+
if (request && question) {
|
|
4598
|
+
request.phase = "uncertain";
|
|
4599
|
+
request.error_code = "terminal_uncertain";
|
|
4600
|
+
request.updated_at = question.updated_at;
|
|
4601
|
+
}
|
|
4602
|
+
});
|
|
4603
|
+
return sdkError(error);
|
|
4604
|
+
}
|
|
3161
4605
|
},
|
|
4606
|
+
true,
|
|
3162
4607
|
);
|
|
3163
4608
|
}
|
|
3164
4609
|
if (name === "gjc_coordinator_report_status") {
|
|
@@ -3180,6 +4625,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3180
4625
|
allow_mutation: true,
|
|
3181
4626
|
},
|
|
3182
4627
|
async () => {
|
|
4628
|
+
const reportId = `report-${randomUUID()}`;
|
|
3183
4629
|
const report = {
|
|
3184
4630
|
session_id: sessionId,
|
|
3185
4631
|
turn_id: args.turn_id,
|
|
@@ -3202,11 +4648,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3202
4648
|
turn = {
|
|
3203
4649
|
...turn,
|
|
3204
4650
|
status: terminalStatus,
|
|
3205
|
-
delivery: {
|
|
3206
|
-
...turn.delivery,
|
|
3207
|
-
prompt_acknowledged: true,
|
|
3208
|
-
state: "acknowledged",
|
|
3209
|
-
},
|
|
4651
|
+
delivery: { ...turn.delivery, prompt_acknowledged: true, state: "acknowledged" },
|
|
3210
4652
|
final_response: {
|
|
3211
4653
|
text:
|
|
3212
4654
|
typeof args.summary === "string"
|
|
@@ -3234,22 +4676,44 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3234
4676
|
updated_at: timestamp,
|
|
3235
4677
|
completed_at: timestamp,
|
|
3236
4678
|
};
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
turn.session_id,
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
4679
|
+
const canonicalReport: CanonicalReportSnapshotV1 = {
|
|
4680
|
+
schema_version: 1,
|
|
4681
|
+
report_id: reportId,
|
|
4682
|
+
operation_id: `report:${idempotencyKey}`,
|
|
4683
|
+
session_id: turn.session_id,
|
|
4684
|
+
turn_id: turn.turn_id,
|
|
4685
|
+
status: String(args.status ?? "unknown"),
|
|
4686
|
+
summary: typeof args.summary === "string" ? args.summary : "",
|
|
4687
|
+
blocker: optionalString(args.blocker),
|
|
4688
|
+
pr_url: optionalString(args.pr_url),
|
|
4689
|
+
evidence_paths: evidence.map(item => item.path),
|
|
4690
|
+
created_at: report.created_at,
|
|
4691
|
+
};
|
|
4692
|
+
await projectTerminalTransition(turn, {
|
|
4693
|
+
desiredState: terminalStatus === "failed" ? "errored" : "completed",
|
|
4694
|
+
reason: terminalStatus === "failed" ? "reported_failure" : null,
|
|
4695
|
+
report: canonicalReport,
|
|
4696
|
+
});
|
|
3250
4697
|
}
|
|
3251
4698
|
}
|
|
3252
|
-
|
|
4699
|
+
if (sessionId && (!args.turn_id || !asTerminalTurnStatus(args.status))) {
|
|
4700
|
+
await ensureQuestionTransaction(sessionId);
|
|
4701
|
+
await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
|
|
4702
|
+
transaction.canonical.reports[reportId] = {
|
|
4703
|
+
schema_version: 1,
|
|
4704
|
+
report_id: reportId,
|
|
4705
|
+
operation_id: `report:${idempotencyKey}`,
|
|
4706
|
+
session_id: sessionId,
|
|
4707
|
+
turn_id: typeof args.turn_id === "string" ? args.turn_id : "",
|
|
4708
|
+
status: String(args.status ?? "unknown"),
|
|
4709
|
+
summary: typeof args.summary === "string" ? args.summary : "",
|
|
4710
|
+
blocker: optionalString(args.blocker),
|
|
4711
|
+
pr_url: optionalString(args.pr_url),
|
|
4712
|
+
evidence_paths: evidence.map(item => item.path),
|
|
4713
|
+
created_at: report.created_at,
|
|
4714
|
+
};
|
|
4715
|
+
});
|
|
4716
|
+
}
|
|
3253
4717
|
const reportPath = path.join(namespaceDir, "reports", `${reportId}.json`);
|
|
3254
4718
|
await writeJsonFile(reportPath, report);
|
|
3255
4719
|
await appendCoordinatorEvent(namespaceDir, {
|