@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
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { BigIntStats } from "node:fs";
|
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import type { NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
|
|
6
|
+
import type { NativeBrokerRestartIntent, NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
|
|
7
7
|
import { logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
8
8
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
9
9
|
import type { ModelProfileErrorDetails } from "../../config/model-profile-contract";
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD,
|
|
15
15
|
} from "../host/control/runtime-gate";
|
|
16
16
|
import { createDefaultSdkHostModelResolver, type SdkHostModelResolver } from "../host/model-pin";
|
|
17
|
+
import { canonicalServiceRootDigest } from "../service-artifact-paths";
|
|
17
18
|
import {
|
|
18
19
|
type DirectoryMigrationPolicy,
|
|
19
20
|
listManagedSessionCandidates,
|
|
@@ -32,9 +33,10 @@ import {
|
|
|
32
33
|
type RedactedBrokerDiscovery,
|
|
33
34
|
type RetainedBrokerDiscovery,
|
|
34
35
|
readBrokerDiscovery,
|
|
36
|
+
readBrokerRestartIntent,
|
|
35
37
|
redactBrokerDiscovery,
|
|
36
38
|
} from "./discovery";
|
|
37
|
-
import { matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
|
|
39
|
+
import { endpointIncarnation, matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
|
|
38
40
|
import { deriveIdempotencyIdentity, getBrokerIdentityKey } from "./identity";
|
|
39
41
|
import {
|
|
40
42
|
canonicalDeleteLocatorPath,
|
|
@@ -52,6 +54,7 @@ import {
|
|
|
52
54
|
type LifecycleState,
|
|
53
55
|
} from "./lifecycle-ledger";
|
|
54
56
|
import { createMasterCapabilityVerifier, readEndpoint } from "./master-capability";
|
|
57
|
+
import { sdkInternalRuntimeImage } from "./runtime";
|
|
55
58
|
import { type IndexedSession, isSessionAuthorityEligible, SessionIndex, type SessionList } from "./session-index";
|
|
56
59
|
import {
|
|
57
60
|
type ResolvedScopeV1,
|
|
@@ -77,6 +80,8 @@ import { BrokerTransport } from "./transport";
|
|
|
77
80
|
export interface BrokerSettings {
|
|
78
81
|
agentDir: string;
|
|
79
82
|
packageGeneration?: string;
|
|
83
|
+
/** Published runtime image; tests inject one to model a vanished executable. */
|
|
84
|
+
runtime?: string;
|
|
80
85
|
port?: number;
|
|
81
86
|
heartbeatTtlMs?: number;
|
|
82
87
|
/** Broker-owned migration policy. Client lifecycle frames cannot select it. */
|
|
@@ -91,11 +96,20 @@ export interface BrokerSettings {
|
|
|
91
96
|
masterOrphanGraceMs?: number;
|
|
92
97
|
/** Host model resolver override for lifecycle tests and embedders. */
|
|
93
98
|
resolveModelPin?: SdkHostModelResolver;
|
|
99
|
+
/**
|
|
100
|
+
* Exact restart request this process instance is the authorized successor
|
|
101
|
+
* for. Set only by the real `broker-internal` entry point from its own
|
|
102
|
+
* `GJC_BROKER_RESTART_REQUEST` environment read — never inferred here from
|
|
103
|
+
* `process.env` directly, so the value this broker publishes is exactly the
|
|
104
|
+
* value its launcher decided, with no cast/assumed tag in between.
|
|
105
|
+
*/
|
|
106
|
+
restartRequestId?: string;
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
type ResolvedBrokerSettings = {
|
|
97
110
|
agentDir: string;
|
|
98
111
|
packageGeneration: string;
|
|
112
|
+
runtime?: string;
|
|
99
113
|
port: number;
|
|
100
114
|
heartbeatTtlMs: number;
|
|
101
115
|
resolveDirectoryMigration: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
|
|
@@ -103,6 +117,7 @@ type ResolvedBrokerSettings = {
|
|
|
103
117
|
spawnSubstrateProvider?: SpawnSubstrateProvider;
|
|
104
118
|
spawnPromptLayer?: SpawnPromptLayer;
|
|
105
119
|
masterOrphanGraceMs: number;
|
|
120
|
+
restartRequestId?: string;
|
|
106
121
|
};
|
|
107
122
|
export function resolveBrokerPackageGeneration(): string {
|
|
108
123
|
const v = (packageJson as { version?: unknown }).version;
|
|
@@ -300,6 +315,16 @@ function lifecycleFingerprint(operation: string, input: Record<string, unknown>)
|
|
|
300
315
|
.update(JSON.stringify({ operation, input: lifecycleRequestIdentity(input) }))
|
|
301
316
|
.digest("hex");
|
|
302
317
|
}
|
|
318
|
+
|
|
319
|
+
function publicLifecycleLookupInput(
|
|
320
|
+
input: Record<string, unknown>,
|
|
321
|
+
): { operation: "session.create"; target: Record<string, unknown> } | BrokerResponse {
|
|
322
|
+
if (input.operation !== "session.create")
|
|
323
|
+
return error("invalid_input", "session.lookup only supports operation session.create");
|
|
324
|
+
const target = objectRecord(input.target);
|
|
325
|
+
if (!target) return error("invalid_input", "session.lookup target must be an object");
|
|
326
|
+
return { operation: "session.create", target };
|
|
327
|
+
}
|
|
303
328
|
function lifecycleResponseState(response: BrokerResponse): LifecycleState {
|
|
304
329
|
if (response.ok) return "terminal_ok";
|
|
305
330
|
if (isCleanupPending(response)) return "effect_started";
|
|
@@ -732,31 +757,6 @@ type LifecycleReplayEndpoint = {
|
|
|
732
757
|
};
|
|
733
758
|
|
|
734
759
|
type EndpointAuthority = { endpointGeneration?: number; endpointIncarnation?: string };
|
|
735
|
-
function endpointIncarnation(
|
|
736
|
-
record: Pick<IndexedSession, "endpointGeneration" | "endpointMtimeMs" | "pid">,
|
|
737
|
-
sessionId: string,
|
|
738
|
-
): string | undefined {
|
|
739
|
-
if (
|
|
740
|
-
!Number.isSafeInteger(record.endpointGeneration) ||
|
|
741
|
-
record.endpointGeneration <= 0 ||
|
|
742
|
-
!Number.isSafeInteger(record.pid) ||
|
|
743
|
-
record.pid <= 0 ||
|
|
744
|
-
typeof record.endpointMtimeMs !== "number" ||
|
|
745
|
-
!Number.isFinite(record.endpointMtimeMs) ||
|
|
746
|
-
record.endpointMtimeMs <= 0
|
|
747
|
-
)
|
|
748
|
-
return undefined;
|
|
749
|
-
return createHash("sha256")
|
|
750
|
-
.update(
|
|
751
|
-
canonicalJson({
|
|
752
|
-
endpointGeneration: record.endpointGeneration,
|
|
753
|
-
endpointMtimeMs: record.endpointMtimeMs,
|
|
754
|
-
pid: record.pid,
|
|
755
|
-
sessionId,
|
|
756
|
-
}),
|
|
757
|
-
)
|
|
758
|
-
.digest("hex");
|
|
759
|
-
}
|
|
760
760
|
function expectedEndpointAuthority(input: Record<string, unknown>): EndpointAuthority | BrokerResponse {
|
|
761
761
|
const endpointGeneration = input.endpointGeneration;
|
|
762
762
|
const endpointIncarnation = input.endpointIncarnation;
|
|
@@ -1117,6 +1117,32 @@ export interface StartupAdmissionTiming {
|
|
|
1117
1117
|
sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
1118
1118
|
}
|
|
1119
1119
|
|
|
1120
|
+
export interface BrokerRestartOwnerIdentity {
|
|
1121
|
+
ownerId: string;
|
|
1122
|
+
generation: string;
|
|
1123
|
+
pid: number;
|
|
1124
|
+
incarnation: string;
|
|
1125
|
+
}
|
|
1126
|
+
export interface BrokerRestartPrepareOptions extends BrokerRestartOwnerIdentity {
|
|
1127
|
+
requestId: string;
|
|
1128
|
+
deadlineAt: number;
|
|
1129
|
+
drain?: boolean;
|
|
1130
|
+
}
|
|
1131
|
+
export interface BrokerRestartPrepareResult {
|
|
1132
|
+
lease: string;
|
|
1133
|
+
occupancyEpoch: number;
|
|
1134
|
+
expiresAt: number;
|
|
1135
|
+
requestId: string;
|
|
1136
|
+
owner: BrokerRestartOwnerIdentity;
|
|
1137
|
+
}
|
|
1138
|
+
export interface BrokerRestartCommitOptions extends BrokerRestartPrepareOptions {
|
|
1139
|
+
lease: string;
|
|
1140
|
+
occupancyEpoch: number;
|
|
1141
|
+
}
|
|
1142
|
+
export type BrokerRestartResult =
|
|
1143
|
+
| { ok: true; result: BrokerRestartPrepareResult | { committed: true } | { cancelled: true } }
|
|
1144
|
+
| { ok: false; error: { code: string; message: string } };
|
|
1145
|
+
|
|
1120
1146
|
export type StartupAdmissionResult<T> =
|
|
1121
1147
|
| { status: "completed"; admittedAt: number; value: T }
|
|
1122
1148
|
| { status: "admission_timeout"; reason: "admission_timeout" }
|
|
@@ -1289,10 +1315,20 @@ export class Broker {
|
|
|
1289
1315
|
#rejectCompletion!: (error: unknown) => void;
|
|
1290
1316
|
#resolveModelPin: SdkHostModelResolver;
|
|
1291
1317
|
#ownsResolveModelPin: boolean;
|
|
1318
|
+
#restart:
|
|
1319
|
+
| {
|
|
1320
|
+
options: BrokerRestartPrepareOptions;
|
|
1321
|
+
lease: string;
|
|
1322
|
+
occupancyEpoch: number;
|
|
1323
|
+
expiresAt: number;
|
|
1324
|
+
timer: NodeJS.Timeout;
|
|
1325
|
+
}
|
|
1326
|
+
| undefined;
|
|
1292
1327
|
constructor(settings: BrokerSettings) {
|
|
1293
1328
|
this.settings = {
|
|
1294
1329
|
agentDir: settings.agentDir,
|
|
1295
1330
|
packageGeneration: settings.packageGeneration ?? resolveBrokerPackageGeneration(),
|
|
1331
|
+
runtime: settings.runtime,
|
|
1296
1332
|
port: settings.port ?? 0,
|
|
1297
1333
|
heartbeatTtlMs: settings.heartbeatTtlMs ?? BROKER_HEARTBEAT_TTL_MS,
|
|
1298
1334
|
resolveDirectoryMigration: settings.resolveDirectoryMigration ?? (async () => "copy-retain"),
|
|
@@ -1300,6 +1336,7 @@ export class Broker {
|
|
|
1300
1336
|
spawnSubstrateProvider: settings.spawnSubstrateProvider,
|
|
1301
1337
|
spawnPromptLayer: settings.spawnPromptLayer,
|
|
1302
1338
|
masterOrphanGraceMs: settings.masterOrphanGraceMs ?? MASTER_ORPHAN_GRACE_DEFAULT_MS,
|
|
1339
|
+
...(settings.restartRequestId === undefined ? {} : { restartRequestId: settings.restartRequestId }),
|
|
1303
1340
|
};
|
|
1304
1341
|
this.index = new SessionIndex(settings.agentDir);
|
|
1305
1342
|
this.ledger = new LifecycleLedger(settings.agentDir);
|
|
@@ -2402,9 +2439,23 @@ export class Broker {
|
|
|
2402
2439
|
async #createLock(): Promise<void> {
|
|
2403
2440
|
await fs.mkdir(this.#lock, { mode: 0o700 });
|
|
2404
2441
|
try {
|
|
2442
|
+
const incarnation = brokerProcessIncarnation(process.pid);
|
|
2443
|
+
// A minimal, non-secret receipt delta for D9 stale-artifact detach
|
|
2444
|
+
// (sequenced follow-up): the OS incarnation and a digest of the exact
|
|
2445
|
+
// resolved root this lock authorizes, so a later positive-death check can
|
|
2446
|
+
// cross-bind this owner record against the discovery record it authored
|
|
2447
|
+
// without re-deriving anything from file content or adding new identity.
|
|
2448
|
+
const rootDigest = await canonicalServiceRootDigest(this.settings.agentDir);
|
|
2405
2449
|
await fs.writeFile(
|
|
2406
2450
|
this.#lockRecordPath(),
|
|
2407
|
-
JSON.stringify({
|
|
2451
|
+
JSON.stringify({
|
|
2452
|
+
version: 1,
|
|
2453
|
+
ownerId: this.#owner,
|
|
2454
|
+
pid: process.pid,
|
|
2455
|
+
acquiredAt: Date.now(),
|
|
2456
|
+
...(incarnation ? { incarnation } : {}),
|
|
2457
|
+
rootDigest,
|
|
2458
|
+
}),
|
|
2408
2459
|
{ flag: "wx", mode: 0o600 },
|
|
2409
2460
|
);
|
|
2410
2461
|
} catch (e) {
|
|
@@ -2577,6 +2628,7 @@ export class Broker {
|
|
|
2577
2628
|
version: 1,
|
|
2578
2629
|
protocolVersion: 3,
|
|
2579
2630
|
packageGeneration: this.settings.packageGeneration,
|
|
2631
|
+
runtime: this.settings.runtime ?? sdkInternalRuntimeImage(),
|
|
2580
2632
|
ownerId: this.#owner,
|
|
2581
2633
|
pid: process.pid,
|
|
2582
2634
|
incarnation,
|
|
@@ -2586,6 +2638,9 @@ export class Broker {
|
|
|
2586
2638
|
token,
|
|
2587
2639
|
startedAt: now,
|
|
2588
2640
|
heartbeatAt: now,
|
|
2641
|
+
...(this.settings.restartRequestId === undefined
|
|
2642
|
+
? {}
|
|
2643
|
+
: { restartRequestId: this.settings.restartRequestId }),
|
|
2589
2644
|
};
|
|
2590
2645
|
// Readiness must not be externally visible until the initial session
|
|
2591
2646
|
// checkpoint settles. The bootstrap watchdog owns this pre-publication
|
|
@@ -2603,6 +2658,23 @@ export class Broker {
|
|
|
2603
2658
|
void this.#watchPublication();
|
|
2604
2659
|
void this.#reapSpawnOrphans();
|
|
2605
2660
|
}, cadenceMs);
|
|
2661
|
+
// This process is now the independently verified successor: its own
|
|
2662
|
+
// discovery just published under its own retained authority. Release the
|
|
2663
|
+
// predecessor's reservation only now, keyed to the exact request this
|
|
2664
|
+
// process was launched for -- never on a startup that carries no restart
|
|
2665
|
+
// request, and never before this publish proved this is the real owner.
|
|
2666
|
+
if (this.settings.restartRequestId !== undefined && this.#publication) {
|
|
2667
|
+
try {
|
|
2668
|
+
const intent = await readBrokerRestartIntent(this.settings.agentDir);
|
|
2669
|
+
if (intent && intent.phase === "committed" && intent.requestId === this.settings.restartRequestId)
|
|
2670
|
+
await this.#publication.clearForeignRestartIntent(intent.identity);
|
|
2671
|
+
} catch {
|
|
2672
|
+
// Best-effort reservation release. The admission gate in ensure.ts keys
|
|
2673
|
+
// off requestId equality, which already matches this exact successor,
|
|
2674
|
+
// so a retained committed intent here blocks nothing further; a failed
|
|
2675
|
+
// clear must never fail this broker's own successful startup.
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2606
2678
|
return this.discovery;
|
|
2607
2679
|
} catch (error) {
|
|
2608
2680
|
await this.#transport?.stop();
|
|
@@ -2620,6 +2692,130 @@ export class Broker {
|
|
|
2620
2692
|
get completion(): Promise<void> {
|
|
2621
2693
|
return this.#completion;
|
|
2622
2694
|
}
|
|
2695
|
+
#restartIdentity(): BrokerRestartOwnerIdentity | undefined {
|
|
2696
|
+
const d = this.discovery;
|
|
2697
|
+
if (!d?.incarnation) return undefined;
|
|
2698
|
+
return { ownerId: d.ownerId, generation: d.packageGeneration, pid: d.pid, incarnation: d.incarnation };
|
|
2699
|
+
}
|
|
2700
|
+
#restartBusy(): boolean {
|
|
2701
|
+
for (const session of this.index.listSessions().sessions) {
|
|
2702
|
+
if (session.live && !session.terminal) return true;
|
|
2703
|
+
}
|
|
2704
|
+
return this.#admitted.size > 1;
|
|
2705
|
+
}
|
|
2706
|
+
async prepareRestart(options: BrokerRestartPrepareOptions): Promise<BrokerRestartResult> {
|
|
2707
|
+
const identity = this.#restartIdentity();
|
|
2708
|
+
if (
|
|
2709
|
+
!identity ||
|
|
2710
|
+
options.ownerId !== identity.ownerId ||
|
|
2711
|
+
options.generation !== identity.generation ||
|
|
2712
|
+
options.pid !== identity.pid ||
|
|
2713
|
+
options.incarnation !== identity.incarnation ||
|
|
2714
|
+
!options.requestId ||
|
|
2715
|
+
!Number.isSafeInteger(options.deadlineAt) ||
|
|
2716
|
+
options.deadlineAt <= Date.now()
|
|
2717
|
+
)
|
|
2718
|
+
return { ok: false, error: { code: "restart_identity_mismatch", message: "broker owner identity mismatch" } };
|
|
2719
|
+
if (this.#restart) {
|
|
2720
|
+
if (this.#restart.options.requestId !== options.requestId)
|
|
2721
|
+
return {
|
|
2722
|
+
ok: false,
|
|
2723
|
+
error: { code: "restart_request_mismatch", message: "restart request is already prepared" },
|
|
2724
|
+
};
|
|
2725
|
+
return {
|
|
2726
|
+
ok: true,
|
|
2727
|
+
result: {
|
|
2728
|
+
lease: this.#restart.lease,
|
|
2729
|
+
occupancyEpoch: this.#restart.occupancyEpoch,
|
|
2730
|
+
expiresAt: this.#restart.expiresAt,
|
|
2731
|
+
requestId: this.#restart.options.requestId,
|
|
2732
|
+
owner: identity,
|
|
2733
|
+
},
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
this.#startupAdmissions.close();
|
|
2737
|
+
const occupancyEpoch = this.index.listSessions().indexSeq;
|
|
2738
|
+
const lease = randomBytes(24).toString("base64url");
|
|
2739
|
+
const expiresAt = Math.min(options.deadlineAt, Date.now() + 30_000);
|
|
2740
|
+
const timer = setTimeout(() => void this.cancelRestart(options.requestId), Math.max(1, expiresAt - Date.now()));
|
|
2741
|
+
this.#restart = { options, lease, occupancyEpoch, expiresAt, timer };
|
|
2742
|
+
const abortPrepare = async (code: string, message: string): Promise<BrokerRestartResult> => {
|
|
2743
|
+
clearTimeout(timer);
|
|
2744
|
+
this.#restart = undefined;
|
|
2745
|
+
this.#startupAdmissions.reopen();
|
|
2746
|
+
return { ok: false, error: { code, message } };
|
|
2747
|
+
};
|
|
2748
|
+
try {
|
|
2749
|
+
await this.index.refresh();
|
|
2750
|
+
} catch {
|
|
2751
|
+
return abortPrepare("restart_prepare_failed", "broker occupancy could not be refreshed");
|
|
2752
|
+
}
|
|
2753
|
+
const deadline = Math.min(options.deadlineAt, Date.now() + 30_000);
|
|
2754
|
+
while (options.drain && this.#restartBusy() && Date.now() < deadline) await Bun.sleep(25);
|
|
2755
|
+
if (this.#restartBusy()) return abortPrepare("restart_busy", "broker has active or admitted work");
|
|
2756
|
+
const intent: NativeBrokerRestartIntent = { requestId: options.requestId, lease, expiresAt };
|
|
2757
|
+
const publication = this.#publication;
|
|
2758
|
+
if (!publication || !(await publication.prepareRestartIntent(intent)))
|
|
2759
|
+
return abortPrepare("restart_prepare_failed", "restart intent could not be durably prepared");
|
|
2760
|
+
return { ok: true, result: { lease, occupancyEpoch, expiresAt, requestId: options.requestId, owner: identity } };
|
|
2761
|
+
}
|
|
2762
|
+
async commitRestart(options: BrokerRestartCommitOptions): Promise<BrokerRestartResult> {
|
|
2763
|
+
const current = this.#restart;
|
|
2764
|
+
const identity = this.#restartIdentity();
|
|
2765
|
+
if (
|
|
2766
|
+
!current ||
|
|
2767
|
+
!identity ||
|
|
2768
|
+
current.lease !== options.lease ||
|
|
2769
|
+
current.occupancyEpoch !== options.occupancyEpoch ||
|
|
2770
|
+
current.options.requestId !== options.requestId ||
|
|
2771
|
+
options.ownerId !== identity.ownerId ||
|
|
2772
|
+
options.generation !== identity.generation ||
|
|
2773
|
+
options.pid !== identity.pid ||
|
|
2774
|
+
options.incarnation !== identity.incarnation ||
|
|
2775
|
+
Date.now() >= current.expiresAt ||
|
|
2776
|
+
this.#restartBusy()
|
|
2777
|
+
)
|
|
2778
|
+
return { ok: false, error: { code: "restart_commit_refused", message: "restart commit proof is invalid" } };
|
|
2779
|
+
const publication = this.#publication;
|
|
2780
|
+
const intent: NativeBrokerRestartIntent = {
|
|
2781
|
+
requestId: options.requestId,
|
|
2782
|
+
lease: current.lease,
|
|
2783
|
+
expiresAt: current.expiresAt,
|
|
2784
|
+
};
|
|
2785
|
+
if (!publication || !(await publication.commitRestartIntent(intent)))
|
|
2786
|
+
return {
|
|
2787
|
+
ok: false,
|
|
2788
|
+
error: { code: "restart_commit_refused", message: "restart intent could not be durably committed" },
|
|
2789
|
+
};
|
|
2790
|
+
clearTimeout(current.timer);
|
|
2791
|
+
// The retained restart-intent slot (independent of `sdk/broker.lock/`)
|
|
2792
|
+
// deliberately survives this owned-root exit: it is the durable proof a
|
|
2793
|
+
// successor's ordinary startup consults, and clearing it here -- before
|
|
2794
|
+
// this process has actually exited -- would let a racing ordinary
|
|
2795
|
+
// `ensureBroker` treat the still-live old owner as idle and spawn early.
|
|
2796
|
+
setTimeout(() => {
|
|
2797
|
+
void this.#complete("owned-root").finally(() => {
|
|
2798
|
+
if (this.#restart === current) this.#restart = undefined;
|
|
2799
|
+
});
|
|
2800
|
+
}, 0);
|
|
2801
|
+
return { ok: true, result: { committed: true } };
|
|
2802
|
+
}
|
|
2803
|
+
async cancelRestart(requestId: string): Promise<BrokerRestartResult> {
|
|
2804
|
+
const current = this.#restart;
|
|
2805
|
+
if (!current) return { ok: true, result: { cancelled: true } };
|
|
2806
|
+
if (current.options.requestId !== requestId)
|
|
2807
|
+
return { ok: false, error: { code: "restart_request_mismatch", message: "restart request does not match" } };
|
|
2808
|
+
const publication = this.#publication;
|
|
2809
|
+
// Best-effort: the owner's own descriptor-bound cancel removes the intent
|
|
2810
|
+
// it prepared. A publication that is no longer reachable (already fenced,
|
|
2811
|
+
// already stopping) leaves the slot for lease-expiry discovery by the
|
|
2812
|
+
// caller instead of retrying indefinitely inside this admission path.
|
|
2813
|
+
if (publication) await publication.cancelRestartIntent().catch(() => undefined);
|
|
2814
|
+
clearTimeout(current.timer);
|
|
2815
|
+
this.#restart = undefined;
|
|
2816
|
+
this.#startupAdmissions.reopen();
|
|
2817
|
+
return { ok: true, result: { cancelled: true } };
|
|
2818
|
+
}
|
|
2623
2819
|
status(): RedactedBrokerDiscovery | null {
|
|
2624
2820
|
return this.discovery ? redactBrokerDiscovery(this.discovery) : null;
|
|
2625
2821
|
}
|
|
@@ -3160,7 +3356,8 @@ export class Broker {
|
|
|
3160
3356
|
};
|
|
3161
3357
|
const sessions = snapshot.sessions.slice(snapshot.offset, snapshot.offset + snapshot.limit).map(session => {
|
|
3162
3358
|
const { lifecycleRequestId: _lifecycleRequestId, ...publicSession } = session;
|
|
3163
|
-
|
|
3359
|
+
const incarnation = endpointIncarnation(session, session.sessionId);
|
|
3360
|
+
return incarnation === undefined ? publicSession : { ...publicSession, endpointIncarnation: incarnation };
|
|
3164
3361
|
});
|
|
3165
3362
|
const offset = snapshot.offset + sessions.length;
|
|
3166
3363
|
if (offset >= snapshot.sessions.length && typeof cursor === "string") this.#sessionListCursors.delete(cursor);
|
|
@@ -3183,9 +3380,45 @@ export class Broker {
|
|
|
3183
3380
|
indexSeq: snapshot.indexSeq,
|
|
3184
3381
|
};
|
|
3185
3382
|
}
|
|
3383
|
+
async #lookupLifecycle(
|
|
3384
|
+
requestedOperation: string,
|
|
3385
|
+
idempotencyKey: string | undefined,
|
|
3386
|
+
requestedFingerprint: string,
|
|
3387
|
+
): Promise<BrokerResponse> {
|
|
3388
|
+
if (!idempotencyKey) return error("invalid_input", "operation, idempotencyKey, and fingerprint are required");
|
|
3389
|
+
if (requestedOperation === "session.spawn")
|
|
3390
|
+
return error("invalid_input", "session.spawn does not support lifecycle lookup");
|
|
3391
|
+
if (!LIFECYCLE_OPERATIONS.has(requestedOperation)) return error("not_found", "lifecycle operation was not found");
|
|
3392
|
+
const identity = await deriveIdempotencyIdentity(this.settings.agentDir, requestedOperation, idempotencyKey);
|
|
3393
|
+
const entry = this.ledger.get(identity);
|
|
3394
|
+
if (!entry) return error("not_found", "lifecycle operation was not found");
|
|
3395
|
+
if (entry.fingerprint !== requestedFingerprint)
|
|
3396
|
+
return error("idempotency_conflict", "lifecycle request fingerprint differs");
|
|
3397
|
+
if (entry.state === "terminal_uncertain")
|
|
3398
|
+
return error("terminal_uncertain", "lifecycle outcome is still uncertain");
|
|
3399
|
+
if (entry.state !== "terminal_ok" && entry.state !== "terminal_error")
|
|
3400
|
+
return error("lifecycle_pending", "lifecycle outcome is still pending");
|
|
3401
|
+
return isBrokerResponse(entry.response)
|
|
3402
|
+
? entry.response
|
|
3403
|
+
: error("terminal_uncertain", "lifecycle outcome has no recorded response");
|
|
3404
|
+
}
|
|
3186
3405
|
handleRequest(operation: string, input: Record<string, unknown>, idempotencyKey?: string): Promise<BrokerResponse> {
|
|
3406
|
+
if (operation === "broker.status") return Promise.resolve({ ok: true, result: this.status() });
|
|
3407
|
+
if (operation === "broker.prepare_restart")
|
|
3408
|
+
return this.prepareRestart(input as unknown as BrokerRestartPrepareOptions).then(
|
|
3409
|
+
result => result as BrokerResponse,
|
|
3410
|
+
);
|
|
3411
|
+
if (operation === "broker.commit_restart")
|
|
3412
|
+
return this.commitRestart(input as unknown as BrokerRestartCommitOptions).then(
|
|
3413
|
+
result => result as BrokerResponse,
|
|
3414
|
+
);
|
|
3415
|
+
if (operation === "broker.cancel_restart") {
|
|
3416
|
+
const requestId = typeof input.requestId === "string" ? input.requestId : "";
|
|
3417
|
+
return this.cancelRestart(requestId).then(result => result as BrokerResponse);
|
|
3418
|
+
}
|
|
3187
3419
|
if (this.#stopping || (this.#publication !== null && this.#publicationState !== "healthy-owned"))
|
|
3188
3420
|
return Promise.resolve(error("unavailable", "broker publication is unavailable"));
|
|
3421
|
+
if (this.#restart) return Promise.resolve(error("broker_restarting", "broker restart is prepared"));
|
|
3189
3422
|
let release!: () => void;
|
|
3190
3423
|
const admission = new Promise<void>(resolve => (release = resolve));
|
|
3191
3424
|
this.#admitted.add(admission);
|
|
@@ -3208,6 +3441,15 @@ export class Broker {
|
|
|
3208
3441
|
if (spawnClose) return spawnClose;
|
|
3209
3442
|
}
|
|
3210
3443
|
if (operation === "session.control") return this.#sessionControl(input, idempotencyKey);
|
|
3444
|
+
if (operation === "session.lookup") {
|
|
3445
|
+
const lookup = publicLifecycleLookupInput(input);
|
|
3446
|
+
if (isBrokerResponse(lookup)) return lookup;
|
|
3447
|
+
return await this.#lookupLifecycle(
|
|
3448
|
+
lookup.operation,
|
|
3449
|
+
idempotencyKey,
|
|
3450
|
+
lifecycleFingerprint(lookup.operation, lookup.target),
|
|
3451
|
+
);
|
|
3452
|
+
}
|
|
3211
3453
|
const fingerprint = lifecycleFingerprint(operation, input);
|
|
3212
3454
|
const normalization = normalizeBrokerInput(operation, input);
|
|
3213
3455
|
if (isBrokerResponse(normalization)) return normalization;
|
|
@@ -3270,22 +3512,9 @@ export class Broker {
|
|
|
3270
3512
|
if (operation === "broker.lookup_lifecycle") {
|
|
3271
3513
|
const requestedOperation = typeof input.operation === "string" ? input.operation : undefined;
|
|
3272
3514
|
const requestedFingerprint = typeof input.fingerprint === "string" ? input.fingerprint : undefined;
|
|
3273
|
-
if (!
|
|
3515
|
+
if (!requestedOperation || !requestedFingerprint)
|
|
3274
3516
|
return error("invalid_input", "operation, idempotencyKey, and fingerprint are required");
|
|
3275
|
-
|
|
3276
|
-
return error("invalid_input", "session.spawn does not support lifecycle lookup");
|
|
3277
|
-
if (!LIFECYCLE_OPERATIONS.has(requestedOperation))
|
|
3278
|
-
return error("not_found", "lifecycle operation was not found");
|
|
3279
|
-
const identity = await deriveIdempotencyIdentity(this.settings.agentDir, requestedOperation, idempotencyKey);
|
|
3280
|
-
const entry = this.ledger.get(identity);
|
|
3281
|
-
if (!entry) return error("not_found", "lifecycle operation was not found");
|
|
3282
|
-
if (entry.fingerprint !== requestedFingerprint)
|
|
3283
|
-
return error("idempotency_conflict", "lifecycle request fingerprint differs");
|
|
3284
|
-
if (entry.state === "terminal_uncertain")
|
|
3285
|
-
return error("terminal_uncertain", "lifecycle outcome is still uncertain");
|
|
3286
|
-
return isBrokerResponse(entry.response)
|
|
3287
|
-
? entry.response
|
|
3288
|
-
: error("terminal_uncertain", "lifecycle outcome has no recorded response");
|
|
3517
|
+
return await this.#lookupLifecycle(requestedOperation, idempotencyKey, requestedFingerprint);
|
|
3289
3518
|
}
|
|
3290
3519
|
|
|
3291
3520
|
if (!idempotencyKey) return error("invalid_input", "idempotencyKey is required for lifecycle operations");
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { type BrokerDiscovery, readBrokerDiscovery, readBrokerRestartIntent } from "./discovery";
|
|
3
|
+
import { withBrokerStartupLock } from "./ensure";
|
|
4
|
+
import { observeProcessIncarnation } from "./process-incarnation";
|
|
5
|
+
import { resolveSdkInternalSpawnCommand } from "./runtime";
|
|
6
|
+
|
|
7
|
+
export interface AuthorizedBrokerSuccessorOptions {
|
|
8
|
+
agentDir: string;
|
|
9
|
+
requestId: string;
|
|
10
|
+
deadlineAt: number;
|
|
11
|
+
packageGeneration: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Typed outcome of a successor-launch attempt; never an opaque thrown message. */
|
|
15
|
+
export type AuthorizedBrokerSuccessorResult =
|
|
16
|
+
| { kind: "adopted"; discovery: BrokerDiscovery }
|
|
17
|
+
| { kind: "spawned"; discovery: BrokerDiscovery }
|
|
18
|
+
| {
|
|
19
|
+
kind: "refused";
|
|
20
|
+
reason:
|
|
21
|
+
| "deadline_elapsed"
|
|
22
|
+
| "intent_not_committed"
|
|
23
|
+
| "spawn_failed"
|
|
24
|
+
| "spawn_exited_before_publication"
|
|
25
|
+
| "publication_timeout";
|
|
26
|
+
detail?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const SUCCESSOR_SPAWN_POLL_MS = 25;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Launches (or adopts) exactly one authorized successor for `requestId`.
|
|
33
|
+
*
|
|
34
|
+
* Holds `withBrokerStartupLock` only around the decision to spawn and the
|
|
35
|
+
* detached `spawn()` call itself -- never across the successor's own
|
|
36
|
+
* publication wait. The successor process (`commands/sdk.ts`'s
|
|
37
|
+
* `broker-internal` branch) acquires that SAME lock during its own startup to
|
|
38
|
+
* serialize against ordinary concurrent starts; holding it here while
|
|
39
|
+
* blocking on that child's publication would deadlock the parent against its
|
|
40
|
+
* own child. The unguarded polling loop below runs strictly after the lock
|
|
41
|
+
* has been released.
|
|
42
|
+
*/
|
|
43
|
+
export async function launchAuthorizedBrokerSuccessor(
|
|
44
|
+
options: AuthorizedBrokerSuccessorOptions,
|
|
45
|
+
): Promise<AuthorizedBrokerSuccessorResult> {
|
|
46
|
+
const spawnOutcome = await withBrokerStartupLock(options.agentDir, async deadline => {
|
|
47
|
+
if (Date.now() >= Math.min(deadline, options.deadlineAt))
|
|
48
|
+
return { kind: "refused" as const, reason: "deadline_elapsed" as const };
|
|
49
|
+
// Only a durably COMMITTED intent for this exact requestId authorizes a
|
|
50
|
+
// spawn. A prepared-but-not-committed or absent intent means the owner
|
|
51
|
+
// side of the protocol never reached its commit boundary, so no successor
|
|
52
|
+
// may exist yet -- spawning here would double-launch a broker the owner
|
|
53
|
+
// might still be about to retire cleanly on its own.
|
|
54
|
+
const intent = await readBrokerRestartIntent(options.agentDir);
|
|
55
|
+
if (intent?.phase !== "committed" || intent.requestId !== options.requestId)
|
|
56
|
+
return { kind: "refused" as const, reason: "intent_not_committed" as const };
|
|
57
|
+
const existing = await readBrokerDiscovery(options.agentDir);
|
|
58
|
+
if (
|
|
59
|
+
existing &&
|
|
60
|
+
existing.packageGeneration === options.packageGeneration &&
|
|
61
|
+
existing.restartRequestId === options.requestId
|
|
62
|
+
)
|
|
63
|
+
return { kind: "adopted" as const, discovery: existing };
|
|
64
|
+
const command = resolveSdkInternalSpawnCommand("broker-internal");
|
|
65
|
+
let child: ChildProcess;
|
|
66
|
+
try {
|
|
67
|
+
child = spawn(command.file, [...command.args, "--agent-dir", options.agentDir], {
|
|
68
|
+
detached: true,
|
|
69
|
+
stdio: "ignore",
|
|
70
|
+
env: { ...command.env, GJC_BROKER_RESTART_REQUEST: options.requestId },
|
|
71
|
+
...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
|
|
72
|
+
});
|
|
73
|
+
} catch (spawnError) {
|
|
74
|
+
return {
|
|
75
|
+
kind: "refused" as const,
|
|
76
|
+
reason: "spawn_failed" as const,
|
|
77
|
+
detail: spawnError instanceof Error ? spawnError.message : String(spawnError),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
let spawnError: Error | undefined;
|
|
81
|
+
child.once("error", childError => {
|
|
82
|
+
spawnError = childError;
|
|
83
|
+
});
|
|
84
|
+
child.unref();
|
|
85
|
+
return { kind: "spawned" as const, child, spawnError: () => spawnError };
|
|
86
|
+
});
|
|
87
|
+
if (spawnOutcome.kind !== "spawned") return spawnOutcome;
|
|
88
|
+
const { child } = spawnOutcome;
|
|
89
|
+
const until = Math.min(Date.now() + Math.max(1, options.deadlineAt - Date.now()), options.deadlineAt);
|
|
90
|
+
for (;;) {
|
|
91
|
+
if (spawnOutcome.spawnError())
|
|
92
|
+
return {
|
|
93
|
+
kind: "refused",
|
|
94
|
+
reason: "spawn_failed",
|
|
95
|
+
detail: spawnOutcome.spawnError()?.message,
|
|
96
|
+
};
|
|
97
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
98
|
+
return { kind: "refused", reason: "spawn_exited_before_publication" };
|
|
99
|
+
const discovered = await readBrokerDiscovery(options.agentDir);
|
|
100
|
+
if (
|
|
101
|
+
discovered &&
|
|
102
|
+
discovered.packageGeneration === options.packageGeneration &&
|
|
103
|
+
discovered.restartRequestId === options.requestId
|
|
104
|
+
)
|
|
105
|
+
return { kind: "spawned", discovery: discovered };
|
|
106
|
+
if (Date.now() >= until) return { kind: "refused", reason: "publication_timeout" };
|
|
107
|
+
await Bun.sleep(SUCCESSOR_SPAWN_POLL_MS);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Positive OS-confirmed absence of the exact prior owner, distinguished from
|
|
113
|
+
* every merely-inconclusive outcome (permission denial, PID reuse ambiguity,
|
|
114
|
+
* missing native addon). Never treats a null/changed incarnation read alone as
|
|
115
|
+
* death proof; only `observeProcessIncarnation`'s `"absent"` status counts.
|
|
116
|
+
*/
|
|
117
|
+
export function oldOwnerConfirmedExited(pid: number): boolean {
|
|
118
|
+
return observeProcessIncarnation(pid).status === "absent";
|
|
119
|
+
}
|