@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
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
sanitizeSdkStartupMessage,
|
|
70
70
|
} from "../startup-capability";
|
|
71
71
|
import type { Broker, BrokerCleanupEvidence, BrokerCleanupIdentity, BrokerResponse } from "./broker";
|
|
72
|
-
import { matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
|
|
72
|
+
import { endpointIncarnation, matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
|
|
73
73
|
import { decodeLifecycleUtf8, parseLifecycleJson } from "./lifecycle-codec";
|
|
74
74
|
import type {
|
|
75
75
|
LifecycleCleanupProof,
|
|
@@ -79,6 +79,7 @@ import type {
|
|
|
79
79
|
LifecycleWorktreeIntent,
|
|
80
80
|
} from "./lifecycle-ledger";
|
|
81
81
|
import {
|
|
82
|
+
observeProcessIncarnation,
|
|
82
83
|
type ProcessIncarnationCommandRunner,
|
|
83
84
|
type ProcessIncarnationOptions,
|
|
84
85
|
parseDarwinProcessIncarnation,
|
|
@@ -2404,6 +2405,7 @@ async function executeUncertainRetirement(
|
|
|
2404
2405
|
...(record.processIncarnation === undefined ? {} : { processIncarnation: record.processIncarnation }),
|
|
2405
2406
|
...(record.hostIncarnation === undefined ? {} : { hostIncarnation: record.hostIncarnation }),
|
|
2406
2407
|
...(record.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: record.endpointMtimeMs }),
|
|
2408
|
+
...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
|
|
2407
2409
|
...(record.lifecycleRequestId === undefined ? {} : { lifecycleRequestId: record.lifecycleRequestId }),
|
|
2408
2410
|
});
|
|
2409
2411
|
const verifiedIndexSeq = broker.index.findSessionClosedEvidence(record);
|
|
@@ -2537,6 +2539,13 @@ function validateLifecycleMetadataReplay(cleanup: CleanupEvidence): BrokerRespon
|
|
|
2537
2539
|
return fail("terminal_uncertain", "Lifecycle metadata candidate could not be safely inspected.");
|
|
2538
2540
|
}
|
|
2539
2541
|
if (!current) continue;
|
|
2542
|
+
if (
|
|
2543
|
+
file.completed &&
|
|
2544
|
+
path.resolve(candidate) === path.resolve(file.plannedPath) &&
|
|
2545
|
+
current.identity.size === 0n &&
|
|
2546
|
+
current.identity.nlink === 1n
|
|
2547
|
+
)
|
|
2548
|
+
continue;
|
|
2540
2549
|
if (!sameLifecycleCleanupIdentity(current.identity, file.identity))
|
|
2541
2550
|
return fail("terminal_uncertain", "Lifecycle metadata candidate lacks exact replay authority.");
|
|
2542
2551
|
// A completed file's recorded retained quarantine is receipt-bound durable
|
|
@@ -2876,14 +2885,11 @@ async function reconcileLifecycleCleanup(
|
|
|
2876
2885
|
// A completed file's recorded retained quarantine is durable evidence,
|
|
2877
2886
|
// not a survivor — accept it only at its receipt-bound path and identity.
|
|
2878
2887
|
if (
|
|
2879
|
-
file.detachedPath &&
|
|
2880
|
-
path.resolve(candidate) === path.resolve(file.
|
|
2881
|
-
|
|
2882
|
-
!stat.isSymbolicLink() &&
|
|
2883
|
-
|
|
2884
|
-
stat.size === 0
|
|
2885
|
-
)
|
|
2886
|
-
continue;
|
|
2888
|
+
(file.detachedPath && path.resolve(candidate) === path.resolve(file.detachedPath)) ||
|
|
2889
|
+
path.resolve(candidate) === path.resolve(file.plannedPath)
|
|
2890
|
+
) {
|
|
2891
|
+
if (stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1 && stat.size === 0) continue;
|
|
2892
|
+
}
|
|
2887
2893
|
return fail(
|
|
2888
2894
|
"terminal_uncertain",
|
|
2889
2895
|
"Lifecycle cleanup receipt marks a target complete while an authorized candidate remains.",
|
|
@@ -3411,6 +3417,22 @@ async function removeOwnedLifecycleArtifacts(
|
|
|
3411
3417
|
{ dev: BigInt(endpointParent.dev), ino: BigInt(endpointParent.ino) },
|
|
3412
3418
|
);
|
|
3413
3419
|
if (!lifecycleProofWithinDeadline(proofBudget)) return false;
|
|
3420
|
+
if (endpointRemoval.ok) {
|
|
3421
|
+
// POSIX exact-unlink scrubs the detached exchange payload and may retain
|
|
3422
|
+
// its zero-byte quarantine name as durable internal evidence even when
|
|
3423
|
+
// the canonical removal itself succeeded. Carry that known placeholder
|
|
3424
|
+
// into the enclosing close reconciliation so it is not mistaken for a
|
|
3425
|
+
// live endpoint payload.
|
|
3426
|
+
for (const candidate of [plannedEndpointPath, retryEndpointPath, finalEndpointPath]) {
|
|
3427
|
+
try {
|
|
3428
|
+
const metadata = fsSync.lstatSync(candidate);
|
|
3429
|
+
if (metadata.isFile() && metadata.nlink === 1 && metadata.size === 0)
|
|
3430
|
+
onDurablePlaceholder?.(path.resolve(candidate));
|
|
3431
|
+
} catch {
|
|
3432
|
+
// A missing quarantine alias carries no retained authority.
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3414
3436
|
if (!endpointRemoval.ok) {
|
|
3415
3437
|
if (endpointRemoval.retainedUnknownPath) {
|
|
3416
3438
|
onRetainedUnknown?.();
|
|
@@ -3499,6 +3521,14 @@ async function recordTerminalUncertain(
|
|
|
3499
3521
|
...(registered.processIncarnation === undefined ? {} : { processIncarnation: registered.processIncarnation }),
|
|
3500
3522
|
...(registered.hostIncarnation === undefined ? {} : { hostIncarnation: registered.hostIncarnation }),
|
|
3501
3523
|
...(registered.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: registered.endpointMtimeMs }),
|
|
3524
|
+
...(registered.endpointFileId !== undefined &&
|
|
3525
|
+
registered.pid === pid &&
|
|
3526
|
+
path.resolve(registered.locator.stateRoot) === path.resolve(root) &&
|
|
3527
|
+
expected !== undefined &&
|
|
3528
|
+
registered.lifecycleRequestId === expected.effectMarker &&
|
|
3529
|
+
(registered.hostIncarnation ?? registered.processIncarnation) === expected.incarnation
|
|
3530
|
+
? { endpointFileId: registered.endpointFileId }
|
|
3531
|
+
: {}),
|
|
3502
3532
|
...(registered.lifecycleRequestId === undefined
|
|
3503
3533
|
? expected?.effectMarker === undefined
|
|
3504
3534
|
? {}
|
|
@@ -3517,6 +3547,70 @@ async function recordTerminalUncertain(
|
|
|
3517
3547
|
});
|
|
3518
3548
|
}
|
|
3519
3549
|
|
|
3550
|
+
/**
|
|
3551
|
+
* Release the worktree held by a forced stop of a stale-endpoint session, bound to
|
|
3552
|
+
* the exact stale identity the caller already validated against the requested close
|
|
3553
|
+
* authority. A successor incarnation can re-register under this session id in the
|
|
3554
|
+
* window between that authority check and this claim, so refresh once and append a
|
|
3555
|
+
* terminal-uncertain marker ONLY when the current row is still that captured
|
|
3556
|
+
* identity and its owning process is not observably alive. A rotated successor no
|
|
3557
|
+
* longer matches, so it is left untouched and nothing is released — the strict
|
|
3558
|
+
* "uncertain counts as occupied" rule keeps protecting it.
|
|
3559
|
+
*
|
|
3560
|
+
* Unlike `recordTerminalUncertain`, the compare and the append share one refresh
|
|
3561
|
+
* boundary and the marker is keyed to the captured generation/pid/incarnation
|
|
3562
|
+
* rather than to whatever row a second refresh happens to read. A successor that
|
|
3563
|
+
* rotates in while the append is in flight therefore cannot be marked terminal:
|
|
3564
|
+
* the claim targets the old generation, whose row never outranks the successor's
|
|
3565
|
+
* higher generation in the projection (#5581).
|
|
3566
|
+
*/
|
|
3567
|
+
async function releaseForcedStaleWorktree(broker: Broker, id: string, expected: IndexedSession): Promise<void> {
|
|
3568
|
+
await broker.index.refresh();
|
|
3569
|
+
const current = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
3570
|
+
if (
|
|
3571
|
+
!current ||
|
|
3572
|
+
current.endpointGeneration !== expected.endpointGeneration ||
|
|
3573
|
+
current.pid !== expected.pid ||
|
|
3574
|
+
(current.hostIncarnation ?? current.processIncarnation) !==
|
|
3575
|
+
(expected.hostIncarnation ?? expected.processIncarnation)
|
|
3576
|
+
)
|
|
3577
|
+
return;
|
|
3578
|
+
// For a force-stopped stale-endpoint session, release the worktree unless the
|
|
3579
|
+
// owning process is provably alive. An `alive` process still owns its checkout,
|
|
3580
|
+
// so it stays occupied even under force. Both `exited` (proven gone) and
|
|
3581
|
+
// `uncertain` (the pid-reuse case, which can never be proven exited) are treated
|
|
3582
|
+
// as terminal here: otherwise the row stays occupied forever and follow-up
|
|
3583
|
+
// delegate launches into the same checkout are refused with worktree_in_use
|
|
3584
|
+
// indefinitely. The identity guards above (generation/PID/incarnation) already
|
|
3585
|
+
// ensure a live successor that rotated in under the same id is never released.
|
|
3586
|
+
const observation = observeProcess(current.pid, current.hostIncarnation ?? current.processIncarnation, value =>
|
|
3587
|
+
processIncarnationForBroker(broker, value),
|
|
3588
|
+
);
|
|
3589
|
+
if (observation === "alive") return;
|
|
3590
|
+
if (observation === "uncertain")
|
|
3591
|
+
logger.warn("sdk broker recording forced stale-worktree release under uncertain process state", {
|
|
3592
|
+
sessionId: id,
|
|
3593
|
+
pid: current.pid,
|
|
3594
|
+
endpointGeneration: current.endpointGeneration,
|
|
3595
|
+
});
|
|
3596
|
+
await broker.index.append({
|
|
3597
|
+
type: "lifecycle_terminal",
|
|
3598
|
+
sessionId: id,
|
|
3599
|
+
locator: current.locator,
|
|
3600
|
+
endpointGeneration: current.endpointGeneration,
|
|
3601
|
+
pid: current.pid,
|
|
3602
|
+
...(current.processIncarnation === undefined ? {} : { processIncarnation: current.processIncarnation }),
|
|
3603
|
+
...(current.hostIncarnation === undefined ? {} : { hostIncarnation: current.hostIncarnation }),
|
|
3604
|
+
...(current.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: current.endpointMtimeMs }),
|
|
3605
|
+
...(current.lifecycleRequestId === undefined ? {} : { lifecycleRequestId: current.lifecycleRequestId }),
|
|
3606
|
+
terminalUncertain: true,
|
|
3607
|
+
// Marks this claim as the forced release, which is the only terminal-uncertain
|
|
3608
|
+
// claim that frees the worktree. The fail-closed teardown tail writes
|
|
3609
|
+
// `terminalUncertain` without it and keeps its checkout (see `worktreeOccupant`).
|
|
3610
|
+
forcedStaleRelease: true,
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3520
3614
|
async function waitUntil(timing: LifecycleTiming, deadline: number): Promise<void> {
|
|
3521
3615
|
while (timing.now() < deadline) await timing.sleep(Math.max(0, Math.min(POLL_MS, deadline - timing.now())));
|
|
3522
3616
|
}
|
|
@@ -3734,13 +3828,20 @@ async function signalVerifiedSession(
|
|
|
3734
3828
|
signal: NodeJS.Signals,
|
|
3735
3829
|
expected?: EffectMarker,
|
|
3736
3830
|
): Promise<boolean> {
|
|
3737
|
-
|
|
3831
|
+
// An exited process may remain a zombie until its parent reaps it. Its
|
|
3832
|
+
// incarnation is intentionally unavailable at that point, but native
|
|
3833
|
+
// observation positively classifies the pid as absent. Treat that as a
|
|
3834
|
+
// successful no-op so close can continue with endpoint/index cleanup rather
|
|
3835
|
+
// than escalating to an unverifiable signal and reporting terminal uncertainty.
|
|
3836
|
+
if (!(await hasDurableProcessIdentity(record, id, expected)))
|
|
3837
|
+
return observeProcessIncarnation(record.pid).status === "absent";
|
|
3738
3838
|
try {
|
|
3739
|
-
if (!(await hasDurableProcessIdentity(record, id, expected)))
|
|
3839
|
+
if (!(await hasDurableProcessIdentity(record, id, expected)))
|
|
3840
|
+
return observeProcessIncarnation(record.pid).status === "absent";
|
|
3740
3841
|
process.kill(record.pid, signal);
|
|
3741
3842
|
return true;
|
|
3742
3843
|
} catch {
|
|
3743
|
-
return
|
|
3844
|
+
return observeProcessIncarnation(record.pid).status === "absent";
|
|
3744
3845
|
}
|
|
3745
3846
|
}
|
|
3746
3847
|
|
|
@@ -3812,7 +3913,17 @@ async function removeExactDeadSessionEndpoint(
|
|
|
3812
3913
|
? authorizedDetachedSource
|
|
3813
3914
|
: candidates.find(candidate => {
|
|
3814
3915
|
try {
|
|
3815
|
-
|
|
3916
|
+
const metadata = fsSync.lstatSync(candidate);
|
|
3917
|
+
// A native-proven scrubbed placeholder is no longer endpoint JSON.
|
|
3918
|
+
// Unknown empty files and nonempty replacements still require validation.
|
|
3919
|
+
if (
|
|
3920
|
+
durablePlaceholders?.has(path.resolve(candidate)) &&
|
|
3921
|
+
metadata.isFile() &&
|
|
3922
|
+
metadata.nlink === 1 &&
|
|
3923
|
+
metadata.size === 0
|
|
3924
|
+
)
|
|
3925
|
+
return false;
|
|
3926
|
+
return metadata.isFile();
|
|
3816
3927
|
} catch {
|
|
3817
3928
|
return false;
|
|
3818
3929
|
}
|
|
@@ -3902,6 +4013,12 @@ async function hasOwnedEndpointPayload(
|
|
|
3902
4013
|
): Promise<boolean> {
|
|
3903
4014
|
const directory = path.join(root, "sdk");
|
|
3904
4015
|
const endpointName = `${id}.json`;
|
|
4016
|
+
const knownScrubbedPlaceholders = new Set([
|
|
4017
|
+
`.gjc-delete-endpoint-${effectMarker}-${endpointName}`,
|
|
4018
|
+
`.gjc-delete-endpoint-retry-${effectMarker}-${endpointName}`,
|
|
4019
|
+
`.gjc-delete-endpoint-final-${effectMarker}-${endpointName}`,
|
|
4020
|
+
`.gjc-delete-endpoint-detached-${effectMarker}-${endpointName}`,
|
|
4021
|
+
]);
|
|
3905
4022
|
let names: string[];
|
|
3906
4023
|
try {
|
|
3907
4024
|
names = await fs.readdir(directory);
|
|
@@ -3917,7 +4034,9 @@ async function hasOwnedEndpointPayload(
|
|
|
3917
4034
|
if (
|
|
3918
4035
|
!metadata.isFile() ||
|
|
3919
4036
|
metadata.size > 0 ||
|
|
3920
|
-
(metadata.size === 0 &&
|
|
4037
|
+
(metadata.size === 0 &&
|
|
4038
|
+
!durablePlaceholders.has(path.resolve(candidate)) &&
|
|
4039
|
+
!knownScrubbedPlaceholders.has(name))
|
|
3921
4040
|
)
|
|
3922
4041
|
return true;
|
|
3923
4042
|
} catch (error) {
|
|
@@ -4210,6 +4329,15 @@ export function worktreeOccupantForTest(
|
|
|
4210
4329
|
return worktreeOccupant(sessions, worktreePath, observe);
|
|
4211
4330
|
}
|
|
4212
4331
|
|
|
4332
|
+
/** Test seam for the forced stale-worktree release boundary. */
|
|
4333
|
+
export async function releaseForcedStaleWorktreeForTest(
|
|
4334
|
+
broker: Broker,
|
|
4335
|
+
id: string,
|
|
4336
|
+
expected: IndexedSession,
|
|
4337
|
+
): Promise<void> {
|
|
4338
|
+
await releaseForcedStaleWorktree(broker, id, expected);
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4213
4341
|
async function preparePlannedWorktree(
|
|
4214
4342
|
plan: GjcLaunchWorktreePlan,
|
|
4215
4343
|
opts: { signal: AbortSignal; deadlineAt: number; now: () => number },
|
|
@@ -4832,32 +4960,6 @@ type CloseRecord = {
|
|
|
4832
4960
|
processIncarnation?: string;
|
|
4833
4961
|
};
|
|
4834
4962
|
|
|
4835
|
-
function endpointIncarnation(
|
|
4836
|
-
record: Pick<CloseRecord, "endpointGeneration" | "endpointMtimeMs" | "pid">,
|
|
4837
|
-
sessionId: string,
|
|
4838
|
-
): string | undefined {
|
|
4839
|
-
if (
|
|
4840
|
-
!Number.isSafeInteger(record.endpointGeneration) ||
|
|
4841
|
-
record.endpointGeneration <= 0 ||
|
|
4842
|
-
!Number.isSafeInteger(record.pid) ||
|
|
4843
|
-
record.pid <= 0 ||
|
|
4844
|
-
typeof record.endpointMtimeMs !== "number" ||
|
|
4845
|
-
!Number.isFinite(record.endpointMtimeMs) ||
|
|
4846
|
-
record.endpointMtimeMs <= 0
|
|
4847
|
-
)
|
|
4848
|
-
return undefined;
|
|
4849
|
-
return createHash("sha256")
|
|
4850
|
-
.update(
|
|
4851
|
-
JSON.stringify({
|
|
4852
|
-
endpointGeneration: record.endpointGeneration,
|
|
4853
|
-
endpointMtimeMs: record.endpointMtimeMs,
|
|
4854
|
-
pid: record.pid,
|
|
4855
|
-
sessionId,
|
|
4856
|
-
}),
|
|
4857
|
-
)
|
|
4858
|
-
.digest("hex");
|
|
4859
|
-
}
|
|
4860
|
-
|
|
4861
4963
|
function requestedCloseAuthority(input: Input): { authority: CloseAuthority | undefined } | { error: BrokerResponse } {
|
|
4862
4964
|
const endpointGeneration = input.endpointGeneration;
|
|
4863
4965
|
const endpointIncarnation = input.endpointIncarnation;
|
|
@@ -5028,13 +5130,15 @@ async function executeLifecycleResponse(
|
|
|
5028
5130
|
return fail("endpoint_stale", "Live session changed while its resume authority was being verified.");
|
|
5029
5131
|
if (current.endpointMtimeMs === undefined)
|
|
5030
5132
|
return fail("endpoint_stale", "Live session endpoint authority is incomplete.");
|
|
5133
|
+
const finalIncarnation = endpointIncarnation(current, requestedSessionId!);
|
|
5134
|
+
if (!finalIncarnation) return fail("endpoint_stale", "Live session endpoint incarnation is unavailable.");
|
|
5031
5135
|
return {
|
|
5032
5136
|
ok: true,
|
|
5033
5137
|
result: {
|
|
5034
5138
|
sessionId: requestedSessionId,
|
|
5035
5139
|
cwd: finalScope.cwd,
|
|
5036
5140
|
endpointGeneration: current.endpointGeneration,
|
|
5037
|
-
endpointIncarnation:
|
|
5141
|
+
endpointIncarnation: finalIncarnation,
|
|
5038
5142
|
pid: current.pid,
|
|
5039
5143
|
endpointMtimeMs: current.endpointMtimeMs,
|
|
5040
5144
|
endpoint: endpoint.result,
|
|
@@ -5515,6 +5619,7 @@ async function executeLifecycleResponse(
|
|
|
5515
5619
|
endpointGeneration: verified.endpointGeneration,
|
|
5516
5620
|
endpointMtimeMs: verified.endpointMtimeMs,
|
|
5517
5621
|
pid: spawnedAuthority.pid,
|
|
5622
|
+
...(verified.endpointFileId === undefined ? {} : { endpointFileId: verified.endpointFileId }),
|
|
5518
5623
|
},
|
|
5519
5624
|
launch.id,
|
|
5520
5625
|
);
|
|
@@ -5545,6 +5650,14 @@ async function executeLifecycleResponse(
|
|
|
5545
5650
|
let record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
5546
5651
|
if (operation === "session.close") {
|
|
5547
5652
|
if (!record) return fail("not_found", "session is not indexed");
|
|
5653
|
+
// A forced stop of a session whose endpoint is already stale (its coordinator
|
|
5654
|
+
// service went away mid-run) cannot reach the runtime to prove teardown. The
|
|
5655
|
+
// caller has explicitly requested force, so when the endpoint proves stale
|
|
5656
|
+
// below and the owning process is no longer observably alive, record a
|
|
5657
|
+
// terminal-uncertain claim before surfacing endpoint_stale — otherwise the
|
|
5658
|
+
// row keeps holding its worktree forever, since an OS probe of a reused pid
|
|
5659
|
+
// returns `uncertain` and never `exited` (#5581).
|
|
5660
|
+
const forceReleaseStaleWorktree = input.forceReleaseStaleWorktree === true;
|
|
5548
5661
|
if (record.terminalUncertain)
|
|
5549
5662
|
return fail("terminal_uncertain", "Session ownership is uncertain and cannot be closed safely.");
|
|
5550
5663
|
if (!isSessionAuthorityEligible(record))
|
|
@@ -5589,7 +5702,26 @@ async function executeLifecycleResponse(
|
|
|
5589
5702
|
}
|
|
5590
5703
|
}
|
|
5591
5704
|
if (!endpointResult.ok) {
|
|
5592
|
-
if (endpointResult.error.code === "endpoint_stale")
|
|
5705
|
+
if (endpointResult.error.code === "endpoint_stale") {
|
|
5706
|
+
// `record` matched the requested close authority above, so it is the exact
|
|
5707
|
+
// stale identity the forced stop targeted — never a live successor, which
|
|
5708
|
+
// would have failed that authority check. Release its worktree bound to
|
|
5709
|
+
// that identity so a rotated successor is left untouched (#5581).
|
|
5710
|
+
// Best-effort: the endpoint_stale outcome is already proven and must be
|
|
5711
|
+
// returned. A release failure (index/broker write error) must not mask it;
|
|
5712
|
+
// the stale-endpoint recovery paths retry the release later.
|
|
5713
|
+
if (forceReleaseStaleWorktree) {
|
|
5714
|
+
try {
|
|
5715
|
+
await releaseForcedStaleWorktree(broker, id, record);
|
|
5716
|
+
} catch (error) {
|
|
5717
|
+
logger.warn("sdk broker forced stale-worktree release failed; returning endpoint_stale", {
|
|
5718
|
+
sessionId: id,
|
|
5719
|
+
error: String(error),
|
|
5720
|
+
});
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
return endpointResult;
|
|
5724
|
+
}
|
|
5593
5725
|
if (endpointResult.error.code !== "resource_gone") return endpointResult;
|
|
5594
5726
|
usedSignalFallback = true;
|
|
5595
5727
|
} else {
|
|
@@ -121,6 +121,90 @@ export function isProcessIncarnation(value: unknown): value is string {
|
|
|
121
121
|
);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Read-only, non-mutating observation of whether a pid currently names a
|
|
126
|
+
* verifiable process incarnation.
|
|
127
|
+
*
|
|
128
|
+
* - `status: "present"` carries the exact kernel-reported identity evidence
|
|
129
|
+
* (the same value `processIncarnation` returns).
|
|
130
|
+
* - `status: "absent"` is positive OS-confirmed death proof (POSIX
|
|
131
|
+
* `kill(pid, 0)` returning `ESRCH`, or Windows `OpenProcess` failing with
|
|
132
|
+
* the exact code the kernel reports for an unresolved client id) — never a
|
|
133
|
+
* permission failure or an ambiguous disappearance.
|
|
134
|
+
* - `status: "unknown"` covers every inconclusive case (invalid pid, missing
|
|
135
|
+
* or unavailable native addon, permission denial, platform limitation).
|
|
136
|
+
* Callers MUST NOT treat this as death proof.
|
|
137
|
+
*/
|
|
138
|
+
export type ProcessIncarnationObservation =
|
|
139
|
+
| { status: "present"; incarnation: string }
|
|
140
|
+
| { status: "absent" }
|
|
141
|
+
| { status: "unknown"; reasonCode: string };
|
|
142
|
+
|
|
143
|
+
/** Raw shape returned by the native `Process.observe` binding. */
|
|
144
|
+
export type NativeProcessObserveResult = { status?: unknown; incarnation?: unknown; reasonCode?: unknown };
|
|
145
|
+
|
|
146
|
+
export interface ObserveProcessIncarnationOptions {
|
|
147
|
+
/**
|
|
148
|
+
* Injectable native observe function, used by tests to exercise error
|
|
149
|
+
* classification (malformed shapes, unavailable addon) without a global
|
|
150
|
+
* mock. Defaults to the real `Process.observe` binding.
|
|
151
|
+
*/
|
|
152
|
+
observe?: (pid: number) => NativeProcessObserveResult;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Read-only process-incarnation observation for `pid`, distinguishing a
|
|
157
|
+
* proved-live incarnation from positive OS-confirmed absence from every
|
|
158
|
+
* inconclusive outcome. Never signals, kills, reaps, waits on, or spawns any
|
|
159
|
+
* process — it only reads existing native process-table state via
|
|
160
|
+
* `Process.observe`.
|
|
161
|
+
*
|
|
162
|
+
* `pid` must be a positive safe integer; any other value (including `0`,
|
|
163
|
+
* negative, `NaN`, or non-integer) is reported as `unknown` rather than
|
|
164
|
+
* risking an ambiguous platform-specific interpretation (`0`/negative PIDs
|
|
165
|
+
* address process groups on POSIX, not a single process).
|
|
166
|
+
*/
|
|
167
|
+
export function observeProcessIncarnation(
|
|
168
|
+
pid: number,
|
|
169
|
+
options: ObserveProcessIncarnationOptions = {},
|
|
170
|
+
): ProcessIncarnationObservation {
|
|
171
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 0x7fff_ffff) {
|
|
172
|
+
return { status: "unknown", reasonCode: "invalid_pid" };
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const observe =
|
|
176
|
+
options.observe ?? (nativeProcessBindings().Process as { observe?: (pid: number) => unknown }).observe;
|
|
177
|
+
if (typeof observe !== "function") {
|
|
178
|
+
// Older addon build predating Process.observe: liveness cannot be
|
|
179
|
+
// determined through this path, and that is not death proof.
|
|
180
|
+
return { status: "unknown", reasonCode: "observe_unavailable" };
|
|
181
|
+
}
|
|
182
|
+
const result = observe(pid) as NativeProcessObserveResult;
|
|
183
|
+
if (result?.status === "present" && isProcessIncarnation(result.incarnation) && result.reasonCode === undefined) {
|
|
184
|
+
return { status: "present", incarnation: result.incarnation };
|
|
185
|
+
}
|
|
186
|
+
if (result?.status === "absent" && result.incarnation === undefined && result.reasonCode === undefined) {
|
|
187
|
+
return { status: "absent" };
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
result?.status === "unknown" &&
|
|
191
|
+
result.incarnation === undefined &&
|
|
192
|
+
typeof result.reasonCode === "string" &&
|
|
193
|
+
/^[a-z][a-z0-9_]{0,63}$/.test(result.reasonCode)
|
|
194
|
+
) {
|
|
195
|
+
return { status: "unknown", reasonCode: result.reasonCode };
|
|
196
|
+
}
|
|
197
|
+
// A native result shape that matches none of the above (e.g. "present"
|
|
198
|
+
// without a canonical incarnation) is a contract mismatch, not evidence of
|
|
199
|
+
// anything — report unknown rather than guessing.
|
|
200
|
+
return { status: "unknown", reasonCode: "malformed_observation" };
|
|
201
|
+
} catch {
|
|
202
|
+
// The native addon is missing/unloadable on this host. Not proof of
|
|
203
|
+
// death: the caller has no OS confirmation either way.
|
|
204
|
+
return { status: "unknown", reasonCode: "native_addon_unavailable" };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
124
208
|
/** A PID is reusable; bind it to the strongest OS-provided process start incarnation available. */
|
|
125
209
|
export function processIncarnation(pid: number, options: ProcessIncarnationOptions = {}): string | undefined {
|
|
126
210
|
if (!Number.isSafeInteger(pid) || pid <= 0) return undefined;
|
|
@@ -128,8 +212,8 @@ export function processIncarnation(pid: number, options: ProcessIncarnationOptio
|
|
|
128
212
|
if (platform === process.platform && options.runCommand === undefined) {
|
|
129
213
|
try {
|
|
130
214
|
const nativeProcess = nativeProcessBindings().Process.fromPid(pid) as { incarnation?: unknown } | null;
|
|
131
|
-
// null
|
|
132
|
-
//
|
|
215
|
+
// A null native reference cannot provide an incarnation: the process
|
|
216
|
+
// may be absent or inaccessible. Returning undefined here
|
|
133
217
|
// avoids repeatedly spawning powershell.exe (whose Get-Process uses the same
|
|
134
218
|
// OpenProcess path and therefore cannot recover a valid incarnation either)
|
|
135
219
|
// during the broker's ~5 s liveness polling, which on Windows 11 produces a
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as fsp from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
|
|
4
5
|
import { currentExecutablePath } from "@gajae-code/natives";
|
|
@@ -123,17 +124,92 @@ function expectedPackageName(packageDirectory: string): void {
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
type SdkInternalRuntimeEvidence = { kind: "bun-source" | "compiled"; markerPath: string };
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Classify this process's runtime from exact marker and embedded-file evidence.
|
|
131
|
+
* Both the spawn descriptor and the published runtime image derive from this one
|
|
132
|
+
* authority; neither may fall back to argv or PATH.
|
|
133
|
+
*/
|
|
134
|
+
function classifySdkInternalRuntime(options: SdkInternalRuntimeDescriptorTestOptions): SdkInternalRuntimeEvidence {
|
|
135
|
+
const markerPath = options.markerPath ?? internalSourceMarker;
|
|
136
|
+
const embeddedFiles = options.embeddedFiles ?? (typeof Bun === "undefined" ? undefined : Bun.embeddedFiles);
|
|
137
|
+
if (!embeddedFiles) throw new Error("SDK internal launch refused: Bun runtime evidence is unavailable.");
|
|
138
|
+
const markerName = path.basename(markerPath.replaceAll("\\", "/"));
|
|
139
|
+
const markerEntries = embeddedFiles.filter(file => embeddedFileName(file) === markerName);
|
|
140
|
+
const compiledMarkerPath = isCompiledMarkerPath(markerPath);
|
|
141
|
+
const exactCompiledArtifact = COMPILED_MARKER_NAME.test(markerName) && markerEntries.length === 1;
|
|
142
|
+
const isSourceMarker = path.isAbsolute(markerPath) && !compiledMarkerPath;
|
|
143
|
+
if (embeddedFiles.length === 0 && isSourceMarker) return { kind: "bun-source", markerPath };
|
|
144
|
+
if (exactCompiledArtifact && compiledMarkerPath) return { kind: "compiled", markerPath };
|
|
145
|
+
throw new Error("SDK internal launch refused: compiled-runtime marker evidence is inconsistent.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The image a source-runtime internal spawn executes: this process's own Bun.
|
|
150
|
+
* It is the spawn command file, so it must be executable, not merely readable.
|
|
151
|
+
*/
|
|
152
|
+
function sourceRuntimeImage(options: SdkInternalRuntimeDescriptorTestOptions): string {
|
|
153
|
+
if (options.bunAvailable === false || typeof Bun === "undefined")
|
|
154
|
+
throw new Error("SDK internal launch refused: Bun source runtime is unavailable.");
|
|
155
|
+
return regularExecutablePath(path.resolve(options.execPath ?? process.execPath), "runtime executable");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The runtime image alone, without the source-tree assets a full spawn also proves. */
|
|
159
|
+
function provenRuntimeImage(options: SdkInternalRuntimeDescriptorTestOptions): string {
|
|
160
|
+
const evidence = classifySdkInternalRuntime(options);
|
|
161
|
+
return evidence.kind === "bun-source" ? sourceRuntimeImage(options) : compiledExecutable(options);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The image the same classification names, without the on-disk proof a spawn
|
|
166
|
+
* requires. A proof-first publication is exactly backwards for the broker that
|
|
167
|
+
* matters here: a runtime deleted between this process's exec and its
|
|
168
|
+
* publication makes `regularExecutablePath` throw, so the one broker that can
|
|
169
|
+
* never spawn would publish no evidence and read as reusable forever.
|
|
170
|
+
*
|
|
171
|
+
* Publishing the classified path instead moves the verdict to the reader, which
|
|
172
|
+
* already retires only on proven absence. It is evidence, never a spawn source.
|
|
173
|
+
* A Bun virtual bundle path is not publishable: nothing on disk backs it, so a
|
|
174
|
+
* reader would probe `ENOENT` and retire a healthy compiled broker.
|
|
175
|
+
*/
|
|
176
|
+
function classifiedRuntimeImage(options: SdkInternalRuntimeDescriptorTestOptions): string | undefined {
|
|
177
|
+
const evidence = classifySdkInternalRuntime(options);
|
|
178
|
+
const execPath = options.execPath ?? process.execPath;
|
|
179
|
+
if (evidence.kind === "bun-source") {
|
|
180
|
+
// Without Bun there is no source runtime to name; `execPath` would be some
|
|
181
|
+
// other interpreter, which is worse evidence than none.
|
|
182
|
+
if (options.bunAvailable === false || typeof Bun === "undefined") return undefined;
|
|
183
|
+
return isBunVirtualExecutablePath(execPath) ? undefined : path.resolve(execPath);
|
|
184
|
+
}
|
|
185
|
+
if (!isBunVirtualExecutablePath(execPath)) return path.resolve(execPath);
|
|
186
|
+
const currentExecutable = currentExecutablePath();
|
|
187
|
+
if (!currentExecutable || isBunVirtualExecutablePath(currentExecutable)) return undefined;
|
|
188
|
+
return path.resolve(currentExecutable);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Publication evidence: the proven image, else the classified path, else nothing. */
|
|
192
|
+
function publishedRuntimeImage(options: SdkInternalRuntimeDescriptorTestOptions): string | undefined {
|
|
193
|
+
try {
|
|
194
|
+
return provenRuntimeImage(options);
|
|
195
|
+
} catch {
|
|
196
|
+
try {
|
|
197
|
+
return classifiedRuntimeImage(options);
|
|
198
|
+
} catch {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
126
204
|
function sourceDescriptor(
|
|
127
205
|
action: SdkInternalAction,
|
|
128
206
|
options: SdkInternalRuntimeDescriptorTestOptions,
|
|
129
207
|
markerPath: string,
|
|
130
208
|
): SdkInternalSpawnCommand {
|
|
131
|
-
|
|
132
|
-
throw new Error("SDK internal launch refused: Bun source runtime is unavailable.");
|
|
209
|
+
const runtime = sourceRuntimeImage(options);
|
|
133
210
|
const brokerDirectory = path.resolve(options.brokerDirectory ?? import.meta.dir);
|
|
134
211
|
const packageDirectory = path.resolve(brokerDirectory, "../../..");
|
|
135
212
|
const sourceDirectory = path.resolve(brokerDirectory, "../..");
|
|
136
|
-
const runtime = regularReadablePath(path.resolve(options.execPath ?? process.execPath), "runtime executable");
|
|
137
213
|
const cli = regularReadablePath(
|
|
138
214
|
path.resolve(options.cliPath ?? path.join(sourceDirectory, "cli.ts")),
|
|
139
215
|
"CLI entrypoint",
|
|
@@ -168,25 +244,68 @@ function resolveSdkInternalSpawnCommandWithEvidence(
|
|
|
168
244
|
action: SdkInternalAction,
|
|
169
245
|
options: SdkInternalRuntimeDescriptorTestOptions,
|
|
170
246
|
): SdkInternalSpawnCommand {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
247
|
+
const evidence = classifySdkInternalRuntime(options);
|
|
248
|
+
if (evidence.kind === "bun-source") return sourceDescriptor(action, options, evidence.markerPath);
|
|
249
|
+
return {
|
|
250
|
+
kind: "compiled",
|
|
251
|
+
file: compiledExecutable(options),
|
|
252
|
+
args: ["sdk", action],
|
|
253
|
+
env: internalEnvironment(options.environment ?? process.env, false),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The on-disk image internal SDK spawns from this process execute, or
|
|
259
|
+
* `undefined` only when runtime evidence cannot be classified at all. It comes
|
|
260
|
+
* from the same marker authority a spawn uses, but deliberately not from the
|
|
261
|
+
* source-tree assets a spawn also proves (CLI entrypoint, isolated Bun config,
|
|
262
|
+
* package identity): a failure to read one of those says nothing about the
|
|
263
|
+
* runtime image, and publishing `undefined` for it would blind every reader.
|
|
264
|
+
*
|
|
265
|
+
* An image the classification names but this process can no longer prove is
|
|
266
|
+
* still published. `undefined` means only "no evidence" -- readers keep such a
|
|
267
|
+
* broker reusable, so a broker whose own image vanished before it published
|
|
268
|
+
* must not hide behind it.
|
|
269
|
+
*/
|
|
270
|
+
export function sdkInternalRuntimeImage(): string | undefined {
|
|
271
|
+
return publishedRuntimeImage({});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** A stat that has not answered by here is inconclusive, never proof of absence. */
|
|
275
|
+
const RUNTIME_IMAGE_PROBE_TIMEOUT_MS = 1_000;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Only these prove the image is gone. Every other failure (`EACCES`, `EPERM`,
|
|
279
|
+
* `ELOOP`, `EIO`, a sandbox that hides the path from this caller, anything
|
|
280
|
+
* unrecognized) describes the caller's view, not the image: the broker may still
|
|
281
|
+
* execute a file this process cannot even inspect.
|
|
282
|
+
*/
|
|
283
|
+
function isProvenRuntimeImageAbsence(error: unknown): boolean {
|
|
284
|
+
const code = (error as NodeJS.ErrnoException | null)?.code;
|
|
285
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Whether a published runtime image is still a regular file on disk. A resident broker keeps
|
|
290
|
+
* answering requests after the executable it was started from is deleted (a
|
|
291
|
+
* Homebrew Bun upgrade removing the Cellar path, an install directory swapped
|
|
292
|
+
* underneath it), yet every internal spawn it then attempts is refused. Callers
|
|
293
|
+
* use this to retire such an incumbent before a session launch reaches it, so it
|
|
294
|
+
* answers `true` on every inconclusive outcome, including a probe that outlives
|
|
295
|
+
* its bound: a false retirement kills a healthy broker.
|
|
296
|
+
*/
|
|
297
|
+
export async function isSdkInternalRuntimeImagePresent(file: string): Promise<boolean> {
|
|
298
|
+
const probe = fsp.stat(path.resolve(file)).then(
|
|
299
|
+
stats => stats.isFile(),
|
|
300
|
+
(error: unknown) => !isProvenRuntimeImageAbsence(error),
|
|
301
|
+
);
|
|
302
|
+
const inconclusive = Promise.withResolvers<boolean>();
|
|
303
|
+
const timer: NodeJS.Timeout = setTimeout(() => inconclusive.resolve(true), RUNTIME_IMAGE_PROBE_TIMEOUT_MS);
|
|
304
|
+
try {
|
|
305
|
+
return await Promise.race([probe, inconclusive.promise]);
|
|
306
|
+
} finally {
|
|
307
|
+
clearTimeout(timer);
|
|
188
308
|
}
|
|
189
|
-
throw new Error("SDK internal launch refused: compiled-runtime marker evidence is inconsistent.");
|
|
190
309
|
}
|
|
191
310
|
|
|
192
311
|
/** Resolve the production descriptor from the statically imported marker and current Bun runtime evidence. */
|
|
@@ -201,3 +320,8 @@ export function resolveSdkInternalSpawnCommandForTest(
|
|
|
201
320
|
): SdkInternalSpawnCommand {
|
|
202
321
|
return resolveSdkInternalSpawnCommandWithEvidence(action, options);
|
|
203
322
|
}
|
|
323
|
+
|
|
324
|
+
/** Test hook: the publication decision under injected evidence, same authority as production. */
|
|
325
|
+
export function sdkInternalRuntimeImageForTest(options: SdkInternalRuntimeDescriptorTestOptions): string | undefined {
|
|
326
|
+
return publishedRuntimeImage(options);
|
|
327
|
+
}
|