@gajae-code/coding-agent 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +120 -1
- package/dist/types/async/job-manager.d.ts +4 -1
- package/dist/types/autoresearch/dashboard.d.ts +29 -4
- package/dist/types/autoresearch/data-context.d.ts +25 -0
- package/dist/types/autoresearch/git.d.ts +65 -7
- package/dist/types/autoresearch/harness.d.ts +58 -0
- package/dist/types/autoresearch/prompts.d.ts +51 -0
- package/dist/types/autoresearch/runs.d.ts +173 -0
- package/dist/types/autoresearch/types.d.ts +9 -139
- package/dist/types/capability/types.d.ts +20 -0
- package/dist/types/cli/accounts-cli.d.ts +1 -0
- package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
- package/dist/types/config/autorouting-contract.d.ts +146 -0
- package/dist/types/config/autorouting-generator.d.ts +40 -0
- package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
- package/dist/types/config/autorouting.d.ts +35 -0
- package/dist/types/config/model-discovery-manager.d.ts +8 -1
- package/dist/types/config/model-registry.d.ts +30 -1
- package/dist/types/config/provider-selection-policy.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +61 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/durability.d.ts +29 -0
- package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +14 -0
- package/dist/types/crash/index-store.d.ts +3 -0
- package/dist/types/crash/record-loader.d.ts +2 -0
- package/dist/types/crash/upstream/relay.d.ts +5 -3
- package/dist/types/cursor.d.ts +12 -0
- package/dist/types/defaults/gjc-defaults.d.ts +35 -1
- package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/edit/index.d.ts +2 -1
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +32 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
- package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
- package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
- package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
- package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
- package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
- package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
- package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
- package/dist/types/hooks/skill-keywords.d.ts +1 -1
- package/dist/types/main.d.ts +0 -10
- package/dist/types/modes/acp/acp-agent.d.ts +13 -6
- package/dist/types/modes/components/model-selector.d.ts +14 -0
- package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
- package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +9 -0
- package/dist/types/rlm/artifacts.d.ts +4 -4
- package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
- package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
- package/dist/types/runtime-mcp/config.d.ts +6 -0
- package/dist/types/runtime-mcp/manager.d.ts +10 -0
- package/dist/types/sdk/acp/adapter.d.ts +0 -4
- package/dist/types/sdk/broker/broker.d.ts +1 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
- package/dist/types/sdk/broker/session-index.d.ts +15 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/index.d.ts +8 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
- package/dist/types/sdk/client/client.d.ts +39 -4
- package/dist/types/sdk/host/host.d.ts +1 -0
- package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
- package/dist/types/sdk/host/model-pin.d.ts +29 -0
- package/dist/types/sdk/host/session-runtime.d.ts +2 -1
- package/dist/types/sdk/lifecycle-session.d.ts +13 -0
- package/dist/types/sdk/router/session-router.d.ts +27 -1
- package/dist/types/sdk/session.d.ts +11 -1
- package/dist/types/session/agent-session.d.ts +36 -6
- package/dist/types/session/artifacts.d.ts +16 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/session-manager.d.ts +84 -2
- package/dist/types/session/session-staging-paths.d.ts +3 -0
- package/dist/types/skill-state/canonical-skills.d.ts +1 -1
- package/dist/types/skill-state/workflow-hud.d.ts +12 -21
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
- package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
- package/dist/types/system-prompt.d.ts +23 -0
- package/dist/types/task/executor.d.ts +35 -4
- package/dist/types/task/index.d.ts +17 -3
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +144 -0
- package/dist/types/tools/atomic-file-write.d.ts +72 -0
- package/dist/types/tools/descriptors.d.ts +1 -0
- package/dist/types/tools/index.d.ts +36 -1
- package/dist/types/tools/move-session.d.ts +40 -0
- package/dist/types/tools/python.d.ts +12 -0
- package/dist/types/tools/resource-gc.d.ts +0 -3
- package/dist/types/tools/search.d.ts +8 -1
- package/dist/types/tools/skill.d.ts +9 -4
- package/dist/types/tools/subagent.d.ts +33 -3
- package/dist/types/utils/xml-escape.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
- package/package.json +11 -12
- package/scripts/build-sdk-package-smoke.ts +7 -5
- package/scripts/check-autorouting-tier-map.ts +183 -0
- package/scripts/generate-sdk-operation-inventory.ts +12 -0
- package/scripts/generate-tool-catalog.ts +40 -5
- package/src/async/job-manager.ts +5 -2
- package/src/autoresearch/dashboard.ts +134 -314
- package/src/autoresearch/data-context.ts +35 -0
- package/src/autoresearch/git.ts +186 -26
- package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
- package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
- package/src/autoresearch/prompts/prompt.md +103 -0
- package/src/autoresearch/prompts.ts +126 -0
- package/src/autoresearch/runs.ts +708 -0
- package/src/autoresearch/types.ts +9 -150
- package/src/capability/index.ts +4 -4
- package/src/capability/types.ts +20 -0
- package/src/cli/accounts-cli.ts +21 -5
- package/src/cli/config-cli.ts +25 -1
- package/src/cli/fast-help.ts +1 -2
- package/src/cli/list-models.ts +5 -8
- package/src/cli/setup-cli.ts +8 -0
- package/src/cli.ts +2 -21
- package/src/commands/autoresearch.ts +33 -0
- package/src/commands/coordinator.ts +15 -0
- package/src/commands/launch.ts +2 -4
- package/src/commands/quick-lane.ts +1 -1
- package/src/commands/sdk.ts +24 -0
- package/src/commands/state.ts +1 -1
- package/src/config/autorouting-contract.ts +474 -0
- package/src/config/autorouting-generator.ts +238 -0
- package/src/config/autorouting-tier-map.ts +6108 -0
- package/src/config/autorouting.ts +120 -0
- package/src/config/model-discovery-manager.ts +42 -11
- package/src/config/model-registry.ts +348 -68
- package/src/config/provider-selection-policy.ts +60 -9
- package/src/config/settings-schema.ts +195 -6
- package/src/config/settings.ts +62 -1
- package/src/config.ts +2 -3
- package/src/coordinator/contract.ts +0 -1
- package/src/coordinator-mcp/codex-handoff.ts +68 -42
- package/src/coordinator-mcp/durability.ts +200 -0
- package/src/coordinator-mcp/event-webhook.ts +278 -0
- package/src/coordinator-mcp/question-state.ts +109 -46
- package/src/coordinator-mcp/server.ts +932 -504
- package/src/crash/index-store.ts +126 -22
- package/src/crash/record-loader.ts +7 -0
- package/src/crash/upstream/relay.ts +82 -34
- package/src/cursor.ts +84 -1
- package/src/debug/index.ts +26 -0
- package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
- package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
- package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
- package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
- package/src/defaults/gjc-defaults.ts +89 -2
- package/src/defaults/gjc-skills.generated.ts +24 -17
- package/src/discovery/builtin.ts +18 -5
- package/src/discovery/helpers.ts +6 -6
- package/src/edit/index.ts +5 -2
- package/src/eval/py/executor.ts +8 -0
- package/src/extensibility/extensions/loader.ts +135 -13
- package/src/extensibility/extensions/wrapper.ts +1 -2
- package/src/extensibility/gjc-plugins/schema.ts +1 -1
- package/src/extensibility/runtime-skill-discovery.ts +2 -2
- package/src/extensibility/skill-management.ts +2 -3
- package/src/extensibility/skills.ts +16 -8
- package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
- package/src/gjc-runtime/gc-render.ts +0 -1
- package/src/gjc-runtime/gc-runtime.ts +1 -11
- package/src/gjc-runtime/launch-tmux.ts +3 -3
- package/src/gjc-runtime/psmux-detect.ts +2 -2
- package/src/gjc-runtime/python-transcript.ts +58 -0
- package/src/gjc-runtime/ralplan-runtime.ts +6 -18
- package/src/gjc-runtime/session-layout.ts +18 -7
- package/src/gjc-runtime/session-state-sidecar.ts +58 -9
- package/src/gjc-runtime/state-renderer.ts +0 -38
- package/src/gjc-runtime/state-runtime.ts +43 -22
- package/src/gjc-runtime/state-schema.ts +1 -1
- package/src/gjc-runtime/tmux-common.ts +7 -7
- package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
- package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
- package/src/gjc-runtime/workflow-command-ref.ts +15 -11
- package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
- package/src/gjc-runtime/workflow-manifest.ts +43 -83
- package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
- package/src/hooks/skill-keywords.ts +3 -3
- package/src/hooks/skill-state.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +26 -24
- package/src/lsp/index.ts +25 -5
- package/src/main.ts +2 -27
- package/src/modes/acp/acp-agent.ts +200 -75
- package/src/modes/components/model-selector.ts +216 -13
- package/src/modes/components/oauth-selector.ts +5 -5
- package/src/modes/components/skill-hud/render.ts +173 -35
- package/src/modes/components/smart-routing-panel.ts +531 -0
- package/src/modes/components/tool-status-header.ts +6 -3
- package/src/modes/controllers/command-controller.ts +17 -7
- package/src/modes/controllers/input-controller.ts +44 -9
- package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +298 -65
- package/src/modes/interactive-mode.ts +8 -1
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
- package/src/modes/types.ts +9 -1
- package/src/prompts/agents/planner.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/volatile-project-context.md +2 -1
- package/src/prompts/tools/move-session.md +15 -0
- package/src/prompts/tools/python.md +12 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/prompts/tools/skill.md +2 -2
- package/src/prompts/tools/task-summary.md +1 -0
- package/src/prompts/tools/task.md +6 -0
- package/src/rlm/artifacts.ts +12 -10
- package/src/runtime/optional-runtime-services.ts +3 -2
- package/src/runtime/workspace-tree-service.ts +11 -3
- package/src/runtime-mcp/config.ts +8 -1
- package/src/runtime-mcp/manager.ts +33 -0
- package/src/sdk/acp/adapter.ts +48 -1
- package/src/sdk/broker/broker.ts +30 -2
- package/src/sdk/broker/discovery.ts +217 -2
- package/src/sdk/broker/lifecycle.ts +385 -59
- package/src/sdk/broker/session-index.ts +243 -48
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
- package/src/sdk/bus/index.ts +208 -11
- package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
- package/src/sdk/bus/reconciliation-store.ts +27 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
- package/src/sdk/client/client.ts +204 -22
- package/src/sdk/host/host.ts +16 -0
- package/src/sdk/host/internal-autorouting-state.ts +13 -0
- package/src/sdk/host/model-pin.ts +101 -0
- package/src/sdk/host/reverse-leases.ts +2 -1
- package/src/sdk/host/session-runtime.test.ts +224 -1
- package/src/sdk/host/session-runtime.ts +125 -37
- package/src/sdk/lifecycle/service.ts +3 -1
- package/src/sdk/lifecycle-session.ts +42 -1
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/router/session-router.ts +375 -36
- package/src/sdk/session.ts +547 -47
- package/src/session/account-inventory.ts +1 -1
- package/src/session/agent-session.ts +765 -189
- package/src/session/artifacts.ts +234 -4
- package/src/session/internal/managed-session-storage.ts +91 -4
- package/src/session/provider-safety-stop-hint.ts +3 -2
- package/src/session/session-manager.ts +775 -16
- package/src/session/session-staging-paths.ts +8 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
- package/src/setup/host-plugin-setup.ts +2 -2
- package/src/skill-state/active-state.ts +2 -2
- package/src/skill-state/canonical-skills.ts +1 -1
- package/src/skill-state/initial-phase.ts +1 -1
- package/src/skill-state/workflow-hud.ts +41 -41
- package/src/skill-state/workflow-mutation-guard.ts +26 -7
- package/src/slash-commands/builtin-registry.ts +77 -0
- package/src/slash-commands/helpers/autorouting-status.ts +55 -0
- package/src/system-prompt.ts +102 -5
- package/src/task/discovery.ts +2 -3
- package/src/task/executor.ts +617 -43
- package/src/task/index.ts +262 -15
- package/src/task/receipt.ts +30 -0
- package/src/task/render.ts +11 -1
- package/src/task/types.ts +316 -0
- package/src/tools/atomic-file-write.ts +594 -0
- package/src/tools/bisect.ts +3 -2
- package/src/tools/descriptors.test.ts +3 -0
- package/src/tools/descriptors.ts +24 -0
- package/src/tools/image-gen.ts +5 -1
- package/src/tools/index.ts +62 -5
- package/src/tools/move-session.ts +107 -0
- package/src/tools/python.ts +205 -0
- package/src/tools/read.ts +88 -15
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resource-gc.ts +8 -177
- package/src/tools/search.ts +30 -6
- package/src/tools/skill-discovery.ts +1 -0
- package/src/tools/skill.ts +37 -7
- package/src/tools/subagent-render.ts +119 -13
- package/src/tools/subagent.ts +91 -74
- package/src/tools/tool-catalog.generated.ts +64 -2
- package/src/tools/write.ts +16 -12
- package/src/utils/xml-escape.ts +14 -0
- package/src/web/search/providers/codex.ts +2 -2
- package/src/workflow/workflow-intent-diff.ts +13 -12
- package/src/workflow/workflow-intent-report-consumer.ts +6 -6
- package/dist/types/autoresearch/helpers.d.ts +0 -24
- package/dist/types/autoresearch/index.d.ts +0 -2
- package/dist/types/autoresearch/state.d.ts +0 -17
- package/dist/types/autoresearch/storage.d.ts +0 -142
- package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
- package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
- package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
- package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
- package/dist/types/commands/team.d.ts +0 -24
- package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
- package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
- package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
- package/dist/types/gjc-runtime/team-store.d.ts +0 -168
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
- package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
- package/dist/types/rlm/index.d.ts +0 -47
- package/dist/types/rlm/preset.d.ts +0 -23
- package/dist/types/rlm/python-tool.d.ts +0 -16
- package/src/autoresearch/command-resume.md +0 -14
- package/src/autoresearch/index.ts +0 -536
- package/src/autoresearch/prompt.md +0 -103
- package/src/autoresearch/resume-message.md +0 -10
- package/src/autoresearch/state.ts +0 -273
- package/src/autoresearch/storage.ts +0 -699
- package/src/autoresearch/tools/init-experiment.ts +0 -272
- package/src/autoresearch/tools/log-experiment.ts +0 -524
- package/src/autoresearch/tools/run-experiment.ts +0 -475
- package/src/autoresearch/tools/update-notes.ts +0 -109
- package/src/commands/rlm.ts +0 -19
- package/src/commands/team.ts +0 -238
- package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
- package/src/defaults/gjc/skills/team/SKILL.md +0 -467
- package/src/gjc-runtime/team-gc.ts +0 -49
- package/src/gjc-runtime/team-launch.ts +0 -404
- package/src/gjc-runtime/team-notify.ts +0 -326
- package/src/gjc-runtime/team-runtime.ts +0 -5923
- package/src/gjc-runtime/team-store.ts +0 -878
- package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
- package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
- package/src/gjc-runtime/team-workers.ts +0 -1123
- package/src/prompts/system/rlm-report-command.md +0 -1
- package/src/prompts/system/rlm-research.md +0 -23
- package/src/rlm/index.ts +0 -365
- package/src/rlm/preset.ts +0 -76
- package/src/rlm/python-tool.ts +0 -68
|
@@ -2,8 +2,9 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as nodeFs from "node:fs";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { getAgentDir, isKnownSinkPeerClosedError } from "@gajae-code/utils";
|
|
5
|
+
import { getAgentDir, isKnownSinkPeerClosedError, logger } from "@gajae-code/utils";
|
|
6
6
|
import { normalizePathForComparison, VERSION } from "@gajae-code/utils/dirs";
|
|
7
|
+
import { withFileLock } from "../config/file-lock";
|
|
7
8
|
import {
|
|
8
9
|
COORDINATOR_MCP_PROTOCOL_VERSION,
|
|
9
10
|
COORDINATOR_MCP_SERVER_NAME,
|
|
@@ -52,6 +53,20 @@ import {
|
|
|
52
53
|
createDefaultCodexTransportFactory,
|
|
53
54
|
publishCodexWake,
|
|
54
55
|
} from "./codex-wake-publisher";
|
|
56
|
+
import {
|
|
57
|
+
appendCoordinatorFile,
|
|
58
|
+
CoordinatorPublicationUncertainError,
|
|
59
|
+
ensureCoordinatorDirectory,
|
|
60
|
+
syncCoordinatorDirectory,
|
|
61
|
+
writeCoordinatorAtomic,
|
|
62
|
+
} from "./durability";
|
|
63
|
+
import {
|
|
64
|
+
createDefaultEventWebhookDelivery,
|
|
65
|
+
deliverEventWebhook,
|
|
66
|
+
type EventWebhookConfig,
|
|
67
|
+
parseEventWebhookConfig,
|
|
68
|
+
type WebhookDelivery,
|
|
69
|
+
} from "./event-webhook";
|
|
55
70
|
import {
|
|
56
71
|
type CoordinatorModelProfileLoader,
|
|
57
72
|
loadCoordinatorModelProfiles,
|
|
@@ -190,10 +205,16 @@ interface CoordinatorServices {
|
|
|
190
205
|
readSdkBrokerDiscovery?: (agentDir: string) => Promise<BrokerDiscovery | null>;
|
|
191
206
|
getAgentDir?: () => string;
|
|
192
207
|
resolveModelProfiles?: CoordinatorModelProfileLoader;
|
|
208
|
+
resolveModelPin?: (raw: unknown, cwd?: string) => Promise<CoordinatorModelResolution>;
|
|
193
209
|
canonicalizePath?: (value: string) => Promise<string>;
|
|
194
210
|
codexTransportFactory?: CodexTransportFactory;
|
|
211
|
+
eventWebhookDelivery?: WebhookDelivery;
|
|
195
212
|
}
|
|
196
213
|
|
|
214
|
+
type CoordinatorModelResolution =
|
|
215
|
+
| { ok: true; model: string | null }
|
|
216
|
+
| { ok: false; reason: "unknown_model"; model: string; error: string };
|
|
217
|
+
|
|
197
218
|
interface CoordinatorMcpServerOptions {
|
|
198
219
|
env?: NodeJS.ProcessEnv;
|
|
199
220
|
services?: CoordinatorServices;
|
|
@@ -308,6 +329,28 @@ type CoordinatorEventKind =
|
|
|
308
329
|
| "tmux.delivery_failed"
|
|
309
330
|
| "delegation.started";
|
|
310
331
|
|
|
332
|
+
const COORDINATOR_EVENT_KINDS: ReadonlySet<string> = new Set<CoordinatorEventKind>([
|
|
333
|
+
"session.registered",
|
|
334
|
+
"session.started",
|
|
335
|
+
"session.reaped",
|
|
336
|
+
"session.state_changed",
|
|
337
|
+
"turn.queued",
|
|
338
|
+
"turn.delivering",
|
|
339
|
+
"turn.active",
|
|
340
|
+
"turn.acknowledged",
|
|
341
|
+
"turn.waiting_for_answer",
|
|
342
|
+
"turn.completed",
|
|
343
|
+
"turn.failed",
|
|
344
|
+
"turn.cancelled",
|
|
345
|
+
"turn.superseded",
|
|
346
|
+
"question.opened",
|
|
347
|
+
"question.answered",
|
|
348
|
+
"report.written",
|
|
349
|
+
"tmux.delivery_succeeded",
|
|
350
|
+
"tmux.delivery_failed",
|
|
351
|
+
"delegation.started",
|
|
352
|
+
]);
|
|
353
|
+
|
|
311
354
|
interface CoordinatorEvent {
|
|
312
355
|
schema_version: 1;
|
|
313
356
|
seq: number;
|
|
@@ -385,6 +428,11 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
385
428
|
description:
|
|
386
429
|
"Optional GJC model profile (`gjc --mpreset <profile>`). Unknown names are rejected with the available-profile listing.",
|
|
387
430
|
};
|
|
431
|
+
const modelPin = {
|
|
432
|
+
type: "string",
|
|
433
|
+
description:
|
|
434
|
+
"Optional explicit model pin (`gjc --model <provider/model>`, e.g. cursor/claude-fable-5-xhigh). Unknown ids are rejected with the same not-found error as the CLI; when both model and mpreset are given, the explicit model wins exactly like `gjc --mpreset <p> --model <m>`.",
|
|
435
|
+
};
|
|
388
436
|
|
|
389
437
|
const common = { type: "object", properties: {} as Record<string, unknown> };
|
|
390
438
|
const idempotencyKey = {
|
|
@@ -431,6 +479,7 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
431
479
|
"Create the session prepared instead of ready: no readiness is published and no initial prompt is accepted until gjc_coordinator_activate_session proves activation.",
|
|
432
480
|
},
|
|
433
481
|
mpreset,
|
|
482
|
+
model: modelPin,
|
|
434
483
|
idempotency_key: idempotencyKey,
|
|
435
484
|
allow_mutation: allowMutation,
|
|
436
485
|
},
|
|
@@ -714,11 +763,7 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
714
763
|
description: "When reusing a session with an active turn, supersede it before sending.",
|
|
715
764
|
},
|
|
716
765
|
mpreset,
|
|
717
|
-
|
|
718
|
-
model: {
|
|
719
|
-
type: "string",
|
|
720
|
-
description: "Optional model hint passed in prompt metadata; no provider default is implied.",
|
|
721
|
-
},
|
|
766
|
+
model: modelPin,
|
|
722
767
|
await_completion: { type: "boolean", description: "If true, poll the turn until terminal or timeout." },
|
|
723
768
|
timeout_ms: {
|
|
724
769
|
type: "number",
|
|
@@ -734,7 +779,7 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
734
779
|
return { name, description: "List known scoped GJC coordinator bridge sessions.", inputSchema: common };
|
|
735
780
|
}
|
|
736
781
|
|
|
737
|
-
type DelegateWorkflow = "plan" | "execute"
|
|
782
|
+
type DelegateWorkflow = "plan" | "execute";
|
|
738
783
|
|
|
739
784
|
function workflowForDelegateTool(name: string): DelegateWorkflow | null {
|
|
740
785
|
switch (name) {
|
|
@@ -742,21 +787,17 @@ function workflowForDelegateTool(name: string): DelegateWorkflow | null {
|
|
|
742
787
|
return "plan";
|
|
743
788
|
case "gjc_delegate_execute":
|
|
744
789
|
return "execute";
|
|
745
|
-
case "gjc_delegate_team":
|
|
746
|
-
return "team";
|
|
747
790
|
default:
|
|
748
791
|
return null;
|
|
749
792
|
}
|
|
750
793
|
}
|
|
751
794
|
|
|
752
|
-
function workflowSkill(workflow: DelegateWorkflow): "ralplan" | "ultragoal"
|
|
795
|
+
function workflowSkill(workflow: DelegateWorkflow): "ralplan" | "ultragoal" {
|
|
753
796
|
switch (workflow) {
|
|
754
797
|
case "plan":
|
|
755
798
|
return "ralplan";
|
|
756
799
|
case "execute":
|
|
757
800
|
return "ultragoal";
|
|
758
|
-
case "team":
|
|
759
|
-
return "team";
|
|
760
801
|
}
|
|
761
802
|
}
|
|
762
803
|
|
|
@@ -766,8 +807,6 @@ function delegateToolDescription(workflow: DelegateWorkflow): string {
|
|
|
766
807
|
return "Delegate consensus planning to GJC: start a session and run /skill:ralplan to completion, returning durable turn status and artifact references.";
|
|
767
808
|
case "execute":
|
|
768
809
|
return "Delegate execution to GJC: start a session and run /skill:ultragoal to completion, returning durable turn status and artifact references.";
|
|
769
|
-
case "team":
|
|
770
|
-
return "Delegate parallel team execution to GJC: start a session and run /skill:team to completion, returning durable turn status and artifact references.";
|
|
771
810
|
}
|
|
772
811
|
}
|
|
773
812
|
|
|
@@ -853,20 +892,46 @@ function coordinatorLifecycleTarget(sessionCommand: string | null, cwd: string):
|
|
|
853
892
|
}
|
|
854
893
|
|
|
855
894
|
async function ensureDir(dir: string): Promise<void> {
|
|
856
|
-
await
|
|
895
|
+
await ensureCoordinatorDirectory(dir);
|
|
857
896
|
}
|
|
858
897
|
|
|
859
898
|
async function readJsonFile(file: string): Promise<unknown | null> {
|
|
899
|
+
let source: string;
|
|
860
900
|
try {
|
|
861
|
-
|
|
862
|
-
} catch {
|
|
901
|
+
source = await fs.readFile(file, "utf8");
|
|
902
|
+
} catch (error) {
|
|
903
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
863
904
|
return null;
|
|
864
905
|
}
|
|
906
|
+
try {
|
|
907
|
+
return JSON.parse(source);
|
|
908
|
+
} catch (error) {
|
|
909
|
+
throw new Error(
|
|
910
|
+
`invalid coordinator projection ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async function removeCoordinatorFile(file: string): Promise<void> {
|
|
916
|
+
try {
|
|
917
|
+
await fs.lstat(file);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
920
|
+
try {
|
|
921
|
+
await fs.stat(path.dirname(file));
|
|
922
|
+
} catch (parentError) {
|
|
923
|
+
if ((parentError as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
924
|
+
throw parentError;
|
|
925
|
+
}
|
|
926
|
+
await syncCoordinatorDirectory(path.dirname(file));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
await fs.rm(file, { force: true });
|
|
930
|
+
await syncCoordinatorDirectory(path.dirname(file));
|
|
865
931
|
}
|
|
866
932
|
|
|
867
933
|
async function writeJsonFile(file: string, value: unknown): Promise<void> {
|
|
868
|
-
await
|
|
869
|
-
await fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
934
|
+
await writeCoordinatorAtomic(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
870
935
|
}
|
|
871
936
|
|
|
872
937
|
const COORDINATOR_IDEMPOTENCY_RESPONSE_BYTE_CAP = 64 * 1024;
|
|
@@ -904,27 +969,7 @@ async function readCoordinatorIdempotencyFile(file: string): Promise<Coordinator
|
|
|
904
969
|
}
|
|
905
970
|
|
|
906
971
|
async function writeCoordinatorIdempotencyFile(file: string, value: CoordinatorToolIdempotencyRecord): Promise<void> {
|
|
907
|
-
await
|
|
908
|
-
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
909
|
-
try {
|
|
910
|
-
const handle = await fs.open(temporary, "wx", 0o600);
|
|
911
|
-
try {
|
|
912
|
-
await handle.writeFile(`${JSON.stringify(value)}\n`);
|
|
913
|
-
await handle.sync();
|
|
914
|
-
} finally {
|
|
915
|
-
await handle.close();
|
|
916
|
-
}
|
|
917
|
-
await fs.rename(temporary, file);
|
|
918
|
-
const directory = await fs.open(path.dirname(file), "r");
|
|
919
|
-
try {
|
|
920
|
-
await directory.sync();
|
|
921
|
-
} finally {
|
|
922
|
-
await directory.close();
|
|
923
|
-
}
|
|
924
|
-
} catch (error) {
|
|
925
|
-
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
926
|
-
throw error;
|
|
927
|
-
}
|
|
972
|
+
await writeCoordinatorAtomic(file, `${JSON.stringify(value)}\n`);
|
|
928
973
|
}
|
|
929
974
|
|
|
930
975
|
function canonicalJson(value: unknown): string {
|
|
@@ -1139,11 +1184,22 @@ async function listJsonFiles(dir: string): Promise<unknown[]> {
|
|
|
1139
1184
|
try {
|
|
1140
1185
|
const entries = await fs.readdir(dir);
|
|
1141
1186
|
const values = await Promise.all(
|
|
1142
|
-
entries
|
|
1187
|
+
entries
|
|
1188
|
+
.filter(entry => entry.endsWith(".json") && !entry.startsWith(".gjc-"))
|
|
1189
|
+
.map(async entry => {
|
|
1190
|
+
try {
|
|
1191
|
+
return await readJsonFile(path.join(dir, entry));
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
1194
|
+
logger.warn("Coordinator projection read failed", { path: entry, error: String(error) });
|
|
1195
|
+
throw error;
|
|
1196
|
+
}
|
|
1197
|
+
}),
|
|
1143
1198
|
);
|
|
1144
1199
|
return values.filter(value => value !== null);
|
|
1145
|
-
} catch {
|
|
1146
|
-
return [];
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
1202
|
+
throw error;
|
|
1147
1203
|
}
|
|
1148
1204
|
}
|
|
1149
1205
|
|
|
@@ -1207,7 +1263,7 @@ function publicCoordinatorSession(session: Record<string, unknown>): Record<stri
|
|
|
1207
1263
|
const result: Record<string, unknown> = {
|
|
1208
1264
|
session_id: firstString(session, ["session_id", "sessionId"]) ?? "unknown",
|
|
1209
1265
|
};
|
|
1210
|
-
for (const key of ["cwd", "created_at", "mpreset"]) {
|
|
1266
|
+
for (const key of ["cwd", "created_at", "mpreset", "model"]) {
|
|
1211
1267
|
const value = session[key];
|
|
1212
1268
|
if (typeof value === "string") result[key] = value;
|
|
1213
1269
|
}
|
|
@@ -1328,6 +1384,10 @@ function eventSequenceFile(namespaceDir: string): string {
|
|
|
1328
1384
|
return path.join(eventsDir(namespaceDir), "latest-seq.json");
|
|
1329
1385
|
}
|
|
1330
1386
|
|
|
1387
|
+
function eventJournalLockFile(namespaceDir: string): string {
|
|
1388
|
+
return path.join(eventsDir(namespaceDir), "event-journal.lock");
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1331
1391
|
function boundSummary(value: string): string {
|
|
1332
1392
|
const normalized = value
|
|
1333
1393
|
.replace(/[\r\n\t]+/g, " ")
|
|
@@ -1337,17 +1397,64 @@ function boundSummary(value: string): string {
|
|
|
1337
1397
|
}
|
|
1338
1398
|
|
|
1339
1399
|
async function readLatestEventSeq(namespaceDir: string): Promise<number> {
|
|
1340
|
-
|
|
1400
|
+
let sequence: Record<string, unknown> | null = null;
|
|
1401
|
+
try {
|
|
1402
|
+
sequence = asRecord(await readJsonFile(eventSequenceFile(namespaceDir)));
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
logger.warn("Coordinator sequence cache unreadable; recovering from journal", { error: String(error) });
|
|
1405
|
+
}
|
|
1406
|
+
const events = await readCoordinatorEvents(namespaceDir);
|
|
1407
|
+
const journalSeq = events.at(-1)?.seq ?? 0;
|
|
1341
1408
|
const seq = sequence?.seq;
|
|
1342
|
-
if (typeof seq === "number" && Number.
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
return latestSeq;
|
|
1409
|
+
if (typeof seq === "number" && Number.isSafeInteger(seq) && seq >= 0 && seq > journalSeq)
|
|
1410
|
+
throw new Error("state_corrupt");
|
|
1411
|
+
return journalSeq;
|
|
1346
1412
|
}
|
|
1347
1413
|
|
|
1348
1414
|
const eventAppendQueues = new Map<string, Promise<unknown>>();
|
|
1349
1415
|
const codexWakeTransportFactories = new Map<string, CodexTransportFactory>();
|
|
1350
1416
|
const codexWakePublishTails = new Map<string, Promise<void>>();
|
|
1417
|
+
const eventWebhookConfigs = new Map<string, EventWebhookConfig | null>();
|
|
1418
|
+
const eventWebhookDeliveries = new Map<string, WebhookDelivery>();
|
|
1419
|
+
const eventWebhookTails = new Map<string, Promise<void>>();
|
|
1420
|
+
const EVENT_WEBHOOK_ERROR_CAP = 240;
|
|
1421
|
+
|
|
1422
|
+
async function appendEventWebhookDiagnostic(namespaceDir: string, eventId: string, error: unknown): Promise<void> {
|
|
1423
|
+
const code =
|
|
1424
|
+
error instanceof Error && /^[a-z0-9_]+$/.test(error.message)
|
|
1425
|
+
? error.message.slice(0, EVENT_WEBHOOK_ERROR_CAP)
|
|
1426
|
+
: "event_webhook_delivery_failed";
|
|
1427
|
+
const line = `${new Date().toISOString()} event=${eventId} error=${code}\n`;
|
|
1428
|
+
try {
|
|
1429
|
+
await fs.appendFile(path.join(namespaceDir, "event-webhook-errors.log"), line.slice(0, 512), { mode: 0o600 });
|
|
1430
|
+
} catch {
|
|
1431
|
+
// Diagnostics are best-effort; sink problems must never propagate upward.
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function enqueueEventWebhook(namespaceDir: string, event: CoordinatorEvent): void {
|
|
1436
|
+
const config = eventWebhookConfigs.get(namespaceDir);
|
|
1437
|
+
if (config === null || config === undefined) return;
|
|
1438
|
+
const delivery = eventWebhookDeliveries.get(namespaceDir);
|
|
1439
|
+
if (!delivery) return;
|
|
1440
|
+
const previous = eventWebhookTails.get(namespaceDir) ?? Promise.resolve();
|
|
1441
|
+
const next = previous
|
|
1442
|
+
.then(async () => {
|
|
1443
|
+
await deliverEventWebhook(namespaceDir, config, event, () => JSON.stringify(event), delivery);
|
|
1444
|
+
})
|
|
1445
|
+
.catch(async error => {
|
|
1446
|
+
await appendEventWebhookDiagnostic(namespaceDir, event.id, error);
|
|
1447
|
+
});
|
|
1448
|
+
eventWebhookTails.set(namespaceDir, next);
|
|
1449
|
+
void next.finally(() => {
|
|
1450
|
+
if (eventWebhookTails.get(namespaceDir) === next) eventWebhookTails.delete(namespaceDir);
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/** Test-only helper that waits for queued event webhook deliveries in a namespace. */
|
|
1455
|
+
export async function awaitEventWebhookDeliveriesForTest(namespaceDir: string): Promise<void> {
|
|
1456
|
+
await Promise.all([...eventWebhookTails.entries()].filter(([key]) => key === namespaceDir).map(([, tail]) => tail));
|
|
1457
|
+
}
|
|
1351
1458
|
|
|
1352
1459
|
const CODEX_WAKE_ERROR_CAP = 240;
|
|
1353
1460
|
const CODEX_WAKE_DIAGNOSTIC_CAP = 512;
|
|
@@ -1366,13 +1473,15 @@ async function appendCodexWakeDiagnostic(
|
|
|
1366
1473
|
): Promise<void> {
|
|
1367
1474
|
const line = `${new Date().toISOString()} event=${event.id} error=${codexWakeErrorCode(error)}\n`;
|
|
1368
1475
|
try {
|
|
1369
|
-
await
|
|
1370
|
-
|
|
1476
|
+
await appendCoordinatorFile(
|
|
1477
|
+
path.join(namespaceDir, "codex-wake-errors.log"),
|
|
1478
|
+
line.slice(0, CODEX_WAKE_DIAGNOSTIC_CAP),
|
|
1479
|
+
);
|
|
1480
|
+
} catch (diagnosticError) {
|
|
1481
|
+
logger.warn("Coordinator Codex wake diagnostic persistence failed", {
|
|
1482
|
+
eventId: event.id,
|
|
1483
|
+
error: String(diagnosticError),
|
|
1371
1484
|
});
|
|
1372
|
-
} catch {
|
|
1373
|
-
try {
|
|
1374
|
-
process.stderr.write("codex-wake-diagnostic-unwritable\n");
|
|
1375
|
-
} catch {}
|
|
1376
1485
|
}
|
|
1377
1486
|
}
|
|
1378
1487
|
|
|
@@ -1380,17 +1489,21 @@ async function appendCodexWakePublishDiagnostic(
|
|
|
1380
1489
|
namespaceDir: string,
|
|
1381
1490
|
event: CodexWakeEventV1,
|
|
1382
1491
|
error: unknown,
|
|
1383
|
-
): Promise<
|
|
1492
|
+
): Promise<unknown | null> {
|
|
1384
1493
|
const line = `${new Date().toISOString()} wake=${event.key} error=${codexWakeErrorCode(error)}\n`;
|
|
1385
1494
|
try {
|
|
1386
|
-
await
|
|
1387
|
-
|
|
1495
|
+
await appendCoordinatorFile(
|
|
1496
|
+
path.join(namespaceDir, "codex-wake-errors.log"),
|
|
1497
|
+
line.slice(0, CODEX_WAKE_DIAGNOSTIC_CAP),
|
|
1498
|
+
);
|
|
1499
|
+
} catch (diagnosticError) {
|
|
1500
|
+
logger.warn("Coordinator Codex wake publish diagnostic persistence failed", {
|
|
1501
|
+
wakeKey: event.key,
|
|
1502
|
+
error: String(diagnosticError),
|
|
1388
1503
|
});
|
|
1389
|
-
|
|
1390
|
-
try {
|
|
1391
|
-
process.stderr.write("codex-wake-diagnostic-unwritable\n");
|
|
1392
|
-
} catch {}
|
|
1504
|
+
return diagnosticError;
|
|
1393
1505
|
}
|
|
1506
|
+
return null;
|
|
1394
1507
|
}
|
|
1395
1508
|
|
|
1396
1509
|
async function autoBindDelegateCodexHandoff(
|
|
@@ -1414,13 +1527,13 @@ async function autoBindDelegateCodexHandoff(
|
|
|
1414
1527
|
let source: CodexHandoffRegistrationV1 | null;
|
|
1415
1528
|
try {
|
|
1416
1529
|
source = await readCodexHandoff(namespaceDir, explicitHostWorkUnit);
|
|
1417
|
-
} catch {
|
|
1530
|
+
} catch (error) {
|
|
1418
1531
|
await appendCodexWakeDiagnostic(
|
|
1419
1532
|
namespaceDir,
|
|
1420
1533
|
diagnosticEvent,
|
|
1421
1534
|
new Error("codex_handoff_explicit_source_missing"),
|
|
1422
1535
|
);
|
|
1423
|
-
|
|
1536
|
+
throw error;
|
|
1424
1537
|
}
|
|
1425
1538
|
if (!source) {
|
|
1426
1539
|
await appendCodexWakeDiagnostic(
|
|
@@ -1448,7 +1561,7 @@ async function autoBindDelegateCodexHandoff(
|
|
|
1448
1561
|
return { auto_bound: true, thread_id: binding.handoff.thread_id };
|
|
1449
1562
|
} catch (error) {
|
|
1450
1563
|
await appendCodexWakeDiagnostic(namespaceDir, diagnosticEvent, error);
|
|
1451
|
-
|
|
1564
|
+
throw error;
|
|
1452
1565
|
}
|
|
1453
1566
|
}
|
|
1454
1567
|
try {
|
|
@@ -1505,7 +1618,7 @@ async function autoBindDelegateCodexHandoff(
|
|
|
1505
1618
|
return { auto_bound: true, thread_id: binding.handoff.thread_id };
|
|
1506
1619
|
} catch (error) {
|
|
1507
1620
|
await appendCodexWakeDiagnostic(namespaceDir, diagnosticEvent, error);
|
|
1508
|
-
|
|
1621
|
+
throw error;
|
|
1509
1622
|
}
|
|
1510
1623
|
}
|
|
1511
1624
|
|
|
@@ -1540,27 +1653,31 @@ async function publishRecordedCodexWake(
|
|
|
1540
1653
|
if (event.status !== "pending" && event.status !== "failed") return "skipped";
|
|
1541
1654
|
const transportFactory = codexWakeTransportFactories.get(namespaceDir);
|
|
1542
1655
|
if (!transportFactory) return "skipped";
|
|
1656
|
+
let published: { published: boolean };
|
|
1543
1657
|
try {
|
|
1544
|
-
|
|
1545
|
-
await updateCodexWakeEvent(namespaceDir, event.key, {
|
|
1546
|
-
...(published.published ? { status: "published" as const } : {}),
|
|
1547
|
-
attempts_delta: 1,
|
|
1548
|
-
last_error: null,
|
|
1549
|
-
});
|
|
1550
|
-
return published.published ? "published" : "thread_active_pending";
|
|
1658
|
+
published = await publishCodexWake({ handoff, event, transportFactory });
|
|
1551
1659
|
} catch (error) {
|
|
1552
|
-
await appendCodexWakePublishDiagnostic(namespaceDir, event, error);
|
|
1660
|
+
const diagnosticError = await appendCodexWakePublishDiagnostic(namespaceDir, event, error);
|
|
1553
1661
|
try {
|
|
1554
1662
|
await updateCodexWakeEvent(namespaceDir, event.key, {
|
|
1555
1663
|
status: "failed",
|
|
1556
1664
|
attempts_delta: 1,
|
|
1557
1665
|
last_error: codexWakeErrorCode(error),
|
|
1558
1666
|
});
|
|
1559
|
-
} catch (
|
|
1560
|
-
|
|
1667
|
+
} catch (stateError) {
|
|
1668
|
+
throw new AggregateError(
|
|
1669
|
+
[error, stateError, ...(diagnosticError ? [diagnosticError] : [])],
|
|
1670
|
+
"Codex wake publication and failure-state persistence failed",
|
|
1671
|
+
);
|
|
1561
1672
|
}
|
|
1562
1673
|
return "failed";
|
|
1563
1674
|
}
|
|
1675
|
+
await updateCodexWakeEvent(namespaceDir, event.key, {
|
|
1676
|
+
...(published.published ? { status: "published" as const } : {}),
|
|
1677
|
+
attempts_delta: 1,
|
|
1678
|
+
last_error: null,
|
|
1679
|
+
});
|
|
1680
|
+
return published.published ? "published" : "thread_active_pending";
|
|
1564
1681
|
}
|
|
1565
1682
|
|
|
1566
1683
|
async function publishPendingCodexWakes(namespaceDir: string, threadId: string): Promise<void> {
|
|
@@ -1586,18 +1703,20 @@ function enqueueCodexWakePublish(namespaceDir: string, handoff: CodexHandoffRegi
|
|
|
1586
1703
|
const tailKey = codexWakeTailKey(namespaceDir, handoff.thread_id);
|
|
1587
1704
|
const previous = codexWakePublishTails.get(tailKey) ?? Promise.resolve();
|
|
1588
1705
|
const next = previous
|
|
1589
|
-
.then(() => publishPendingCodexWakes(namespaceDir, handoff.thread_id))
|
|
1590
1706
|
.catch(async error => {
|
|
1591
1707
|
await appendCodexWakeDiagnostic(
|
|
1592
1708
|
namespaceDir,
|
|
1593
1709
|
{ id: `wake-queue:${handoff.thread_id}` } as CoordinatorEvent,
|
|
1594
1710
|
error,
|
|
1595
1711
|
);
|
|
1596
|
-
})
|
|
1712
|
+
})
|
|
1713
|
+
.then(() => publishPendingCodexWakes(namespaceDir, handoff.thread_id));
|
|
1597
1714
|
codexWakePublishTails.set(tailKey, next);
|
|
1598
|
-
void next
|
|
1599
|
-
|
|
1600
|
-
|
|
1715
|
+
void next
|
|
1716
|
+
.catch(() => undefined)
|
|
1717
|
+
.finally(() => {
|
|
1718
|
+
if (codexWakePublishTails.get(tailKey) === next) codexWakePublishTails.delete(tailKey);
|
|
1719
|
+
});
|
|
1601
1720
|
}
|
|
1602
1721
|
|
|
1603
1722
|
/** Test-only helper that waits for queued Codex wake publishes in a namespace. */
|
|
@@ -1623,31 +1742,48 @@ async function appendCoordinatorEvent(namespaceDir: string, input: CoordinatorEv
|
|
|
1623
1742
|
|
|
1624
1743
|
await previous.catch(() => undefined);
|
|
1625
1744
|
try {
|
|
1626
|
-
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
await
|
|
1648
|
-
return
|
|
1745
|
+
await ensureCoordinatorDirectory(eventsDir(namespaceDir));
|
|
1746
|
+
const event = await withFileLock(eventJournalLockFile(namespaceDir), async () => {
|
|
1747
|
+
const latestSeq = await readLatestEventSeq(namespaceDir);
|
|
1748
|
+
if (!Number.isSafeInteger(latestSeq) || latestSeq >= Number.MAX_SAFE_INTEGER) throw new Error("state_corrupt");
|
|
1749
|
+
const seq = latestSeq + 1;
|
|
1750
|
+
const timestamp = new Date().toISOString();
|
|
1751
|
+
const event: CoordinatorEvent = {
|
|
1752
|
+
schema_version: 1,
|
|
1753
|
+
seq,
|
|
1754
|
+
id: `event-${seq.toString().padStart(12, "0")}`,
|
|
1755
|
+
timestamp,
|
|
1756
|
+
kind: input.kind,
|
|
1757
|
+
summary: boundSummary(input.summary),
|
|
1758
|
+
...(input.sessionId ? { session_id: input.sessionId } : {}),
|
|
1759
|
+
...(input.turnId ? { turn_id: input.turnId } : {}),
|
|
1760
|
+
...(input.questionId ? { question_id: input.questionId } : {}),
|
|
1761
|
+
...(input.reportId ? { report_id: input.reportId } : {}),
|
|
1762
|
+
...(input.payloadRef ? { payload_ref: input.payloadRef } : {}),
|
|
1763
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
1764
|
+
};
|
|
1765
|
+
await appendCoordinatorFile(eventJournalFile(namespaceDir), `${JSON.stringify(event)}\n`);
|
|
1766
|
+
await writeJsonFile(eventSequenceFile(namespaceDir), { seq, updated_at: timestamp });
|
|
1767
|
+
return event;
|
|
1649
1768
|
});
|
|
1769
|
+
let codexWake: { handoff: CodexHandoffRegistrationV1; event: CodexWakeEventV1 | null } | null;
|
|
1770
|
+
try {
|
|
1771
|
+
codexWake = await maybeRecordCodexWake(namespaceDir, event);
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
try {
|
|
1774
|
+
await appendCodexWakeDiagnostic(namespaceDir, event, error);
|
|
1775
|
+
} catch (diagnosticError) {
|
|
1776
|
+
// The canonical event is already durable. Preserve that committed
|
|
1777
|
+
// result even when the optional diagnostic cannot be persisted.
|
|
1778
|
+
logger.warn("Coordinator Codex wake diagnostic persistence failed", {
|
|
1779
|
+
eventId: event.id,
|
|
1780
|
+
error: String(diagnosticError),
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
codexWake = null;
|
|
1784
|
+
}
|
|
1650
1785
|
if (codexWake) enqueueCodexWakePublish(namespaceDir, codexWake.handoff);
|
|
1786
|
+
if (eventWebhookConfigs.has(namespaceDir)) enqueueEventWebhook(namespaceDir, event);
|
|
1651
1787
|
return event;
|
|
1652
1788
|
} finally {
|
|
1653
1789
|
release();
|
|
@@ -1664,9 +1800,22 @@ export async function appendCoordinatorEventForTest(
|
|
|
1664
1800
|
|
|
1665
1801
|
function parseCoordinatorEvent(line: string): CoordinatorEvent | null {
|
|
1666
1802
|
try {
|
|
1667
|
-
const event = JSON.parse(line) as CoordinatorEvent
|
|
1668
|
-
if (
|
|
1669
|
-
|
|
1803
|
+
const event = JSON.parse(line) as Partial<CoordinatorEvent>;
|
|
1804
|
+
if (
|
|
1805
|
+
event.schema_version !== 1 ||
|
|
1806
|
+
typeof event.seq !== "number" ||
|
|
1807
|
+
!Number.isSafeInteger(event.seq) ||
|
|
1808
|
+
event.seq < 0 ||
|
|
1809
|
+
typeof event.id !== "string" ||
|
|
1810
|
+
event.id.length === 0 ||
|
|
1811
|
+
typeof event.timestamp !== "string" ||
|
|
1812
|
+
typeof event.summary !== "string" ||
|
|
1813
|
+
typeof event.kind !== "string" ||
|
|
1814
|
+
!COORDINATOR_EVENT_KINDS.has(event.kind) ||
|
|
1815
|
+
event.id !== `event-${event.seq.toString().padStart(12, "0")}`
|
|
1816
|
+
)
|
|
1817
|
+
return null;
|
|
1818
|
+
return event as CoordinatorEvent;
|
|
1670
1819
|
} catch {
|
|
1671
1820
|
return null;
|
|
1672
1821
|
}
|
|
@@ -1675,13 +1824,23 @@ function parseCoordinatorEvent(line: string): CoordinatorEvent | null {
|
|
|
1675
1824
|
async function readCoordinatorEvents(namespaceDir: string): Promise<CoordinatorEvent[]> {
|
|
1676
1825
|
try {
|
|
1677
1826
|
const content = await fs.readFile(eventJournalFile(namespaceDir), "utf8");
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
.
|
|
1683
|
-
|
|
1684
|
-
|
|
1827
|
+
const events: CoordinatorEvent[] = [];
|
|
1828
|
+
const seenIds = new Set<string>();
|
|
1829
|
+
const seenSeqs = new Set<number>();
|
|
1830
|
+
for (const rawLine of content.split("\n")) {
|
|
1831
|
+
const line = rawLine.trim();
|
|
1832
|
+
if (!line) continue;
|
|
1833
|
+
const event = parseCoordinatorEvent(line);
|
|
1834
|
+
if (!event) throw new Error("state_corrupt");
|
|
1835
|
+
if (seenIds.has(event.id) || seenSeqs.has(event.seq)) throw new Error("state_corrupt");
|
|
1836
|
+
seenIds.add(event.id);
|
|
1837
|
+
seenSeqs.add(event.seq);
|
|
1838
|
+
events.push(event);
|
|
1839
|
+
}
|
|
1840
|
+
events.sort((left, right) => left.seq - right.seq);
|
|
1841
|
+
for (let index = 0; index < events.length; index += 1)
|
|
1842
|
+
if (events[index]!.seq !== index + 1) throw new Error("state_corrupt");
|
|
1843
|
+
return events;
|
|
1685
1844
|
} catch (error) {
|
|
1686
1845
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
1687
1846
|
throw error;
|
|
@@ -1837,7 +1996,7 @@ async function writeActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<
|
|
|
1837
1996
|
|
|
1838
1997
|
async function clearActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<void> {
|
|
1839
1998
|
const active = asRecord(await readJsonFile(activeTurnFile(namespaceDir, turn.session_id)));
|
|
1840
|
-
if (active?.turn_id === turn.turn_id) await
|
|
1999
|
+
if (active?.turn_id === turn.turn_id) await removeCoordinatorFile(activeTurnFile(namespaceDir, turn.session_id));
|
|
1841
2000
|
}
|
|
1842
2001
|
|
|
1843
2002
|
async function readSessionState(namespaceDir: string, sessionId: string): Promise<CoordinatorSessionState | null> {
|
|
@@ -2386,13 +2545,59 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2386
2545
|
namespaceDir,
|
|
2387
2546
|
services.codexTransportFactory ?? createDefaultCodexTransportFactory(),
|
|
2388
2547
|
);
|
|
2389
|
-
|
|
2548
|
+
let eventWebhookConfig: EventWebhookConfig | null;
|
|
2549
|
+
try {
|
|
2550
|
+
// `env` here may be the merged process environment, which Bun has
|
|
2551
|
+
// already loaded the cwd `.env` overlay into; that is not a trusted
|
|
2552
|
+
// source for an egress destination. Only an explicitly provided
|
|
2553
|
+
// operator env (rendered hermes block, tests) may select it.
|
|
2554
|
+
eventWebhookConfig = parseEventWebhookConfig(options.env);
|
|
2555
|
+
} catch (error) {
|
|
2556
|
+
// Fail closed: an invalid webhook config disables delivery rather than
|
|
2557
|
+
// crashing the whole coordinator bridge. `gjc coordinator doctor`
|
|
2558
|
+
// surfaces the same error as a failing check.
|
|
2559
|
+
eventWebhookConfig = null;
|
|
2560
|
+
const reason = error instanceof Error ? error.message : "coordinator_event_webhook_invalid";
|
|
2561
|
+
void (async () => {
|
|
2562
|
+
try {
|
|
2563
|
+
await fs.mkdir(namespaceDir, { recursive: true });
|
|
2564
|
+
await fs.appendFile(
|
|
2565
|
+
path.join(namespaceDir, "event-webhook-errors.log"),
|
|
2566
|
+
`${new Date().toISOString()} config_error=${reason}\n`,
|
|
2567
|
+
{ mode: 0o600 },
|
|
2568
|
+
);
|
|
2569
|
+
} catch {
|
|
2570
|
+
// Diagnostics are best-effort; delivery stays disabled either way.
|
|
2571
|
+
}
|
|
2572
|
+
})();
|
|
2573
|
+
}
|
|
2574
|
+
eventWebhookConfigs.set(namespaceDir, eventWebhookConfig);
|
|
2575
|
+
eventWebhookDeliveries.set(namespaceDir, services.eventWebhookDelivery ?? createDefaultEventWebhookDelivery());
|
|
2576
|
+
const startupEventWebhookReplay = (async () => {
|
|
2577
|
+
if (eventWebhookConfig === null) return;
|
|
2390
2578
|
try {
|
|
2391
|
-
for (const
|
|
2579
|
+
for (const event of await readCoordinatorEvents(namespaceDir)) enqueueEventWebhook(namespaceDir, event);
|
|
2580
|
+
await awaitEventWebhookDeliveriesForTest(namespaceDir);
|
|
2581
|
+
} catch (error) {
|
|
2582
|
+
await appendEventWebhookDiagnostic(namespaceDir, "startup-replay", error);
|
|
2583
|
+
}
|
|
2584
|
+
})();
|
|
2585
|
+
void startupEventWebhookReplay;
|
|
2586
|
+
const startupCodexWakeReplay = (async () => {
|
|
2587
|
+
try {
|
|
2588
|
+
for (const handoff of await listCodexHandoffs(namespaceDir)) {
|
|
2589
|
+
enqueueCodexWakePublish(namespaceDir, handoff);
|
|
2590
|
+
await awaitCodexWakePublishesForTest(namespaceDir);
|
|
2591
|
+
}
|
|
2392
2592
|
} catch (error) {
|
|
2393
2593
|
await appendCodexWakeDiagnostic(namespaceDir, { id: "startup-drain" }, error);
|
|
2594
|
+
throw error;
|
|
2394
2595
|
}
|
|
2395
2596
|
})();
|
|
2597
|
+
// Startup replay is optional. Keep its rejection observable to callers that
|
|
2598
|
+
// explicitly await the replay, but never let an unhandled replay rejection
|
|
2599
|
+
// gate canonical question-state initialization.
|
|
2600
|
+
void startupCodexWakeReplay.catch(() => undefined);
|
|
2396
2601
|
let questionStateReady: Promise<void> | null = null;
|
|
2397
2602
|
|
|
2398
2603
|
function ensureQuestionStateReady(): Promise<void> {
|
|
@@ -3096,6 +3301,8 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3096
3301
|
|
|
3097
3302
|
function sdkError(error: unknown): Record<string, unknown> {
|
|
3098
3303
|
if (error instanceof SdkClientError) return { ok: false, error: { code: error.code, message: error.message } };
|
|
3304
|
+
if (error instanceof CoordinatorPublicationUncertainError)
|
|
3305
|
+
return { ok: false, error: { code: "ambiguous", message: error.message } };
|
|
3099
3306
|
return {
|
|
3100
3307
|
ok: false,
|
|
3101
3308
|
error: { code: "unavailable", message: error instanceof Error ? error.message : String(error) },
|
|
@@ -3289,6 +3496,57 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3289
3496
|
return result;
|
|
3290
3497
|
}
|
|
3291
3498
|
|
|
3499
|
+
const resolveModelPin =
|
|
3500
|
+
services.resolveModelPin ??
|
|
3501
|
+
(async (raw: unknown, cwd?: string): Promise<CoordinatorModelResolution> => {
|
|
3502
|
+
if (raw === undefined || raw === null) return { ok: true, model: null };
|
|
3503
|
+
const result = brokerResult(
|
|
3504
|
+
await brokerSession("", "model.resolve", {
|
|
3505
|
+
model: raw,
|
|
3506
|
+
cwd,
|
|
3507
|
+
target: coordinatorLifecycleTarget(config.sessionCommand, cwd ?? ""),
|
|
3508
|
+
}),
|
|
3509
|
+
);
|
|
3510
|
+
if (result) {
|
|
3511
|
+
if (result.ok === true && (result.model === null || typeof result.model === "string"))
|
|
3512
|
+
return { ok: true, model: result.model };
|
|
3513
|
+
if (
|
|
3514
|
+
result.ok === false &&
|
|
3515
|
+
result.reason === "unknown_model" &&
|
|
3516
|
+
typeof result.model === "string" &&
|
|
3517
|
+
typeof result.error === "string"
|
|
3518
|
+
)
|
|
3519
|
+
return { ok: false, reason: "unknown_model", model: result.model, error: result.error };
|
|
3520
|
+
}
|
|
3521
|
+
throw new SdkClientError("unavailable", "SDK host returned an invalid model resolution.");
|
|
3522
|
+
});
|
|
3523
|
+
|
|
3524
|
+
async function withRemoteSessionCompensation<T>(
|
|
3525
|
+
cwd: string,
|
|
3526
|
+
remoteSession: { value: string | null },
|
|
3527
|
+
operation: () => Promise<T>,
|
|
3528
|
+
): Promise<T> {
|
|
3529
|
+
try {
|
|
3530
|
+
return await operation();
|
|
3531
|
+
} catch (error) {
|
|
3532
|
+
const sessionId = remoteSession.value;
|
|
3533
|
+
if (!sessionId) throw error;
|
|
3534
|
+
try {
|
|
3535
|
+
strictBrokerSessionClose(
|
|
3536
|
+
await brokerSession(cwd, "session.close", { sessionId }, `${sessionId}:compensate-create`),
|
|
3537
|
+
sessionId,
|
|
3538
|
+
);
|
|
3539
|
+
} catch (closeError) {
|
|
3540
|
+
throw new SdkClientError(
|
|
3541
|
+
UNOBSERVED_COMPENSATION_CODE,
|
|
3542
|
+
`Coordinator creation failed after remote session ${sessionId} was created, and compensation failed.`,
|
|
3543
|
+
{ primary: error, compensation: closeError, session_id: sessionId },
|
|
3544
|
+
);
|
|
3545
|
+
}
|
|
3546
|
+
throw error;
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3292
3550
|
function brokerResult(value: unknown): Record<string, unknown> {
|
|
3293
3551
|
const response = asRecord(value);
|
|
3294
3552
|
if (response?.ok === false) {
|
|
@@ -3301,6 +3559,37 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3301
3559
|
return asRecord(response?.result) ?? response ?? {};
|
|
3302
3560
|
}
|
|
3303
3561
|
|
|
3562
|
+
function strictBrokerResult(value: unknown, operation: string): Record<string, unknown> {
|
|
3563
|
+
const response = asRecord(value);
|
|
3564
|
+
if (response?.ok === false) brokerResult(response);
|
|
3565
|
+
if (response?.ok !== true || !Object.hasOwn(response, "result"))
|
|
3566
|
+
throw new SdkClientError(
|
|
3567
|
+
UNOBSERVED_COMPENSATION_CODE,
|
|
3568
|
+
`SDK broker returned a malformed ${operation} response; the lifecycle outcome is unobserved.`,
|
|
3569
|
+
{ response },
|
|
3570
|
+
);
|
|
3571
|
+
const result = asRecord(response.result);
|
|
3572
|
+
if (!result)
|
|
3573
|
+
throw new SdkClientError(
|
|
3574
|
+
UNOBSERVED_COMPENSATION_CODE,
|
|
3575
|
+
`SDK broker returned a malformed ${operation} result; the lifecycle outcome is unobserved.`,
|
|
3576
|
+
{ response },
|
|
3577
|
+
);
|
|
3578
|
+
return result;
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
function strictBrokerSessionClose(value: unknown, expectedSessionId: string): Record<string, unknown> {
|
|
3582
|
+
const result = strictBrokerResult(value, "session.close");
|
|
3583
|
+
const sessionId = optionalString(result.sessionId ?? result.session_id);
|
|
3584
|
+
if (sessionId !== expectedSessionId)
|
|
3585
|
+
throw new SdkClientError(
|
|
3586
|
+
UNOBSERVED_COMPENSATION_CODE,
|
|
3587
|
+
"SDK broker returned no proof that the expected session was closed.",
|
|
3588
|
+
{ expected_session_id: expectedSessionId, result },
|
|
3589
|
+
);
|
|
3590
|
+
return result;
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3304
3593
|
async function paginatedBrokerSessionList(
|
|
3305
3594
|
cwd: string,
|
|
3306
3595
|
input: Record<string, unknown>,
|
|
@@ -3333,8 +3622,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3333
3622
|
async function canonicalBrokerWorkspace(cwd: string): Promise<string> {
|
|
3334
3623
|
try {
|
|
3335
3624
|
return await (services.canonicalizePath ?? (value => fs.realpath(value)))(cwd);
|
|
3336
|
-
} catch {
|
|
3337
|
-
|
|
3625
|
+
} catch (error) {
|
|
3626
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT")
|
|
3627
|
+
throw new SdkClientError("not_found", "Coordinator workspace cannot be resolved.");
|
|
3628
|
+
throw error;
|
|
3338
3629
|
}
|
|
3339
3630
|
}
|
|
3340
3631
|
|
|
@@ -3452,14 +3743,44 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3452
3743
|
const session = asRecord(await readJsonFile(sessionFile(id)));
|
|
3453
3744
|
if (!session) {
|
|
3454
3745
|
await ensureQuestionStateReady();
|
|
3455
|
-
const deletion = await withNamespaceRegistry(
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3746
|
+
const deletion = await withNamespaceRegistry(questionPaths, async registry => {
|
|
3747
|
+
const matching = Object.values(registry.deletions).filter(entry => entry.session_id === id);
|
|
3748
|
+
return (
|
|
3749
|
+
matching.find(entry => entry.phase !== "completed") ??
|
|
3750
|
+
matching.find(entry => entry.safe_response !== undefined) ??
|
|
3751
|
+
null
|
|
3752
|
+
);
|
|
3753
|
+
});
|
|
3754
|
+
if (deletion?.phase === "broker_closed" || deletion?.phase === "cleanup_pending") {
|
|
3755
|
+
await removeCoordinatorFile(sessionFile(id));
|
|
3756
|
+
await removeCoordinatorFile(sessionStateFile(namespaceDir, id));
|
|
3757
|
+
await removeCoordinatorFile(activeTurnFile(namespaceDir, id));
|
|
3758
|
+
await appendCoordinatorEvent(namespaceDir, {
|
|
3759
|
+
kind: "session.reaped",
|
|
3760
|
+
sessionId: id,
|
|
3761
|
+
summary: `Session ${id} cleanup resumed`,
|
|
3762
|
+
metadata: { recovered: true, closed: true },
|
|
3763
|
+
});
|
|
3764
|
+
await advanceDeletion(
|
|
3765
|
+
questionPaths,
|
|
3766
|
+
deletion.deletion_id,
|
|
3767
|
+
"completed",
|
|
3768
|
+
{ wal: true, turns: true, reports: false, session: true, events: true },
|
|
3769
|
+
{ ok: true, closed: true },
|
|
3770
|
+
);
|
|
3771
|
+
return { ok: true, closed: true };
|
|
3772
|
+
}
|
|
3773
|
+
if (deletion?.safe_response) {
|
|
3774
|
+
const safeResponse = asRecord(deletion.safe_response);
|
|
3775
|
+
const safeSessionId = optionalString(safeResponse?.session_id ?? safeResponse?.sessionId);
|
|
3776
|
+
if (
|
|
3777
|
+
safeResponse?.ok === true &&
|
|
3778
|
+
safeResponse.closed === true &&
|
|
3779
|
+
(safeSessionId === null || safeSessionId === id)
|
|
3780
|
+
)
|
|
3781
|
+
return deletion.safe_response as { ok: boolean; closed: boolean };
|
|
3782
|
+
throw new SdkClientError("state_corrupt", "Persisted reap response does not prove completed cleanup.");
|
|
3783
|
+
}
|
|
3463
3784
|
return { ok: false, reason: "unknown_session", closed: false };
|
|
3464
3785
|
}
|
|
3465
3786
|
|
|
@@ -3481,58 +3802,75 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3481
3802
|
const deletionId = `delete:${id}:${persistedIncarnation}`;
|
|
3482
3803
|
const deletionKey = createHash("sha256").update(deletionId).digest("hex");
|
|
3483
3804
|
await ensureQuestionStateReady();
|
|
3484
|
-
await
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
tool: "gjc_coordinator_stop_session",
|
|
3490
|
-
key_digest: deletionKey,
|
|
3491
|
-
request_digest: deletionKey,
|
|
3492
|
-
local_id: id,
|
|
3493
|
-
phase: "remote_started",
|
|
3494
|
-
intent: { kind: "reap", endpoint_incarnation: persistedIncarnation },
|
|
3495
|
-
created_at: now,
|
|
3496
|
-
updated_at: now,
|
|
3497
|
-
};
|
|
3498
|
-
});
|
|
3499
|
-
await recordDeletionIntent(questionPaths, {
|
|
3500
|
-
deletion_id: deletionId,
|
|
3501
|
-
session_id: id,
|
|
3502
|
-
endpoint_incarnation: persistedIncarnation,
|
|
3503
|
-
operation_id: deletionId,
|
|
3504
|
-
key_digest: deletionKey,
|
|
3505
|
-
request_digest: deletionKey,
|
|
3506
|
-
close_key: deletionId,
|
|
3507
|
-
phase: "intent",
|
|
3508
|
-
cleanup: { wal: false, turns: false, reports: false, session: false, events: false },
|
|
3509
|
-
authority_digest: deletionKey,
|
|
3510
|
-
created_at: new Date().toISOString(),
|
|
3511
|
-
updated_at: new Date().toISOString(),
|
|
3512
|
-
});
|
|
3513
|
-
|
|
3805
|
+
const deletionPhase = await withNamespaceRegistry(
|
|
3806
|
+
questionPaths,
|
|
3807
|
+
async registry => registry.deletions[deletionId]?.phase,
|
|
3808
|
+
);
|
|
3809
|
+
const closeAlreadyProven = deletionPhase === "broker_closed" || deletionPhase === "cleanup_pending";
|
|
3514
3810
|
let workspace = "";
|
|
3515
3811
|
try {
|
|
3516
3812
|
workspace = await canonicalBrokerWorkspace(cwd);
|
|
3517
|
-
|
|
3518
|
-
if (
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3813
|
+
let authority: BrokerSessionAuthority | null = null;
|
|
3814
|
+
if (!closeAlreadyProven) {
|
|
3815
|
+
authority = await exactBrokerSessionAuthority(id, workspace);
|
|
3816
|
+
if (
|
|
3817
|
+
!sameCanonicalPath(authority.workspace, persistedWorkspace, platform) ||
|
|
3818
|
+
authority.endpointGeneration !== persistedGeneration ||
|
|
3819
|
+
authority.endpointIncarnation !== persistedIncarnation
|
|
3820
|
+
)
|
|
3821
|
+
return { ok: false, reason: "endpoint_stale", closed: false };
|
|
3822
|
+
}
|
|
3823
|
+
await ensureQuestionStateReady();
|
|
3824
|
+
await ensureQuestionTransaction(id);
|
|
3825
|
+
await withSessionTransaction(questionPaths, id, async transaction => {
|
|
3826
|
+
const now = new Date().toISOString();
|
|
3827
|
+
transaction.requests.operations[deletionId] = {
|
|
3828
|
+
operation_id: deletionId,
|
|
3829
|
+
tool: "gjc_coordinator_stop_session",
|
|
3830
|
+
key_digest: deletionKey,
|
|
3831
|
+
request_digest: deletionKey,
|
|
3832
|
+
local_id: id,
|
|
3833
|
+
phase: "remote_started",
|
|
3834
|
+
intent: { kind: "reap", endpoint_incarnation: persistedIncarnation },
|
|
3835
|
+
created_at: now,
|
|
3836
|
+
updated_at: now,
|
|
3837
|
+
};
|
|
3838
|
+
});
|
|
3839
|
+
await recordDeletionIntent(questionPaths, {
|
|
3840
|
+
deletion_id: deletionId,
|
|
3841
|
+
session_id: id,
|
|
3842
|
+
endpoint_incarnation: persistedIncarnation,
|
|
3843
|
+
operation_id: deletionId,
|
|
3844
|
+
key_digest: deletionKey,
|
|
3845
|
+
request_digest: deletionKey,
|
|
3846
|
+
close_key: deletionId,
|
|
3847
|
+
phase: "intent",
|
|
3848
|
+
cleanup: { wal: false, turns: false, reports: false, session: false, events: false },
|
|
3849
|
+
authority_digest: deletionKey,
|
|
3850
|
+
created_at: new Date().toISOString(),
|
|
3851
|
+
updated_at: new Date().toISOString(),
|
|
3852
|
+
});
|
|
3853
|
+
if (!closeAlreadyProven) {
|
|
3854
|
+
if (!authority) throw new Error("state_corrupt");
|
|
3855
|
+
strictBrokerSessionClose(
|
|
3856
|
+
await brokerSession(
|
|
3857
|
+
cwd,
|
|
3858
|
+
"session.close",
|
|
3859
|
+
{
|
|
3860
|
+
sessionId: id,
|
|
3861
|
+
endpointGeneration: authority.endpointGeneration,
|
|
3862
|
+
endpointIncarnation: authority.endpointIncarnation,
|
|
3863
|
+
},
|
|
3864
|
+
`coordinator-reap:${id}:${authority.endpointIncarnation}`,
|
|
3865
|
+
),
|
|
3866
|
+
id,
|
|
3867
|
+
);
|
|
3868
|
+
await advanceDeletion(questionPaths, deletionId, "broker_closed", undefined, {
|
|
3869
|
+
ok: true,
|
|
3870
|
+
closed: true,
|
|
3871
|
+
session_id: id,
|
|
3872
|
+
});
|
|
3873
|
+
}
|
|
3536
3874
|
} catch (error) {
|
|
3537
3875
|
return {
|
|
3538
3876
|
ok: false,
|
|
@@ -3541,8 +3879,6 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3541
3879
|
closed: false,
|
|
3542
3880
|
};
|
|
3543
3881
|
}
|
|
3544
|
-
await advanceDeletion(questionPaths, deletionId, "broker_closed");
|
|
3545
|
-
|
|
3546
3882
|
try {
|
|
3547
3883
|
const listing = await paginatedBrokerSessionList(workspace, { cwd: workspace });
|
|
3548
3884
|
const rows = jsonRecords(
|
|
@@ -3588,9 +3924,9 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3588
3924
|
return { ok: false, reason: "close_failed", detail: error.code, closed: false };
|
|
3589
3925
|
return { ok: false, reason: "close_failed", detail: "unavailable", closed: false };
|
|
3590
3926
|
}
|
|
3591
|
-
await
|
|
3592
|
-
await
|
|
3593
|
-
await
|
|
3927
|
+
await removeCoordinatorFile(sessionFile(id));
|
|
3928
|
+
await removeCoordinatorFile(sessionStateFile(namespaceDir, id));
|
|
3929
|
+
await removeCoordinatorFile(activeTurnFile(namespaceDir, id));
|
|
3594
3930
|
await appendCoordinatorEvent(namespaceDir, {
|
|
3595
3931
|
kind: "session.reaped",
|
|
3596
3932
|
sessionId: id,
|
|
@@ -3988,22 +4324,30 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3988
4324
|
for (const report of Object.values(canonical.reports))
|
|
3989
4325
|
await writeJsonFile(path.join(namespaceDir, "reports", `${report.report_id}.json`), report);
|
|
3990
4326
|
const canonicalTurnIds = new Set(Object.keys(canonical.turns));
|
|
3991
|
-
|
|
4327
|
+
const turnEntries = await fs.readdir(turnsDir(namespaceDir)).catch(error => {
|
|
4328
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
4329
|
+
throw error;
|
|
4330
|
+
});
|
|
4331
|
+
for (const entry of turnEntries) {
|
|
3992
4332
|
if (!entry.endsWith(".json")) continue;
|
|
3993
4333
|
const turnId = entry.slice(0, -5);
|
|
3994
4334
|
if (canonicalTurnIds.has(turnId)) continue;
|
|
3995
4335
|
const projected = asRecord(await readJsonFile(path.join(turnsDir(namespaceDir), entry)));
|
|
3996
4336
|
if (projected?.session_id === sessionId)
|
|
3997
|
-
await
|
|
4337
|
+
await removeCoordinatorFile(path.join(turnsDir(namespaceDir), entry));
|
|
3998
4338
|
}
|
|
3999
4339
|
const reportsDirectory = path.join(namespaceDir, "reports");
|
|
4000
4340
|
const canonicalReportIds = new Set(Object.keys(canonical.reports));
|
|
4001
|
-
|
|
4341
|
+
const reportEntries = await fs.readdir(reportsDirectory).catch(error => {
|
|
4342
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
4343
|
+
throw error;
|
|
4344
|
+
});
|
|
4345
|
+
for (const entry of reportEntries) {
|
|
4002
4346
|
if (!entry.endsWith(".json")) continue;
|
|
4003
4347
|
const reportId = entry.slice(0, -5);
|
|
4004
4348
|
if (canonicalReportIds.has(reportId)) continue;
|
|
4005
4349
|
const projected = asRecord(await readJsonFile(path.join(reportsDirectory, entry)));
|
|
4006
|
-
if (projected?.session_id === sessionId) await
|
|
4350
|
+
if (projected?.session_id === sessionId) await removeCoordinatorFile(path.join(reportsDirectory, entry));
|
|
4007
4351
|
}
|
|
4008
4352
|
const activeId = canonical.queue.selected_promotion?.to_turn_id ?? canonical.queue.active_turn_id;
|
|
4009
4353
|
const active = activeId ? canonical.turns[activeId] : null;
|
|
@@ -4535,13 +4879,25 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4535
4879
|
available_profiles: mpresetResolution.available_profiles,
|
|
4536
4880
|
};
|
|
4537
4881
|
}
|
|
4882
|
+
// Explicit model pin (#4707) is resolved by the SDK host. The
|
|
4883
|
+
// coordinator remains a transport/controller boundary and never loads
|
|
4884
|
+
// session, settings, discovery, or MCP-manager authority.
|
|
4885
|
+
const modelResolution = await resolveModelPin(args.model, canonicalCwd);
|
|
4886
|
+
if (!modelResolution.ok) {
|
|
4887
|
+
return {
|
|
4888
|
+
ok: false,
|
|
4889
|
+
reason: modelResolution.reason,
|
|
4890
|
+
model: modelResolution.model,
|
|
4891
|
+
error: { code: "unknown_model", message: modelResolution.error },
|
|
4892
|
+
};
|
|
4893
|
+
}
|
|
4538
4894
|
const hasTask = typeof args.task === "string" && args.task.trim().length > 0;
|
|
4539
4895
|
const hasPrompt = typeof args.prompt === "string" && args.prompt.trim().length > 0;
|
|
4540
4896
|
const task = hasTask ? String(args.task) : hasPrompt ? String(args.prompt) : null;
|
|
4541
4897
|
if (!task) return { ok: false, reason: "task_required" };
|
|
4542
4898
|
const taggedPrompt = workflowPrompt(delegateWorkflow, name, canonicalCwd, task, {
|
|
4543
4899
|
mutationRequested: args.allow_mutation === true,
|
|
4544
|
-
model:
|
|
4900
|
+
model: modelResolution.model,
|
|
4545
4901
|
});
|
|
4546
4902
|
const reusedSessionId = args.session_id == null ? undefined : safeExternalId("session", args.session_id);
|
|
4547
4903
|
const explicitHostWorkUnit =
|
|
@@ -4558,7 +4914,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4558
4914
|
queue: args.queue === true,
|
|
4559
4915
|
force: args.force === true,
|
|
4560
4916
|
mpreset: mpresetResolution.mpreset,
|
|
4561
|
-
model:
|
|
4917
|
+
model: modelResolution.model,
|
|
4562
4918
|
await_completion: args.await_completion === true,
|
|
4563
4919
|
...(args.await_completion === true
|
|
4564
4920
|
? { timeout_ms: args.timeout_ms, poll_interval_ms: args.poll_interval_ms }
|
|
@@ -4572,215 +4928,244 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4572
4928
|
idempotencyKey,
|
|
4573
4929
|
canonicalArgs,
|
|
4574
4930
|
async () => {
|
|
4575
|
-
const
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
}
|
|
4609
|
-
}
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
};
|
|
4627
|
-
session = normalizeSession({
|
|
4628
|
-
...existing,
|
|
4629
|
-
session_id: sessionId,
|
|
4630
|
-
cwd: canonicalCwd,
|
|
4631
|
-
broker_workspace: binding.workspace,
|
|
4632
|
-
endpoint_generation: binding.endpointGeneration,
|
|
4633
|
-
endpoint_incarnation: binding.endpointIncarnation,
|
|
4634
|
-
});
|
|
4635
|
-
reusedSession = true;
|
|
4636
|
-
} else {
|
|
4637
|
-
const creation = await claimProductionCreation(name, idempotencyKey, canonicalArgs);
|
|
4638
|
-
if (creation.request.phase === "completed" && creation.request.safe_response)
|
|
4639
|
-
return creation.request.safe_response;
|
|
4640
|
-
creationKey = creation.keyDigest;
|
|
4641
|
-
if (creation.request.canonical_create_intent) {
|
|
4642
|
-
session = sessionFromCreationSnapshot(creation.request.canonical_create_intent.session);
|
|
4643
|
-
await bindCreationRequest(
|
|
4644
|
-
questionPaths,
|
|
4645
|
-
creation.keyDigest,
|
|
4646
|
-
creation.request.canonical_create_intent,
|
|
4647
|
-
);
|
|
4648
|
-
await commitCreationWal(
|
|
4649
|
-
questionPaths,
|
|
4650
|
-
creation.keyDigest,
|
|
4651
|
-
creation.request.canonical_create_intent,
|
|
4652
|
-
);
|
|
4653
|
-
sessionId = creation.request.canonical_create_intent.session.session_id;
|
|
4654
|
-
} else {
|
|
4655
|
-
const created = brokerResult(
|
|
4656
|
-
await brokerSession(
|
|
4931
|
+
const remoteSession = { value: null as string | null };
|
|
4932
|
+
const delegate = async () =>
|
|
4933
|
+
withRemoteSessionCompensation(canonicalCwd, remoteSession, async () => {
|
|
4934
|
+
let sessionId: string;
|
|
4935
|
+
let session: Record<string, unknown>;
|
|
4936
|
+
let reusedSession = false;
|
|
4937
|
+
let creationKey: string | null = null;
|
|
4938
|
+
if (reusedSessionId) {
|
|
4939
|
+
sessionId = reusedSessionId;
|
|
4940
|
+
const existing = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
4941
|
+
if (!existing)
|
|
4942
|
+
return {
|
|
4943
|
+
ok: false,
|
|
4944
|
+
error: { code: "not_found", message: `Coordinator session not found: ${sessionId}` },
|
|
4945
|
+
};
|
|
4946
|
+
const sessionMpreset = optionalString(existing.mpreset);
|
|
4947
|
+
if (mpresetResolution.mpreset !== null && sessionMpreset !== mpresetResolution.mpreset) {
|
|
4948
|
+
return {
|
|
4949
|
+
ok: false,
|
|
4950
|
+
reason: "mpreset_conflict",
|
|
4951
|
+
session_id: sessionId,
|
|
4952
|
+
session_mpreset: sessionMpreset,
|
|
4953
|
+
requested_mpreset: mpresetResolution.mpreset,
|
|
4954
|
+
};
|
|
4955
|
+
}
|
|
4956
|
+
const sessionModel = optionalString(existing.model);
|
|
4957
|
+
if (modelResolution.model !== null && sessionModel !== modelResolution.model) {
|
|
4958
|
+
return {
|
|
4959
|
+
ok: false,
|
|
4960
|
+
reason: "model_conflict",
|
|
4961
|
+
session_id: sessionId,
|
|
4962
|
+
session_model: sessionModel,
|
|
4963
|
+
requested_model: modelResolution.model,
|
|
4964
|
+
};
|
|
4965
|
+
}
|
|
4966
|
+
const existingCwd = optionalString(existing.cwd);
|
|
4967
|
+
if (
|
|
4968
|
+
!existingCwd ||
|
|
4969
|
+
!sameCanonicalPath(await canonicalBrokerWorkspace(existingCwd), canonicalCwd, platform)
|
|
4970
|
+
)
|
|
4971
|
+
return {
|
|
4972
|
+
ok: false,
|
|
4973
|
+
error: {
|
|
4974
|
+
code: "workspace_mismatch",
|
|
4975
|
+
message: "Coordinator session is bound to another workspace.",
|
|
4976
|
+
},
|
|
4977
|
+
};
|
|
4978
|
+
const binding = await exactBrokerSessionBinding(sessionId, canonicalCwd);
|
|
4979
|
+
if (
|
|
4980
|
+
!sameCanonicalPath(
|
|
4981
|
+
optionalString(existing.broker_workspace) ?? "",
|
|
4657
4982
|
canonicalCwd,
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4983
|
+
platform,
|
|
4984
|
+
) ||
|
|
4985
|
+
existing.endpoint_generation !== binding.endpointGeneration ||
|
|
4986
|
+
optionalString(existing.endpoint_incarnation) !== binding.endpointIncarnation
|
|
4987
|
+
)
|
|
4988
|
+
return {
|
|
4989
|
+
ok: false,
|
|
4990
|
+
error: {
|
|
4991
|
+
code: "endpoint_stale",
|
|
4992
|
+
message: "Coordinator session endpoint incarnation binding is stale.",
|
|
4666
4993
|
},
|
|
4667
|
-
|
|
4668
|
-
),
|
|
4669
|
-
);
|
|
4670
|
-
sessionId = safeExternalId("session", created.sessionId ?? created.session_id);
|
|
4671
|
-
const createdCwd = await canonicalBrokerWorkspace(
|
|
4672
|
-
optionalString(created.cwd) ?? canonicalCwd,
|
|
4673
|
-
);
|
|
4674
|
-
const binding = await exactBrokerSessionBinding(sessionId, createdCwd);
|
|
4994
|
+
};
|
|
4675
4995
|
session = normalizeSession({
|
|
4996
|
+
...existing,
|
|
4676
4997
|
session_id: sessionId,
|
|
4677
|
-
cwd:
|
|
4678
|
-
ephemeral: true,
|
|
4679
|
-
created_at: new Date().toISOString(),
|
|
4680
|
-
...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
|
|
4998
|
+
cwd: canonicalCwd,
|
|
4681
4999
|
broker_workspace: binding.workspace,
|
|
4682
5000
|
endpoint_generation: binding.endpointGeneration,
|
|
4683
5001
|
endpoint_incarnation: binding.endpointIncarnation,
|
|
4684
5002
|
});
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
5003
|
+
reusedSession = true;
|
|
5004
|
+
} else {
|
|
5005
|
+
const creation = await claimProductionCreation(name, idempotencyKey, canonicalArgs);
|
|
5006
|
+
if (creation.request.phase === "completed" && creation.request.safe_response)
|
|
5007
|
+
return creation.request.safe_response;
|
|
5008
|
+
creationKey = creation.keyDigest;
|
|
5009
|
+
if (creation.request.canonical_create_intent) {
|
|
5010
|
+
session = sessionFromCreationSnapshot(creation.request.canonical_create_intent.session);
|
|
5011
|
+
await bindCreationRequest(
|
|
5012
|
+
questionPaths,
|
|
5013
|
+
creation.keyDigest,
|
|
5014
|
+
creation.request.canonical_create_intent,
|
|
5015
|
+
);
|
|
5016
|
+
await commitCreationWal(
|
|
5017
|
+
questionPaths,
|
|
5018
|
+
creation.keyDigest,
|
|
5019
|
+
creation.request.canonical_create_intent,
|
|
5020
|
+
);
|
|
5021
|
+
sessionId = creation.request.canonical_create_intent.session.session_id;
|
|
5022
|
+
} else {
|
|
5023
|
+
const created = brokerResult(
|
|
5024
|
+
await brokerSession(
|
|
5025
|
+
canonicalCwd,
|
|
5026
|
+
"session.create",
|
|
5027
|
+
{
|
|
5028
|
+
cwd: canonicalCwd,
|
|
5029
|
+
target: coordinatorLifecycleTarget(config.sessionCommand, canonicalCwd),
|
|
5030
|
+
...(mpresetResolution.mpreset ? { modelPreset: mpresetResolution.mpreset } : {}),
|
|
5031
|
+
...(modelResolution.model ? { modelId: modelResolution.model } : {}),
|
|
5032
|
+
// Thread the coordinator state dir so the broker-spawned runtime
|
|
5033
|
+
// writes terminal state to the coordinator-shared file (#2549).
|
|
5034
|
+
coordinatorStateDir: namespaceDir,
|
|
5035
|
+
},
|
|
5036
|
+
idempotencyKey,
|
|
5037
|
+
),
|
|
5038
|
+
);
|
|
5039
|
+
const createdSessionId = optionalString(created.sessionId ?? created.session_id);
|
|
5040
|
+
if (!createdSessionId)
|
|
5041
|
+
throw new SdkClientError(
|
|
5042
|
+
UNOBSERVED_COMPENSATION_CODE,
|
|
5043
|
+
"SDK broker created a session but returned no usable session identity; the outcome is unobserved.",
|
|
5044
|
+
{ creation_response: created },
|
|
5045
|
+
);
|
|
5046
|
+
remoteSession.value = createdSessionId;
|
|
5047
|
+
sessionId = safeExternalId("session", createdSessionId);
|
|
5048
|
+
const createdCwd = await canonicalBrokerWorkspace(
|
|
5049
|
+
optionalString(created.cwd) ?? canonicalCwd,
|
|
5050
|
+
);
|
|
5051
|
+
const binding = await exactBrokerSessionBinding(sessionId, createdCwd);
|
|
5052
|
+
session = normalizeSession({
|
|
5053
|
+
session_id: sessionId,
|
|
5054
|
+
cwd: createdCwd,
|
|
5055
|
+
ephemeral: true,
|
|
5056
|
+
created_at: new Date().toISOString(),
|
|
5057
|
+
...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
|
|
5058
|
+
...(modelResolution.model ? { model: modelResolution.model } : {}),
|
|
5059
|
+
broker_workspace: binding.workspace,
|
|
5060
|
+
endpoint_generation: binding.endpointGeneration,
|
|
5061
|
+
endpoint_incarnation: binding.endpointIncarnation,
|
|
5062
|
+
});
|
|
5063
|
+
const intent: CanonicalCreateIntentV1 = {
|
|
5064
|
+
kind: "delegate",
|
|
5065
|
+
workflow: delegateWorkflow,
|
|
5066
|
+
session: canonicalCreationSnapshot(session),
|
|
5067
|
+
remote_create_key: creation.request.remote_create_key,
|
|
5068
|
+
initial_state: "running",
|
|
5069
|
+
initial_prompt: {
|
|
5070
|
+
text: taggedPrompt,
|
|
5071
|
+
caller_key_digest: createHash("sha256").update(idempotencyKey).digest("hex"),
|
|
5072
|
+
},
|
|
5073
|
+
initial_events: [],
|
|
5074
|
+
};
|
|
5075
|
+
await bindCreationRequest(questionPaths, creation.keyDigest, intent);
|
|
5076
|
+
await commitCreationWal(questionPaths, creation.keyDigest, intent);
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
await writeJsonFile(sessionFile(sessionId), session);
|
|
5080
|
+
const previousActiveTurn = await readActiveTurn(namespaceDir, sessionId);
|
|
5081
|
+
if (previousActiveTurn && args.queue !== true && args.force !== true) {
|
|
5082
|
+
return {
|
|
5083
|
+
ok: false,
|
|
5084
|
+
error: {
|
|
5085
|
+
code: "active_turn_exists",
|
|
5086
|
+
message: `Session ${sessionId} already has active turn ${previousActiveTurn.turn_id}.`,
|
|
4694
5087
|
},
|
|
4695
|
-
|
|
5088
|
+
turn_id: previousActiveTurn.turn_id,
|
|
4696
5089
|
};
|
|
4697
|
-
await bindCreationRequest(questionPaths, creation.keyDigest, intent);
|
|
4698
|
-
await commitCreationWal(questionPaths, creation.keyDigest, intent);
|
|
4699
5090
|
}
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
5091
|
+
const operation =
|
|
5092
|
+
args.force === true
|
|
5093
|
+
? "turn.abort_and_prompt"
|
|
5094
|
+
: args.queue === true
|
|
5095
|
+
? "turn.follow_up"
|
|
5096
|
+
: "turn.prompt";
|
|
5097
|
+
const promptKey = await claimCanonicalPrompt(
|
|
5098
|
+
sessionId,
|
|
5099
|
+
taggedPrompt,
|
|
5100
|
+
operation,
|
|
5101
|
+
idempotencyKey,
|
|
5102
|
+
);
|
|
5103
|
+
const result = await controlSession(session, operation, { text: taggedPrompt }, idempotencyKey);
|
|
5104
|
+
const acknowledgement = requirePromptAcknowledgement(result);
|
|
5105
|
+
const turn = await recordAcceptedPrompt(
|
|
5106
|
+
sessionId,
|
|
5107
|
+
taggedPrompt,
|
|
5108
|
+
operation,
|
|
5109
|
+
previousActiveTurn,
|
|
5110
|
+
acknowledgement,
|
|
5111
|
+
promptKey,
|
|
5112
|
+
);
|
|
5113
|
+
const codexHandoff = await autoBindDelegateCodexHandoff(
|
|
5114
|
+
namespaceDir,
|
|
5115
|
+
canonicalCwd,
|
|
5116
|
+
sessionId,
|
|
5117
|
+
turn.turn_id,
|
|
5118
|
+
delegateWorkflow,
|
|
5119
|
+
explicitHostWorkUnit,
|
|
5120
|
+
);
|
|
5121
|
+
await appendCoordinatorEvent(namespaceDir, {
|
|
5122
|
+
kind: "delegation.started",
|
|
5123
|
+
sessionId,
|
|
5124
|
+
turnId: turn.turn_id,
|
|
5125
|
+
summary: `Delegated ${delegateWorkflow} via ${name} on session ${sessionId}`,
|
|
5126
|
+
metadata: {
|
|
5127
|
+
workflow: delegateWorkflow,
|
|
5128
|
+
tool_name: name,
|
|
5129
|
+
reused_session: reusedSession,
|
|
5130
|
+
sdk_operation: operation,
|
|
4709
5131
|
},
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
const operation =
|
|
4714
|
-
args.force === true
|
|
4715
|
-
? "turn.abort_and_prompt"
|
|
4716
|
-
: args.queue === true
|
|
4717
|
-
? "turn.follow_up"
|
|
4718
|
-
: "turn.prompt";
|
|
4719
|
-
const promptKey = await claimCanonicalPrompt(sessionId, taggedPrompt, operation, idempotencyKey);
|
|
4720
|
-
const result = await controlSession(session, operation, { text: taggedPrompt }, idempotencyKey);
|
|
4721
|
-
const acknowledgement = requirePromptAcknowledgement(result);
|
|
4722
|
-
const turn = await recordAcceptedPrompt(
|
|
4723
|
-
sessionId,
|
|
4724
|
-
taggedPrompt,
|
|
4725
|
-
operation,
|
|
4726
|
-
previousActiveTurn,
|
|
4727
|
-
acknowledgement,
|
|
4728
|
-
promptKey,
|
|
4729
|
-
);
|
|
4730
|
-
const codexHandoff = await autoBindDelegateCodexHandoff(
|
|
4731
|
-
namespaceDir,
|
|
4732
|
-
canonicalCwd,
|
|
4733
|
-
sessionId,
|
|
4734
|
-
turn.turn_id,
|
|
4735
|
-
delegateWorkflow,
|
|
4736
|
-
explicitHostWorkUnit,
|
|
4737
|
-
);
|
|
4738
|
-
await appendCoordinatorEvent(namespaceDir, {
|
|
4739
|
-
kind: "delegation.started",
|
|
4740
|
-
sessionId,
|
|
4741
|
-
turnId: turn.turn_id,
|
|
4742
|
-
summary: `Delegated ${delegateWorkflow} via ${name} on session ${sessionId}`,
|
|
4743
|
-
metadata: {
|
|
5132
|
+
});
|
|
5133
|
+
const response = {
|
|
5134
|
+
ok: true,
|
|
4744
5135
|
workflow: delegateWorkflow,
|
|
4745
5136
|
tool_name: name,
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
5137
|
+
session_id: sessionId,
|
|
5138
|
+
turn_id: turn.turn_id,
|
|
5139
|
+
active_turn_id: turn.delivery.queued ? (previousActiveTurn?.turn_id ?? null) : turn.turn_id,
|
|
5140
|
+
status: turn.status,
|
|
5141
|
+
queued: turn.delivery.queued,
|
|
5142
|
+
delivered: turn.delivery.delivered,
|
|
5143
|
+
delivery: turn.delivery,
|
|
5144
|
+
session: publicCoordinatorSession(session),
|
|
5145
|
+
session_state: publicCoordinatorSessionState(
|
|
5146
|
+
await readSessionState(namespaceDir, sessionId),
|
|
5147
|
+
),
|
|
5148
|
+
turn: boundedPublicValue(turn, { remaining: COORDINATOR_IDEMPOTENCY_RESPONSE_BYTE_CAP }),
|
|
5149
|
+
result: publicSdkAcknowledgement(acknowledgement),
|
|
5150
|
+
codex_handoff: codexHandoff,
|
|
5151
|
+
...(hasTask && hasPrompt ? { prompt_alias_ignored: true } : {}),
|
|
5152
|
+
};
|
|
5153
|
+
if (creationKey) {
|
|
5154
|
+
await advanceCreationReceipt(questionPaths, creationKey, "projected", response);
|
|
5155
|
+
await advanceCreationReceipt(questionPaths, creationKey, "completed", response);
|
|
5156
|
+
}
|
|
5157
|
+
return args.await_completion === true
|
|
5158
|
+
? {
|
|
5159
|
+
...response,
|
|
5160
|
+
completion: await awaitTurnPayload(
|
|
5161
|
+
turn.turn_id,
|
|
5162
|
+
sessionId,
|
|
5163
|
+
args.timeout_ms,
|
|
5164
|
+
args.poll_interval_ms,
|
|
5165
|
+
),
|
|
5166
|
+
}
|
|
5167
|
+
: response;
|
|
4749
5168
|
});
|
|
4750
|
-
const response = {
|
|
4751
|
-
ok: true,
|
|
4752
|
-
workflow: delegateWorkflow,
|
|
4753
|
-
tool_name: name,
|
|
4754
|
-
session_id: sessionId,
|
|
4755
|
-
turn_id: turn.turn_id,
|
|
4756
|
-
active_turn_id: turn.delivery.queued ? (previousActiveTurn?.turn_id ?? null) : turn.turn_id,
|
|
4757
|
-
status: turn.status,
|
|
4758
|
-
queued: turn.delivery.queued,
|
|
4759
|
-
delivered: turn.delivery.delivered,
|
|
4760
|
-
delivery: turn.delivery,
|
|
4761
|
-
session: publicCoordinatorSession(session),
|
|
4762
|
-
session_state: publicCoordinatorSessionState(await readSessionState(namespaceDir, sessionId)),
|
|
4763
|
-
turn: boundedPublicValue(turn, { remaining: COORDINATOR_IDEMPOTENCY_RESPONSE_BYTE_CAP }),
|
|
4764
|
-
result: publicSdkAcknowledgement(acknowledgement),
|
|
4765
|
-
codex_handoff: codexHandoff,
|
|
4766
|
-
...(hasTask && hasPrompt ? { prompt_alias_ignored: true } : {}),
|
|
4767
|
-
};
|
|
4768
|
-
if (creationKey) {
|
|
4769
|
-
await advanceCreationReceipt(questionPaths, creationKey, "projected", response);
|
|
4770
|
-
await advanceCreationReceipt(questionPaths, creationKey, "completed", response);
|
|
4771
|
-
}
|
|
4772
|
-
return args.await_completion === true
|
|
4773
|
-
? {
|
|
4774
|
-
...response,
|
|
4775
|
-
completion: await awaitTurnPayload(
|
|
4776
|
-
turn.turn_id,
|
|
4777
|
-
sessionId,
|
|
4778
|
-
args.timeout_ms,
|
|
4779
|
-
args.poll_interval_ms,
|
|
4780
|
-
),
|
|
4781
|
-
}
|
|
4782
|
-
: response;
|
|
4783
|
-
};
|
|
4784
5169
|
return reusedSessionId ? await withSessionTransition(reusedSessionId, delegate) : await delegate();
|
|
4785
5170
|
},
|
|
4786
5171
|
true,
|
|
@@ -4821,6 +5206,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4821
5206
|
available_profiles: mpresetResolution.available_profiles,
|
|
4822
5207
|
};
|
|
4823
5208
|
}
|
|
5209
|
+
// Explicit model pin (#4707): resolve with CLI grammar at the SDK host
|
|
5210
|
+
// boundary before any coordinator mutation or lifecycle request.
|
|
5211
|
+
const modelResolution = await resolveModelPin(args.model, cwd);
|
|
5212
|
+
if (!modelResolution.ok) {
|
|
5213
|
+
return {
|
|
5214
|
+
ok: false,
|
|
5215
|
+
reason: modelResolution.reason,
|
|
5216
|
+
model: modelResolution.model,
|
|
5217
|
+
error: { code: "unknown_model", message: modelResolution.error },
|
|
5218
|
+
};
|
|
5219
|
+
}
|
|
4824
5220
|
const prompt = typeof args.prompt === "string" && args.prompt.length > 0 ? args.prompt : null;
|
|
4825
5221
|
/**
|
|
4826
5222
|
* A prepared session is deliberately not ready for input: its readiness
|
|
@@ -4855,6 +5251,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4855
5251
|
const canonicalArgs = {
|
|
4856
5252
|
cwd,
|
|
4857
5253
|
mpreset: mpresetResolution.mpreset,
|
|
5254
|
+
model: modelResolution.model,
|
|
4858
5255
|
...(prompt ? { prompt } : {}),
|
|
4859
5256
|
...(preparesExistingThread ? { prepare_existing_thread: true } : {}),
|
|
4860
5257
|
allow_mutation: true,
|
|
@@ -4864,148 +5261,178 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4864
5261
|
idempotencyKey,
|
|
4865
5262
|
canonicalArgs,
|
|
4866
5263
|
async () => {
|
|
4867
|
-
const
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
{
|
|
4883
|
-
cwd,
|
|
4884
|
-
target: coordinatorLifecycleTarget(config.sessionCommand, cwd),
|
|
4885
|
-
...(mpresetResolution.mpreset ? { modelPreset: mpresetResolution.mpreset } : {}),
|
|
4886
|
-
...(preparesExistingThread ? { readiness: "deferred" } : {}),
|
|
4887
|
-
// Thread the coordinator state dir so the broker-spawned runtime
|
|
4888
|
-
// writes terminal state to the coordinator-shared file (#2549).
|
|
4889
|
-
coordinatorStateDir: namespaceDir,
|
|
4890
|
-
},
|
|
4891
|
-
idempotencyKey,
|
|
4892
|
-
),
|
|
4893
|
-
);
|
|
4894
|
-
/**
|
|
4895
|
-
* Preparation is only real when the broker proves it. A create that
|
|
4896
|
-
* silently published readiness would leave a live session whose root
|
|
4897
|
-
* is already claimed, so the session is closed rather than reported
|
|
4898
|
-
* as prepared.
|
|
4899
|
-
*/
|
|
4900
|
-
if (preparesExistingThread && created.readiness !== "prepared") {
|
|
4901
|
-
const unpreparedId = optionalString(created.sessionId ?? created.session_id);
|
|
4902
|
-
let compensated = true;
|
|
4903
|
-
if (unpreparedId) {
|
|
4904
|
-
compensated = await brokerSession(
|
|
5264
|
+
const remoteSession = { value: null as string | null };
|
|
5265
|
+
return await withRemoteSessionCompensation(cwd, remoteSession, async () => {
|
|
5266
|
+
const creation = await claimProductionCreation(name, idempotencyKey, canonicalArgs);
|
|
5267
|
+
if (creation.request.phase === "completed" && creation.request.safe_response)
|
|
5268
|
+
return creation.request.safe_response;
|
|
5269
|
+
let created: Record<string, unknown>;
|
|
5270
|
+
let session: Record<string, unknown>;
|
|
5271
|
+
let sessionId: string;
|
|
5272
|
+
if (creation.request.canonical_create_intent) {
|
|
5273
|
+
session = sessionFromCreationSnapshot(creation.request.canonical_create_intent.session);
|
|
5274
|
+
sessionId = creation.request.canonical_create_intent.session.session_id;
|
|
5275
|
+
created = { session_id: sessionId };
|
|
5276
|
+
} else {
|
|
5277
|
+
created = brokerResult(
|
|
5278
|
+
await brokerSession(
|
|
4905
5279
|
cwd,
|
|
4906
|
-
"session.
|
|
4907
|
-
{
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
5280
|
+
"session.create",
|
|
5281
|
+
{
|
|
5282
|
+
cwd,
|
|
5283
|
+
target: coordinatorLifecycleTarget(config.sessionCommand, cwd),
|
|
5284
|
+
...(mpresetResolution.mpreset ? { modelPreset: mpresetResolution.mpreset } : {}),
|
|
5285
|
+
...(modelResolution.model ? { modelId: modelResolution.model } : {}),
|
|
5286
|
+
...(preparesExistingThread ? { readiness: "deferred" } : {}),
|
|
5287
|
+
// Thread the coordinator state dir so the broker-spawned runtime
|
|
5288
|
+
// writes terminal state to the coordinator-shared file (#2549).
|
|
5289
|
+
coordinatorStateDir: namespaceDir,
|
|
5290
|
+
},
|
|
5291
|
+
idempotencyKey,
|
|
5292
|
+
),
|
|
5293
|
+
);
|
|
5294
|
+
const createdSessionId = optionalString(created.sessionId ?? created.session_id);
|
|
5295
|
+
/**
|
|
5296
|
+
* Preparation is only real when the broker proves it. A create that
|
|
5297
|
+
* silently published readiness would leave a live session whose root
|
|
5298
|
+
* is already claimed, so the session is closed rather than reported
|
|
5299
|
+
* what the schema promises. Reject a non-boolean before any mutation.
|
|
5300
|
+
*/
|
|
5301
|
+
if (preparesExistingThread && created.readiness !== "prepared") {
|
|
5302
|
+
if (!createdSessionId)
|
|
5303
|
+
throw new SdkClientError(
|
|
5304
|
+
UNOBSERVED_COMPENSATION_CODE,
|
|
5305
|
+
"SDK broker did not prepare the requested session and returned no usable session identity.",
|
|
5306
|
+
{ creation_response: created },
|
|
5307
|
+
);
|
|
5308
|
+
remoteSession.value = createdSessionId;
|
|
5309
|
+
const unpreparedId = safeExternalId("session", createdSessionId);
|
|
5310
|
+
let compensated = true;
|
|
5311
|
+
if (unpreparedId) {
|
|
5312
|
+
try {
|
|
5313
|
+
strictBrokerSessionClose(
|
|
5314
|
+
await brokerSession(
|
|
5315
|
+
cwd,
|
|
5316
|
+
"session.close",
|
|
5317
|
+
{ sessionId: unpreparedId },
|
|
5318
|
+
`${idempotencyKey}:unprepared-close`,
|
|
5319
|
+
),
|
|
5320
|
+
unpreparedId,
|
|
5321
|
+
);
|
|
5322
|
+
} catch {
|
|
5323
|
+
compensated = false;
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
// A swallowed compensation leaves a live, untracked session while
|
|
5327
|
+
// idempotency seals the failure, so exact retries only replay the
|
|
5328
|
+
// cached error and never reach the session again. Name the session
|
|
5329
|
+
// and mark the outcome unobserved so the key is not sealed.
|
|
5330
|
+
if (!compensated)
|
|
5331
|
+
throw new SdkClientError(
|
|
5332
|
+
UNOBSERVED_COMPENSATION_CODE,
|
|
5333
|
+
`SDK broker did not prepare the requested session, and closing the unprepared session ${unpreparedId} failed; it may still be running.`,
|
|
5334
|
+
);
|
|
5335
|
+
remoteSession.value = null;
|
|
5336
|
+
throw new SdkClientError(
|
|
5337
|
+
"broker_request_unavailable",
|
|
5338
|
+
"SDK broker did not prepare the requested session.",
|
|
4912
5339
|
);
|
|
4913
5340
|
}
|
|
4914
|
-
|
|
4915
|
-
// idempotency seals the failure, so exact retries only replay the
|
|
4916
|
-
// cached error and never reach the session again. Name the session
|
|
4917
|
-
// and mark the outcome unobserved so the key is not sealed.
|
|
4918
|
-
if (!compensated)
|
|
5341
|
+
if (!createdSessionId)
|
|
4919
5342
|
throw new SdkClientError(
|
|
4920
5343
|
UNOBSERVED_COMPENSATION_CODE,
|
|
4921
|
-
|
|
5344
|
+
"SDK broker created a session but returned no usable session identity; the outcome is unobserved.",
|
|
5345
|
+
{ creation_response: created },
|
|
4922
5346
|
);
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
5347
|
+
remoteSession.value = createdSessionId;
|
|
5348
|
+
sessionId = safeExternalId("session", createdSessionId);
|
|
5349
|
+
const sessionCwd = await canonicalBrokerWorkspace(optionalString(created.cwd) ?? cwd);
|
|
5350
|
+
const binding = await exactBrokerSessionBinding(sessionId, sessionCwd);
|
|
5351
|
+
session = normalizeSession({
|
|
5352
|
+
session_id: sessionId,
|
|
5353
|
+
cwd: sessionCwd,
|
|
5354
|
+
...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
|
|
5355
|
+
...(modelResolution.model ? { model: modelResolution.model } : {}),
|
|
5356
|
+
broker_workspace: binding.workspace,
|
|
5357
|
+
endpoint_generation: binding.endpointGeneration,
|
|
5358
|
+
endpoint_incarnation: binding.endpointIncarnation,
|
|
5359
|
+
});
|
|
5360
|
+
}
|
|
5361
|
+
const intent: CanonicalCreateIntentV1 = {
|
|
5362
|
+
kind: "start",
|
|
5363
|
+
session: canonicalCreationSnapshot(session),
|
|
5364
|
+
remote_create_key: creation.request.remote_create_key,
|
|
5365
|
+
initial_state: prompt ? "running" : preparesExistingThread ? "prepared" : "ready_for_input",
|
|
5366
|
+
initial_prompt: prompt
|
|
5367
|
+
? {
|
|
5368
|
+
text: prompt,
|
|
5369
|
+
caller_key_digest: createHash("sha256").update(idempotencyKey).digest("hex"),
|
|
5370
|
+
}
|
|
5371
|
+
: null,
|
|
5372
|
+
initial_events: [],
|
|
5373
|
+
};
|
|
5374
|
+
await bindCreationRequest(questionPaths, creation.keyDigest, intent);
|
|
5375
|
+
await commitCreationWal(questionPaths, creation.keyDigest, intent);
|
|
5376
|
+
await writeJsonFile(sessionFile(sessionId), session);
|
|
5377
|
+
const lifecycle = publicLifecycleReceipt(created, sessionId);
|
|
5378
|
+
if (prompt) {
|
|
5379
|
+
const promptKey = await claimCanonicalPrompt(sessionId, prompt, "turn.prompt", idempotencyKey);
|
|
5380
|
+
const result = await controlSession(session, "turn.prompt", { text: prompt }, idempotencyKey);
|
|
5381
|
+
const acknowledgement = requirePromptAcknowledgement(result);
|
|
5382
|
+
const turn = await recordAcceptedPrompt(
|
|
5383
|
+
sessionId,
|
|
5384
|
+
prompt,
|
|
5385
|
+
"turn.prompt",
|
|
5386
|
+
null,
|
|
5387
|
+
acknowledgement,
|
|
5388
|
+
promptKey,
|
|
4926
5389
|
);
|
|
5390
|
+
const response = {
|
|
5391
|
+
ok: true,
|
|
5392
|
+
session: publicCoordinatorSession(session),
|
|
5393
|
+
session_id: sessionId,
|
|
5394
|
+
lifecycle,
|
|
5395
|
+
turn_id: turn.turn_id,
|
|
5396
|
+
active_turn_id: turn.turn_id,
|
|
5397
|
+
status: turn.status,
|
|
5398
|
+
queued: turn.delivery.queued,
|
|
5399
|
+
delivered: turn.delivery.delivered,
|
|
5400
|
+
operation: "turn.prompt",
|
|
5401
|
+
turn: boundedPublicValue(turn, { remaining: COORDINATOR_IDEMPOTENCY_RESPONSE_BYTE_CAP }),
|
|
5402
|
+
result: publicSdkAcknowledgement(acknowledgement),
|
|
5403
|
+
session_state: publicCoordinatorSessionState(
|
|
5404
|
+
await readSessionState(namespaceDir, sessionId),
|
|
5405
|
+
),
|
|
5406
|
+
};
|
|
5407
|
+
await advanceCreationReceipt(questionPaths, creation.keyDigest, "projected", response);
|
|
5408
|
+
await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
|
|
5409
|
+
return response;
|
|
4927
5410
|
}
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
const binding = await exactBrokerSessionBinding(sessionId, sessionCwd);
|
|
4931
|
-
session = normalizeSession({
|
|
4932
|
-
session_id: sessionId,
|
|
4933
|
-
cwd: sessionCwd,
|
|
4934
|
-
...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
|
|
4935
|
-
broker_workspace: binding.workspace,
|
|
4936
|
-
endpoint_generation: binding.endpointGeneration,
|
|
4937
|
-
endpoint_incarnation: binding.endpointIncarnation,
|
|
4938
|
-
});
|
|
4939
|
-
}
|
|
4940
|
-
const intent: CanonicalCreateIntentV1 = {
|
|
4941
|
-
kind: "start",
|
|
4942
|
-
session: canonicalCreationSnapshot(session),
|
|
4943
|
-
remote_create_key: creation.request.remote_create_key,
|
|
4944
|
-
initial_state: prompt ? "running" : preparesExistingThread ? "prepared" : "ready_for_input",
|
|
4945
|
-
initial_prompt: prompt
|
|
4946
|
-
? { text: prompt, caller_key_digest: createHash("sha256").update(idempotencyKey).digest("hex") }
|
|
4947
|
-
: null,
|
|
4948
|
-
initial_events: [],
|
|
4949
|
-
};
|
|
4950
|
-
await bindCreationRequest(questionPaths, creation.keyDigest, intent);
|
|
4951
|
-
await commitCreationWal(questionPaths, creation.keyDigest, intent);
|
|
4952
|
-
await writeJsonFile(sessionFile(sessionId), session);
|
|
4953
|
-
const lifecycle = publicLifecycleReceipt(created, sessionId);
|
|
4954
|
-
if (prompt) {
|
|
4955
|
-
const promptKey = await claimCanonicalPrompt(sessionId, prompt, "turn.prompt", idempotencyKey);
|
|
4956
|
-
const result = await controlSession(session, "turn.prompt", { text: prompt }, idempotencyKey);
|
|
4957
|
-
const acknowledgement = requirePromptAcknowledgement(result);
|
|
4958
|
-
const turn = await recordAcceptedPrompt(
|
|
5411
|
+
const sessionState = await writeSessionState(
|
|
5412
|
+
namespaceDir,
|
|
4959
5413
|
sessionId,
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
null,
|
|
4963
|
-
acknowledgement,
|
|
4964
|
-
promptKey,
|
|
5414
|
+
preparesExistingThread ? "prepared" : "ready_for_input",
|
|
5415
|
+
{ live: null, reason: null },
|
|
4965
5416
|
);
|
|
5417
|
+
await appendCoordinatorEvent(namespaceDir, {
|
|
5418
|
+
kind: "session.started",
|
|
5419
|
+
sessionId,
|
|
5420
|
+
summary: preparesExistingThread
|
|
5421
|
+
? `Session ${sessionId} prepared through SDK lifecycle control`
|
|
5422
|
+
: `Session ${sessionId} started through SDK lifecycle control`,
|
|
5423
|
+
payloadRef: path.relative(namespaceDir, sessionFile(sessionId)),
|
|
5424
|
+
});
|
|
4966
5425
|
const response = {
|
|
4967
5426
|
ok: true,
|
|
4968
5427
|
session: publicCoordinatorSession(session),
|
|
4969
|
-
|
|
5428
|
+
session_state: publicCoordinatorSessionState(sessionState),
|
|
4970
5429
|
lifecycle,
|
|
4971
|
-
|
|
4972
|
-
active_turn_id: turn.turn_id,
|
|
4973
|
-
status: turn.status,
|
|
4974
|
-
queued: turn.delivery.queued,
|
|
4975
|
-
delivered: turn.delivery.delivered,
|
|
4976
|
-
operation: "turn.prompt",
|
|
4977
|
-
turn: boundedPublicValue(turn, { remaining: COORDINATOR_IDEMPOTENCY_RESPONSE_BYTE_CAP }),
|
|
4978
|
-
result: publicSdkAcknowledgement(acknowledgement),
|
|
4979
|
-
session_state: publicCoordinatorSessionState(await readSessionState(namespaceDir, sessionId)),
|
|
5430
|
+
...(preparesExistingThread ? { session_id: sessionId, state: "prepared" as const } : {}),
|
|
4980
5431
|
};
|
|
4981
5432
|
await advanceCreationReceipt(questionPaths, creation.keyDigest, "projected", response);
|
|
4982
5433
|
await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
|
|
4983
5434
|
return response;
|
|
4984
|
-
}
|
|
4985
|
-
const sessionState = await writeSessionState(
|
|
4986
|
-
namespaceDir,
|
|
4987
|
-
sessionId,
|
|
4988
|
-
preparesExistingThread ? "prepared" : "ready_for_input",
|
|
4989
|
-
{ live: null, reason: null },
|
|
4990
|
-
);
|
|
4991
|
-
await appendCoordinatorEvent(namespaceDir, {
|
|
4992
|
-
kind: "session.started",
|
|
4993
|
-
sessionId,
|
|
4994
|
-
summary: preparesExistingThread
|
|
4995
|
-
? `Session ${sessionId} prepared through SDK lifecycle control`
|
|
4996
|
-
: `Session ${sessionId} started through SDK lifecycle control`,
|
|
4997
|
-
payloadRef: path.relative(namespaceDir, sessionFile(sessionId)),
|
|
4998
5435
|
});
|
|
4999
|
-
const response = {
|
|
5000
|
-
ok: true,
|
|
5001
|
-
session: publicCoordinatorSession(session),
|
|
5002
|
-
session_state: publicCoordinatorSessionState(sessionState),
|
|
5003
|
-
lifecycle,
|
|
5004
|
-
...(preparesExistingThread ? { session_id: sessionId, state: "prepared" as const } : {}),
|
|
5005
|
-
};
|
|
5006
|
-
await advanceCreationReceipt(questionPaths, creation.keyDigest, "projected", response);
|
|
5007
|
-
await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
|
|
5008
|
-
return response;
|
|
5009
5436
|
},
|
|
5010
5437
|
true,
|
|
5011
5438
|
response => isUnobservedCompensation(response) || isRouterRequestAmbiguous(response),
|
|
@@ -5700,6 +6127,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5700
6127
|
const reportPath = path.join(namespaceDir, "reports", `${reportId}.json`);
|
|
5701
6128
|
await writeJsonFile(reportPath, report);
|
|
5702
6129
|
await appendCoordinatorEvent(namespaceDir, {
|
|
6130
|
+
...(sessionId == null ? { stableId: `report-written:${reportId}` } : {}),
|
|
5703
6131
|
kind: "report.written",
|
|
5704
6132
|
sessionId,
|
|
5705
6133
|
turnId: typeof args.turn_id === "string" ? args.turn_id : null,
|