@gajae-code/coding-agent 0.15.5 → 0.16.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 +85 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +120 -0
- package/dist/types/config/file-lock.d.ts +54 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +17 -5
- package/dist/types/config/settings-schema.d.ts +19 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +80 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +67 -1
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +24 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -3
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +671 -39
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +4 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +117 -16
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/launch.ts +86 -18
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +44 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +890 -99
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2795 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1044 -122
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +19 -1
- package/src/config/settings.ts +11 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +592 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +111 -42
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +1321 -147
- package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +66 -4
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +616 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +29 -10
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +51 -6
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +1671 -34
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +217 -46
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +276 -6
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +101 -37
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +581 -43
- package/src/sdk/host/session-runtime.ts +655 -101
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +53 -2
- package/src/sdk/protocol/operation-registry.ts +17 -4
- package/src/sdk/router/session-router.ts +294 -154
- package/src/sdk/session.ts +440 -167
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +1292 -180
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +342 -28
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +1268 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +1028 -109
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +11 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { logger } from "@gajae-code/utils";
|
|
6
|
+
import { nativeProcessBindings } from "@gajae-code/utils/native-process";
|
|
7
|
+
import { resolveGjcTmuxBinary } from "../../gjc-runtime/psmux-detect";
|
|
8
|
+
import { sessionRuntimeDir } from "../../gjc-runtime/session-layout";
|
|
9
|
+
import { resolveGjcTmuxProviderContext } from "../../gjc-runtime/tmux-provider-context";
|
|
10
|
+
import {
|
|
11
|
+
createManagedGjcTmuxSession,
|
|
12
|
+
forceCloseManagedGjcTmuxSession,
|
|
13
|
+
type ManagedTmuxLaunchProof,
|
|
14
|
+
verifyManagedGjcTmuxSession,
|
|
15
|
+
} from "../../gjc-runtime/tmux-sessions";
|
|
16
|
+
import { processIncarnation } from "./process-incarnation";
|
|
17
|
+
import type { SpawnSubstrateLaunchSpec, SpawnSubstrateProof, SpawnSubstrateProvider } from "./spawn-authority";
|
|
18
|
+
|
|
19
|
+
export type SpawnMultiplexerSelection = "none" | "tmux" | "psmux" | "proof_failed";
|
|
20
|
+
|
|
21
|
+
export interface SpawnHeadlessProcess {
|
|
22
|
+
pid: number;
|
|
23
|
+
terminate(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SpawnSubstrateProviderDependencies {
|
|
27
|
+
platform?: NodeJS.Platform;
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
/** @internal Test seam for deterministic provider selection. */
|
|
30
|
+
selectMultiplexer?: () => SpawnMultiplexerSelection;
|
|
31
|
+
/** @internal Test seam for the managed multiplexer command layer. */
|
|
32
|
+
launchManaged?: (
|
|
33
|
+
spec: SpawnSubstrateLaunchSpec,
|
|
34
|
+
env: NodeJS.ProcessEnv,
|
|
35
|
+
platform: NodeJS.Platform,
|
|
36
|
+
) => ManagedTmuxLaunchProof;
|
|
37
|
+
verifyManaged?: (proof: ManagedTmuxLaunchProof, env: NodeJS.ProcessEnv) => "verified" | "mismatch" | "gone";
|
|
38
|
+
closeManaged?: (proof: ManagedTmuxLaunchProof, env: NodeJS.ProcessEnv) => Promise<void>;
|
|
39
|
+
processIncarnation?: (pid: number) => string | undefined;
|
|
40
|
+
startHeadless?: (spec: SpawnSubstrateLaunchSpec, env: NodeJS.ProcessEnv) => SpawnHeadlessProcess;
|
|
41
|
+
signalHeadless?: (
|
|
42
|
+
pid: number,
|
|
43
|
+
incarnation: string,
|
|
44
|
+
platform: NodeJS.Platform,
|
|
45
|
+
signal: "SIGTERM" | "SIGKILL",
|
|
46
|
+
) => boolean;
|
|
47
|
+
isProcessGone?: (pid: number) => boolean;
|
|
48
|
+
/** @internal Test seam for bounded close polling. */
|
|
49
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
50
|
+
/** @internal Test seam for non-secret inherited-environment launch diagnostics. */
|
|
51
|
+
onInheritedEnvironmentDrop?: (names: readonly string[]) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type HeadlessState = {
|
|
55
|
+
version: 1;
|
|
56
|
+
pid: number;
|
|
57
|
+
processIncarnation: string;
|
|
58
|
+
providerIdentity: string;
|
|
59
|
+
childSessionId: string;
|
|
60
|
+
createdAt: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const MAX_SUBSTRATE_DIAGNOSTIC_LENGTH = 512;
|
|
64
|
+
|
|
65
|
+
const HEADLESS_STATE_KEYS = new Set([
|
|
66
|
+
"version",
|
|
67
|
+
"pid",
|
|
68
|
+
"processIncarnation",
|
|
69
|
+
"providerIdentity",
|
|
70
|
+
"childSessionId",
|
|
71
|
+
"createdAt",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
const ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
75
|
+
const HEADLESS_CLOSE_GRACE_MS = 2_000;
|
|
76
|
+
const HEADLESS_CLOSE_POLL_MS = 50;
|
|
77
|
+
|
|
78
|
+
function messageFor(code: "substrate_unavailable" | "substrate_proof_failed"): string {
|
|
79
|
+
return code === "substrate_unavailable"
|
|
80
|
+
? "No safe spawn substrate is available."
|
|
81
|
+
: "The selected spawn substrate could not be proven exactly.";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function launchFailureMessage(selected: "tmux" | "psmux" | "headless", error: unknown): string {
|
|
85
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
86
|
+
const bounded = detail
|
|
87
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
88
|
+
.trim()
|
|
89
|
+
.slice(0, MAX_SUBSTRATE_DIAGNOSTIC_LENGTH);
|
|
90
|
+
return `${selected} substrate launch failed${bounded ? `: ${bounded}` : ""}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasOnlyKeys(value: Record<string, unknown>, keys: Set<string>): boolean {
|
|
94
|
+
return Object.keys(value).every(key => keys.has(key));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
98
|
+
return typeof value === "string" && value.length > 0 && value.length <= 4096;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** An environment VALUE may be empty but is still length-bounded. */
|
|
102
|
+
function isBoundedString(value: unknown): value is string {
|
|
103
|
+
return typeof value === "string" && value.length <= 4096;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isPositiveInteger(value: unknown): value is number {
|
|
107
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isTimestamp(value: unknown): value is number {
|
|
111
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validLaunchSpec(spec: SpawnSubstrateLaunchSpec, platform: NodeJS.Platform): boolean {
|
|
115
|
+
const pathModule = platform === "win32" ? path.win32 : path;
|
|
116
|
+
return (
|
|
117
|
+
isNonEmptyString(spec.childSessionId) &&
|
|
118
|
+
!spec.childSessionId.includes("\0") &&
|
|
119
|
+
isNonEmptyString(spec.cwd) &&
|
|
120
|
+
!spec.cwd.includes("\0") &&
|
|
121
|
+
pathModule.isAbsolute(spec.cwd) &&
|
|
122
|
+
Array.isArray(spec.argv) &&
|
|
123
|
+
spec.argv.length > 0 &&
|
|
124
|
+
spec.argv.every(value => isNonEmptyString(value) && !value.includes("\0")) &&
|
|
125
|
+
(spec.env === undefined ||
|
|
126
|
+
Object.entries(spec.env).every(
|
|
127
|
+
([name, value]) => ENVIRONMENT_NAME.test(name) && isBoundedString(value) && !value.includes("\0"),
|
|
128
|
+
))
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function filterInheritedEnvironment(environment: NodeJS.ProcessEnv | Readonly<Record<string, string>>): {
|
|
133
|
+
environment: NodeJS.ProcessEnv;
|
|
134
|
+
dropped: string[];
|
|
135
|
+
} {
|
|
136
|
+
const filtered: NodeJS.ProcessEnv = {};
|
|
137
|
+
const dropped: string[] = [];
|
|
138
|
+
for (const [name, value] of Object.entries(environment).sort(([left], [right]) =>
|
|
139
|
+
left < right ? -1 : left > right ? 1 : 0,
|
|
140
|
+
)) {
|
|
141
|
+
if (ENVIRONMENT_NAME.test(name) && isBoundedString(value) && !value.includes("\0")) filtered[name] = value;
|
|
142
|
+
else dropped.push(name);
|
|
143
|
+
}
|
|
144
|
+
return { environment: filtered, dropped };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isHeadlessState(value: unknown): value is HeadlessState {
|
|
148
|
+
return (
|
|
149
|
+
typeof value === "object" &&
|
|
150
|
+
value !== null &&
|
|
151
|
+
hasOnlyKeys(value as Record<string, unknown>, HEADLESS_STATE_KEYS) &&
|
|
152
|
+
(value as { version?: unknown }).version === 1 &&
|
|
153
|
+
isPositiveInteger((value as { pid?: unknown }).pid) &&
|
|
154
|
+
isNonEmptyString((value as { processIncarnation?: unknown }).processIncarnation) &&
|
|
155
|
+
isNonEmptyString((value as { providerIdentity?: unknown }).providerIdentity) &&
|
|
156
|
+
isNonEmptyString((value as { childSessionId?: unknown }).childSessionId) &&
|
|
157
|
+
isTimestamp((value as { createdAt?: unknown }).createdAt)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function stateFileFor(spec: SpawnSubstrateLaunchSpec, providerIdentity: string): string {
|
|
162
|
+
return path.join(sessionRuntimeDir(spec.cwd, spec.childSessionId), "spawn-substrates", `${providerIdentity}.json`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function writeHeadlessState(file: string, value: HeadlessState): Promise<void> {
|
|
166
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
167
|
+
const handle = await fs.open(file, "wx", 0o600);
|
|
168
|
+
try {
|
|
169
|
+
await handle.writeFile(`${JSON.stringify(value)}\n`);
|
|
170
|
+
await handle.sync();
|
|
171
|
+
} finally {
|
|
172
|
+
await handle.close();
|
|
173
|
+
}
|
|
174
|
+
const directory = await fs.open(path.dirname(file), "r");
|
|
175
|
+
try {
|
|
176
|
+
await directory.sync();
|
|
177
|
+
} finally {
|
|
178
|
+
await directory.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function readHeadlessState(file: string): Promise<HeadlessState | null> {
|
|
183
|
+
try {
|
|
184
|
+
const parsed: unknown = JSON.parse(await fs.readFile(file, "utf8"));
|
|
185
|
+
return isHeadlessState(parsed) ? parsed : null;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function commandAvailable(command: string): boolean {
|
|
192
|
+
return Bun.which(command) !== null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function defaultMultiplexerSelection(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): SpawnMultiplexerSelection {
|
|
196
|
+
try {
|
|
197
|
+
if (platform === "darwin" || platform === "linux") {
|
|
198
|
+
const binary = resolveGjcTmuxBinary({ env, platform });
|
|
199
|
+
if (!commandAvailable(binary.command)) return "none";
|
|
200
|
+
if (binary.isPsmux) return "proof_failed";
|
|
201
|
+
const provider = resolveGjcTmuxProviderContext({ env, platform, binary });
|
|
202
|
+
return provider.kind === "native-tmux" ? "tmux" : "proof_failed";
|
|
203
|
+
}
|
|
204
|
+
if (platform !== "win32") return "none";
|
|
205
|
+
const binary = resolveGjcTmuxBinary({ env, platform });
|
|
206
|
+
if (!commandAvailable(binary.command)) return "none";
|
|
207
|
+
if (!binary.isPsmux) return env.GJC_TMUX_COMMAND?.trim() ? "proof_failed" : "none";
|
|
208
|
+
const provider = resolveGjcTmuxProviderContext({ env, platform, binary });
|
|
209
|
+
return provider.kind === "windows-psmux" ? "psmux" : "proof_failed";
|
|
210
|
+
} catch {
|
|
211
|
+
return "proof_failed";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function startHeadless(spec: SpawnSubstrateLaunchSpec, env: NodeJS.ProcessEnv): SpawnHeadlessProcess {
|
|
216
|
+
const child = Bun.spawn({
|
|
217
|
+
cmd: [...spec.argv],
|
|
218
|
+
cwd: spec.cwd,
|
|
219
|
+
env,
|
|
220
|
+
stdin: "ignore",
|
|
221
|
+
stdout: "ignore",
|
|
222
|
+
stderr: "ignore",
|
|
223
|
+
});
|
|
224
|
+
return { pid: child.pid, terminate: () => child.kill() };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function defaultIsProcessGone(pid: number): boolean {
|
|
228
|
+
try {
|
|
229
|
+
process.kill(pid, 0);
|
|
230
|
+
return false;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function signalExactHeadless(
|
|
237
|
+
pid: number,
|
|
238
|
+
expectedIncarnation: string,
|
|
239
|
+
platform: NodeJS.Platform,
|
|
240
|
+
signal: "SIGTERM" | "SIGKILL",
|
|
241
|
+
): boolean {
|
|
242
|
+
try {
|
|
243
|
+
const reference = nativeProcessBindings().Process.fromPid(pid);
|
|
244
|
+
if (!reference || reference.incarnation !== expectedIncarnation) return false;
|
|
245
|
+
if (platform === "darwin") {
|
|
246
|
+
const current = nativeProcessBindings().Process.fromPid(pid);
|
|
247
|
+
if (!current || current.incarnation !== expectedIncarnation) return false;
|
|
248
|
+
process.kill(pid, signal);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
const rootProcess = reference as typeof reference & { signalRoot(signal: number): boolean };
|
|
252
|
+
const signalNumber = signal === "SIGTERM" ? os.constants.signals.SIGTERM : os.constants.signals.SIGKILL;
|
|
253
|
+
return signalNumber !== undefined && rootProcess.signalRoot(signalNumber);
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function readStateProof(proof: SpawnSubstrateProof): { stateFile: string; childSessionId: string } | null {
|
|
260
|
+
const stateFile = proof.stateFileProof?.stateFile;
|
|
261
|
+
const childSessionId = proof.stateFileProof?.childSessionId;
|
|
262
|
+
return isNonEmptyString(stateFile) && isNonEmptyString(childSessionId) ? { stateFile, childSessionId } : null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function managedProofFrom(proof: SpawnSubstrateProof): ManagedTmuxLaunchProof | null {
|
|
266
|
+
if (proof.substrateKind !== "tmux" && proof.substrateKind !== "psmux") return null;
|
|
267
|
+
const stateProof = proof.stateFileProof;
|
|
268
|
+
const name = stateProof?.sessionName;
|
|
269
|
+
const sessionId = stateProof?.sessionId;
|
|
270
|
+
const sessionStateFile = stateProof?.sessionStateFile;
|
|
271
|
+
const ownerGeneration = stateProof?.ownerGeneration;
|
|
272
|
+
const serverPid = stateProof?.serverPid;
|
|
273
|
+
const serverStartTime = stateProof?.serverStartTime;
|
|
274
|
+
const psmuxIncarnation = stateProof?.psmuxIncarnation;
|
|
275
|
+
const validatedPsmuxIncarnation = isNonEmptyString(psmuxIncarnation) ? psmuxIncarnation : undefined;
|
|
276
|
+
if (
|
|
277
|
+
!isNonEmptyString(proof.nativeSessionId) ||
|
|
278
|
+
!isPositiveInteger(proof.pid) ||
|
|
279
|
+
!isNonEmptyString(name) ||
|
|
280
|
+
!isNonEmptyString(sessionId) ||
|
|
281
|
+
!isNonEmptyString(sessionStateFile) ||
|
|
282
|
+
!isNonEmptyString(ownerGeneration) ||
|
|
283
|
+
!isPositiveInteger(serverPid) ||
|
|
284
|
+
!isNonEmptyString(serverStartTime) ||
|
|
285
|
+
(proof.substrateKind === "psmux" && validatedPsmuxIncarnation === undefined) ||
|
|
286
|
+
(proof.substrateKind === "tmux" && psmuxIncarnation !== undefined)
|
|
287
|
+
)
|
|
288
|
+
return null;
|
|
289
|
+
return {
|
|
290
|
+
name,
|
|
291
|
+
nativeSessionId: proof.nativeSessionId,
|
|
292
|
+
serverPid,
|
|
293
|
+
serverStartTime,
|
|
294
|
+
ownerGeneration,
|
|
295
|
+
sessionId,
|
|
296
|
+
sessionStateFile,
|
|
297
|
+
pid: proof.pid,
|
|
298
|
+
providerIdentity: proof.providerIdentity,
|
|
299
|
+
...(validatedPsmuxIncarnation === undefined ? {} : { psmuxIncarnation: validatedPsmuxIncarnation }),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function headlessProofMatches(proof: SpawnSubstrateProof, state: HeadlessState): boolean {
|
|
304
|
+
const stateProof = readStateProof(proof);
|
|
305
|
+
return (
|
|
306
|
+
proof.substrateKind === "headless" &&
|
|
307
|
+
isPositiveInteger(proof.pid) &&
|
|
308
|
+
isNonEmptyString(proof.processIncarnation) &&
|
|
309
|
+
stateProof !== null &&
|
|
310
|
+
proof.pid === state.pid &&
|
|
311
|
+
proof.processIncarnation === state.processIncarnation &&
|
|
312
|
+
proof.providerIdentity === state.providerIdentity &&
|
|
313
|
+
stateProof.childSessionId === state.childSessionId
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function waitForHeadlessExit(
|
|
318
|
+
proof: SpawnSubstrateProof,
|
|
319
|
+
verify: (proof: SpawnSubstrateProof) => Promise<"verified" | "mismatch" | "gone">,
|
|
320
|
+
sleep: (milliseconds: number) => Promise<void>,
|
|
321
|
+
): Promise<"verified" | "mismatch" | "gone"> {
|
|
322
|
+
let verdict = await verify(proof);
|
|
323
|
+
if (verdict !== "verified") return verdict;
|
|
324
|
+
const polls = Math.ceil(HEADLESS_CLOSE_GRACE_MS / HEADLESS_CLOSE_POLL_MS);
|
|
325
|
+
for (let attempt = 0; attempt < polls; attempt++) {
|
|
326
|
+
await sleep(HEADLESS_CLOSE_POLL_MS);
|
|
327
|
+
verdict = await verify(proof);
|
|
328
|
+
if (verdict !== "verified") return verdict;
|
|
329
|
+
}
|
|
330
|
+
return verdict;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function managedStateProof(proof: ManagedTmuxLaunchProof): Readonly<Record<string, string | number>> {
|
|
334
|
+
return {
|
|
335
|
+
sessionName: proof.name,
|
|
336
|
+
sessionId: proof.sessionId,
|
|
337
|
+
sessionStateFile: proof.sessionStateFile,
|
|
338
|
+
ownerGeneration: proof.ownerGeneration,
|
|
339
|
+
serverPid: proof.serverPid,
|
|
340
|
+
serverStartTime: proof.serverStartTime,
|
|
341
|
+
...(proof.psmuxIncarnation === undefined ? {} : { psmuxIncarnation: proof.psmuxIncarnation }),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Provides the Broker's only substrate authority. Every close and replay starts
|
|
347
|
+
* from its durable opaque proof; no PID/name-only operation is exposed.
|
|
348
|
+
*/
|
|
349
|
+
export function createSpawnSubstrateProvider(
|
|
350
|
+
dependencies: SpawnSubstrateProviderDependencies = {},
|
|
351
|
+
): SpawnSubstrateProvider {
|
|
352
|
+
const platform = dependencies.platform ?? process.platform;
|
|
353
|
+
const env = dependencies.env ?? process.env;
|
|
354
|
+
const selectMultiplexer = dependencies.selectMultiplexer ?? (() => defaultMultiplexerSelection(platform, env));
|
|
355
|
+
const launchManaged =
|
|
356
|
+
dependencies.launchManaged ??
|
|
357
|
+
((spec, launchEnv, launchPlatform) => createManagedGjcTmuxSession(spec, launchEnv, { platform: launchPlatform }));
|
|
358
|
+
const verifyManaged = dependencies.verifyManaged ?? verifyManagedGjcTmuxSession;
|
|
359
|
+
const closeManaged =
|
|
360
|
+
dependencies.closeManaged ??
|
|
361
|
+
(async (proof, closeEnv) => {
|
|
362
|
+
await forceCloseManagedGjcTmuxSession(proof, closeEnv);
|
|
363
|
+
});
|
|
364
|
+
const readIncarnation = dependencies.processIncarnation ?? processIncarnation;
|
|
365
|
+
const launchHeadless = dependencies.startHeadless ?? startHeadless;
|
|
366
|
+
const signalHeadless = dependencies.signalHeadless ?? signalExactHeadless;
|
|
367
|
+
const isGone = dependencies.isProcessGone ?? defaultIsProcessGone;
|
|
368
|
+
const sleep = dependencies.sleep ?? (milliseconds => Bun.sleep(milliseconds));
|
|
369
|
+
const launchHeadlessSubstrate = async (spec: SpawnSubstrateLaunchSpec, launchEnv: NodeJS.ProcessEnv) => {
|
|
370
|
+
let child: SpawnHeadlessProcess;
|
|
371
|
+
try {
|
|
372
|
+
child = launchHeadless(spec, launchEnv);
|
|
373
|
+
} catch {
|
|
374
|
+
return {
|
|
375
|
+
ok: false as const,
|
|
376
|
+
code: "substrate_unavailable" as const,
|
|
377
|
+
message: messageFor("substrate_unavailable"),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const incarnation = readIncarnation(child.pid);
|
|
381
|
+
if (!incarnation) {
|
|
382
|
+
child.terminate();
|
|
383
|
+
return {
|
|
384
|
+
ok: false as const,
|
|
385
|
+
code: "substrate_proof_failed" as const,
|
|
386
|
+
message: messageFor("substrate_proof_failed"),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const providerIdentity = crypto.randomUUID();
|
|
390
|
+
const state: HeadlessState = {
|
|
391
|
+
version: 1,
|
|
392
|
+
pid: child.pid,
|
|
393
|
+
processIncarnation: incarnation,
|
|
394
|
+
providerIdentity,
|
|
395
|
+
childSessionId: spec.childSessionId,
|
|
396
|
+
createdAt: Date.now(),
|
|
397
|
+
};
|
|
398
|
+
const stateFile = stateFileFor(spec, providerIdentity);
|
|
399
|
+
try {
|
|
400
|
+
await writeHeadlessState(stateFile, state);
|
|
401
|
+
} catch {
|
|
402
|
+
child.terminate();
|
|
403
|
+
return {
|
|
404
|
+
ok: false as const,
|
|
405
|
+
code: "substrate_proof_failed" as const,
|
|
406
|
+
message: messageFor("substrate_proof_failed"),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
ok: true as const,
|
|
411
|
+
proof: {
|
|
412
|
+
substrateKind: "headless" as const,
|
|
413
|
+
providerIdentity,
|
|
414
|
+
pid: child.pid,
|
|
415
|
+
processIncarnation: incarnation,
|
|
416
|
+
stateFileProof: { stateFile, childSessionId: spec.childSessionId },
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const provider: SpawnSubstrateProvider = {
|
|
422
|
+
async launch(spec) {
|
|
423
|
+
if (!validLaunchSpec(spec, platform))
|
|
424
|
+
return { ok: false, code: "substrate_proof_failed", message: messageFor("substrate_proof_failed") };
|
|
425
|
+
const inherited = filterInheritedEnvironment(spec.inheritedEnv ?? env);
|
|
426
|
+
if (inherited.dropped.length > 0) {
|
|
427
|
+
if (dependencies.onInheritedEnvironmentDrop) dependencies.onInheritedEnvironmentDrop(inherited.dropped);
|
|
428
|
+
else
|
|
429
|
+
logger.warn("sdk broker dropped unsupported inherited child environment entries", {
|
|
430
|
+
names: inherited.dropped,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const launchEnv: NodeJS.ProcessEnv = { ...inherited.environment, ...(spec.env ?? {}) };
|
|
434
|
+
const selected = selectMultiplexer();
|
|
435
|
+
if (selected === "proof_failed")
|
|
436
|
+
return { ok: false, code: "substrate_proof_failed", message: messageFor("substrate_proof_failed") };
|
|
437
|
+
if (selected === "tmux" || selected === "psmux") {
|
|
438
|
+
let managed: ManagedTmuxLaunchProof;
|
|
439
|
+
try {
|
|
440
|
+
managed = launchManaged(spec, launchEnv, platform);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
const failure = launchFailureMessage(selected, error);
|
|
443
|
+
try {
|
|
444
|
+
const fallback = await launchHeadlessSubstrate(spec, launchEnv);
|
|
445
|
+
if (fallback.ok) return fallback;
|
|
446
|
+
return {
|
|
447
|
+
ok: false,
|
|
448
|
+
code: fallback.code,
|
|
449
|
+
message: `${failure}; headless substrate failed: ${fallback.message}`,
|
|
450
|
+
};
|
|
451
|
+
} catch (fallbackError) {
|
|
452
|
+
return {
|
|
453
|
+
ok: false,
|
|
454
|
+
code: "substrate_unavailable",
|
|
455
|
+
message: `${failure}; headless substrate failed: ${launchFailureMessage("headless", fallbackError)}`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const incarnation = readIncarnation(managed.pid);
|
|
460
|
+
if (!incarnation || verifyManaged(managed, launchEnv) !== "verified") {
|
|
461
|
+
try {
|
|
462
|
+
await closeManaged(managed, launchEnv);
|
|
463
|
+
} catch {
|
|
464
|
+
// The managed close primitive is already exact-proof fenced. Retain its result only in authority state.
|
|
465
|
+
}
|
|
466
|
+
const failure = launchFailureMessage(selected, new Error("exact substrate proof failed"));
|
|
467
|
+
const fallback = await launchHeadlessSubstrate(spec, launchEnv);
|
|
468
|
+
if (fallback.ok) return fallback;
|
|
469
|
+
return {
|
|
470
|
+
ok: false,
|
|
471
|
+
code: fallback.code,
|
|
472
|
+
message: `${failure}; headless substrate failed: ${fallback.message}`,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
ok: true,
|
|
477
|
+
proof: {
|
|
478
|
+
substrateKind: selected,
|
|
479
|
+
providerIdentity: managed.providerIdentity,
|
|
480
|
+
nativeSessionId: managed.nativeSessionId,
|
|
481
|
+
pid: managed.pid,
|
|
482
|
+
processIncarnation: incarnation,
|
|
483
|
+
stateFileProof: managedStateProof(managed),
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
return launchHeadlessSubstrate(spec, launchEnv);
|
|
488
|
+
},
|
|
489
|
+
async verify(proof) {
|
|
490
|
+
const managed = managedProofFrom(proof);
|
|
491
|
+
if (managed) {
|
|
492
|
+
const verdict = verifyManaged(managed, env);
|
|
493
|
+
if (verdict !== "verified") return verdict;
|
|
494
|
+
const incarnation = proof.pid === undefined ? undefined : readIncarnation(proof.pid);
|
|
495
|
+
if (!incarnation) return proof.pid !== undefined && isGone(proof.pid) ? "gone" : "mismatch";
|
|
496
|
+
return incarnation === proof.processIncarnation ? "verified" : "mismatch";
|
|
497
|
+
}
|
|
498
|
+
if (proof.substrateKind !== "headless") return "mismatch";
|
|
499
|
+
const stateProof = readStateProof(proof);
|
|
500
|
+
if (!stateProof) return "mismatch";
|
|
501
|
+
const state = await readHeadlessState(stateProof.stateFile);
|
|
502
|
+
if (!state || !headlessProofMatches(proof, state)) return "mismatch";
|
|
503
|
+
const incarnation = proof.pid === undefined ? undefined : readIncarnation(proof.pid);
|
|
504
|
+
if (!incarnation) return proof.pid !== undefined && isGone(proof.pid) ? "gone" : "mismatch";
|
|
505
|
+
return incarnation === proof.processIncarnation ? "verified" : "mismatch";
|
|
506
|
+
},
|
|
507
|
+
async close(proof) {
|
|
508
|
+
const verification = await provider.verify(proof);
|
|
509
|
+
if (verification !== "verified") return { ok: false, code: `substrate_${verification}` };
|
|
510
|
+
const managed = managedProofFrom(proof);
|
|
511
|
+
if (managed) {
|
|
512
|
+
try {
|
|
513
|
+
await closeManaged(managed, env);
|
|
514
|
+
return { ok: true };
|
|
515
|
+
} catch {
|
|
516
|
+
return { ok: false, code: "substrate_close_failed" };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (!isPositiveInteger(proof.pid) || !isNonEmptyString(proof.processIncarnation))
|
|
520
|
+
return { ok: false, code: "substrate_mismatch" };
|
|
521
|
+
const pid = proof.pid;
|
|
522
|
+
const closeFailure = (verdict: "verified" | "mismatch" | "gone") => ({
|
|
523
|
+
ok: false,
|
|
524
|
+
code: verdict === "verified" ? "substrate_close_pending" : `substrate_${verdict}`,
|
|
525
|
+
});
|
|
526
|
+
const verifyAfterSignal = async (): Promise<"verified" | "mismatch" | "gone"> => {
|
|
527
|
+
const verdict = await provider.verify(proof);
|
|
528
|
+
if (verdict === "mismatch" && readIncarnation(pid) === undefined && isGone(pid)) return "gone";
|
|
529
|
+
return verdict;
|
|
530
|
+
};
|
|
531
|
+
// Delivery starts shutdown; only a later exact proof of absence completes it.
|
|
532
|
+
if (!signalHeadless(proof.pid, proof.processIncarnation, platform, "SIGTERM")) {
|
|
533
|
+
const afterSignalFailure = await verifyAfterSignal();
|
|
534
|
+
return afterSignalFailure === "gone" ? { ok: true } : closeFailure(afterSignalFailure);
|
|
535
|
+
}
|
|
536
|
+
const afterTerm = await waitForHeadlessExit(proof, verifyAfterSignal, sleep);
|
|
537
|
+
if (afterTerm === "gone") return { ok: true };
|
|
538
|
+
if (afterTerm !== "verified") return closeFailure(afterTerm);
|
|
539
|
+
if (!signalHeadless(proof.pid, proof.processIncarnation, platform, "SIGKILL")) {
|
|
540
|
+
const afterSignalFailure = await verifyAfterSignal();
|
|
541
|
+
return afterSignalFailure === "gone" ? { ok: true } : closeFailure(afterSignalFailure);
|
|
542
|
+
}
|
|
543
|
+
const afterKill = await waitForHeadlessExit(proof, verifyAfterSignal, sleep);
|
|
544
|
+
return afterKill === "gone" ? { ok: true } : closeFailure(afterKill);
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
return provider;
|
|
548
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
export const SDK_STATE_VERSION = 1;
|
|
2
2
|
|
|
3
|
-
// The session-index snapshot
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// remain readable. Bumping only this constant keeps the broker protocol untouched.
|
|
11
|
-
export const SESSION_INDEX_SNAPSHOT_VERSION = 3;
|
|
3
|
+
// The session-index snapshot and event log share one format version. Version 4
|
|
4
|
+
// requires SessionLocatorV2 (`cwd`, `worktreeRoot`, `stateRoot`) on every
|
|
5
|
+
// retained row. New v4 events force older brokers to fail closed before they
|
|
6
|
+
// misread `cwd` as the former `repo`; pre-v4 snapshots remain readable only to
|
|
7
|
+
// quarantine legacy rows.
|
|
8
|
+
export const SESSION_INDEX_SNAPSHOT_VERSION = 4;
|
|
9
|
+
export const SESSION_INDEX_EVENT_VERSION = SESSION_INDEX_SNAPSHOT_VERSION;
|
|
12
10
|
|
|
13
11
|
export class UnsupportedStateVersionError extends Error {
|
|
14
12
|
readonly code = "unsupported_state_version";
|
|
@@ -35,11 +33,25 @@ export function assertSupportedStateVersion(file: string, value: unknown): void
|
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
/** Rejects session-index events outside the known legacy and current formats. */
|
|
37
|
+
export function assertSupportedSessionIndexEventVersion(file: string, value: unknown): void {
|
|
38
|
+
if (!value || typeof value !== "object") return;
|
|
39
|
+
const record = value as { version?: unknown; stateVersion?: unknown };
|
|
40
|
+
for (const version of [record.version, record.stateVersion]) {
|
|
41
|
+
if (
|
|
42
|
+
typeof version === "number" &&
|
|
43
|
+
Number.isFinite(version) &&
|
|
44
|
+
version !== SDK_STATE_VERSION &&
|
|
45
|
+
version !== SESSION_INDEX_EVENT_VERSION
|
|
46
|
+
) {
|
|
47
|
+
throw new UnsupportedStateVersionError(file, version, SESSION_INDEX_EVENT_VERSION);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fences future session-index snapshot formats. Locator-v2 row validation below
|
|
53
|
+
// rejects legacy `repo` rows without translating them, while snapshots predating
|
|
54
|
+
// v4 remain readable solely to quarantine those rows with a re-register diagnostic.
|
|
43
55
|
export function assertSupportedSnapshotVersion(file: string, value: unknown): void {
|
|
44
56
|
if (!value || typeof value !== "object") return;
|
|
45
57
|
const record = value as { version?: unknown; stateVersion?: unknown };
|