@gajae-code/coding-agent 0.16.7 → 0.17.1
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 +180 -1
- package/dist/types/capability/fs.d.ts +37 -4
- package/dist/types/capability/index.d.ts +19 -0
- package/dist/types/capability/types.d.ts +9 -0
- package/dist/types/cli/doctor/args.d.ts +36 -0
- package/dist/types/cli/doctor/checks.d.ts +18 -0
- package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
- package/dist/types/cli/doctor/context.d.ts +89 -0
- package/dist/types/cli/doctor/files.d.ts +27 -0
- package/dist/types/cli/doctor/ids.d.ts +20 -0
- package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
- package/dist/types/cli/doctor/journal.d.ts +43 -0
- package/dist/types/cli/doctor/managed-link.d.ts +39 -0
- package/dist/types/cli/doctor/native-probe.d.ts +2 -0
- package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
- package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
- package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
- package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
- package/dist/types/cli/doctor/probe-client.d.ts +9 -0
- package/dist/types/cli/doctor/probe-types.d.ts +18 -0
- package/dist/types/cli/doctor/probe.d.ts +3 -0
- package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
- package/dist/types/cli/doctor/repairs.d.ts +28 -0
- package/dist/types/cli/doctor/report.d.ts +27 -0
- package/dist/types/cli/doctor/runner.d.ts +4 -0
- package/dist/types/cli/doctor/safety.d.ts +10 -0
- package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
- package/dist/types/cli/doctor/service-targets.d.ts +41 -0
- package/dist/types/cli/doctor/types.d.ts +145 -0
- package/dist/types/cli/doctor-cli.d.ts +7 -0
- package/dist/types/cli/doctor-supervisor.d.ts +121 -0
- package/dist/types/cli/doctor-worker.d.ts +29 -0
- package/dist/types/cli/install-activation.d.ts +158 -0
- package/dist/types/cli/macos-community-app.d.ts +41 -0
- package/dist/types/cli/mcp-cli.d.ts +0 -11
- package/dist/types/cli/update-cli.d.ts +41 -4
- package/dist/types/cli-main.d.ts +174 -0
- package/dist/types/cli.d.ts +1 -171
- package/dist/types/commands/doctor.d.ts +7 -0
- package/dist/types/commands/sdk.d.ts +17 -8
- package/dist/types/config/autorouting-tier-map.d.ts +19 -0
- package/dist/types/config/file-lock.d.ts +27 -3
- package/dist/types/config/model-preset-registry.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +12 -5
- package/dist/types/config/model-resolver.d.ts +16 -3
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/discovery/agents-md.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +36 -7
- package/dist/types/discovery/index.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +22 -1
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/plugins/manager.d.ts +33 -4
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
- package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
- package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
- package/dist/types/main.d.ts +19 -1
- package/dist/types/modes/acp/acp-agent.d.ts +40 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
- package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -2
- package/dist/types/sdk/acp/adapter.d.ts +5 -1
- package/dist/types/sdk/broker/broker.d.ts +51 -0
- package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
- package/dist/types/sdk/broker/discovery.d.ts +38 -0
- package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
- package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
- package/dist/types/sdk/broker/runtime.d.ts +26 -0
- package/dist/types/sdk/broker/session-index.d.ts +13 -4
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
- package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
- package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
- package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/bus/notification-service.d.ts +3 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
- package/dist/types/sdk/cli/session-cli.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/host/surface-policy.d.ts +3 -1
- package/dist/types/sdk/lifecycle/service.d.ts +44 -10
- package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
- package/dist/types/sdk/prompt-failure.d.ts +47 -0
- package/dist/types/sdk/prompt-status.d.ts +19 -0
- package/dist/types/sdk/router/session-router.d.ts +3 -4
- package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
- package/dist/types/sdk/session-authority.d.ts +9 -0
- package/dist/types/sdk/session.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +1 -1
- package/dist/types/session/fallback-chain-controller.d.ts +3 -6
- package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
- package/dist/types/session/primary-control-surface.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +6 -0
- package/dist/types/tools/ask.d.ts +25 -0
- package/dist/types/tools/image-gen.d.ts +0 -4
- package/dist/types/tools/skill-discovery.d.ts +7 -5
- package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
- package/dist/types/utils/changelog.d.ts +0 -9
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +9 -8
- package/scripts/compile-args.ts +8 -4
- package/src/capability/fs.ts +395 -17
- package/src/capability/index.ts +273 -10
- package/src/capability/types.ts +9 -0
- package/src/cli/customize-doctor.ts +18 -12
- package/src/cli/doctor/args.ts +236 -0
- package/src/cli/doctor/checks.ts +431 -0
- package/src/cli/doctor/config-repairs.ts +517 -0
- package/src/cli/doctor/context.ts +149 -0
- package/src/cli/doctor/files.ts +149 -0
- package/src/cli/doctor/ids.ts +115 -0
- package/src/cli/doctor/install-repairs.ts +335 -0
- package/src/cli/doctor/journal.ts +423 -0
- package/src/cli/doctor/managed-link.ts +514 -0
- package/src/cli/doctor/native-probe.ts +13 -0
- package/src/cli/doctor/permission-repairs.ts +242 -0
- package/src/cli/doctor/plugin-quarantine.ts +377 -0
- package/src/cli/doctor/plugin-restore.ts +59 -0
- package/src/cli/doctor/plugin-targets.ts +373 -0
- package/src/cli/doctor/probe-client.ts +195 -0
- package/src/cli/doctor/probe-types.ts +21 -0
- package/src/cli/doctor/probe.ts +42 -0
- package/src/cli/doctor/projection-probe.ts +249 -0
- package/src/cli/doctor/repairs.ts +1157 -0
- package/src/cli/doctor/report.ts +176 -0
- package/src/cli/doctor/runner.ts +342 -0
- package/src/cli/doctor/safety.ts +40 -0
- package/src/cli/doctor/service-repairs.ts +452 -0
- package/src/cli/doctor/service-targets.ts +274 -0
- package/src/cli/doctor/types.ts +164 -0
- package/src/cli/doctor-cli.ts +62 -0
- package/src/cli/doctor-supervisor.ts +571 -0
- package/src/cli/doctor-worker.ts +181 -0
- package/src/cli/install-activation.ts +666 -0
- package/src/cli/macos-community-app.ts +1449 -0
- package/src/cli/mcp-cli.ts +11 -21
- package/src/cli/nofile-limit.ts +5 -1
- package/src/cli/plugin-cli.ts +6 -1
- package/src/cli/update-cli.ts +362 -264
- package/src/cli-main.ts +517 -0
- package/src/cli.ts +64 -433
- package/src/commands/autoresearch.ts +11 -5
- package/src/commands/completion.ts +1 -1
- package/src/commands/doctor.ts +13 -0
- package/src/commands/sdk.ts +58 -10
- package/src/commands/ultragoal.ts +11 -5
- package/src/config/autorouting-tier-map.ts +7 -0
- package/src/config/file-lock.ts +736 -71
- package/src/config/mcp-schema.json +3 -3
- package/src/config/model-preset-registry.ts +1 -0
- package/src/config/model-registry.ts +80 -27
- package/src/config/model-resolver.ts +33 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -0
- package/src/coordinator-mcp/server.ts +534 -51
- package/src/coordinator-mcp/session-reaper.ts +64 -8
- package/src/cursor.ts +223 -53
- package/src/discovery/agents-md.ts +74 -8
- package/src/discovery/agents.ts +32 -7
- package/src/discovery/builtin.ts +184 -75
- package/src/discovery/claude-plugins.ts +85 -17
- package/src/discovery/cline.ts +38 -10
- package/src/discovery/cursor.ts +19 -6
- package/src/discovery/gemini.ts +62 -22
- package/src/discovery/github.ts +13 -3
- package/src/discovery/helpers.ts +483 -89
- package/src/discovery/index.ts +1 -0
- package/src/discovery/mcp-json.ts +36 -10
- package/src/discovery/opencode.ts +36 -19
- package/src/discovery/ssh.ts +11 -8
- package/src/discovery/windsurf.ts +12 -3
- package/src/exec/bash-executor.ts +10 -3
- package/src/exec/isolated-shell.ts +15 -2
- package/src/extensibility/extensions/loader.ts +88 -26
- package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
- package/src/extensibility/gjc-plugins/paths.ts +4 -1
- package/src/extensibility/gjc-plugins/types.ts +27 -0
- package/src/extensibility/plugins/manager.ts +200 -22
- package/src/extensibility/plugins/marketplace/cache.ts +55 -2
- package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
- package/src/extensibility/plugins/marketplace/registry.ts +153 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
- package/src/extensibility/runtime-skill-discovery.ts +61 -9
- package/src/extensibility/skill-management.ts +7 -1
- package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
- package/src/gjc-runtime/launch-worktree.test.ts +248 -0
- package/src/gjc-runtime/launch-worktree.ts +275 -38
- package/src/gjc-runtime/repository-binding.ts +26 -1
- package/src/gjc-runtime/session-layout.ts +10 -0
- package/src/gjc-runtime/session-state-lock.ts +330 -23
- package/src/gjc-runtime/session-state-sidecar.ts +328 -80
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +28 -26
- package/src/main.ts +150 -29
- package/src/memories/index.ts +24 -2
- package/src/modes/acp/acp-agent.ts +522 -68
- package/src/modes/components/agent-dashboard.ts +3 -1
- package/src/modes/components/model-selector.ts +28 -19
- package/src/modes/components/plugin-settings.ts +2 -1
- package/src/modes/controllers/selector-controller.ts +15 -9
- package/src/modes/interactive-mode.ts +21 -3
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/move-session.md +4 -0
- package/src/prompts/tools/read.md +2 -1
- package/src/prompts/tools/skill-discovery.md +6 -5
- package/src/prompts/tools/task.md +1 -0
- package/src/runtime-mcp/config-writer.ts +228 -0
- package/src/runtime-mcp/oauth-discovery.ts +114 -14
- package/src/runtime-mcp/policy-json-edit.ts +129 -0
- package/src/runtime-mcp/startup-policy.ts +14 -0
- package/src/runtime-mcp/types.ts +2 -2
- package/src/sdk/acp/adapter.ts +27 -12
- package/src/sdk/broker/broker.ts +273 -44
- package/src/sdk/broker/daemon-entry.ts +119 -0
- package/src/sdk/broker/discovery.ts +173 -20
- package/src/sdk/broker/doctor-restart.ts +206 -0
- package/src/sdk/broker/endpoint-authority.ts +36 -0
- package/src/sdk/broker/ensure.ts +49 -14
- package/src/sdk/broker/lifecycle.ts +174 -42
- package/src/sdk/broker/process-incarnation.ts +86 -2
- package/src/sdk/broker/runtime.ts +145 -21
- package/src/sdk/broker/session-index.ts +136 -22
- package/src/sdk/broker/spawn-substrate.ts +4 -1
- package/src/sdk/broker/transport.ts +5 -0
- package/src/sdk/broker/worktree-occupancy.ts +21 -1
- package/src/sdk/bus/chat-daemon-cli.ts +63 -0
- package/src/sdk/bus/chat-daemon-control.ts +260 -64
- package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
- package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
- package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
- package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
- package/src/sdk/bus/index.ts +252 -51
- package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
- package/src/sdk/bus/notification-service.ts +21 -3
- package/src/sdk/bus/reconciliation-store.ts +17 -11
- package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +47 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
- package/src/sdk/bus/telegram-daemon.ts +178 -13
- package/src/sdk/cli/session-cli.ts +100 -12
- package/src/sdk/host/host.ts +2 -7
- package/src/sdk/host/session-runtime.test.ts +329 -8
- package/src/sdk/host/session-runtime.ts +202 -50
- package/src/sdk/host/surface-policy.ts +8 -1
- package/src/sdk/lifecycle/service.ts +199 -22
- package/src/sdk/mcp/server.ts +1 -1
- package/src/sdk/prompt-deadline-lease.ts +12 -4
- package/src/sdk/prompt-deadline-manager.ts +5 -7
- package/src/sdk/prompt-failure.ts +148 -0
- package/src/sdk/prompt-status.ts +30 -1
- package/src/sdk/prompt-watchdog.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +20 -8
- package/src/sdk/router/session-router.ts +8 -3
- package/src/sdk/service-artifact-paths.ts +26 -0
- package/src/sdk/session-authority.ts +10 -0
- package/src/sdk/session.ts +361 -194
- package/src/session/agent-session.ts +398 -93
- package/src/session/blob-store.ts +9 -9
- package/src/session/fallback-chain-controller.ts +3 -7
- package/src/session/internal/managed-session-scope.ts +445 -29
- package/src/session/internal/managed-session-storage.ts +38 -6
- package/src/session/primary-control-surface.ts +12 -0
- package/src/session/session-manager.ts +11 -3
- package/src/session/session-storage.ts +1 -1
- package/src/session-import/codex.ts +6 -1
- package/src/session-import/redact.ts +7 -0
- package/src/skill-state/workflow-mutation-guard.ts +645 -39
- package/src/slash-commands/builtin-registry.ts +7 -3
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +36 -3
- package/src/task/types.ts +27 -5
- package/src/tools/ask.ts +114 -34
- package/src/tools/browser/actions.ts +2 -2
- package/src/tools/browser/tab-worker.ts +11 -8
- package/src/tools/descriptors.ts +6 -1
- package/src/tools/fetch.ts +7 -2
- package/src/tools/image-gen.ts +33 -34
- package/src/tools/read.ts +78 -32
- package/src/tools/skill-discovery.ts +17 -7
- package/src/tools/tool-catalog.generated.ts +8 -8
- package/src/tools/ultragoal-ask-guard.ts +16 -26
- package/src/utils/changelog.ts +7 -1
- package/src/utils/git.ts +3 -1
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- package/src/web/search/providers/openai-compatible.ts +191 -14
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "../coordinator-mcp/durability";
|
|
20
20
|
import { reduceTerminalReceiptState } from "../sdk/receipt-state";
|
|
21
21
|
import { TOOL_CATALOG } from "../tools/tool-catalog.generated";
|
|
22
|
-
import { sessionRoot, sessionRuntimeDir } from "./session-layout";
|
|
22
|
+
import { sessionRoot, sessionRuntimeDir, sessionRuntimeStatePath } from "./session-layout";
|
|
23
23
|
import { SessionStateLockUnavailableError, withSessionStateFileLock } from "./session-state-lock";
|
|
24
24
|
import {
|
|
25
25
|
isValidOwnerIntent,
|
|
@@ -643,6 +643,17 @@ export interface RuntimeStateContext {
|
|
|
643
643
|
sessionId: string;
|
|
644
644
|
cwd: string;
|
|
645
645
|
sessionFile?: string | null;
|
|
646
|
+
/**
|
|
647
|
+
* The marker file THIS context owns.
|
|
648
|
+
*
|
|
649
|
+
* `undefined` keeps the process-wide coordinator pin authoritative (falling back to the
|
|
650
|
+
* session-derived path when this process carries no pin), which is what a top-level
|
|
651
|
+
* session wants. Naming a path is how a session that is NOT the pinned one — an
|
|
652
|
+
* in-process role-agent/subagent session sharing the parent's environment — takes
|
|
653
|
+
* ownership of its own marker instead of reading and writing the parent's (#5473).
|
|
654
|
+
* `null` means this context has no marker at all and every write is skipped.
|
|
655
|
+
*/
|
|
656
|
+
stateFile?: string | null;
|
|
646
657
|
/** Optional platform seam for deterministic cross-platform path identity checks. */
|
|
647
658
|
platform?: NodeJS.Platform;
|
|
648
659
|
branch?: string | null;
|
|
@@ -835,11 +846,34 @@ function sameResolvedPath(left: string, right: string, platform: NodeJS.Platform
|
|
|
835
846
|
return normalizePathForComparison(left, platform) === normalizePathForComparison(right, platform);
|
|
836
847
|
}
|
|
837
848
|
|
|
849
|
+
/**
|
|
850
|
+
* The marker file the launching coordinator pinned for this process, if any.
|
|
851
|
+
*
|
|
852
|
+
* The pin is process-wide environment, but it describes exactly ONE session: the one the
|
|
853
|
+
* pin was minted for. Every other session that runs inside the same process — an
|
|
854
|
+
* in-process role-agent/subagent fan-out — must never read or write that file, or its
|
|
855
|
+
* writes are refused as a foreign identity and the parent's lifecycle gets clobbered.
|
|
856
|
+
*/
|
|
857
|
+
function coordinatorPinnedStateFile(): string | null {
|
|
858
|
+
return process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() || null;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** True when `context` owns the process-wide coordinator pin rather than naming its own marker. */
|
|
862
|
+
function usesCoordinatorPin(context: Pick<RuntimeStateContext, "stateFile" | "platform">): boolean {
|
|
863
|
+
const pinned = coordinatorPinnedStateFile();
|
|
864
|
+
if (!pinned) return false;
|
|
865
|
+
if (context.stateFile === undefined) return true;
|
|
866
|
+
if (context.stateFile === null) return false;
|
|
867
|
+
if (context.stateFile === pinned) return true;
|
|
868
|
+
return sameResolvedPath(context.stateFile, pinned, context.platform ?? process.platform);
|
|
869
|
+
}
|
|
870
|
+
|
|
838
871
|
function normalizedIdentity(
|
|
839
|
-
context: Pick<RuntimeStateContext, "sessionId" | "cwd" | "sessionFile" | "platform">,
|
|
872
|
+
context: Pick<RuntimeStateContext, "sessionId" | "cwd" | "sessionFile" | "platform" | "stateFile">,
|
|
840
873
|
): RuntimeStateIdentity {
|
|
841
|
-
|
|
842
|
-
|
|
874
|
+
// A session that owns its own marker is identified by its OWN session id. Only the
|
|
875
|
+
// pinned session adopts the coordinator-correlation id the pin was launched under.
|
|
876
|
+
const sessionId = usesCoordinatorPin(context)
|
|
843
877
|
? process.env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId.trim()
|
|
844
878
|
: context.sessionId.trim();
|
|
845
879
|
const cwd = context.cwd.trim();
|
|
@@ -855,7 +889,14 @@ function normalizedIdentity(
|
|
|
855
889
|
(sidecarSignatureRequired &&
|
|
856
890
|
(!sidecarKeyId || !/^[a-f0-9]{64}$/.test(sidecarKeyId) || !coordinatorSidecarSigningKey))
|
|
857
891
|
)
|
|
858
|
-
throw new PreviousRuntimeStateReadError(
|
|
892
|
+
throw new PreviousRuntimeStateReadError(
|
|
893
|
+
undefined,
|
|
894
|
+
!sessionId
|
|
895
|
+
? "the runtime session id is empty"
|
|
896
|
+
: !cwd
|
|
897
|
+
? "the runtime cwd is empty"
|
|
898
|
+
: "this launch requires Coordinator sidecar signing but its signing key material is missing",
|
|
899
|
+
);
|
|
859
900
|
return {
|
|
860
901
|
sessionId,
|
|
861
902
|
cwd: pathApi.resolve(cwd),
|
|
@@ -1028,9 +1069,14 @@ class ForeignRuntimeStateError extends Error {
|
|
|
1028
1069
|
readonly currentCwd: string;
|
|
1029
1070
|
constructor(previous: Record<string, unknown>, input: RuntimeStateIdentity) {
|
|
1030
1071
|
const recorded = typeof previous.cwd === "string" ? previous.cwd : "<unknown>";
|
|
1072
|
+
// The message keeps naming both workspaces — that is what distinguishes a foreign marker
|
|
1073
|
+
// from file corruption — but it echoes the recorded path through the diagnostic sanitizer
|
|
1074
|
+
// so a marker cannot inject terminal control sequences into an operator's log. The exact
|
|
1075
|
+
// value stays available on the typed fields below.
|
|
1031
1076
|
super(
|
|
1032
1077
|
`Existing runtime state marker belongs to a different workspace; refusing to overwrite. ` +
|
|
1033
|
-
`recorded cwd ${recorded}, current cwd ${input.cwd}
|
|
1078
|
+
`recorded cwd ${sanitizeDiagnosticText(recorded)}, current cwd ${sanitizeDiagnosticText(input.cwd)}; ` +
|
|
1079
|
+
`${previousPayloadDigestOf(previous)}. ` +
|
|
1034
1080
|
`A live or in-flight marker is never adopted; delete it only if no session is running there.`,
|
|
1035
1081
|
);
|
|
1036
1082
|
this.name = "ForeignRuntimeStateError";
|
|
@@ -1087,82 +1133,217 @@ function isAbsentStateFileError(error: unknown): boolean {
|
|
|
1087
1133
|
}
|
|
1088
1134
|
|
|
1089
1135
|
function parsePreviousPayload(raw: string): Record<string, unknown> {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1136
|
+
let payload: unknown;
|
|
1137
|
+
try {
|
|
1138
|
+
payload = JSON.parse(raw);
|
|
1139
|
+
} catch {
|
|
1140
|
+
throw new PreviousRuntimeStateReadError(
|
|
1141
|
+
undefined,
|
|
1142
|
+
`the marker is not valid JSON (${Buffer.byteLength(raw, "utf8")} bytes read); ${previousPayloadDigest(raw)}`,
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
const violation = previousPayloadShapeViolation(payload);
|
|
1146
|
+
if (violation) throw new PreviousRuntimeStateReadError(undefined, `${violation}; ${previousPayloadDigest(raw)}`);
|
|
1147
|
+
const marker = payload as Record<string, unknown>;
|
|
1148
|
+
// Structural validation above bounds every interpolated lifecycle value to a known state
|
|
1093
1149
|
// or boolean; never include arbitrary marker contents in a terminal diagnostic.
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1150
|
+
// Before #4351, completed markers could retain the readiness bit. Normalize only
|
|
1151
|
+
// that legacy shape; all other lifecycle contradictions remain errors.
|
|
1152
|
+
// This mutation runs before the sidecar fence in {@link assertPreviousRuntimeStateIdentity},
|
|
1153
|
+
// which is safe only because signing postdates the pre-#4351 writer contract (#4351 landed
|
|
1154
|
+
// 2026-08-12, sidecar signing 2026-08-22): no signed payload can carry this shape, so the
|
|
1155
|
+
// fence never sees a normalized predecessor it should have rejected. A future signed
|
|
1156
|
+
// predecessor written under the old contract must be handled here explicitly rather than
|
|
1157
|
+
// escaping as a misleading signature mismatch.
|
|
1158
|
+
if (marker.state === "completed" && marker.ready_for_input === true) marker.ready_for_input = false;
|
|
1159
|
+
if (marker.ready_for_input !== undefined) {
|
|
1160
|
+
const expectedReady = marker.state === "ready_for_input";
|
|
1161
|
+
if (marker.ready_for_input !== expectedReady)
|
|
1097
1162
|
throw new PreviousRuntimeStateReadError(
|
|
1098
1163
|
undefined,
|
|
1099
|
-
`ready_for_input must be ${expectedReady} when state is ${
|
|
1164
|
+
`ready_for_input must be ${expectedReady} when state is ${marker.state} (received ${marker.ready_for_input}); ${previousPayloadDigest(raw)}`,
|
|
1100
1165
|
);
|
|
1101
1166
|
}
|
|
1102
|
-
if (
|
|
1103
|
-
const expectedLive =
|
|
1104
|
-
if (
|
|
1167
|
+
if (marker.live !== undefined && marker.live !== null) {
|
|
1168
|
+
const expectedLive = marker.state === "running";
|
|
1169
|
+
if (marker.live !== expectedLive)
|
|
1105
1170
|
throw new PreviousRuntimeStateReadError(
|
|
1106
1171
|
undefined,
|
|
1107
|
-
`live must be ${expectedLive} when state is ${
|
|
1172
|
+
`live must be ${expectedLive} when state is ${marker.state} (received ${marker.live}); ${previousPayloadDigest(raw)}`,
|
|
1108
1173
|
);
|
|
1109
1174
|
}
|
|
1110
|
-
return
|
|
1175
|
+
return marker;
|
|
1111
1176
|
}
|
|
1112
1177
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1178
|
+
/**
|
|
1179
|
+
* The lifecycle values the shared coordinator/session marker may carry.
|
|
1180
|
+
*
|
|
1181
|
+
* The file is written by BOTH the runtime sidecar and the coordinator projection writer, so
|
|
1182
|
+
* the reader must be total over the union of their vocabularies. `prepared` (a session that
|
|
1183
|
+
* is live and endpoint-addressable but withholds readiness) and `stale`/`unknown` are
|
|
1184
|
+
* coordinator-side lifecycles the sidecar never writes itself but must still read back (#5473).
|
|
1185
|
+
*/
|
|
1186
|
+
const PREVIOUS_RUNTIME_STATE_VALUES = [
|
|
1187
|
+
"booting",
|
|
1188
|
+
"prepared",
|
|
1189
|
+
"ready_for_input",
|
|
1190
|
+
"running",
|
|
1191
|
+
"needs_user_input",
|
|
1192
|
+
"completed",
|
|
1193
|
+
"errored",
|
|
1194
|
+
"stale",
|
|
1195
|
+
"unknown",
|
|
1196
|
+
] as const;
|
|
1197
|
+
|
|
1198
|
+
const PREVIOUS_RUNTIME_STATES = new Set<string>(PREVIOUS_RUNTIME_STATE_VALUES);
|
|
1199
|
+
|
|
1200
|
+
/** Only short, control-character-free identifiers may reach an operator-visible diagnostic. */
|
|
1201
|
+
const SAFE_DIAGNOSTIC_IDENTIFIER = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Describe a received marker field without echoing untrusted marker text.
|
|
1205
|
+
*
|
|
1206
|
+
* A corrupt or hostile marker must never be able to write terminal control sequences into
|
|
1207
|
+
* an operator's diagnostics, so only short identifier-shaped scalars are echoed verbatim;
|
|
1208
|
+
* every other value is reduced to its category (and, for strings, its length). The payload
|
|
1209
|
+
* digest reported alongside binds the description to the exact bytes that were read.
|
|
1210
|
+
*/
|
|
1211
|
+
function describePreviousPayloadValue(value: unknown): string {
|
|
1212
|
+
if (value === undefined) return "absent";
|
|
1213
|
+
if (value === null) return "null";
|
|
1214
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1215
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "a non-finite number";
|
|
1216
|
+
if (typeof value === "string")
|
|
1217
|
+
return SAFE_DIAGNOSTIC_IDENTIFIER.test(value) ? JSON.stringify(value) : `a string (${value.length} chars)`;
|
|
1218
|
+
return Array.isArray(value) ? `an array (${value.length} entries)` : "a non-array object";
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/** The digest that binds a refusal to the exact marker bytes that produced it. */
|
|
1222
|
+
function previousPayloadDigest(raw: string): string {
|
|
1223
|
+
return `payload sha256 ${createHash("sha256").update(raw).digest("hex")}`;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* The digest a refusal can always compute, for paths that hold no raw bytes.
|
|
1228
|
+
*
|
|
1229
|
+
* Canonicalizing the parsed payload makes the digest a deterministic function of the marker's
|
|
1230
|
+
* VALUE as this process read it — key order is normalized, so a re-parse reproduces it. One
|
|
1231
|
+
* legacy shape is normalized before it can reach here (#4351 rewrites a completed marker's
|
|
1232
|
+
* stale `ready_for_input: true` to false), so the digest binds the payload that was actually
|
|
1233
|
+
* refused rather than the file's original bytes; where the raw bytes are still in hand the
|
|
1234
|
+
* caller uses {@link previousPayloadDigest} instead and gets the byte-exact form.
|
|
1235
|
+
*/
|
|
1236
|
+
function previousPayloadDigestOf(previous: Record<string, unknown>): string {
|
|
1237
|
+
return `payload-content sha256 ${createHash("sha256")
|
|
1238
|
+
.update(canonicalCoordinatorSidecarPayload(previous))
|
|
1239
|
+
.digest("hex")}`;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Bound and de-fang operator-visible text taken from a marker.
|
|
1244
|
+
*
|
|
1245
|
+
* A corrupt or hostile marker must never be able to write terminal control sequences or
|
|
1246
|
+
* reorder an operator's line into a diagnostic, so C0/C1 controls and every Unicode `Cf`
|
|
1247
|
+
* (format) character — bidi overrides and isolates, zero-width and BOM marks, invisible
|
|
1248
|
+
* operators — are replaced before the value is bounded.
|
|
1249
|
+
*/
|
|
1250
|
+
function sanitizeDiagnosticText(value: string): string {
|
|
1251
|
+
const sanitized = value.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029\p{Cf}]/gu, "?");
|
|
1252
|
+
return sanitized.length > 240 ? `${sanitized.slice(0, 240)}...` : sanitized;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/** Quote and de-fang a path or identity for a diagnostic. */
|
|
1256
|
+
function quotedDiagnosticText(value: string): string {
|
|
1257
|
+
return JSON.stringify(sanitizeDiagnosticText(value));
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/** Identify the marker THIS writer owns, so a cross-session refusal explains itself. */
|
|
1261
|
+
function thisMarkerHint(input: RuntimeStateIdentity, stateFile: string | null): string {
|
|
1262
|
+
return (
|
|
1263
|
+
`this write targets session ${quotedDiagnosticText(input.sessionId)} at cwd ${quotedDiagnosticText(input.cwd)}` +
|
|
1264
|
+
(input.sessionFile === null ? "" : ` with session file ${quotedDiagnosticText(input.sessionFile)}`) +
|
|
1265
|
+
(stateFile === null ? "" : ` using marker ${quotedDiagnosticText(stateFile)}`)
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Why a previous-marker payload is unusable, or `null` when it is readable.
|
|
1271
|
+
*
|
|
1272
|
+
* Every rejection names the field that failed so the two-writer cases stay distinguishable:
|
|
1273
|
+
* a coordinator-only lifecycle value, a marker written by a different session, damaged JSON,
|
|
1274
|
+
* and a contradictory lifecycle field all used to collapse into one opaque message (#5473).
|
|
1275
|
+
*/
|
|
1276
|
+
function previousPayloadShapeViolation(value: unknown): string | null {
|
|
1277
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1278
|
+
return `the marker payload must be a JSON object (received ${describePreviousPayloadValue(value)})`;
|
|
1115
1279
|
const payload = value as Record<string, unknown>;
|
|
1116
|
-
if (
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
payload.session_id
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
payload.state
|
|
1124
|
-
|
|
1125
|
-
payload.state !== "errored" &&
|
|
1126
|
-
payload.state !== "stale" &&
|
|
1127
|
-
payload.state !== "unknown")
|
|
1128
|
-
)
|
|
1129
|
-
return false;
|
|
1280
|
+
if (payload.schema_version !== 1)
|
|
1281
|
+
return `schema_version must be 1 (received ${describePreviousPayloadValue(payload.schema_version)})`;
|
|
1282
|
+
if (typeof payload.session_id !== "string" || payload.session_id.trim().length === 0)
|
|
1283
|
+
return `session_id must be a non-empty string (received ${describePreviousPayloadValue(payload.session_id)})`;
|
|
1284
|
+
if (typeof payload.state !== "string" || !PREVIOUS_RUNTIME_STATES.has(payload.state))
|
|
1285
|
+
return (
|
|
1286
|
+
`state must be one of ${PREVIOUS_RUNTIME_STATE_VALUES.join(", ")} ` +
|
|
1287
|
+
`(received ${describePreviousPayloadValue(payload.state)})`
|
|
1288
|
+
);
|
|
1130
1289
|
// Coordinator-seeded payloads (#2549) carry session_id and state but not the
|
|
1131
1290
|
// runtime identity fields cwd/workdir/session_file. Accept their absence; when
|
|
1132
1291
|
// present, validate them as before.
|
|
1133
|
-
if (typeof payload.cwd !== "
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
if (payload.
|
|
1144
|
-
|
|
1292
|
+
if (payload.cwd !== undefined && (typeof payload.cwd !== "string" || payload.cwd.trim().length === 0))
|
|
1293
|
+
return `cwd must be a non-empty string when present (received ${describePreviousPayloadValue(payload.cwd)})`;
|
|
1294
|
+
if (payload.workdir !== undefined && (typeof payload.workdir !== "string" || payload.workdir.trim().length === 0))
|
|
1295
|
+
return `workdir must be a non-empty string when present (received ${describePreviousPayloadValue(payload.workdir)})`;
|
|
1296
|
+
if (
|
|
1297
|
+
Object.hasOwn(payload, "session_file") &&
|
|
1298
|
+
payload.session_file !== null &&
|
|
1299
|
+
typeof payload.session_file !== "string"
|
|
1300
|
+
)
|
|
1301
|
+
return `session_file must be null or a string (received ${describePreviousPayloadValue(payload.session_file)})`;
|
|
1302
|
+
if (payload.ready_for_input !== undefined && typeof payload.ready_for_input !== "boolean")
|
|
1303
|
+
return `ready_for_input must be a boolean when present (received ${describePreviousPayloadValue(payload.ready_for_input)})`;
|
|
1304
|
+
if (payload.live !== undefined && payload.live !== null && typeof payload.live !== "boolean")
|
|
1305
|
+
return `live must be null or a boolean when present (received ${describePreviousPayloadValue(payload.live)})`;
|
|
1306
|
+
if (payload.reason !== undefined && payload.reason !== null && typeof payload.reason !== "string")
|
|
1307
|
+
return `reason must be null or a string when present (received ${describePreviousPayloadValue(payload.reason)})`;
|
|
1145
1308
|
if (
|
|
1146
1309
|
payload.updated_at !== undefined &&
|
|
1147
1310
|
(typeof payload.updated_at !== "string" || !Number.isFinite(Date.parse(payload.updated_at)))
|
|
1148
1311
|
)
|
|
1149
|
-
return
|
|
1150
|
-
return
|
|
1312
|
+
return `updated_at must be a parseable timestamp when present (received ${describePreviousPayloadValue(payload.updated_at)})`;
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Categorize a marker read failure without echoing the marker's own bytes.
|
|
1318
|
+
*
|
|
1319
|
+
* Only the OS error code — a closed, identifier-shaped token — reaches the diagnostic; an
|
|
1320
|
+
* arbitrary error message could carry marker-derived text or terminal control sequences.
|
|
1321
|
+
*/
|
|
1322
|
+
function describeMarkerReadFailure(error: unknown): string {
|
|
1323
|
+
const code = (error as { code?: unknown }).code;
|
|
1324
|
+
return typeof code === "string" && SAFE_DIAGNOSTIC_IDENTIFIER.test(code)
|
|
1325
|
+
? `the marker could not be read (${code})`
|
|
1326
|
+
: "the marker could not be read";
|
|
1151
1327
|
}
|
|
1152
1328
|
|
|
1153
1329
|
function readPreviousPayload(stateFile: string): Record<string, unknown> {
|
|
1154
1330
|
let raw: string;
|
|
1155
1331
|
try {
|
|
1332
|
+
// Mirror the event path's shape check so a directory marker reports the same
|
|
1333
|
+
// diagnosis whether a lifecycle event or the postmortem finalizer reaches it.
|
|
1334
|
+
if (!fsSync.statSync(stateFile).isFile())
|
|
1335
|
+
throw new PreviousRuntimeStateReadError(undefined, "the marker path is not a regular file");
|
|
1156
1336
|
raw = fsSync.readFileSync(stateFile, "utf8");
|
|
1157
1337
|
} catch (error) {
|
|
1338
|
+
if (error instanceof PreviousRuntimeStateReadError) throw error;
|
|
1158
1339
|
if (isAbsentStateFileError(error)) return {};
|
|
1159
|
-
throw new PreviousRuntimeStateReadError();
|
|
1340
|
+
throw new PreviousRuntimeStateReadError(error, describeMarkerReadFailure(error));
|
|
1160
1341
|
}
|
|
1161
1342
|
try {
|
|
1162
1343
|
return parsePreviousPayload(raw);
|
|
1163
1344
|
} catch (error) {
|
|
1164
1345
|
if (error instanceof PreviousRuntimeStateReadError) throw error;
|
|
1165
|
-
throw new PreviousRuntimeStateReadError();
|
|
1346
|
+
throw new PreviousRuntimeStateReadError(error, describeMarkerReadFailure(error));
|
|
1166
1347
|
}
|
|
1167
1348
|
}
|
|
1168
1349
|
|
|
@@ -1253,14 +1434,14 @@ async function readPreviousPayloadForEvent(stateFile: string): Promise<Record<st
|
|
|
1253
1434
|
stat = await fs.stat(stateFile);
|
|
1254
1435
|
} catch (error) {
|
|
1255
1436
|
if (isAbsentStateFileError(error)) return {};
|
|
1256
|
-
throw new PreviousRuntimeStateReadError();
|
|
1437
|
+
throw new PreviousRuntimeStateReadError(error, describeMarkerReadFailure(error));
|
|
1257
1438
|
}
|
|
1258
|
-
if (!stat.isFile()) throw new PreviousRuntimeStateReadError();
|
|
1439
|
+
if (!stat.isFile()) throw new PreviousRuntimeStateReadError(undefined, "the marker path is not a regular file");
|
|
1259
1440
|
try {
|
|
1260
1441
|
return parsePreviousPayload(await fs.readFile(stateFile, "utf8"));
|
|
1261
1442
|
} catch (error) {
|
|
1262
1443
|
if (error instanceof PreviousRuntimeStateReadError) throw error;
|
|
1263
|
-
throw new PreviousRuntimeStateReadError();
|
|
1444
|
+
throw new PreviousRuntimeStateReadError(error, describeMarkerReadFailure(error));
|
|
1264
1445
|
}
|
|
1265
1446
|
}
|
|
1266
1447
|
|
|
@@ -1305,7 +1486,12 @@ function assertPreviousRuntimeStateIdentity(
|
|
|
1305
1486
|
stateFile: string | null = null,
|
|
1306
1487
|
): void {
|
|
1307
1488
|
if (Object.keys(previous).length === 0) return;
|
|
1308
|
-
if (previous.session_id !== input.sessionId)
|
|
1489
|
+
if (previous.session_id !== input.sessionId)
|
|
1490
|
+
throw new PreviousRuntimeStateReadError(
|
|
1491
|
+
undefined,
|
|
1492
|
+
`session_id must be ${quotedDiagnosticText(input.sessionId)} (received ${describePreviousPayloadValue(previous.session_id)}); ` +
|
|
1493
|
+
`${thisMarkerHint(input, stateFile)}; ${previousPayloadDigestOf(previous)}`,
|
|
1494
|
+
);
|
|
1309
1495
|
// Signed coordinator mode never trusts an unsigned predecessor. The sole exception
|
|
1310
1496
|
// is the narrow, coordinator-authenticated bootstrap seed: it has no runtime identity
|
|
1311
1497
|
// fields, is explicitly sourced by the coordinator, and carries the turn fence.
|
|
@@ -1325,7 +1511,14 @@ function assertPreviousRuntimeStateIdentity(
|
|
|
1325
1511
|
typeof previous.sidecar_signature !== "string" ||
|
|
1326
1512
|
!coordinatorSidecarSigningKey
|
|
1327
1513
|
)
|
|
1328
|
-
throw new PreviousRuntimeStateReadError(
|
|
1514
|
+
throw new PreviousRuntimeStateReadError(
|
|
1515
|
+
undefined,
|
|
1516
|
+
previous.sidecar_key_id === input.sidecarKeyId
|
|
1517
|
+
? `the marker's sidecar signature is ${describePreviousPayloadValue(previous.sidecar_signature)}; ` +
|
|
1518
|
+
`${thisMarkerHint(input, stateFile)}; ${previousPayloadDigestOf(previous)}`
|
|
1519
|
+
: `the marker is not signed by this session's sidecar key (recorded ${describePreviousPayloadValue(previous.sidecar_key_id)}, required ${input.sidecarKeyId}); ` +
|
|
1520
|
+
`${thisMarkerHint(input, stateFile)}; ${previousPayloadDigestOf(previous)}`,
|
|
1521
|
+
);
|
|
1329
1522
|
const { sidecar_signature: signature, ...unsigned } = previous;
|
|
1330
1523
|
if (
|
|
1331
1524
|
!verify(
|
|
@@ -1335,7 +1528,10 @@ function assertPreviousRuntimeStateIdentity(
|
|
|
1335
1528
|
Buffer.from(signature, "base64"),
|
|
1336
1529
|
)
|
|
1337
1530
|
)
|
|
1338
|
-
throw new PreviousRuntimeStateReadError(
|
|
1531
|
+
throw new PreviousRuntimeStateReadError(
|
|
1532
|
+
undefined,
|
|
1533
|
+
`the marker's sidecar signature does not verify; ${thisMarkerHint(input, stateFile)}; ${previousPayloadDigestOf(previous)}`,
|
|
1534
|
+
);
|
|
1339
1535
|
}
|
|
1340
1536
|
if (coordinatorSeed) return;
|
|
1341
1537
|
// If the previous payload has runtime identity fields, verify them fully.
|
|
@@ -1382,10 +1578,13 @@ function assertRelocationRuntimeStateIdentity(previous: Record<string, unknown>,
|
|
|
1382
1578
|
}
|
|
1383
1579
|
|
|
1384
1580
|
function runtimeStateFileForContext(context: RuntimeStateContext): string | null {
|
|
1385
|
-
|
|
1581
|
+
// An explicitly owned marker — including "this session has none" — always wins. Only a
|
|
1582
|
+
// context that names nothing is answered by the process-wide coordinator pin.
|
|
1583
|
+
if (context.stateFile !== undefined) return context.stateFile;
|
|
1584
|
+
const explicit = coordinatorPinnedStateFile();
|
|
1386
1585
|
if (explicit) return explicit;
|
|
1387
1586
|
if (!context.sessionId.trim()) return null;
|
|
1388
|
-
return
|
|
1587
|
+
return sessionRuntimeStatePath(context.cwd, context.sessionId);
|
|
1389
1588
|
}
|
|
1390
1589
|
function branchForContext(context: RuntimeStateContext): string | null {
|
|
1391
1590
|
return context.branch ?? (process.env[GJC_COORDINATOR_SESSION_BRANCH_ENV]?.trim() || null);
|
|
@@ -2023,6 +2222,7 @@ function previousRescopeIdentity(context: RuntimeStateContext, previousCwd: stri
|
|
|
2023
2222
|
cwd: previousCwd,
|
|
2024
2223
|
sessionFile: context.previousSessionFile ?? context.sessionFile ?? null,
|
|
2025
2224
|
platform: context.platform,
|
|
2225
|
+
stateFile: context.stateFile,
|
|
2026
2226
|
});
|
|
2027
2227
|
}
|
|
2028
2228
|
|
|
@@ -2115,21 +2315,41 @@ function rescopeStateFiles(
|
|
|
2115
2315
|
sessionId: string,
|
|
2116
2316
|
previousCwd: string,
|
|
2117
2317
|
newCwd: string,
|
|
2318
|
+
stateFile?: string | null,
|
|
2118
2319
|
): { oldStateFile: string; newStateFile: string; mode: "pinned" | "derived" } {
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2320
|
+
// Ownership, never the ambient pin, decides whether this session's marker travels with
|
|
2321
|
+
// the cwd: a session that owns its own marker keeps it under the per-session runtime
|
|
2322
|
+
// directory even while the process carries the coordinator pin for a different session
|
|
2323
|
+
// (#5473).
|
|
2324
|
+
const pinned = coordinatorPinnedStateFile();
|
|
2325
|
+
if (pinned && usesCoordinatorPin({ stateFile })) {
|
|
2326
|
+
const canonical = canonicalStateFilePath(pinned);
|
|
2327
|
+
return { oldStateFile: canonical, newStateFile: canonical, mode: "pinned" };
|
|
2123
2328
|
}
|
|
2124
|
-
|
|
2125
|
-
oldStateFile: canonicalStateFilePath(
|
|
2126
|
-
|
|
2127
|
-
),
|
|
2128
|
-
newStateFile: canonicalStateFilePath(
|
|
2129
|
-
path.join(sessionRuntimeDir(fsSync.realpathSync(newCwd), sessionId), "runtime-state.json"),
|
|
2130
|
-
),
|
|
2131
|
-
mode: "derived",
|
|
2329
|
+
const derived = {
|
|
2330
|
+
oldStateFile: canonicalStateFilePath(sessionRuntimeStatePath(fsSync.realpathSync(previousCwd), sessionId)),
|
|
2331
|
+
newStateFile: canonicalStateFilePath(sessionRuntimeStatePath(fsSync.realpathSync(newCwd), sessionId)),
|
|
2132
2332
|
};
|
|
2333
|
+
if (stateFile === undefined) return { ...derived, mode: "derived" };
|
|
2334
|
+
if (stateFile === null)
|
|
2335
|
+
throw new PreviousRuntimeStateReadError(
|
|
2336
|
+
undefined,
|
|
2337
|
+
"this session owns no runtime-state marker; refusing to rescope one",
|
|
2338
|
+
);
|
|
2339
|
+
// The classification is total: a caller-named marker must be the pin (handled above) or
|
|
2340
|
+
// this session's own derived marker. Both sides are canonicalized first, because a caller
|
|
2341
|
+
// may name its marker through a non-canonical spelling of the cwd — a symlinked checkout or
|
|
2342
|
+
// the macOS `/var`, `/tmp` prefixes — and a marker file that does not exist yet has no
|
|
2343
|
+
// realpath to normalize through.
|
|
2344
|
+
if (
|
|
2345
|
+
!sameResolvedPath(canonicalStateFilePath(stateFile), derived.oldStateFile) &&
|
|
2346
|
+
!sameResolvedPath(canonicalStateFilePath(stateFile), derived.newStateFile)
|
|
2347
|
+
)
|
|
2348
|
+
throw new PreviousRuntimeStateReadError(
|
|
2349
|
+
undefined,
|
|
2350
|
+
`the rescope marker ${quotedDiagnosticText(stateFile)} is neither the process pin nor this session's derived marker; refusing to guess its ownership`,
|
|
2351
|
+
);
|
|
2352
|
+
return { ...derived, mode: "derived" };
|
|
2133
2353
|
}
|
|
2134
2354
|
|
|
2135
2355
|
function journalSignaturePayload(journal: CoordinatorRuntimeStateRescopeJournal): Record<string, unknown> {
|
|
@@ -2187,6 +2407,8 @@ export async function prepareCoordinatorRuntimeStateRescope(input: {
|
|
|
2187
2407
|
newCwdIdentity?: { dev: bigint; ino: bigint };
|
|
2188
2408
|
previousSessionFile?: string | null;
|
|
2189
2409
|
newSessionFile?: string | null;
|
|
2410
|
+
/** The marker this session owns; `undefined` lets the process pin answer for it. */
|
|
2411
|
+
stateFile?: string | null;
|
|
2190
2412
|
}): Promise<string> {
|
|
2191
2413
|
const previousStat = fsSync.lstatSync(input.previousCwd, { bigint: true });
|
|
2192
2414
|
const newStat = fsSync.lstatSync(input.newCwd, { bigint: true });
|
|
@@ -2196,13 +2418,14 @@ export async function prepareCoordinatorRuntimeStateRescope(input: {
|
|
|
2196
2418
|
assertPinnedDirectoryIdentity(input.newCwd, newCwdIdentity);
|
|
2197
2419
|
const previousCwd = fsSync.realpathSync(input.previousCwd);
|
|
2198
2420
|
const newCwd = fsSync.realpathSync(input.newCwd);
|
|
2199
|
-
const stateFiles = rescopeStateFiles(input.sessionId, previousCwd, newCwd);
|
|
2421
|
+
const stateFiles = rescopeStateFiles(input.sessionId, previousCwd, newCwd, input.stateFile);
|
|
2200
2422
|
const moveId = input.moveId ?? randomUUID();
|
|
2201
2423
|
if (!RUNTIME_STATE_RESCOPE_MOVE_ID_PATTERN.test(moveId)) throw new PreviousRuntimeStateReadError();
|
|
2202
2424
|
const identity = normalizedIdentity({
|
|
2203
2425
|
sessionId: input.sessionId,
|
|
2204
2426
|
cwd: newCwd,
|
|
2205
2427
|
sessionFile: input.newSessionFile ?? null,
|
|
2428
|
+
stateFile: input.stateFile,
|
|
2206
2429
|
});
|
|
2207
2430
|
const previousJournalFile = confinedJournalPath(previousCwd, input.sessionId, identity.platform);
|
|
2208
2431
|
const newJournalFile = confinedJournalPath(newCwd, input.sessionId, identity.platform);
|
|
@@ -2212,6 +2435,7 @@ export async function prepareCoordinatorRuntimeStateRescope(input: {
|
|
|
2212
2435
|
cwd: newCwd,
|
|
2213
2436
|
sessionFile: input.newSessionFile ?? null,
|
|
2214
2437
|
previousSessionFile: input.previousSessionFile ?? null,
|
|
2438
|
+
stateFile: input.stateFile,
|
|
2215
2439
|
},
|
|
2216
2440
|
previousCwd,
|
|
2217
2441
|
);
|
|
@@ -2380,7 +2604,12 @@ export async function recoverCoordinatorRuntimeStateRescope(context: RuntimeStat
|
|
|
2380
2604
|
const journal = parseRuntimeStateRescopeJournal(observed.raw);
|
|
2381
2605
|
verifyRuntimeStateRescopeJournal(journal);
|
|
2382
2606
|
const current = normalizedIdentity(context);
|
|
2383
|
-
const configuredStateFiles = rescopeStateFiles(
|
|
2607
|
+
const configuredStateFiles = rescopeStateFiles(
|
|
2608
|
+
journal.session_id,
|
|
2609
|
+
journal.previous_cwd,
|
|
2610
|
+
journal.new_cwd,
|
|
2611
|
+
context.stateFile,
|
|
2612
|
+
);
|
|
2384
2613
|
if (
|
|
2385
2614
|
journal.session_id !== context.sessionId ||
|
|
2386
2615
|
journal.key_id !== current.sidecarKeyId ||
|
|
@@ -2388,7 +2617,14 @@ export async function recoverCoordinatorRuntimeStateRescope(context: RuntimeStat
|
|
|
2388
2617
|
!sameResolvedPath(journal.old_state_file, configuredStateFiles.oldStateFile, current.platform) ||
|
|
2389
2618
|
!sameResolvedPath(journal.new_state_file, configuredStateFiles.newStateFile, current.platform)
|
|
2390
2619
|
)
|
|
2391
|
-
throw new PreviousRuntimeStateReadError(
|
|
2620
|
+
throw new PreviousRuntimeStateReadError(
|
|
2621
|
+
undefined,
|
|
2622
|
+
`the rescope journal does not match this session's marker configuration: ` +
|
|
2623
|
+
`journal session ${describePreviousPayloadValue(journal.session_id)} mode ${journal.state_file_mode} ` +
|
|
2624
|
+
`(${sanitizeDiagnosticText(journal.old_state_file)} -> ${sanitizeDiagnosticText(journal.new_state_file)}), ` +
|
|
2625
|
+
`this session ${JSON.stringify(context.sessionId)} mode ${configuredStateFiles.mode} ` +
|
|
2626
|
+
`(${sanitizeDiagnosticText(configuredStateFiles.oldStateFile)} -> ${sanitizeDiagnosticText(configuredStateFiles.newStateFile)})`,
|
|
2627
|
+
);
|
|
2392
2628
|
if (
|
|
2393
2629
|
sameResolvedPath(journal.previous_cwd, current.cwd, current.platform) &&
|
|
2394
2630
|
sameOptionalResolvedPath(journal.previous_session_file, current.sessionFile, current.platform)
|
|
@@ -2474,14 +2710,26 @@ export async function relocateCoordinatorRuntimeStateForRescope(
|
|
|
2474
2710
|
if (!newStateFile) return true;
|
|
2475
2711
|
const identity = normalizedIdentity(context);
|
|
2476
2712
|
const oldIdentity = previousRescopeIdentity(context, previousCwd);
|
|
2477
|
-
const explicitStateFile = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
2478
2713
|
// A pinned state file never moves with the cwd; a cwd-derived one lives under the
|
|
2479
|
-
// per-session runtime directory rooted at the (now former) cwd.
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2714
|
+
// per-session runtime directory rooted at the (now former) cwd. Ownership — not the
|
|
2715
|
+
// ambient pin — decides which of the two this session has, and the same total
|
|
2716
|
+
// classification `prepare`/`recover` apply is enforced here (#5473).
|
|
2717
|
+
let oldStateFile: string;
|
|
2718
|
+
if (usesCoordinatorPin(context)) {
|
|
2719
|
+
oldStateFile = newStateFile;
|
|
2720
|
+
} else {
|
|
2721
|
+
oldStateFile = canonicalStateFilePath(sessionRuntimeStatePath(canonicalPreviousCwd, identity.sessionId));
|
|
2722
|
+
if (context.stateFile !== undefined && context.stateFile !== null) {
|
|
2723
|
+
if (
|
|
2724
|
+
!sameResolvedPath(context.stateFile, oldStateFile, identity.platform) &&
|
|
2725
|
+
!sameResolvedPath(context.stateFile, newStateFile, identity.platform)
|
|
2726
|
+
)
|
|
2727
|
+
throw new PreviousRuntimeStateReadError(
|
|
2728
|
+
undefined,
|
|
2729
|
+
`the rescope marker ${quotedDiagnosticText(context.stateFile)} is neither the process pin nor this session's derived marker; refusing to guess its ownership`,
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2485
2733
|
const samePath = sameResolvedPath(oldStateFile, newStateFile, identity.platform);
|
|
2486
2734
|
let expectedSourceSha256: string | null | undefined;
|
|
2487
2735
|
const journalObservation = readRegularFileForRelocation(
|
|
@@ -4852,6 +4852,15 @@ function commandName(args: readonly string[]): string {
|
|
|
4852
4852
|
return "status";
|
|
4853
4853
|
}
|
|
4854
4854
|
|
|
4855
|
+
/**
|
|
4856
|
+
* A help request is `--help`/`-h` anywhere in the argv, or a leading `help`
|
|
4857
|
+
* verb. Exported so the CLI command class routes help back through the
|
|
4858
|
+
* verb-aware renderer instead of a generic command-level fallback.
|
|
4859
|
+
*/
|
|
4860
|
+
export function isUltragoalHelpInvocation(args: readonly string[]): boolean {
|
|
4861
|
+
return args.some(isHelpArg) || args[0] === "help";
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4855
4864
|
function renderUltragoalHelp(args: readonly string[]): string | null {
|
|
4856
4865
|
if (!args.some(isHelpArg) && args[0] !== "help") return null;
|
|
4857
4866
|
const subject =
|
|
@@ -5745,7 +5754,7 @@ export async function runNativeUltragoalCommand(
|
|
|
5745
5754
|
): Promise<UltragoalCommandResult> {
|
|
5746
5755
|
const command = commandName(args);
|
|
5747
5756
|
const result = await dispatchUltragoalCommand(args, cwd, options.agentDir);
|
|
5748
|
-
const isHelp = args
|
|
5757
|
+
const isHelp = isUltragoalHelpInvocation(args);
|
|
5749
5758
|
if (!isHelp && result.status === 0 && RECONCILE_COMMANDS.has(command)) {
|
|
5750
5759
|
await reconcileUltragoalState(cwd, options.agentDir);
|
|
5751
5760
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
10
10
|
import * as fs from "node:fs/promises";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { isFileLockAcquireTimeout, withFileLock } from "../config/file-lock";
|
|
13
13
|
|
|
14
14
|
import { controlSocketPath, sessionPaths } from "./storage";
|
|
15
15
|
|
|
@@ -76,7 +76,7 @@ async function withLeaseMutationLock<T>(root: string, sessionId: string, fn: ()
|
|
|
76
76
|
retryDelayMs: 20,
|
|
77
77
|
});
|
|
78
78
|
} catch (error) {
|
|
79
|
-
if (error
|
|
79
|
+
if (isFileLockAcquireTimeout(error)) {
|
|
80
80
|
throw new LeaseError(`lease_lock_timeout:${sessionId}`, "lease_lock_timeout");
|
|
81
81
|
}
|
|
82
82
|
throw error;
|