@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
|
@@ -2,17 +2,13 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import * as fsSync from "node:fs";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (!nativeBrokerDiscoveryBindings)
|
|
13
|
-
nativeBrokerDiscoveryBindings = require("@gajae-code/natives") as NativeBrokerDiscoveryBindings;
|
|
14
|
-
return nativeBrokerDiscoveryBindings;
|
|
15
|
-
}
|
|
5
|
+
import type {
|
|
6
|
+
NativeBrokerRestartIntent,
|
|
7
|
+
NativeBrokerRestartIntentIdentity,
|
|
8
|
+
NativeRetainedBrokerPublication,
|
|
9
|
+
} from "@gajae-code/natives";
|
|
10
|
+
import { retainBrokerPublication as retainBrokerPublicationFn } from "@gajae-code/natives";
|
|
11
|
+
import { BROKER_ARTIFACT_PATHS, canonicalServiceRootDigest } from "../service-artifact-paths";
|
|
16
12
|
|
|
17
13
|
import { processIncarnation } from "./process-incarnation";
|
|
18
14
|
import { assertSupportedStateVersion, SDK_STATE_VERSION } from "./state-version";
|
|
@@ -23,6 +19,14 @@ export interface RetainedBrokerDiscovery {
|
|
|
23
19
|
observeAsync(): Promise<BrokerPublicationObservation>;
|
|
24
20
|
heartbeat(heartbeatAt: number): Promise<boolean>;
|
|
25
21
|
close(): void;
|
|
22
|
+
/** Owner-side `prepared`: exclusively creates the durable restart-intent slot. */
|
|
23
|
+
prepareRestartIntent(intent: NativeBrokerRestartIntent): Promise<boolean>;
|
|
24
|
+
/** Owner-side `prepared` -> `committed`: rewrites the exact slot prepare created. */
|
|
25
|
+
commitRestartIntent(intent: NativeBrokerRestartIntent): Promise<boolean>;
|
|
26
|
+
/** Owner-side cancel of this process's own prepared/committed slot. */
|
|
27
|
+
cancelRestartIntent(): Promise<boolean>;
|
|
28
|
+
/** Successor-side clear of a predecessor's slot this process never prepared. */
|
|
29
|
+
clearForeignRestartIntent(identity: NativeBrokerRestartIntentIdentity): Promise<boolean>;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -216,12 +220,8 @@ function describeWithheldPublicationAuthority(agentDir: string): string | undefi
|
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
function requireRetainedBrokerPublication(agentDir: string): NativeRetainedBrokerPublication {
|
|
219
|
-
const retainBrokerPublication = nativeBrokerDiscovery().retainBrokerPublication;
|
|
220
|
-
if (typeof retainBrokerPublication !== "function") {
|
|
221
|
-
throw new Error("Loaded native bindings do not expose retained broker publication authority.");
|
|
222
|
-
}
|
|
223
223
|
try {
|
|
224
|
-
return
|
|
224
|
+
return retainBrokerPublicationFn(agentDir);
|
|
225
225
|
} catch (error) {
|
|
226
226
|
// Fail closed exactly as before; only name the condition on the way out.
|
|
227
227
|
const obstruction = nativeRetainedObstruction(error);
|
|
@@ -299,6 +299,26 @@ class NativeRetainedBrokerDiscovery implements RetainedBrokerDiscovery {
|
|
|
299
299
|
this.#closed = true;
|
|
300
300
|
this.#publication.close();
|
|
301
301
|
}
|
|
302
|
+
async prepareRestartIntent(intent: NativeBrokerRestartIntent): Promise<boolean> {
|
|
303
|
+
if (this.#closed || !isValidRestartToken(intent.requestId) || !isValidRestartToken(intent.lease)) return false;
|
|
304
|
+
return (await this.#publication.prepareRestartIntentAsync(intent)).kind === "written";
|
|
305
|
+
}
|
|
306
|
+
async commitRestartIntent(intent: NativeBrokerRestartIntent): Promise<boolean> {
|
|
307
|
+
if (this.#closed || !isValidRestartToken(intent.requestId) || !isValidRestartToken(intent.lease)) return false;
|
|
308
|
+
return (await this.#publication.commitRestartIntentAsync(intent)).kind === "written";
|
|
309
|
+
}
|
|
310
|
+
async cancelRestartIntent(): Promise<boolean> {
|
|
311
|
+
if (this.#closed) return false;
|
|
312
|
+
return (await this.#publication.cancelRestartIntentAsync()).kind === "cancelled";
|
|
313
|
+
}
|
|
314
|
+
async clearForeignRestartIntent(identity: NativeBrokerRestartIntentIdentity): Promise<boolean> {
|
|
315
|
+
if (this.#closed) return false;
|
|
316
|
+
return (await this.#publication.clearRestartIntentAsync(identity)).kind === "cleared";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isValidRestartToken(value: string): boolean {
|
|
321
|
+
return /^[A-Za-z0-9_-]{1,256}$/.test(value);
|
|
302
322
|
}
|
|
303
323
|
|
|
304
324
|
class LegacyWindowsBrokerDiscovery implements RetainedBrokerDiscovery {
|
|
@@ -322,6 +342,18 @@ class LegacyWindowsBrokerDiscovery implements RetainedBrokerDiscovery {
|
|
|
322
342
|
this.#discovery = next;
|
|
323
343
|
return true;
|
|
324
344
|
}
|
|
345
|
+
async prepareRestartIntent(_intent: NativeBrokerRestartIntent): Promise<boolean> {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
async commitRestartIntent(_intent: NativeBrokerRestartIntent): Promise<boolean> {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
async cancelRestartIntent(): Promise<boolean> {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
async clearForeignRestartIntent(_identity: NativeBrokerRestartIntentIdentity): Promise<boolean> {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
325
357
|
close(): void {
|
|
326
358
|
this.#closed = true;
|
|
327
359
|
}
|
|
@@ -336,6 +368,16 @@ export interface BrokerDiscovery {
|
|
|
336
368
|
version: typeof SDK_STATE_VERSION;
|
|
337
369
|
protocolVersion: 3;
|
|
338
370
|
packageGeneration: string;
|
|
371
|
+
/**
|
|
372
|
+
* The runtime image this broker launches internal SDK processes with. Present
|
|
373
|
+
* whenever the publisher could classify its own runtime, including when that
|
|
374
|
+
* image was already gone at publication time -- absence is the reader's
|
|
375
|
+
* verdict to reach, not a reason to publish nothing. Absent only for records
|
|
376
|
+
* written before this field existed and for a publisher whose runtime
|
|
377
|
+
* evidence could not be classified at all; readers then reuse the broker as
|
|
378
|
+
* before rather than retiring it on unknown evidence.
|
|
379
|
+
*/
|
|
380
|
+
runtime?: string;
|
|
339
381
|
ownerId: string;
|
|
340
382
|
pid: number;
|
|
341
383
|
incarnation: string;
|
|
@@ -345,10 +387,18 @@ export interface BrokerDiscovery {
|
|
|
345
387
|
token: string;
|
|
346
388
|
startedAt: number;
|
|
347
389
|
heartbeatAt: number;
|
|
390
|
+
restartRequestId?: string;
|
|
391
|
+
rootDigest?: string;
|
|
392
|
+
}
|
|
393
|
+
export interface BrokerRestartIntent {
|
|
394
|
+
requestId: string;
|
|
395
|
+
lease: string;
|
|
396
|
+
expiresAt: number;
|
|
397
|
+
phase?: "prepared" | "committed" | "cancelled";
|
|
348
398
|
}
|
|
349
399
|
export type RedactedBrokerDiscovery = Omit<BrokerDiscovery, "token"> & { token: "[redacted]" };
|
|
350
400
|
export type BrokerDiscoveryWrite = Omit<BrokerDiscovery, "incarnation"> & { incarnation?: string };
|
|
351
|
-
export const brokerDiscoveryPath = (agentDir: string) => path.join(agentDir,
|
|
401
|
+
export const brokerDiscoveryPath = (agentDir: string) => path.join(agentDir, BROKER_ARTIFACT_PATHS.discovery);
|
|
352
402
|
export const newBrokerToken = () => randomBytes(32).toString("hex");
|
|
353
403
|
export const brokerProcessIncarnation = processIncarnation;
|
|
354
404
|
export function isPidAlive(pid: number): boolean {
|
|
@@ -389,7 +439,6 @@ async function syncDirectory(directory: string): Promise<void> {
|
|
|
389
439
|
export async function writeBrokerDiscovery(agentDir: string, discovery: BrokerDiscoveryWrite): Promise<void> {
|
|
390
440
|
const incarnation = discovery.incarnation ?? brokerProcessIncarnation(discovery.pid);
|
|
391
441
|
if (!incarnation) throw new Error(`Broker process incarnation is unavailable for pid ${discovery.pid}.`);
|
|
392
|
-
const record: BrokerDiscovery = { ...discovery, incarnation };
|
|
393
442
|
if (!isFixedWidthHeartbeat(discovery.heartbeatAt)) {
|
|
394
443
|
throw new Error("Broker heartbeatAt must be a fixed-width 13-digit millisecond timestamp.");
|
|
395
444
|
}
|
|
@@ -397,6 +446,11 @@ export async function writeBrokerDiscovery(agentDir: string, discovery: BrokerDi
|
|
|
397
446
|
const dir = path.dirname(file);
|
|
398
447
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
399
448
|
await fs.chmod(dir, 0o700);
|
|
449
|
+
const record: BrokerDiscovery = {
|
|
450
|
+
...discovery,
|
|
451
|
+
incarnation,
|
|
452
|
+
rootDigest: await canonicalServiceRootDigest(agentDir),
|
|
453
|
+
};
|
|
400
454
|
const temp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
401
455
|
try {
|
|
402
456
|
await fs.writeFile(temp, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
@@ -417,8 +471,12 @@ export async function publishBrokerDiscovery(
|
|
|
417
471
|
): Promise<RetainedBrokerDiscovery> {
|
|
418
472
|
const incarnation = discovery.incarnation ?? brokerProcessIncarnation(discovery.pid);
|
|
419
473
|
if (!incarnation) throw new Error(`Broker process incarnation is unavailable for pid ${discovery.pid}.`);
|
|
420
|
-
|
|
421
|
-
|
|
474
|
+
await writeBrokerDiscovery(agentDir, { ...discovery, incarnation });
|
|
475
|
+
const published: BrokerDiscovery = {
|
|
476
|
+
...discovery,
|
|
477
|
+
incarnation,
|
|
478
|
+
rootDigest: await canonicalServiceRootDigest(agentDir),
|
|
479
|
+
};
|
|
422
480
|
if (platform === "win32") return new LegacyWindowsBrokerDiscovery(agentDir, published);
|
|
423
481
|
try {
|
|
424
482
|
return new NativeRetainedBrokerDiscovery(requireRetainedBrokerPublication(agentDir));
|
|
@@ -465,9 +523,11 @@ export async function readBrokerDiscovery(
|
|
|
465
523
|
d.pid <= 0 ||
|
|
466
524
|
typeof d.incarnation !== "string" ||
|
|
467
525
|
d.incarnation.length === 0 ||
|
|
526
|
+
(d.runtime !== undefined && (typeof d.runtime !== "string" || d.runtime.length === 0)) ||
|
|
468
527
|
!Number.isFinite(d.heartbeatAt)
|
|
469
528
|
)
|
|
470
529
|
return null;
|
|
530
|
+
if (d.restartRequestId !== undefined && !/^[A-Za-z0-9_-]{1,256}$/.test(d.restartRequestId)) return null;
|
|
471
531
|
if (!isPidAlive(d.pid)) return null;
|
|
472
532
|
const incarnation = brokerProcessIncarnation(d.pid);
|
|
473
533
|
if (!incarnation || incarnation !== d.incarnation || Date.now() - d.heartbeatAt > ttlMs) return null;
|
|
@@ -477,6 +537,99 @@ export async function readBrokerDiscovery(
|
|
|
477
537
|
throw e;
|
|
478
538
|
}
|
|
479
539
|
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The restart-intent slot lives directly beneath `sdk/`, independent of
|
|
543
|
+
* `sdk/broker.lock/` -- so it durably survives a stale-lock reclaim that
|
|
544
|
+
* renames the incumbent's lock directory aside (`Broker#reclaimStaleLock`).
|
|
545
|
+
*/
|
|
546
|
+
export const brokerRestartIntentPath = (agentDir: string) => path.join(agentDir, BROKER_ARTIFACT_PATHS.restartIntent);
|
|
547
|
+
|
|
548
|
+
export interface BrokerRestartIntentWithIdentity extends BrokerRestartIntent {
|
|
549
|
+
/** Existing platform file identity, for a successor's exact-identity clear. */
|
|
550
|
+
identity: NativeBrokerRestartIntentIdentity;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export async function readBrokerRestartIntent(agentDir: string): Promise<BrokerRestartIntentWithIdentity | null> {
|
|
554
|
+
const file = brokerRestartIntentPath(agentDir);
|
|
555
|
+
let handle: fs.FileHandle | undefined;
|
|
556
|
+
try {
|
|
557
|
+
handle = await fs.open(
|
|
558
|
+
file,
|
|
559
|
+
fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW | fsSync.constants.O_NONBLOCK,
|
|
560
|
+
);
|
|
561
|
+
const before = await handle.stat({ bigint: true });
|
|
562
|
+
const parent = await fs.lstat(path.dirname(file), { bigint: true });
|
|
563
|
+
if (
|
|
564
|
+
!before.isFile() ||
|
|
565
|
+
before.nlink !== 1n ||
|
|
566
|
+
before.size > 4096n ||
|
|
567
|
+
!parent.isDirectory() ||
|
|
568
|
+
parent.isSymbolicLink()
|
|
569
|
+
)
|
|
570
|
+
throw new Error("Malformed broker restart intent.");
|
|
571
|
+
const buffer = new Uint8Array(4097);
|
|
572
|
+
let length = 0;
|
|
573
|
+
while (length < buffer.length) {
|
|
574
|
+
const read = await handle.read(buffer, length, buffer.length - length, length);
|
|
575
|
+
if (read.bytesRead === 0) break;
|
|
576
|
+
length += read.bytesRead;
|
|
577
|
+
}
|
|
578
|
+
if (length > 4096) throw new Error("Malformed broker restart intent.");
|
|
579
|
+
const after = await handle.stat({ bigint: true });
|
|
580
|
+
const lexical = await fs.lstat(file, { bigint: true });
|
|
581
|
+
const parentAfter = await fs.lstat(path.dirname(file), { bigint: true });
|
|
582
|
+
if (
|
|
583
|
+
after.dev !== before.dev ||
|
|
584
|
+
after.ino !== before.ino ||
|
|
585
|
+
after.size !== before.size ||
|
|
586
|
+
after.mtimeNs !== before.mtimeNs ||
|
|
587
|
+
after.ctimeNs !== before.ctimeNs ||
|
|
588
|
+
after.nlink !== 1n ||
|
|
589
|
+
lexical.isSymbolicLink() ||
|
|
590
|
+
lexical.dev !== after.dev ||
|
|
591
|
+
lexical.ino !== after.ino ||
|
|
592
|
+
lexical.mtimeNs !== after.mtimeNs ||
|
|
593
|
+
lexical.ctimeNs !== after.ctimeNs ||
|
|
594
|
+
parentAfter.isSymbolicLink() ||
|
|
595
|
+
parent.dev !== parentAfter.dev ||
|
|
596
|
+
parent.ino !== parentAfter.ino
|
|
597
|
+
)
|
|
598
|
+
throw new Error("Broker restart intent changed during observation.");
|
|
599
|
+
let value: unknown;
|
|
600
|
+
try {
|
|
601
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, length)));
|
|
602
|
+
} catch {
|
|
603
|
+
throw new Error("Malformed broker restart intent.");
|
|
604
|
+
}
|
|
605
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
606
|
+
throw new Error("Malformed broker restart intent.");
|
|
607
|
+
const intent = value as Partial<BrokerRestartIntent>;
|
|
608
|
+
if (
|
|
609
|
+
typeof intent.requestId !== "string" ||
|
|
610
|
+
!/^[A-Za-z0-9_-]{1,256}$/.test(intent.requestId) ||
|
|
611
|
+
typeof intent.lease !== "string" ||
|
|
612
|
+
!/^[A-Za-z0-9_-]{1,256}$/.test(intent.lease) ||
|
|
613
|
+
typeof intent.expiresAt !== "number" ||
|
|
614
|
+
!Number.isSafeInteger(intent.expiresAt) ||
|
|
615
|
+
intent.expiresAt <= 0 ||
|
|
616
|
+
(intent.phase !== "prepared" && intent.phase !== "committed" && intent.phase !== "cancelled")
|
|
617
|
+
)
|
|
618
|
+
throw new Error("Malformed broker restart intent.");
|
|
619
|
+
return {
|
|
620
|
+
requestId: intent.requestId,
|
|
621
|
+
lease: intent.lease,
|
|
622
|
+
expiresAt: intent.expiresAt,
|
|
623
|
+
phase: intent.phase,
|
|
624
|
+
identity: { dev: after.dev, ino: after.ino },
|
|
625
|
+
};
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT" && !handle) return null;
|
|
628
|
+
throw error;
|
|
629
|
+
} finally {
|
|
630
|
+
await handle?.close();
|
|
631
|
+
}
|
|
632
|
+
}
|
|
480
633
|
export function redactBrokerDiscovery(discovery: BrokerDiscovery): RedactedBrokerDiscovery {
|
|
481
634
|
return { ...discovery, token: "[redacted]" };
|
|
482
635
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { SdkClient, SdkClientError } from "../client/client";
|
|
3
|
+
import type {
|
|
4
|
+
BrokerRestartCommitOptions,
|
|
5
|
+
BrokerRestartOwnerIdentity,
|
|
6
|
+
BrokerRestartPrepareOptions,
|
|
7
|
+
BrokerRestartPrepareResult,
|
|
8
|
+
BrokerRestartResult,
|
|
9
|
+
} from "./broker";
|
|
10
|
+
import { launchAuthorizedBrokerSuccessor, oldOwnerConfirmedExited } from "./daemon-entry";
|
|
11
|
+
import { type BrokerDiscovery, readBrokerDiscovery } from "./discovery";
|
|
12
|
+
|
|
13
|
+
export interface DoctorBrokerRestartOptions {
|
|
14
|
+
agentDir: string;
|
|
15
|
+
deadlineMs?: number;
|
|
16
|
+
drain?: boolean;
|
|
17
|
+
requestId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Concrete typed before/after-effect outcome of a full D8 broker restart
|
|
22
|
+
* attempt. Every refusal names the exact stage and, where the broker itself
|
|
23
|
+
* refused, the broker's own typed error code -- never an opaque thrown
|
|
24
|
+
* message standing in for a decision this adapter could name.
|
|
25
|
+
*/
|
|
26
|
+
export type DoctorBrokerRestartOutcome =
|
|
27
|
+
| {
|
|
28
|
+
kind: "restarted";
|
|
29
|
+
requestId: string;
|
|
30
|
+
oldOwner: BrokerRestartOwnerIdentity;
|
|
31
|
+
successor: BrokerDiscovery;
|
|
32
|
+
/** True when an already-committed successor for this exact request was found and reused, not spawned. */
|
|
33
|
+
adopted: boolean;
|
|
34
|
+
}
|
|
35
|
+
| { kind: "owner_unavailable"; reason: "no_discovery" | "owner_not_confirmed_live" }
|
|
36
|
+
| { kind: "prepare_refused"; code: string; message: string }
|
|
37
|
+
| { kind: "commit_refused"; code: string; message: string }
|
|
38
|
+
| { kind: "old_owner_exit_timeout"; requestId: string; oldOwner: BrokerRestartOwnerIdentity }
|
|
39
|
+
| {
|
|
40
|
+
kind: "successor_refused";
|
|
41
|
+
requestId: string;
|
|
42
|
+
oldOwner: BrokerRestartOwnerIdentity;
|
|
43
|
+
reason:
|
|
44
|
+
| "deadline_elapsed"
|
|
45
|
+
| "intent_not_committed"
|
|
46
|
+
| "spawn_failed"
|
|
47
|
+
| "spawn_exited_before_publication"
|
|
48
|
+
| "publication_timeout";
|
|
49
|
+
detail?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function identity(discovery: BrokerDiscovery): BrokerRestartOwnerIdentity {
|
|
53
|
+
return {
|
|
54
|
+
ownerId: discovery.ownerId,
|
|
55
|
+
generation: discovery.packageGeneration,
|
|
56
|
+
pid: discovery.pid,
|
|
57
|
+
incarnation: discovery.incarnation,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function typedResult(response: unknown): BrokerRestartResult {
|
|
62
|
+
if (response && typeof response === "object" && "ok" in response) return response as BrokerRestartResult;
|
|
63
|
+
return { ok: false, error: { code: "protocol_error", message: "broker returned a malformed restart response" } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function prepareDoctorBrokerRestart(
|
|
67
|
+
client: SdkClient,
|
|
68
|
+
discovery: BrokerDiscovery,
|
|
69
|
+
options: DoctorBrokerRestartOptions & { requestId: string },
|
|
70
|
+
): Promise<BrokerRestartResult> {
|
|
71
|
+
const input: BrokerRestartPrepareOptions = {
|
|
72
|
+
...identity(discovery),
|
|
73
|
+
requestId: options.requestId,
|
|
74
|
+
deadlineAt: Date.now() + (options.deadlineMs ?? 30_000),
|
|
75
|
+
drain: options.drain,
|
|
76
|
+
};
|
|
77
|
+
return typedResult(await client.global("broker.prepare_restart", { ...input }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function commitDoctorBrokerRestart(
|
|
81
|
+
client: SdkClient,
|
|
82
|
+
discovery: BrokerDiscovery,
|
|
83
|
+
prepared: BrokerRestartPrepareResult,
|
|
84
|
+
requestId: string,
|
|
85
|
+
): Promise<BrokerRestartResult> {
|
|
86
|
+
const input: BrokerRestartCommitOptions = {
|
|
87
|
+
...identity(discovery),
|
|
88
|
+
requestId,
|
|
89
|
+
deadlineAt: prepared.expiresAt,
|
|
90
|
+
lease: prepared.lease,
|
|
91
|
+
occupancyEpoch: prepared.occupancyEpoch,
|
|
92
|
+
};
|
|
93
|
+
return typedResult(await client.global("broker.commit_restart", { ...input }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function cancelDoctorBrokerRestart(client: SdkClient, requestId: string): Promise<BrokerRestartResult> {
|
|
97
|
+
return typedResult(await client.global("broker.cancel_restart", { requestId }));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const OLD_OWNER_EXIT_POLL_MS = 50;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Owner-side prepare/commit/stop, positive-exit-confirmed cleanup, then
|
|
104
|
+
* exactly one successor launch or adoption. The exact sequence:
|
|
105
|
+
*
|
|
106
|
+
* 1. Read the CURRENT discovery snapshot (never a stale caller-supplied one)
|
|
107
|
+
* and prove its owner is live via a real connected client, not an
|
|
108
|
+
* incarnation string comparison alone.
|
|
109
|
+
* 2. `prepare_restart` against that exact owner tuple/generation; on any
|
|
110
|
+
* prepare refusal, cancel the reservation and return the broker's own
|
|
111
|
+
* typed refusal rather than throwing.
|
|
112
|
+
* 3. `commit_restart` with the exact lease/occupancyEpoch prepare returned.
|
|
113
|
+
* Once committed, the owner is durably retiring: cancellation is no longer
|
|
114
|
+
* safe and this function must never call cancel past this point.
|
|
115
|
+
* 4. Close the client (every path below runs with the client closed).
|
|
116
|
+
* 5. Wait, bounded by the request deadline, for `observeProcessIncarnation`
|
|
117
|
+
* to positively confirm the exact old pid is OS-absent -- never inferred
|
|
118
|
+
* from a changed/missing incarnation read, and never from discovery
|
|
119
|
+
* disappearing (a crashed writer can leave a stale discovery file behind
|
|
120
|
+
* just as easily as a clean exit can remove it).
|
|
121
|
+
* 6. Launch (or adopt an already-published) exactly one successor for this
|
|
122
|
+
* requestId, holding the shared startup exclusion only around the launch
|
|
123
|
+
* decision itself -- never across the successor's own publication wait,
|
|
124
|
+
* which would deadlock this process against its own spawned child.
|
|
125
|
+
*
|
|
126
|
+
* The successor clears the durable committed intent itself, from inside its
|
|
127
|
+
* own `Broker#start()`, only after its own publication has independently
|
|
128
|
+
* proven it owns the root -- never here, and never before that proof exists.
|
|
129
|
+
*/
|
|
130
|
+
export async function restartBrokerForDoctor(options: DoctorBrokerRestartOptions): Promise<DoctorBrokerRestartOutcome> {
|
|
131
|
+
const old = await readBrokerDiscovery(options.agentDir);
|
|
132
|
+
if (!old) return { kind: "owner_unavailable", reason: "no_discovery" };
|
|
133
|
+
const oldOwner = identity(old);
|
|
134
|
+
const requestId = options.requestId ?? randomUUID();
|
|
135
|
+
const deadlineMs = options.deadlineMs ?? 30_000;
|
|
136
|
+
const deadlineAt = Date.now() + deadlineMs;
|
|
137
|
+
|
|
138
|
+
let client: SdkClient;
|
|
139
|
+
try {
|
|
140
|
+
client = await SdkClient.connect(old.url, old.token, { timeoutMs: deadlineMs });
|
|
141
|
+
} catch {
|
|
142
|
+
return { kind: "owner_unavailable", reason: "owner_not_confirmed_live" };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let prepared: BrokerRestartPrepareResult | undefined;
|
|
146
|
+
try {
|
|
147
|
+
const prepareResult = await prepareDoctorBrokerRestart(client, old, { ...options, requestId });
|
|
148
|
+
if (!prepareResult.ok)
|
|
149
|
+
return { kind: "prepare_refused", code: prepareResult.error.code, message: prepareResult.error.message };
|
|
150
|
+
prepared = prepareResult.result as BrokerRestartPrepareResult;
|
|
151
|
+
|
|
152
|
+
const commitResult = await commitDoctorBrokerRestart(client, old, prepared, requestId);
|
|
153
|
+
if (!commitResult.ok) {
|
|
154
|
+
// A commit refusal leaves the owner's own reservation in place; the
|
|
155
|
+
// owner's lease-expiry timer (or an explicit cancel by the same
|
|
156
|
+
// requestId) is what releases it, never a resurrect-old-owner retry
|
|
157
|
+
// from here.
|
|
158
|
+
try {
|
|
159
|
+
await cancelDoctorBrokerRestart(client, requestId);
|
|
160
|
+
} catch {
|
|
161
|
+
// Best-effort: an unreachable owner past a refused commit still
|
|
162
|
+
// self-expires its own lease.
|
|
163
|
+
}
|
|
164
|
+
return { kind: "commit_refused", code: commitResult.error.code, message: commitResult.error.message };
|
|
165
|
+
}
|
|
166
|
+
} catch (caught) {
|
|
167
|
+
throw caught instanceof SdkClientError
|
|
168
|
+
? caught
|
|
169
|
+
: new Error(
|
|
170
|
+
`SDK broker restart prepare/commit failed: ${caught instanceof Error ? caught.message : String(caught)}`,
|
|
171
|
+
{
|
|
172
|
+
cause: caught,
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
} finally {
|
|
176
|
+
await client.close().catch(() => undefined);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Committed: the owner is durably retiring. Wait for its exact OS
|
|
180
|
+
// incarnation to be positively confirmed absent before authorizing any
|
|
181
|
+
// successor -- never inferred from discovery disappearing or from a
|
|
182
|
+
// PID-reuse-vulnerable incarnation mismatch alone.
|
|
183
|
+
while (Date.now() < deadlineAt) {
|
|
184
|
+
if (oldOwnerConfirmedExited(oldOwner.pid)) break;
|
|
185
|
+
await Bun.sleep(OLD_OWNER_EXIT_POLL_MS);
|
|
186
|
+
}
|
|
187
|
+
if (!oldOwnerConfirmedExited(oldOwner.pid)) return { kind: "old_owner_exit_timeout", requestId, oldOwner };
|
|
188
|
+
|
|
189
|
+
const successor = await launchAuthorizedBrokerSuccessor({
|
|
190
|
+
agentDir: options.agentDir,
|
|
191
|
+
requestId,
|
|
192
|
+
deadlineAt: prepared!.expiresAt,
|
|
193
|
+
packageGeneration: old.packageGeneration,
|
|
194
|
+
});
|
|
195
|
+
if (successor.kind === "refused")
|
|
196
|
+
return { kind: "successor_refused", requestId, oldOwner, reason: successor.reason, detail: successor.detail };
|
|
197
|
+
return {
|
|
198
|
+
kind: "restarted",
|
|
199
|
+
requestId,
|
|
200
|
+
oldOwner,
|
|
201
|
+
successor: successor.discovery,
|
|
202
|
+
adopted: successor.kind === "adopted",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type { BrokerRestartResult };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import * as fsSync from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
|
|
@@ -20,6 +21,41 @@ export type IndexedEndpointAuthority = {
|
|
|
20
21
|
endpointFileId?: string;
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
/** Derive the broker-compatible opaque authority for one indexed endpoint generation. */
|
|
25
|
+
export function endpointIncarnation(
|
|
26
|
+
record: { endpointGeneration: number; endpointMtimeMs?: number; pid: number; endpointFileId?: string },
|
|
27
|
+
sessionId: string,
|
|
28
|
+
): string | undefined {
|
|
29
|
+
if (
|
|
30
|
+
!Number.isSafeInteger(record.endpointGeneration) ||
|
|
31
|
+
record.endpointGeneration <= 0 ||
|
|
32
|
+
!Number.isSafeInteger(record.pid) ||
|
|
33
|
+
record.pid <= 0 ||
|
|
34
|
+
typeof record.endpointMtimeMs !== "number" ||
|
|
35
|
+
!Number.isFinite(record.endpointMtimeMs) ||
|
|
36
|
+
record.endpointMtimeMs <= 0
|
|
37
|
+
)
|
|
38
|
+
return undefined;
|
|
39
|
+
return createHash("sha256")
|
|
40
|
+
.update(
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
endpointGeneration: record.endpointGeneration,
|
|
43
|
+
// Filesystem mtime is only millisecond-precise, and the two stat
|
|
44
|
+
// spellings used across this path (bigint mtimeNs/1e6 vs libuv
|
|
45
|
+
// double mtimeMs) can disagree by 1 float ulp (~0.00024ms) for the
|
|
46
|
+
// same file (#5376). Quantize so equivalent reads hash identically.
|
|
47
|
+
// The immutable endpoint file identity (dev:ino) stays exact, so a
|
|
48
|
+
// genuine same-millisecond successor replacement still changes the
|
|
49
|
+
// digest even when pid/generation are reused.
|
|
50
|
+
endpointMtimeMs: Math.round(record.endpointMtimeMs),
|
|
51
|
+
...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
|
|
52
|
+
pid: record.pid,
|
|
53
|
+
sessionId,
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
.digest("hex");
|
|
57
|
+
}
|
|
58
|
+
|
|
23
59
|
function readFlags(): number {
|
|
24
60
|
return fsSync.constants.O_RDONLY | (fsSync.constants.O_NOFOLLOW ?? 0) | (fsSync.constants.O_NONBLOCK ?? 0);
|
|
25
61
|
}
|
package/src/sdk/broker/ensure.ts
CHANGED
|
@@ -7,8 +7,17 @@ import packageJson from "../../../package.json" with { type: "json" };
|
|
|
7
7
|
import { acquireFileLock, type FileLockOptions, withFileLock } from "../../config/file-lock";
|
|
8
8
|
import { loadInstallationHostId, loadLegacyInstallationHostId } from "../../config/machine-identity";
|
|
9
9
|
import { SdkClient } from "../client/client";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
type BrokerDiscovery,
|
|
12
|
+
brokerProcessIncarnation,
|
|
13
|
+
readBrokerDiscovery,
|
|
14
|
+
readBrokerRestartIntent,
|
|
15
|
+
} from "./discovery";
|
|
16
|
+
import {
|
|
17
|
+
isSdkInternalRuntimeImagePresent,
|
|
18
|
+
resolveSdkInternalSpawnCommand,
|
|
19
|
+
type SdkInternalSpawnCommand,
|
|
20
|
+
} from "./runtime";
|
|
12
21
|
import { BrokerStartupError, clearBrokerStartupFailureMarker, readBrokerStartupFailureMarker } from "./startup-failure";
|
|
13
22
|
|
|
14
23
|
function resolveExpectedBrokerGeneration(): string {
|
|
@@ -20,8 +29,27 @@ export function isBrokerGenerationCompatible(discovery: BrokerDiscovery | null):
|
|
|
20
29
|
if (!discovery) return false;
|
|
21
30
|
return discovery.packageGeneration === resolveExpectedBrokerGeneration();
|
|
22
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A live incumbent stops being reusable once the runtime image it launches
|
|
35
|
+
* internal processes with is provably gone. A broker outlives its own executable
|
|
36
|
+
* (a package manager replacing the interpreter it was started from, an install
|
|
37
|
+
* directory swapped underneath it): it keeps heartbeating and answering
|
|
38
|
+
* requests, but every `session.create` it accepts is refused at spawn time.
|
|
39
|
+
* Retiring it here repairs the endpoint before a caller reaches session
|
|
40
|
+
* creation. Absent runtime evidence -- a record predating the field, or a
|
|
41
|
+
* publisher that could not classify its own runtime at all -- and evidence this
|
|
42
|
+
* caller merely cannot inspect both keep the incumbent reusable.
|
|
43
|
+
*/
|
|
44
|
+
export async function isBrokerReusable(discovery: BrokerDiscovery | null): Promise<boolean> {
|
|
45
|
+
if (!isBrokerGenerationCompatible(discovery)) return false;
|
|
46
|
+
const runtime = discovery?.runtime;
|
|
47
|
+
return runtime === undefined || (await isSdkInternalRuntimeImagePresent(runtime));
|
|
48
|
+
}
|
|
23
49
|
export interface EnsureBrokerSettings {
|
|
24
50
|
agentDir: string;
|
|
51
|
+
/** Only an authorized doctor successor may consume a restart-intent publication. */
|
|
52
|
+
restartRequestId?: string;
|
|
25
53
|
heartbeatTtlMs?: number;
|
|
26
54
|
/**
|
|
27
55
|
* Environment for the spawned detached broker. Defaults to `process.env`; tests
|
|
@@ -387,7 +415,7 @@ export async function closeBrokerClientBeforeDeadline(
|
|
|
387
415
|
await Promise.race([close, Bun.sleep(remainingMs)]);
|
|
388
416
|
}
|
|
389
417
|
|
|
390
|
-
async function
|
|
418
|
+
async function retireUnusableBroker(
|
|
391
419
|
stale: BrokerDiscovery,
|
|
392
420
|
settings: EnsureBrokerSettings,
|
|
393
421
|
outerDeadline = Number.POSITIVE_INFINITY,
|
|
@@ -429,10 +457,10 @@ export async function reconcileBrokerGenerationForStartup(
|
|
|
429
457
|
): Promise<BrokerDiscovery | undefined> {
|
|
430
458
|
const current = await readBrokerDiscoveryBeforeDeadline(settings.agentDir, settings.heartbeatTtlMs, deadline);
|
|
431
459
|
if (!current) return undefined;
|
|
432
|
-
if (
|
|
433
|
-
await
|
|
460
|
+
if (await isBrokerReusable(current)) return current;
|
|
461
|
+
await retireUnusableBroker(current, settings, deadline);
|
|
434
462
|
const replacement = await readBrokerDiscoveryBeforeDeadline(settings.agentDir, settings.heartbeatTtlMs, deadline);
|
|
435
|
-
return replacement &&
|
|
463
|
+
return replacement && (await isBrokerReusable(replacement)) ? replacement : undefined;
|
|
436
464
|
}
|
|
437
465
|
function createFixtureLeaseFromChild(child: ChildProcess, terminate: () => Promise<void>): ExactFixtureBrokerLease {
|
|
438
466
|
let termination: Promise<void> | undefined;
|
|
@@ -485,23 +513,27 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
485
513
|
settings.heartbeatTtlMs,
|
|
486
514
|
initialDiscoveryDeadline,
|
|
487
515
|
);
|
|
516
|
+
const restartIntent = await readBrokerRestartIntent(settings.agentDir);
|
|
517
|
+
if (restartIntent && restartIntent.expiresAt > Date.now() && restartIntent.requestId !== settings.restartRequestId)
|
|
518
|
+
throw new Error("broker_restart_in_progress");
|
|
488
519
|
if (initiator === "fixture-lease" && (priorOwner || existing)) throw fixtureLeaseUnavailable();
|
|
489
520
|
if (priorOwner) {
|
|
490
521
|
// A retained cleanup failure fences every discovery record. Only a ready
|
|
491
522
|
// record bound to this exact child incarnation may be reused.
|
|
492
|
-
if (priorOwner.canReuse(existing)
|
|
523
|
+
if (priorOwner.canReuse(existing) && (await isBrokerReusable(existing)))
|
|
524
|
+
return { kind: "prior-local-owner", discovery: existing!, owner: priorOwner };
|
|
493
525
|
await priorOwner.stop();
|
|
494
526
|
const discoveredAfterCleanup = await readBrokerDiscoveryBeforeDeadline(
|
|
495
527
|
settings.agentDir,
|
|
496
528
|
settings.heartbeatTtlMs,
|
|
497
529
|
initialDiscoveryDeadline,
|
|
498
530
|
);
|
|
499
|
-
if (discoveredAfterCleanup &&
|
|
531
|
+
if (discoveredAfterCleanup && (await isBrokerReusable(discoveredAfterCleanup)))
|
|
500
532
|
return { kind: "external-discovery", discovery: discoveredAfterCleanup };
|
|
501
|
-
//
|
|
533
|
+
// Unusable-incumbent retirement is serialized under the spawn lock below.
|
|
502
534
|
} else if (existing) {
|
|
503
|
-
if (
|
|
504
|
-
//
|
|
535
|
+
if (await isBrokerReusable(existing)) return { kind: "external-discovery", discovery: existing };
|
|
536
|
+
// Unusable-incumbent retirement is serialized under the spawn lock below.
|
|
505
537
|
}
|
|
506
538
|
|
|
507
539
|
// Only one process may spawn for this agent dir at a time; everyone else
|
|
@@ -512,6 +544,9 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
512
544
|
try {
|
|
513
545
|
const deadline =
|
|
514
546
|
Date.now() + (initiator === "fixture-lease" ? FIXTURE_DISCOVERY_TIMEOUT_MS : DISCOVERY_TIMEOUT_MS);
|
|
547
|
+
const lockedIntent = await readBrokerRestartIntent(settings.agentDir);
|
|
548
|
+
if (lockedIntent && lockedIntent.expiresAt > Date.now() && lockedIntent.requestId !== settings.restartRequestId)
|
|
549
|
+
throw new Error("broker_restart_in_progress");
|
|
515
550
|
// The lock winner may still find a discovery published by an earlier winner
|
|
516
551
|
// that finished between our first read and the lock acquisition.
|
|
517
552
|
const discoveredUnderLock = await reconcileBrokerGenerationForStartup(settings, deadline);
|
|
@@ -546,7 +581,7 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
546
581
|
deadline,
|
|
547
582
|
);
|
|
548
583
|
if (discovered) {
|
|
549
|
-
if (!
|
|
584
|
+
if (!(await isBrokerReusable(discovered))) {
|
|
550
585
|
await sleep(50);
|
|
551
586
|
continue;
|
|
552
587
|
}
|
|
@@ -577,7 +612,7 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
577
612
|
settings.heartbeatTtlMs,
|
|
578
613
|
deadline,
|
|
579
614
|
);
|
|
580
|
-
if (winner &&
|
|
615
|
+
if (winner && (await isBrokerReusable(winner))) {
|
|
581
616
|
await owner.stop();
|
|
582
617
|
return { kind: "external-discovery", discovery: winner };
|
|
583
618
|
}
|
|
@@ -622,7 +657,7 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
622
657
|
settings.heartbeatTtlMs,
|
|
623
658
|
recoveryDeadline,
|
|
624
659
|
);
|
|
625
|
-
if (incumbent &&
|
|
660
|
+
if (incumbent && (await isBrokerReusable(incumbent))) {
|
|
626
661
|
await owner.stop();
|
|
627
662
|
return { kind: "external-discovery", discovery: incumbent };
|
|
628
663
|
}
|