@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
|
@@ -96,6 +96,18 @@ const MAX_RECEIVED_AT_SKEW_MS = 5_000;
|
|
|
96
96
|
const MAX_LIFECYCLE_METADATA_BYTES = 4096;
|
|
97
97
|
const MAX_EFFECT_MARKER_LENGTH = 128;
|
|
98
98
|
const MAX_PROCESS_INCARNATION_LENGTH = 256;
|
|
99
|
+
const READY_THEN_EXIT_MESSAGE = "became ready then exited before live admission";
|
|
100
|
+
|
|
101
|
+
function readyThenExitedResponse(id: string, child?: ChildProcess): BrokerResponse {
|
|
102
|
+
const parts = [`Session ${id} ${READY_THEN_EXIT_MESSAGE}.`];
|
|
103
|
+
if (child && child.exitCode !== null) parts.push(`exit=${child.exitCode}`);
|
|
104
|
+
if (child?.signalCode) parts.push(`signal=${child.signalCode}`);
|
|
105
|
+
// Host stderr is deliberately excluded: the detached host receives launch
|
|
106
|
+
// configuration and inherited credentials, and no pattern-based redaction can
|
|
107
|
+
// prove arbitrary child output free of that material; its stderr is discarded
|
|
108
|
+
// by the OS and never captured (#4712 review).
|
|
109
|
+
return fail("ready_then_exited", parts.join(" "));
|
|
110
|
+
}
|
|
99
111
|
|
|
100
112
|
export interface LifecycleDeadlines {
|
|
101
113
|
receivedAt: number;
|
|
@@ -161,6 +173,25 @@ export function setLifecycleTimingForTest(broker: Broker, timing: LifecycleTimin
|
|
|
161
173
|
else lifecycleTimingsForTest.delete(broker);
|
|
162
174
|
}
|
|
163
175
|
|
|
176
|
+
let lifecycleHostPlatformForTest: NodeJS.Platform | undefined;
|
|
177
|
+
|
|
178
|
+
/** Deterministic platform seam for Windows-only ready-then-exit tolerance. */
|
|
179
|
+
export function setLifecycleHostPlatformForTest(platform: NodeJS.Platform | undefined): void {
|
|
180
|
+
lifecycleHostPlatformForTest = platform;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function lifecycleHostPlatform(): NodeJS.Platform {
|
|
184
|
+
return lifecycleHostPlatformForTest ?? process.platform;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function readyThenExitToleranceEnabled(): boolean {
|
|
188
|
+
return lifecycleHostPlatform() === "win32";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function readyThenExitToleranceEnabledForTest(): boolean {
|
|
192
|
+
return readyThenExitToleranceEnabled();
|
|
193
|
+
}
|
|
194
|
+
|
|
164
195
|
function lifecycleTiming(broker: Broker): LifecycleTiming {
|
|
165
196
|
return lifecycleTimingsForTest.get(broker) ?? defaultLifecycleTiming;
|
|
166
197
|
}
|
|
@@ -244,6 +275,14 @@ export interface SessionLifecycleLaunchRequest {
|
|
|
244
275
|
sessionIdentity?: SessionLifecycleTranscriptIdentity;
|
|
245
276
|
/** Broker-issued effect marker which the child echoes only after host readiness. */
|
|
246
277
|
effectMarker?: string;
|
|
278
|
+
/**
|
|
279
|
+
* Explicit `provider/model` pin with `gjc --model` grammar (#4707). The
|
|
280
|
+
* coordinator resolves it before the broker; the session host applies it
|
|
281
|
+
* exactly like a CLI `--model` selection, so it wins over `modelPreset`
|
|
282
|
+
* (mirroring CLI precedence where an explicit `--model` overrides
|
|
283
|
+
* activated profiles).
|
|
284
|
+
*/
|
|
285
|
+
modelId?: string;
|
|
247
286
|
modelPreset?: string;
|
|
248
287
|
mcpServers?: SessionLifecycleMcpServer[];
|
|
249
288
|
worktree?: SessionLifecycleWorktreeTarget;
|
|
@@ -385,6 +424,7 @@ export function readSessionLifecycleLaunchRequest(
|
|
|
385
424
|
(request.sessionIdentity !== undefined && !isSessionLifecycleTranscriptIdentity(request.sessionIdentity)) ||
|
|
386
425
|
(request.effectMarker !== undefined &&
|
|
387
426
|
(typeof request.effectMarker !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(request.effectMarker))) ||
|
|
427
|
+
(request.modelId !== undefined && (typeof request.modelId !== "string" || !request.modelId.trim())) ||
|
|
388
428
|
(request.modelPreset !== undefined && (typeof request.modelPreset !== "string" || !request.modelPreset)) ||
|
|
389
429
|
(request.mcpServers !== undefined && !isSessionLifecycleMcpServers(request.mcpServers)) ||
|
|
390
430
|
!hasValidLifecycleDeadlines(
|
|
@@ -427,6 +467,13 @@ type SessionLaunch = {
|
|
|
427
467
|
sourceCwd?: string;
|
|
428
468
|
sessionPath?: string;
|
|
429
469
|
sessionIdentity?: SessionLifecycleTranscriptIdentity;
|
|
470
|
+
/**
|
|
471
|
+
* Explicit `provider/model` pin with `gjc --model` grammar (#4707). Applied
|
|
472
|
+
* by the session host exactly like a CLI `--model` selection, so it wins
|
|
473
|
+
* over `modelPreset` (mirroring CLI precedence where an explicit `--model`
|
|
474
|
+
* overrides activated profiles).
|
|
475
|
+
*/
|
|
476
|
+
modelId?: string;
|
|
430
477
|
modelPreset?: string;
|
|
431
478
|
mcpServers?: SessionLifecycleMcpServer[];
|
|
432
479
|
/** Coordinator namespace dir; broker computes the state file path from launch.id (#2549). */
|
|
@@ -798,6 +845,8 @@ type ReadyAuthority = {
|
|
|
798
845
|
type ReadinessResult =
|
|
799
846
|
| { kind: "ready"; authority: ReadyAuthority }
|
|
800
847
|
| { kind: "startup_failed"; failure: SdkStartupFailure }
|
|
848
|
+
| { kind: "ready_then_exited" }
|
|
849
|
+
| { kind: "ready_probe_failed"; probe: ReadyAuthorityProbe }
|
|
801
850
|
| { kind: "child_exited" }
|
|
802
851
|
| { kind: "timeout" };
|
|
803
852
|
type BrokerIndex = Pick<Broker, "index">;
|
|
@@ -974,10 +1023,26 @@ async function writeEffectMarker(root: string, id: string, marker: EffectMarker)
|
|
|
974
1023
|
export async function writeSessionLifecycleReady(root: string, id: string, effectMarker: string): Promise<void> {
|
|
975
1024
|
const incarnation = processIncarnation(process.pid);
|
|
976
1025
|
if (!incarnation) throw new Error("Lifecycle child has no readable OS incarnation.");
|
|
977
|
-
|
|
978
|
-
await fs.
|
|
979
|
-
|
|
980
|
-
|
|
1026
|
+
const directory = path.join(root, "sdk");
|
|
1027
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
1028
|
+
const temporary = path.join(directory, `.${id}.lifecycle.ready.${randomUUID()}.tmp`);
|
|
1029
|
+
const handle = await fs.open(
|
|
1030
|
+
temporary,
|
|
1031
|
+
fsSync.constants.O_CREAT | fsSync.constants.O_EXCL | fsSync.constants.O_WRONLY,
|
|
1032
|
+
0o600,
|
|
1033
|
+
);
|
|
1034
|
+
try {
|
|
1035
|
+
await handle.writeFile(canonicalJson({ pid: process.pid, effectMarker, incarnation }));
|
|
1036
|
+
await handle.sync();
|
|
1037
|
+
} finally {
|
|
1038
|
+
await handle.close();
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
await fs.rename(temporary, lifecycleReadyPath(root, id));
|
|
1042
|
+
await syncDirectory(directory);
|
|
1043
|
+
} finally {
|
|
1044
|
+
await fs.rm(temporary, { force: true });
|
|
1045
|
+
}
|
|
981
1046
|
}
|
|
982
1047
|
|
|
983
1048
|
export interface LifecycleTranscriptEvidence {
|
|
@@ -1074,6 +1139,10 @@ function isLifecycleFailureArtifact(value: unknown): value is LifecycleFailureAr
|
|
|
1074
1139
|
}
|
|
1075
1140
|
|
|
1076
1141
|
async function syncDirectory(directory: string): Promise<void> {
|
|
1142
|
+
// Windows does not support fsync on directory handles. File contents and
|
|
1143
|
+
// lifecycle markers are still written through the normal atomic path; the
|
|
1144
|
+
// directory durability barrier is a POSIX-only operation.
|
|
1145
|
+
if (process.platform === "win32") return;
|
|
1077
1146
|
const handle = await fs.open(directory, fsSync.constants.O_RDONLY);
|
|
1078
1147
|
try {
|
|
1079
1148
|
await handle.sync();
|
|
@@ -1082,7 +1151,7 @@ async function syncDirectory(directory: string): Promise<void> {
|
|
|
1082
1151
|
}
|
|
1083
1152
|
}
|
|
1084
1153
|
|
|
1085
|
-
/** The child
|
|
1154
|
+
/** Writes bounded startup diagnostics. The child stamps its own pid; the broker may stamp a proven child identity. */
|
|
1086
1155
|
export async function writeSessionLifecycleFailure(
|
|
1087
1156
|
root: string,
|
|
1088
1157
|
id: string,
|
|
@@ -1091,6 +1160,7 @@ export async function writeSessionLifecycleFailure(
|
|
|
1091
1160
|
rollback: SdkStartupRollbackResult,
|
|
1092
1161
|
transcript?: LifecycleTranscriptEvidence,
|
|
1093
1162
|
ownerIncarnation?: string,
|
|
1163
|
+
ownerPid?: number,
|
|
1094
1164
|
): Promise<void> {
|
|
1095
1165
|
if (!isSdkStartupFailure(failure))
|
|
1096
1166
|
throw new Error("Lifecycle startup failure does not satisfy the canonical failure contract.");
|
|
@@ -1105,7 +1175,7 @@ export async function writeSessionLifecycleFailure(
|
|
|
1105
1175
|
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
1106
1176
|
await fs.chmod(directory, 0o700);
|
|
1107
1177
|
const artifact: LifecycleFailureArtifact = {
|
|
1108
|
-
pid: process.pid,
|
|
1178
|
+
pid: ownerPid ?? process.pid,
|
|
1109
1179
|
effectMarker,
|
|
1110
1180
|
incarnation,
|
|
1111
1181
|
...failure,
|
|
@@ -2074,6 +2144,89 @@ async function hasOwnedReadinessEvidence(
|
|
|
2074
2144
|
);
|
|
2075
2145
|
}
|
|
2076
2146
|
|
|
2147
|
+
/**
|
|
2148
|
+
* Typed ready-then-exit probe for a dead child (#4712 review: the earlier
|
|
2149
|
+
* boolean collapsed EACCES/EIO/malformed JSON into "absent", routing the
|
|
2150
|
+
* teardown decision through the secondary authority and finally surfacing as
|
|
2151
|
+
* a false `spawn_failed` claim that the child "exited before registering
|
|
2152
|
+
* readiness" when the real failure was reading the endpoint).
|
|
2153
|
+
*
|
|
2154
|
+
* A host that died hard leaves its endpoint file on disk; a host that tore
|
|
2155
|
+
* down gracefully removed it first. `matched` requires the owned marker +
|
|
2156
|
+
* ready marker chain to still name exactly this child and the endpoint file
|
|
2157
|
+
* to still name the same pid and session. `absent_indexed` means the endpoint
|
|
2158
|
+
* file is gone (ENOENT only) and the broker's own session index recorded a
|
|
2159
|
+
* host registration for exactly this incarnation — the sole permitted
|
|
2160
|
+
* fallback authority. `malformed` and `io_error` are their own fail-closed
|
|
2161
|
+
* outcomes: they feed no teardown decision and assert nothing about the
|
|
2162
|
+
* child process.
|
|
2163
|
+
*/
|
|
2164
|
+
type ReadyAuthorityProbe =
|
|
2165
|
+
| { kind: "matched" }
|
|
2166
|
+
| { kind: "absent_indexed" }
|
|
2167
|
+
| { kind: "absent_unindexed" }
|
|
2168
|
+
| { kind: "not_published" }
|
|
2169
|
+
| { kind: "malformed" }
|
|
2170
|
+
| { kind: "io_error"; code: string };
|
|
2171
|
+
|
|
2172
|
+
async function probePublishedReadyAuthority(
|
|
2173
|
+
root: string,
|
|
2174
|
+
id: string,
|
|
2175
|
+
expected: EffectMarker,
|
|
2176
|
+
index?: BrokerIndex,
|
|
2177
|
+
): Promise<ReadyAuthorityProbe> {
|
|
2178
|
+
const [effect, ready] = await Promise.all([
|
|
2179
|
+
readEffectMarker(lifecycleMarkerPath(root, id)),
|
|
2180
|
+
readEffectMarker(lifecycleReadyPath(root, id)),
|
|
2181
|
+
]);
|
|
2182
|
+
if (!effect || !ready || !sameEffectMarker(effect, expected) || !sameEffectMarker(ready, expected))
|
|
2183
|
+
return { kind: "not_published" };
|
|
2184
|
+
let endpointText: string;
|
|
2185
|
+
try {
|
|
2186
|
+
endpointText = await fs.readFile(path.join(root, "sdk", `${id}.json`), "utf8");
|
|
2187
|
+
} catch (error) {
|
|
2188
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
2189
|
+
// Only a missing endpoint means "gracefully removed"; anything else
|
|
2190
|
+
// (EACCES, EIO, ...) is its own condition and must not silently route the
|
|
2191
|
+
// teardown decision to the secondary authority.
|
|
2192
|
+
if (code !== "ENOENT") return { kind: "io_error", code: code ?? "unknown" };
|
|
2193
|
+
if (!index) return { kind: "absent_unindexed" };
|
|
2194
|
+
const sessionIndex = index.index;
|
|
2195
|
+
await sessionIndex.refresh();
|
|
2196
|
+
return sessionIndex
|
|
2197
|
+
.listSessions()
|
|
2198
|
+
.sessions.some(
|
|
2199
|
+
session =>
|
|
2200
|
+
session.sessionId === id &&
|
|
2201
|
+
session.pid === expected.pid &&
|
|
2202
|
+
(session.hostIncarnation ?? session.processIncarnation) === expected.incarnation,
|
|
2203
|
+
)
|
|
2204
|
+
? { kind: "absent_indexed" }
|
|
2205
|
+
: { kind: "absent_unindexed" };
|
|
2206
|
+
}
|
|
2207
|
+
let endpoint: { sessionId?: unknown; pid?: unknown };
|
|
2208
|
+
try {
|
|
2209
|
+
endpoint = JSON.parse(endpointText) as { sessionId?: unknown; pid?: unknown };
|
|
2210
|
+
} catch {
|
|
2211
|
+
return { kind: "malformed" };
|
|
2212
|
+
}
|
|
2213
|
+
return endpoint.pid === expected.pid && endpoint.sessionId === id ? { kind: "matched" } : { kind: "not_published" };
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
/** Test seam for the typed ready-authority probe boundary (#4712 review). */
|
|
2217
|
+
export async function probePublishedReadyAuthorityForTest(
|
|
2218
|
+
root: string,
|
|
2219
|
+
id: string,
|
|
2220
|
+
expected: EffectMarker,
|
|
2221
|
+
): Promise<ReadyAuthorityProbe> {
|
|
2222
|
+
return probePublishedReadyAuthority(root, id, expected);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/** Human-readable, honest description of a fail-closed probe outcome. */
|
|
2226
|
+
function describeReadyProbeFailure(probe: ReadyAuthorityProbe): string {
|
|
2227
|
+
return probe.kind === "io_error" ? `endpoint read failed (${probe.code})` : "endpoint file is malformed";
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2077
2230
|
type LifecycleFileCapture = {
|
|
2078
2231
|
bytes: Buffer;
|
|
2079
2232
|
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint };
|
|
@@ -2176,6 +2329,15 @@ async function removeOwnedLifecycleArtifacts(
|
|
|
2176
2329
|
}
|
|
2177
2330
|
if (parsed.pid !== expected.pid || observeProcess(expected.pid, expected.incarnation) !== "exited") return false;
|
|
2178
2331
|
if (createHash("sha256").update(endpoint.bytes).digest("hex") !== endpoint.digest) return false;
|
|
2332
|
+
// Bind the deletion to the expected process incarnation and endpoint
|
|
2333
|
+
// identity, rechecking immediately before unlink: a successor launch for
|
|
2334
|
+
// this session rewrites the lifecycle marker through the broker before its
|
|
2335
|
+
// child can publish anything, so a marker that no longer matches `expected`
|
|
2336
|
+
// means this endpoint file is no longer provably the dead child's — it may
|
|
2337
|
+
// belong to a PID-reusing successor and must not be unlinked (#4712
|
|
2338
|
+
// review: PID-reuse race must not delete a successor endpoint).
|
|
2339
|
+
const preUnlinkMarker = await readEffectMarker(lifecycleMarkerPath(root, id));
|
|
2340
|
+
if (!preUnlinkMarker || !sameEffectMarker(preUnlinkMarker, expected)) return false;
|
|
2179
2341
|
const endpointRemoval = exactUnlinkLifecycleFile(
|
|
2180
2342
|
endpointSource,
|
|
2181
2343
|
endpoint.identity,
|
|
@@ -2239,6 +2401,19 @@ async function removeOwnedLifecycleArtifacts(
|
|
|
2239
2401
|
return true;
|
|
2240
2402
|
}
|
|
2241
2403
|
|
|
2404
|
+
/**
|
|
2405
|
+
* Test seam for the PID-reuse deletion boundary: a successor launch rewriting
|
|
2406
|
+
* the lifecycle marker must stop the unlink of an endpoint that only matches
|
|
2407
|
+
* by a reused pid (#4712 review).
|
|
2408
|
+
*/
|
|
2409
|
+
export async function removeOwnedLifecycleArtifactsForTest(
|
|
2410
|
+
root: string,
|
|
2411
|
+
id: string,
|
|
2412
|
+
expected: EffectMarker,
|
|
2413
|
+
): Promise<boolean> {
|
|
2414
|
+
return removeOwnedLifecycleArtifacts(root, id, expected);
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2242
2417
|
async function recordTerminalUncertain(broker: Broker, id: string, root: string, pid: number): Promise<void> {
|
|
2243
2418
|
await broker.index.refresh();
|
|
2244
2419
|
const registered = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
@@ -2344,12 +2519,61 @@ async function terminateSpawnedChild(
|
|
|
2344
2519
|
lifecycleFailurePath(root, id, expected.effectMarker),
|
|
2345
2520
|
expected,
|
|
2346
2521
|
);
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
) {
|
|
2522
|
+
const rollbackComplete =
|
|
2523
|
+
failure?.artifact.rollback.fenced === true &&
|
|
2524
|
+
failure.artifact.rollback.runtimeRemoved &&
|
|
2525
|
+
failure.artifact.rollback.hostStopped &&
|
|
2526
|
+
failure.artifact.rollback.brokerRegistrationReleased;
|
|
2527
|
+
if (!rollbackComplete) {
|
|
2528
|
+
if (!readyThenExitToleranceEnabled()) {
|
|
2529
|
+
await recordTerminalUncertain(broker, id, root, pid);
|
|
2530
|
+
return false;
|
|
2531
|
+
}
|
|
2532
|
+
const publishedReady = (await probePublishedReadyAuthority(root, id, expected)).kind === "matched";
|
|
2533
|
+
const artifactsRemoved = await removeOwnedLifecycleArtifacts(root, id, expected);
|
|
2534
|
+
await broker.index.refresh();
|
|
2535
|
+
const registered = broker.index
|
|
2536
|
+
.listSessions()
|
|
2537
|
+
.sessions.find(session => session.sessionId === id && session.pid === pid);
|
|
2538
|
+
const registeredRowTerminal = registered?.terminal === true || registered?.terminalUncertain === true;
|
|
2539
|
+
let registrationReleased =
|
|
2540
|
+
!broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) || registeredRowTerminal;
|
|
2541
|
+
if (registered && !registrationReleased) {
|
|
2542
|
+
registrationReleased = await broker.index.unregisterIfCurrent(registered);
|
|
2543
|
+
await broker.index.refresh();
|
|
2544
|
+
registrationReleased =
|
|
2545
|
+
registrationReleased || !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker);
|
|
2546
|
+
}
|
|
2547
|
+
const stillExited =
|
|
2548
|
+
observeProcess(pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) === "exited";
|
|
2549
|
+
const endpointGone = await endpointRemoved(root, id);
|
|
2550
|
+
try {
|
|
2551
|
+
if (publishedReady && !failure) {
|
|
2552
|
+
await writeSessionLifecycleFailure(
|
|
2553
|
+
root,
|
|
2554
|
+
id,
|
|
2555
|
+
expected.effectMarker,
|
|
2556
|
+
{
|
|
2557
|
+
phase: "startup",
|
|
2558
|
+
reason: "failed",
|
|
2559
|
+
message: `Session ${id} ${READY_THEN_EXIT_MESSAGE}.`,
|
|
2560
|
+
},
|
|
2561
|
+
{
|
|
2562
|
+
endpointGeneration: registered?.endpointGeneration ?? null,
|
|
2563
|
+
fenced: stillExited && registrationReleased,
|
|
2564
|
+
runtimeRemoved: artifactsRemoved && endpointGone,
|
|
2565
|
+
hostStopped: stillExited,
|
|
2566
|
+
brokerRegistrationReleased: registrationReleased,
|
|
2567
|
+
},
|
|
2568
|
+
undefined,
|
|
2569
|
+
expected.incarnation,
|
|
2570
|
+
expected.pid,
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
} catch {
|
|
2574
|
+
// A missing broker-authored receipt must not hide a proven dead child.
|
|
2575
|
+
}
|
|
2576
|
+
if (stillExited && artifactsRemoved && endpointGone && registrationReleased) return true;
|
|
2353
2577
|
await recordTerminalUncertain(broker, id, root, pid);
|
|
2354
2578
|
return false;
|
|
2355
2579
|
}
|
|
@@ -2668,14 +2892,17 @@ async function currentReadyAuthority(
|
|
|
2668
2892
|
token?: unknown;
|
|
2669
2893
|
pid?: unknown;
|
|
2670
2894
|
};
|
|
2671
|
-
//
|
|
2672
|
-
//
|
|
2673
|
-
//
|
|
2895
|
+
// Native-alive owned readiness is the admission authority. `record.live`
|
|
2896
|
+
// also requires a fresh index heartbeat projection, which can lag a just-
|
|
2897
|
+
// registered detached Windows host. Never admit a terminal/uncertain or
|
|
2898
|
+
// native-dead child; do not refuse a native-alive ready host for a stale live bit.
|
|
2674
2899
|
await broker.heartbeatSessions();
|
|
2675
2900
|
await broker.index.refresh();
|
|
2676
2901
|
const record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
2677
2902
|
if (
|
|
2678
|
-
!record
|
|
2903
|
+
!record ||
|
|
2904
|
+
record.terminal ||
|
|
2905
|
+
record.terminalUncertain ||
|
|
2679
2906
|
record.pid !== expected.pid ||
|
|
2680
2907
|
resolveEquivalentPath(record.locator.stateRoot) !== resolveEquivalentPath(root) ||
|
|
2681
2908
|
record.endpointMtimeMs !== endpointMetadata.mtimeMs ||
|
|
@@ -2685,6 +2912,11 @@ async function currentReadyAuthority(
|
|
|
2685
2912
|
typeof endpoint.token !== "string"
|
|
2686
2913
|
)
|
|
2687
2914
|
return undefined;
|
|
2915
|
+
if (
|
|
2916
|
+
observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) !==
|
|
2917
|
+
"alive"
|
|
2918
|
+
)
|
|
2919
|
+
return undefined;
|
|
2688
2920
|
return {
|
|
2689
2921
|
endpoint: endpoint as Record<string, unknown>,
|
|
2690
2922
|
endpointSource,
|
|
@@ -2723,17 +2955,40 @@ async function waitForReady(
|
|
|
2723
2955
|
timing: LifecycleTiming,
|
|
2724
2956
|
signal: "session_ready" | typeof SESSION_PREPARED_EVENT = "session_ready",
|
|
2725
2957
|
): Promise<ReadinessResult> {
|
|
2958
|
+
const classifyExitedAfterReady = async (): Promise<ReadinessResult> => {
|
|
2959
|
+
const probe = await probePublishedReadyAuthority(root, id, expected, broker);
|
|
2960
|
+
if (probe.kind === "malformed" || probe.kind === "io_error") return { kind: "ready_probe_failed", probe };
|
|
2961
|
+
const publishedReady = probe.kind === "matched" || probe.kind === "absent_indexed";
|
|
2962
|
+
return publishedReady && readyThenExitToleranceEnabled()
|
|
2963
|
+
? { kind: "ready_then_exited" }
|
|
2964
|
+
: { kind: "child_exited" };
|
|
2965
|
+
};
|
|
2726
2966
|
while (timing.now() < deadline) {
|
|
2727
2967
|
const startupFailure = await readSessionLifecycleFailure(root, id, expected);
|
|
2728
|
-
if (startupFailure)
|
|
2968
|
+
if (startupFailure) {
|
|
2969
|
+
if (
|
|
2970
|
+
observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
|
|
2971
|
+
"exited"
|
|
2972
|
+
) {
|
|
2973
|
+
const afterReady = await classifyExitedAfterReady();
|
|
2974
|
+
if (afterReady.kind !== "child_exited") return afterReady;
|
|
2975
|
+
}
|
|
2976
|
+
return { kind: "startup_failed", failure: startupFailure };
|
|
2977
|
+
}
|
|
2729
2978
|
if (
|
|
2730
2979
|
observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
|
|
2731
2980
|
"exited"
|
|
2732
2981
|
) {
|
|
2733
2982
|
const finalStartupFailure = await readSessionLifecycleFailure(root, id, expected);
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2983
|
+
if (finalStartupFailure) {
|
|
2984
|
+
const afterReady = await classifyExitedAfterReady();
|
|
2985
|
+
if (afterReady.kind !== "child_exited") return afterReady;
|
|
2986
|
+
return { kind: "startup_failed", failure: finalStartupFailure };
|
|
2987
|
+
}
|
|
2988
|
+
// A malformed or unreadable endpoint is its own fail-closed outcome: it
|
|
2989
|
+
// never feeds the teardown decision and never claims the child "exited
|
|
2990
|
+
// before registering readiness" (#4712 review).
|
|
2991
|
+
return classifyExitedAfterReady();
|
|
2737
2992
|
}
|
|
2738
2993
|
try {
|
|
2739
2994
|
const authority = await currentReadyAuthority(broker, id, root, expected);
|
|
@@ -2840,6 +3095,14 @@ async function launchInput(
|
|
|
2840
3095
|
if (typeof validatedModelPreset !== "string") return validatedModelPreset;
|
|
2841
3096
|
modelPreset = validatedModelPreset;
|
|
2842
3097
|
}
|
|
3098
|
+
// An explicit model pin (#4707) is coordinator-resolved before it reaches
|
|
3099
|
+
// the broker; the broker only guards shape here. It deliberately does NOT
|
|
3100
|
+
// shadow modelPreset: both are threaded to the child, whose startup applies
|
|
3101
|
+
// the profile first and then overrides it with the explicit model exactly
|
|
3102
|
+
// like `gjc --mpreset p --model m` behaves on the CLI.
|
|
3103
|
+
const modelId = text(input.modelId)?.trim();
|
|
3104
|
+
if (input.modelId !== undefined && !modelId)
|
|
3105
|
+
return fail("invalid_input", "modelId must be a non-empty explicit provider/model selector.");
|
|
2843
3106
|
const worktree = lifecycleWorktreeTarget(input);
|
|
2844
3107
|
if (worktree === null || (worktree !== undefined && requestedCwd === undefined))
|
|
2845
3108
|
return fail("invalid_input", "Lifecycle worktree target is invalid.");
|
|
@@ -2896,6 +3159,7 @@ async function launchInput(
|
|
|
2896
3159
|
id: randomUUID(),
|
|
2897
3160
|
cwd,
|
|
2898
3161
|
root: resolvedRoot,
|
|
3162
|
+
modelId,
|
|
2899
3163
|
modelPreset,
|
|
2900
3164
|
mcpServers,
|
|
2901
3165
|
worktree,
|
|
@@ -2916,6 +3180,7 @@ async function launchInput(
|
|
|
2916
3180
|
cwd,
|
|
2917
3181
|
root: resolvedRoot,
|
|
2918
3182
|
sessionPath: saved.path,
|
|
3183
|
+
modelId,
|
|
2919
3184
|
sessionIdentity: saved.identity,
|
|
2920
3185
|
modelPreset,
|
|
2921
3186
|
mcpServers,
|
|
@@ -2942,6 +3207,7 @@ async function launchInput(
|
|
|
2942
3207
|
sourceSessionPath: source.path,
|
|
2943
3208
|
sourceSessionIdentity: source.identity,
|
|
2944
3209
|
sourceCwd,
|
|
3210
|
+
modelId,
|
|
2945
3211
|
modelPreset,
|
|
2946
3212
|
mcpServers,
|
|
2947
3213
|
worktree,
|
|
@@ -3645,6 +3911,7 @@ async function executeLifecycleResponse(
|
|
|
3645
3911
|
...(launch.sessionPath ? { sessionPath: launch.sessionPath } : {}),
|
|
3646
3912
|
...(launch.sessionIdentity ? { sessionIdentity: launch.sessionIdentity } : {}),
|
|
3647
3913
|
...(launch.modelPreset ? { modelPreset: launch.modelPreset } : {}),
|
|
3914
|
+
...(launch.modelId ? { modelId: launch.modelId } : {}),
|
|
3648
3915
|
...(launch.mcpServers ? { mcpServers: launch.mcpServers } : {}),
|
|
3649
3916
|
...(launch.worktree ? { worktree: launch.worktree } : {}),
|
|
3650
3917
|
...(launch.readiness ? { readiness: launch.readiness } : {}),
|
|
@@ -3660,6 +3927,11 @@ async function executeLifecycleResponse(
|
|
|
3660
3927
|
return spawn(cmd.file, cmd.args, {
|
|
3661
3928
|
cwd: launch.cwd,
|
|
3662
3929
|
detached: true,
|
|
3930
|
+
// stdio stays "ignore": `unref()` does not detach an active
|
|
3931
|
+
// stdio handle, so a parent-owned stderr pipe would keep the
|
|
3932
|
+
// broker process alive for the host's whole lifetime (retention
|
|
3933
|
+
// through restart/shutdown). Host stderr is therefore discarded
|
|
3934
|
+
// by the OS rather than captured (#4712 review).
|
|
3663
3935
|
stdio: "ignore",
|
|
3664
3936
|
env: {
|
|
3665
3937
|
...("kind" in cmd ? cmd.env : process.env),
|
|
@@ -3692,11 +3964,12 @@ async function executeLifecycleResponse(
|
|
|
3692
3964
|
},
|
|
3693
3965
|
});
|
|
3694
3966
|
});
|
|
3695
|
-
if (!authorizedSpawn.authorized)
|
|
3967
|
+
if (!authorizedSpawn.authorized) {
|
|
3696
3968
|
return fail(
|
|
3697
3969
|
"startup_admission_refused",
|
|
3698
3970
|
"SDK host startup was refused because the broker no longer owns the session root.",
|
|
3699
3971
|
);
|
|
3972
|
+
}
|
|
3700
3973
|
const spawned = authorizedSpawn.value;
|
|
3701
3974
|
child = spawned;
|
|
3702
3975
|
const pid = spawned.pid;
|
|
@@ -3730,8 +4003,9 @@ async function executeLifecycleResponse(
|
|
|
3730
4003
|
`Unable to establish spawned-session ownership and could not prove the child dead: ${error instanceof Error ? error.message : String(error)}`,
|
|
3731
4004
|
);
|
|
3732
4005
|
}
|
|
3733
|
-
if (!child || !spawnedAuthority)
|
|
4006
|
+
if (!child || !spawnedAuthority) {
|
|
3734
4007
|
return fail("spawn_failed", "Unable to retain the spawned session process identity.");
|
|
4008
|
+
}
|
|
3735
4009
|
await broker.ledger.transition(identity, "awaiting_ready", { intendedSessionId: launch.id, effectMarker });
|
|
3736
4010
|
const readiness = await waitForReady(
|
|
3737
4011
|
broker,
|
|
@@ -3756,10 +4030,19 @@ async function executeLifecycleResponse(
|
|
|
3756
4030
|
);
|
|
3757
4031
|
|
|
3758
4032
|
if (!terminated)
|
|
3759
|
-
return
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
4033
|
+
return readiness.kind === "ready_probe_failed"
|
|
4034
|
+
? fail(
|
|
4035
|
+
"endpoint_unreadable",
|
|
4036
|
+
`Session ${launch.id} exited, and its readiness could not be determined: ${describeReadyProbeFailure(readiness.probe)}; cleanup could not be proven.`,
|
|
4037
|
+
)
|
|
4038
|
+
: fail(
|
|
4039
|
+
"terminal_uncertain",
|
|
4040
|
+
`Session ${launch.id} did not become ready and its spawned process could not be verified dead.`,
|
|
4041
|
+
);
|
|
4042
|
+
// Host stderr never reaches caller-visible error strings: the detached
|
|
4043
|
+
// host handles launch configuration and inherited credentials, so its
|
|
4044
|
+
// output cannot be proven free of secret material — it is discarded at
|
|
4045
|
+
// the OS level (stdio "ignore"), never captured (#4712 review).
|
|
3763
4046
|
return readiness.kind === "startup_failed"
|
|
3764
4047
|
? fail(
|
|
3765
4048
|
readiness.failure.code ?? "spawn_failed",
|
|
@@ -3767,16 +4050,28 @@ async function executeLifecycleResponse(
|
|
|
3767
4050
|
undefined,
|
|
3768
4051
|
readiness.failure.details,
|
|
3769
4052
|
)
|
|
3770
|
-
: readiness.kind === "
|
|
3771
|
-
?
|
|
3772
|
-
:
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
4053
|
+
: readiness.kind === "ready_then_exited"
|
|
4054
|
+
? readyThenExitedResponse(launch.id, child)
|
|
4055
|
+
: readiness.kind === "ready_probe_failed"
|
|
4056
|
+
? fail(
|
|
4057
|
+
"endpoint_unreadable",
|
|
4058
|
+
`Session ${launch.id} exited, and its readiness could not be determined: ${describeReadyProbeFailure(readiness.probe)}.`,
|
|
4059
|
+
)
|
|
4060
|
+
: readiness.kind === "child_exited"
|
|
4061
|
+
? fail("spawn_failed", `Session ${launch.id} exited before registering readiness.`)
|
|
4062
|
+
: fail(
|
|
4063
|
+
"readiness_timeout",
|
|
4064
|
+
`Session ${launch.id} did not register an endpoint before the readiness timeout.`,
|
|
4065
|
+
);
|
|
3776
4066
|
}
|
|
3777
4067
|
await reconcileReadyScope(broker, launch.id, launch.cwd);
|
|
3778
4068
|
const verified = await currentReadyAuthority(broker, launch.id, launch.root, spawnedAuthority);
|
|
3779
4069
|
if (!verified || !sameReadyAuthority(readiness.authority, verified)) {
|
|
4070
|
+
const exited =
|
|
4071
|
+
child.exitCode !== null ||
|
|
4072
|
+
observeProcess(spawnedAuthority.pid, spawnedAuthority.incarnation, value =>
|
|
4073
|
+
processIncarnationForBroker(broker, value),
|
|
4074
|
+
) === "exited";
|
|
3780
4075
|
const terminated = await terminateSpawnedChild(
|
|
3781
4076
|
child,
|
|
3782
4077
|
broker,
|
|
@@ -3787,6 +4082,13 @@ async function executeLifecycleResponse(
|
|
|
3787
4082
|
spawnedAuthority,
|
|
3788
4083
|
timing,
|
|
3789
4084
|
);
|
|
4085
|
+
if (exited && readyThenExitToleranceEnabled())
|
|
4086
|
+
return terminated
|
|
4087
|
+
? readyThenExitedResponse(launch.id, child)
|
|
4088
|
+
: fail(
|
|
4089
|
+
"terminal_uncertain",
|
|
4090
|
+
`Session ${launch.id} ${READY_THEN_EXIT_MESSAGE} and cleanup could not be proven.`,
|
|
4091
|
+
);
|
|
3790
4092
|
return terminated
|
|
3791
4093
|
? fail("endpoint_stale", "Session endpoint changed while lifecycle readiness was being verified.")
|
|
3792
4094
|
: fail(
|
|
@@ -4870,40 +5172,64 @@ export async function executeLifecycle(
|
|
|
4870
5172
|
...(durableEffects ? { durableEffects } : {}),
|
|
4871
5173
|
...(startupFailure ? { startupFailure } : {}),
|
|
4872
5174
|
};
|
|
5175
|
+
const provenExpected = expected;
|
|
5176
|
+
const provenRoot = root;
|
|
5177
|
+
const provenId = entry?.intendedSessionId;
|
|
5178
|
+
const provenDeadCleanup =
|
|
5179
|
+
Boolean(provenExpected) &&
|
|
5180
|
+
Boolean(provenRoot) &&
|
|
5181
|
+
Boolean(provenId) &&
|
|
5182
|
+
observeProcess(provenExpected!.pid, provenExpected!.incarnation, value =>
|
|
5183
|
+
processIncarnationForBroker(broker, value),
|
|
5184
|
+
) === "exited" &&
|
|
5185
|
+
(await endpointRemoved(provenRoot!, provenId!));
|
|
4873
5186
|
const terminalResponse: BrokerResponse =
|
|
4874
5187
|
!response.ok &&
|
|
4875
5188
|
entry?.effectMarker &&
|
|
4876
5189
|
(operation === "session.create" || operation === "session.fork" || operation === "session.resume")
|
|
4877
5190
|
? entry.effectIntent?.childOwnershipEstablished === false
|
|
4878
5191
|
? response
|
|
4879
|
-
:
|
|
4880
|
-
?
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
5192
|
+
: response.error.code === "endpoint_unreadable"
|
|
5193
|
+
? response
|
|
5194
|
+
: // An unreadable/corrupt endpoint is already the fail-closed honest
|
|
5195
|
+
// terminal classification (it names the artifact, not the
|
|
5196
|
+
// child); the reconciliation wrapper must not erase it.
|
|
5197
|
+
response.error.code === "ready_then_exited" && (cleanupProof || provenDeadCleanup)
|
|
5198
|
+
? {
|
|
5199
|
+
...response,
|
|
5200
|
+
...(durableEffects ? { durableEffects } : {}),
|
|
5201
|
+
...(startupFailure ? { startupFailure } : {}),
|
|
5202
|
+
}
|
|
5203
|
+
: startupFailure && cleanupProof
|
|
5204
|
+
? {
|
|
5205
|
+
ok: false,
|
|
5206
|
+
error: startupFailure.code
|
|
5207
|
+
? {
|
|
5208
|
+
code: startupFailure.code,
|
|
5209
|
+
message: startupFailure.message,
|
|
5210
|
+
details: startupFailure.details!,
|
|
5211
|
+
endpoint: "unavailable" as const,
|
|
5212
|
+
}
|
|
5213
|
+
: {
|
|
5214
|
+
code: "spawn_failed",
|
|
5215
|
+
message: startupFailure.message,
|
|
5216
|
+
endpoint: "unavailable" as const,
|
|
5217
|
+
},
|
|
5218
|
+
...(durableEffects ? { durableEffects } : {}),
|
|
5219
|
+
startupFailure,
|
|
5220
|
+
}
|
|
5221
|
+
: provenDeadCleanup && response.error.code !== "terminal_uncertain"
|
|
5222
|
+
? response
|
|
4889
5223
|
: {
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
error: {
|
|
4900
|
-
code: "terminal_uncertain",
|
|
4901
|
-
message:
|
|
4902
|
-
"Lifecycle startup cleanup could not be proven; retained artifacts require reconciliation.",
|
|
4903
|
-
},
|
|
4904
|
-
...(durableEffects ? { durableEffects } : {}),
|
|
4905
|
-
...(startupFailure ? { startupFailure } : {}),
|
|
4906
|
-
}
|
|
5224
|
+
ok: false,
|
|
5225
|
+
error: {
|
|
5226
|
+
code: "terminal_uncertain",
|
|
5227
|
+
message:
|
|
5228
|
+
"Lifecycle startup cleanup could not be proven; retained artifacts require reconciliation.",
|
|
5229
|
+
},
|
|
5230
|
+
...(durableEffects ? { durableEffects } : {}),
|
|
5231
|
+
...(startupFailure ? { startupFailure } : {}),
|
|
5232
|
+
}
|
|
4907
5233
|
: response;
|
|
4908
5234
|
return {
|
|
4909
5235
|
response: terminalResponse,
|