@gajae-code/coding-agent 0.18.10 → 0.18.12
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 +295 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/update-cli.d.ts +14 -1
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +5 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +30 -0
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +39 -0
- package/dist/types/config/file-lock.d.ts +8 -1
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -6
- package/dist/types/config/model-registry.d.ts +56 -5
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +46 -3
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/modes/patch.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +10 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/types.d.ts +15 -3
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
- package/dist/types/sdk/broker/broker.d.ts +7 -1
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +136 -7
- package/dist/types/sdk/broker/hop.d.ts +36 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +4 -1
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +23 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.d.ts +32 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +58 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +13 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
- package/dist/types/tools/descriptors.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +22 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +27 -19
- package/src/capability/index.ts +2 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +7 -5
- package/src/cli/public-command-errors.ts +4 -0
- package/src/cli/root-flags.ts +2 -2
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +224 -7
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +199 -42
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/message.ts +10 -3
- package/src/config/autorouting-tier-map.ts +30 -0
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +734 -0
- package/src/config/file-lock.ts +216 -56
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +716 -113
- package/src/config/model-registry.ts +1334 -158
- package/src/config/model-resolver.ts +3 -1
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +60 -3
- package/src/config/settings.ts +458 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/crash/sanitize.ts +4 -0
- package/src/customization/import.ts +3 -1
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/debug/crash-diagnostics.ts +2 -1
- package/src/debug/report-bundle.ts +19 -0
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/helpers.ts +1 -2
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +13 -17
- package/src/edit/modes/patch.ts +187 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/export/html/index.ts +46 -6
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +8 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/gjc-runtime/gc-runtime.ts +33 -6
- package/src/gjc-runtime/launch-tmux.ts +7 -2
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/hooks/native-skill-hook.ts +14 -11
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/local-protocol.ts +142 -8
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/migrate/action-planner.ts +57 -0
- package/src/migrate/adapters/claude-code.ts +23 -4
- package/src/migrate/adapters/codex.ts +22 -4
- package/src/migrate/adapters/index.ts +22 -4
- package/src/migrate/adapters/opencode.ts +6 -1
- package/src/migrate/executor.ts +84 -7
- package/src/migrate/report.ts +19 -2
- package/src/migrate/types.ts +14 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/controllers/command-controller.ts +38 -2
- package/src/modes/controllers/plan-mode-controller.ts +17 -7
- package/src/modes/controllers/selector-controller.ts +62 -6
- package/src/modes/interactive-mode.ts +68 -0
- package/src/modes/theme/theme.ts +15 -0
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +32 -0
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +126 -17
- package/src/sdk/broker/broker-exit.ts +8 -5
- package/src/sdk/broker/broker.ts +342 -82
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1143 -265
- package/src/sdk/broker/hop.ts +274 -0
- package/src/sdk/broker/lifecycle-ledger.ts +35 -11
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +62 -12
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/index.ts +52 -21
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/cli/session-cli.ts +21 -6
- package/src/sdk/host/session-runtime.test.ts +118 -23
- package/src/sdk/host/session-runtime.ts +352 -47
- package/src/sdk/lifecycle/service.ts +1 -0
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/protocol/adapter-validation.ts +53 -11
- package/src/sdk/protocol/operation-inventory.generated.json +101 -2
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +114 -61
- package/src/session/agent-session.ts +1043 -81
- package/src/session/internal/managed-session-scope.ts +126 -39
- package/src/session/internal/managed-session-storage.ts +14 -11
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +236 -14
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +491 -32
- package/src/slash-commands/builtin-registry.ts +15 -3
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/task/executor.ts +103 -12
- package/src/task/index.ts +389 -175
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +3 -0
- package/src/tools/bash-allowed-prefixes.ts +153 -5
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash.ts +14 -0
- package/src/tools/debug.ts +7 -6
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +34 -2
- package/src/tools/fetch.ts +3 -2
- package/src/tools/index.ts +2 -0
- package/src/tools/output-meta.ts +23 -2
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +17 -35
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +108 -1
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +31 -3
- package/src/tools/write.ts +35 -3
- package/src/utils/commit-message-generator.ts +9 -2
- package/src/utils/file-mentions.ts +29 -1
- package/src/utils/title-generator.ts +7 -2
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -3,10 +3,15 @@ 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
|
|
6
|
+
import {
|
|
7
|
+
exactRemoveDirectoryTree,
|
|
8
|
+
type NativeBrokerRestartIntent,
|
|
9
|
+
type NativeDirectoryTreeSnapshot,
|
|
10
|
+
snapshotDirectoryTree,
|
|
11
|
+
} from "@gajae-code/natives";
|
|
7
12
|
import { logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
8
13
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
9
|
-
import { FileLockAcquireError } from "../../config/file-lock";
|
|
14
|
+
import { FileLockAcquireError, withFileLock } from "../../config/file-lock";
|
|
10
15
|
import type { ModelProfileErrorDetails } from "../../config/model-profile-contract";
|
|
11
16
|
import { planLaunchWorktree } from "../../gjc-runtime/launch-worktree";
|
|
12
17
|
import { readExistingStateForMutation, withWorkflowStateLock } from "../../gjc-runtime/state-writer";
|
|
@@ -80,6 +85,7 @@ import {
|
|
|
80
85
|
type LifecycleState,
|
|
81
86
|
type TerminalReadBack,
|
|
82
87
|
} from "./lifecycle-ledger";
|
|
88
|
+
import { brokerLockHostIdentity } from "./lock-host-identity";
|
|
83
89
|
import {
|
|
84
90
|
cancelManagedTasks,
|
|
85
91
|
createManagedDomainBinding,
|
|
@@ -111,8 +117,15 @@ import {
|
|
|
111
117
|
} from "./managed-task-dag";
|
|
112
118
|
import { reconcileRunningManagedVerification, verifyManagedTaskAttempt } from "./managed-task-verification";
|
|
113
119
|
import { createMasterCapabilityVerifier, readEndpoint } from "./master-capability";
|
|
120
|
+
import { isProcessIncarnation } from "./process-incarnation";
|
|
114
121
|
import { sdkInternalRuntimeImage } from "./runtime";
|
|
115
|
-
import {
|
|
122
|
+
import {
|
|
123
|
+
type IndexedSession,
|
|
124
|
+
isSessionAuthorityEligible,
|
|
125
|
+
SessionIndex,
|
|
126
|
+
type SessionList,
|
|
127
|
+
sessionIndexStartupLockOptions,
|
|
128
|
+
} from "./session-index";
|
|
116
129
|
import {
|
|
117
130
|
type ResolvedScopeV1,
|
|
118
131
|
resolveScopeRequest,
|
|
@@ -1088,6 +1101,13 @@ function sameSessionControlAuthority(
|
|
|
1088
1101
|
);
|
|
1089
1102
|
}
|
|
1090
1103
|
|
|
1104
|
+
const SESSION_TARGETED_LIFECYCLE_OPERATIONS: ReadonlySet<string> = new Set([
|
|
1105
|
+
"session.close",
|
|
1106
|
+
"session.resume",
|
|
1107
|
+
"session.delete",
|
|
1108
|
+
"session.reconcile_uncertain",
|
|
1109
|
+
]);
|
|
1110
|
+
|
|
1091
1111
|
function lifecycleTarget(operation: string, input: Record<string, unknown>): unknown {
|
|
1092
1112
|
const target = input.target as Record<string, unknown> | undefined;
|
|
1093
1113
|
const string = (...values: unknown[]): string | undefined =>
|
|
@@ -1163,10 +1183,29 @@ const BROKER_LOCK_RETRY_MS = 10;
|
|
|
1163
1183
|
type BrokerLockSnapshot = {
|
|
1164
1184
|
ownerId?: string;
|
|
1165
1185
|
pid: number;
|
|
1186
|
+
incarnation?: string;
|
|
1187
|
+
ownerHostId?: string;
|
|
1166
1188
|
identity: string;
|
|
1167
1189
|
lockIdentity: string;
|
|
1168
1190
|
};
|
|
1169
1191
|
|
|
1192
|
+
function isBrokerLockOwnerAlive(
|
|
1193
|
+
snapshot: Pick<BrokerLockSnapshot, "pid" | "incarnation" | "ownerHostId">,
|
|
1194
|
+
currentHostId: string | undefined,
|
|
1195
|
+
pidAlive: (pid: number) => boolean = isPidAlive,
|
|
1196
|
+
readProcessIncarnation: (pid: number) => string | undefined = brokerProcessIncarnation,
|
|
1197
|
+
): boolean {
|
|
1198
|
+
if (snapshot.pid <= 0) return false;
|
|
1199
|
+
// Local PID evidence is meaningful only when the owner record identifies this host.
|
|
1200
|
+
// Foreign and legacy host-unqualified owners are never reclaimed from this process table.
|
|
1201
|
+
if (snapshot.ownerHostId === undefined || snapshot.ownerHostId !== currentHostId) return true;
|
|
1202
|
+
if (!pidAlive(snapshot.pid)) return false;
|
|
1203
|
+
// A live PID is retained unless its recorded incarnation proves reuse.
|
|
1204
|
+
if (!snapshot.incarnation) return true;
|
|
1205
|
+
const incarnation = readProcessIncarnation(snapshot.pid);
|
|
1206
|
+
return !incarnation || incarnation === snapshot.incarnation;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1170
1209
|
/** Tombstone prefix used by {@link Broker.reclaimStaleLock} when a dead owner's lock is renamed aside. */
|
|
1171
1210
|
export const BROKER_LOCK_TOMBSTONE_PREFIX = ".broker.lock.stale-";
|
|
1172
1211
|
|
|
@@ -1214,11 +1253,12 @@ function isBrokerLockArtifactName(name: string): boolean {
|
|
|
1214
1253
|
* Decide whether one candidate directory is provably abandoned.
|
|
1215
1254
|
*
|
|
1216
1255
|
* Fail-closed by construction: every branch that cannot prove abandonment
|
|
1217
|
-
* returns a retention reason. A tombstone with an owner record is
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
*
|
|
1256
|
+
* returns a retention reason. A tombstone with an owner record is abandoned
|
|
1257
|
+
* only when its PID is dead or validated incarnation evidence proves PID reuse;
|
|
1258
|
+
* missing or unavailable evidence remains retained. A missing record is
|
|
1259
|
+
* abandoned only when the aged tombstone is empty; non-empty tombstones remain
|
|
1260
|
+
* retained as ambiguous evidence. Backup directories carry no owner contract,
|
|
1261
|
+
* so an absent record there is not ambiguity and age alone governs.
|
|
1222
1262
|
*/
|
|
1223
1263
|
async function classifyBrokerLockArtifact(
|
|
1224
1264
|
directory: string,
|
|
@@ -1226,6 +1266,8 @@ async function classifyBrokerLockArtifact(
|
|
|
1226
1266
|
now: number,
|
|
1227
1267
|
graceMs: number,
|
|
1228
1268
|
pidAlive: (pid: number) => boolean,
|
|
1269
|
+
readProcessIncarnation: (pid: number) => string | undefined,
|
|
1270
|
+
currentHostId: string | undefined,
|
|
1229
1271
|
): Promise<BrokerLockArtifactRetentionReason | "abandoned"> {
|
|
1230
1272
|
const target = path.join(directory, name);
|
|
1231
1273
|
// lstat, never stat: a symlink pointing at live state must never be followed
|
|
@@ -1248,14 +1290,26 @@ async function classifyBrokerLockArtifact(
|
|
|
1248
1290
|
return "owner-record-unreadable";
|
|
1249
1291
|
}
|
|
1250
1292
|
}
|
|
1251
|
-
let
|
|
1293
|
+
let owner: { pid?: unknown; incarnation?: unknown; ownerHostId?: unknown };
|
|
1252
1294
|
try {
|
|
1253
|
-
|
|
1295
|
+
owner = JSON.parse(raw) as { pid?: unknown; incarnation?: unknown; ownerHostId?: unknown };
|
|
1254
1296
|
} catch {
|
|
1255
1297
|
return "owner-record-unreadable";
|
|
1256
1298
|
}
|
|
1257
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
1258
|
-
|
|
1299
|
+
if (typeof owner.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0)
|
|
1300
|
+
return "owner-record-unreadable";
|
|
1301
|
+
return isBrokerLockOwnerAlive(
|
|
1302
|
+
{
|
|
1303
|
+
pid: owner.pid,
|
|
1304
|
+
incarnation: isProcessIncarnation(owner.incarnation) ? owner.incarnation : undefined,
|
|
1305
|
+
ownerHostId: typeof owner.ownerHostId === "string" ? owner.ownerHostId : undefined,
|
|
1306
|
+
},
|
|
1307
|
+
currentHostId,
|
|
1308
|
+
pidAlive,
|
|
1309
|
+
readProcessIncarnation,
|
|
1310
|
+
)
|
|
1311
|
+
? "owner-alive"
|
|
1312
|
+
: "abandoned";
|
|
1259
1313
|
}
|
|
1260
1314
|
|
|
1261
1315
|
/**
|
|
@@ -1274,11 +1328,18 @@ export async function reapStaleBrokerLockArtifacts(input: {
|
|
|
1274
1328
|
now?: number;
|
|
1275
1329
|
graceMs?: number;
|
|
1276
1330
|
pidAlive?: (pid: number) => boolean;
|
|
1331
|
+
processIncarnation?: (pid: number) => string | undefined;
|
|
1277
1332
|
}): Promise<BrokerLockArtifactReapResult> {
|
|
1278
1333
|
const directory = path.join(input.agentDir, "sdk");
|
|
1279
1334
|
const now = input.now ?? Date.now();
|
|
1280
1335
|
const graceMs = input.graceMs ?? BROKER_LOCK_ARTIFACT_GRACE_MS;
|
|
1281
1336
|
const pidAlive = input.pidAlive ?? isPidAlive;
|
|
1337
|
+
const readProcessIncarnation = input.processIncarnation ?? brokerProcessIncarnation;
|
|
1338
|
+
// Without this host's identity no host-qualified owner can be matched, so all of them are retained.
|
|
1339
|
+
const currentHostId = await brokerLockHostIdentity(input.agentDir).then(
|
|
1340
|
+
identity => identity.ownerHostId,
|
|
1341
|
+
() => undefined,
|
|
1342
|
+
);
|
|
1282
1343
|
const removed: string[] = [];
|
|
1283
1344
|
const retained: BrokerLockArtifactRetention[] = [];
|
|
1284
1345
|
const retainedByReason = new Map<BrokerLockArtifactRetentionReason, number>();
|
|
@@ -1295,7 +1356,15 @@ export async function reapStaleBrokerLockArtifacts(input: {
|
|
|
1295
1356
|
const target = path.join(directory, name);
|
|
1296
1357
|
let verdict: BrokerLockArtifactRetentionReason | "abandoned";
|
|
1297
1358
|
try {
|
|
1298
|
-
verdict = await classifyBrokerLockArtifact(
|
|
1359
|
+
verdict = await classifyBrokerLockArtifact(
|
|
1360
|
+
directory,
|
|
1361
|
+
name,
|
|
1362
|
+
now,
|
|
1363
|
+
graceMs,
|
|
1364
|
+
pidAlive,
|
|
1365
|
+
readProcessIncarnation,
|
|
1366
|
+
currentHostId,
|
|
1367
|
+
);
|
|
1299
1368
|
} catch (error) {
|
|
1300
1369
|
// A vanished candidate needs no decision; anything else is ambiguous.
|
|
1301
1370
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
@@ -1528,6 +1597,10 @@ const terminalPersistenceHooksForTest = new WeakMap<Broker, () => void>();
|
|
|
1528
1597
|
const ambiguityGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
1529
1598
|
const publicationObservationOverridesForTest = new WeakMap<Broker, BrokerPublicationObservation>();
|
|
1530
1599
|
const lockArtifactGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
1600
|
+
const brokerLockReclaimHooksForTest = new WeakMap<
|
|
1601
|
+
Broker,
|
|
1602
|
+
{ beforeQuarantine?: () => Promise<void>; onContended?: () => void }
|
|
1603
|
+
>();
|
|
1531
1604
|
const livenessGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
1532
1605
|
const heartbeatStallOverridesForTest = new WeakMap<Broker, PromiseWithResolvers<void>>();
|
|
1533
1606
|
const managedCloseWaitOverridesForTest = new WeakMap<Broker, number>();
|
|
@@ -1539,6 +1612,8 @@ export class Broker {
|
|
|
1539
1612
|
discovery: BrokerDiscovery | null = null;
|
|
1540
1613
|
#lock: string;
|
|
1541
1614
|
#owner = randomBytes(12).toString("hex");
|
|
1615
|
+
#ownedLockIdentity: string | undefined;
|
|
1616
|
+
#ownedLockIncarnation: string | undefined;
|
|
1542
1617
|
/** Public publication-incarnation id, fixed at startup; never the endpoint generation. */
|
|
1543
1618
|
#diagnosticGeneration = randomBytes(16).toString("hex");
|
|
1544
1619
|
/** Trusted build snapshot captured at startup; unknown buildId stays null. */
|
|
@@ -3663,7 +3738,12 @@ export class Broker {
|
|
|
3663
3738
|
}
|
|
3664
3739
|
async #lockSnapshot(raw: string, lockIdentity: string): Promise<BrokerLockSnapshot> {
|
|
3665
3740
|
try {
|
|
3666
|
-
const lock = JSON.parse(raw) as {
|
|
3741
|
+
const lock = JSON.parse(raw) as {
|
|
3742
|
+
ownerId?: unknown;
|
|
3743
|
+
pid?: unknown;
|
|
3744
|
+
incarnation?: unknown;
|
|
3745
|
+
ownerHostId?: unknown;
|
|
3746
|
+
};
|
|
3667
3747
|
if (
|
|
3668
3748
|
typeof lock.ownerId === "string" &&
|
|
3669
3749
|
lock.ownerId.length > 0 &&
|
|
@@ -3671,7 +3751,14 @@ export class Broker {
|
|
|
3671
3751
|
Number.isInteger(lock.pid) &&
|
|
3672
3752
|
lock.pid > 0
|
|
3673
3753
|
)
|
|
3674
|
-
return {
|
|
3754
|
+
return {
|
|
3755
|
+
ownerId: lock.ownerId,
|
|
3756
|
+
pid: lock.pid,
|
|
3757
|
+
incarnation: isProcessIncarnation(lock.incarnation) ? lock.incarnation : undefined,
|
|
3758
|
+
ownerHostId: typeof lock.ownerHostId === "string" ? lock.ownerHostId : undefined,
|
|
3759
|
+
identity: `owner:${lock.ownerId}`,
|
|
3760
|
+
lockIdentity,
|
|
3761
|
+
};
|
|
3675
3762
|
} catch {}
|
|
3676
3763
|
return { pid: 0, identity: `contents:${createHash("sha256").update(raw).digest("hex")}`, lockIdentity };
|
|
3677
3764
|
}
|
|
@@ -3703,35 +3790,103 @@ export class Broker {
|
|
|
3703
3790
|
}
|
|
3704
3791
|
return this.#lockSnapshot(raw, lockIdentity);
|
|
3705
3792
|
}
|
|
3706
|
-
async #
|
|
3793
|
+
async #withLockReclaimGuard<T>(operation: () => Promise<T>): Promise<T> {
|
|
3794
|
+
const hostIdentity = await brokerLockHostIdentity(this.settings.agentDir);
|
|
3795
|
+
return withFileLock(`${this.#lock}.reclaim`, operation, {
|
|
3796
|
+
...hostIdentity,
|
|
3797
|
+
onContended: () => brokerLockReclaimHooksForTest.get(this)?.onContended?.(),
|
|
3798
|
+
});
|
|
3799
|
+
}
|
|
3800
|
+
async #hasCanonicalLockOwnership(): Promise<boolean> {
|
|
3801
|
+
if (!this.#ownedLockIdentity || !this.#ownedLockIncarnation) return false;
|
|
3802
|
+
try {
|
|
3803
|
+
const current = await this.#readLock();
|
|
3804
|
+
return (
|
|
3805
|
+
current?.identity === `owner:${this.#owner}` &&
|
|
3806
|
+
current.lockIdentity === this.#ownedLockIdentity &&
|
|
3807
|
+
current.pid === process.pid &&
|
|
3808
|
+
current.incarnation === this.#ownedLockIncarnation
|
|
3809
|
+
);
|
|
3810
|
+
} catch {
|
|
3811
|
+
return false;
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
#removeCreatedLockGeneration(
|
|
3815
|
+
lockIdentity: { dev: string; ino: string },
|
|
3816
|
+
parentIdentity: { dev: bigint; ino: bigint },
|
|
3817
|
+
ownerHash?: string,
|
|
3818
|
+
): void {
|
|
3819
|
+
const captured = snapshotDirectoryTree(this.#lock);
|
|
3820
|
+
const snapshot = captured.snapshot;
|
|
3821
|
+
const root = snapshot?.entries.find(entry => entry.relativePath === "");
|
|
3822
|
+
if (
|
|
3823
|
+
!captured.ok ||
|
|
3824
|
+
!snapshot ||
|
|
3825
|
+
root?.kind !== "directory" ||
|
|
3826
|
+
snapshot.rootDev !== lockIdentity.dev ||
|
|
3827
|
+
snapshot.rootIno !== lockIdentity.ino
|
|
3828
|
+
)
|
|
3829
|
+
return;
|
|
3830
|
+
|
|
3831
|
+
const owner = snapshot.entries.find(entry => entry.relativePath === BROKER_LOCK_RECORD);
|
|
3832
|
+
const exactOwner =
|
|
3833
|
+
ownerHash !== undefined &&
|
|
3834
|
+
snapshot.entries.length === 2 &&
|
|
3835
|
+
owner?.kind === "file" &&
|
|
3836
|
+
owner.sha256 === ownerHash;
|
|
3837
|
+
if (!exactOwner && snapshot.entries.length !== 1) return;
|
|
3838
|
+
|
|
3839
|
+
exactRemoveDirectoryTree(this.#lock, snapshot, parentIdentity);
|
|
3840
|
+
}
|
|
3841
|
+
async #createLockInsideGuard(): Promise<void> {
|
|
3842
|
+
const rootDigest = await canonicalServiceRootDigest(this.settings.agentDir);
|
|
3843
|
+
const hostIdentity = await brokerLockHostIdentity(this.settings.agentDir);
|
|
3844
|
+
const parent = await fs.lstat(path.dirname(this.#lock), { bigint: true });
|
|
3707
3845
|
await fs.mkdir(this.#lock, { mode: 0o700 });
|
|
3846
|
+
let createdLockIdentity: { dev: string; ino: string } | undefined;
|
|
3847
|
+
let createdOwnerHash: string | undefined;
|
|
3708
3848
|
try {
|
|
3849
|
+
const lockStat = await fs.lstat(this.#lock, { bigint: true });
|
|
3850
|
+
if (!lockStat.isDirectory()) throw new Error("SDK broker lock is not a directory.");
|
|
3851
|
+
createdLockIdentity = { dev: String(lockStat.dev), ino: String(lockStat.ino) };
|
|
3709
3852
|
const incarnation = brokerProcessIncarnation(process.pid);
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3853
|
+
if (!incarnation) throw new Error("Broker process incarnation is unavailable.");
|
|
3854
|
+
const ownerRecord = JSON.stringify({
|
|
3855
|
+
version: 1,
|
|
3856
|
+
ownerId: this.#owner,
|
|
3857
|
+
pid: process.pid,
|
|
3858
|
+
acquiredAt: Date.now(),
|
|
3859
|
+
incarnation,
|
|
3860
|
+
ownerHostId: hostIdentity.ownerHostId,
|
|
3861
|
+
rootDigest,
|
|
3862
|
+
});
|
|
3863
|
+
createdOwnerHash = createHash("sha256").update(ownerRecord).digest("hex");
|
|
3864
|
+
await fs.writeFile(this.#lockRecordPath(), ownerRecord, { flag: "wx", mode: 0o600 });
|
|
3865
|
+
const current = await this.#readLock();
|
|
3866
|
+
if (
|
|
3867
|
+
!current ||
|
|
3868
|
+
current.ownerId !== this.#owner ||
|
|
3869
|
+
current.pid !== process.pid ||
|
|
3870
|
+
current.incarnation !== incarnation
|
|
3871
|
+
)
|
|
3872
|
+
throw new Error("SDK broker lock ownership changed during startup.");
|
|
3873
|
+
this.#ownedLockIdentity = current.lockIdentity;
|
|
3874
|
+
this.#ownedLockIncarnation = incarnation;
|
|
3875
|
+
} catch (error) {
|
|
3729
3876
|
try {
|
|
3730
|
-
|
|
3877
|
+
if (createdLockIdentity)
|
|
3878
|
+
this.#removeCreatedLockGeneration(
|
|
3879
|
+
createdLockIdentity,
|
|
3880
|
+
{ dev: parent.dev, ino: parent.ino },
|
|
3881
|
+
createdOwnerHash,
|
|
3882
|
+
);
|
|
3731
3883
|
} catch {}
|
|
3732
|
-
throw
|
|
3884
|
+
throw error;
|
|
3733
3885
|
}
|
|
3734
3886
|
}
|
|
3887
|
+
async #createLock(): Promise<void> {
|
|
3888
|
+
await this.#withLockReclaimGuard(() => this.#createLockInsideGuard());
|
|
3889
|
+
}
|
|
3735
3890
|
async #waitForBrokerDiscovery(): Promise<BrokerDiscovery | null> {
|
|
3736
3891
|
const deadline = Date.now() + BROKER_LOCK_STARTUP_WAIT_MS;
|
|
3737
3892
|
while (Date.now() < deadline) {
|
|
@@ -3741,50 +3896,63 @@ export class Broker {
|
|
|
3741
3896
|
}
|
|
3742
3897
|
return readBrokerDiscovery(this.settings.agentDir, this.settings.heartbeatTtlMs);
|
|
3743
3898
|
}
|
|
3744
|
-
async #reclaimStaleLock(snapshot: BrokerLockSnapshot): Promise<
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3899
|
+
async #reclaimStaleLock(snapshot: BrokerLockSnapshot): Promise<boolean> {
|
|
3900
|
+
return await this.#withLockReclaimGuard(async () => {
|
|
3901
|
+
const current = await this.#readLock();
|
|
3902
|
+
const hostIdentity = await brokerLockHostIdentity(this.settings.agentDir);
|
|
3903
|
+
if (
|
|
3904
|
+
!current ||
|
|
3905
|
+
current.identity !== snapshot.identity ||
|
|
3906
|
+
current.lockIdentity !== snapshot.lockIdentity ||
|
|
3907
|
+
isBrokerLockOwnerAlive(current, hostIdentity.ownerHostId)
|
|
3908
|
+
)
|
|
3909
|
+
return false;
|
|
3910
|
+
await brokerLockReclaimHooksForTest.get(this)?.beforeQuarantine?.();
|
|
3911
|
+
|
|
3912
|
+
// Keep validation, quarantine, and successor lock publication under one
|
|
3913
|
+
// cross-process fence so a stale snapshot can never move a newer lock.
|
|
3914
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3915
|
+
const tombstone = path.join(
|
|
3916
|
+
path.dirname(this.#lock),
|
|
3917
|
+
`.broker.lock.stale-${createHash("sha256").update(snapshot.lockIdentity).digest("hex")}-${randomBytes(8).toString("hex")}`,
|
|
3918
|
+
);
|
|
3919
|
+
try {
|
|
3920
|
+
await fs.rename(this.#lock, tombstone);
|
|
3921
|
+
} catch (error) {
|
|
3922
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
3923
|
+
if (code === "EEXIST" || code === "ENOTEMPTY") continue;
|
|
3924
|
+
if (["ENOENT", "EISDIR", "ENOTDIR"].includes(code ?? "")) return false;
|
|
3925
|
+
if (code === "EPERM") {
|
|
3926
|
+
try {
|
|
3927
|
+
await fs.lstat(tombstone);
|
|
3928
|
+
continue;
|
|
3929
|
+
} catch (statError) {
|
|
3930
|
+
if ((statError as NodeJS.ErrnoException).code !== "ENOENT") throw statError;
|
|
3931
|
+
}
|
|
3777
3932
|
}
|
|
3933
|
+
throw error;
|
|
3934
|
+
}
|
|
3935
|
+
try {
|
|
3936
|
+
await this.#createLockInsideGuard();
|
|
3937
|
+
return true;
|
|
3938
|
+
} catch (error) {
|
|
3939
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
|
|
3940
|
+
throw error;
|
|
3778
3941
|
}
|
|
3779
|
-
throw e;
|
|
3780
3942
|
}
|
|
3781
|
-
|
|
3782
|
-
|
|
3943
|
+
throw new Error(`Broker lock quarantine namespace is saturated for ${this.#lock}`);
|
|
3944
|
+
});
|
|
3783
3945
|
}
|
|
3784
3946
|
async #releaseOwnedLock(): Promise<void> {
|
|
3785
3947
|
try {
|
|
3786
3948
|
const lock = await this.#readLock();
|
|
3787
|
-
if (
|
|
3949
|
+
if (
|
|
3950
|
+
!lock ||
|
|
3951
|
+
lock.ownerId !== this.#owner ||
|
|
3952
|
+
lock.lockIdentity !== this.#ownedLockIdentity ||
|
|
3953
|
+
lock.incarnation !== this.#ownedLockIncarnation
|
|
3954
|
+
)
|
|
3955
|
+
return;
|
|
3788
3956
|
await fs.unlink(this.#lockRecordPath());
|
|
3789
3957
|
await fs.rmdir(this.#lock);
|
|
3790
3958
|
} catch (error) {
|
|
@@ -3833,6 +4001,8 @@ export class Broker {
|
|
|
3833
4001
|
this.#ambiguousAt = null;
|
|
3834
4002
|
this.#publishedAt = null;
|
|
3835
4003
|
this.#startedAt = process.hrtime.bigint();
|
|
4004
|
+
this.#ownedLockIdentity = undefined;
|
|
4005
|
+
this.#ownedLockIncarnation = undefined;
|
|
3836
4006
|
this.#watchInFlight = false;
|
|
3837
4007
|
this.#checkpointInFlight = false;
|
|
3838
4008
|
this.#throwIfStartupAborted();
|
|
@@ -3861,7 +4031,8 @@ export class Broker {
|
|
|
3861
4031
|
}
|
|
3862
4032
|
const snapshot = await this.#readLock();
|
|
3863
4033
|
if (!snapshot) continue;
|
|
3864
|
-
|
|
4034
|
+
const hostIdentity = await brokerLockHostIdentity(this.settings.agentDir);
|
|
4035
|
+
if (isBrokerLockOwnerAlive(snapshot, hostIdentity.ownerHostId)) {
|
|
3865
4036
|
const starting = await this.#waitForBrokerDiscovery();
|
|
3866
4037
|
if (starting) {
|
|
3867
4038
|
logger.info(
|
|
@@ -3872,22 +4043,59 @@ export class Broker {
|
|
|
3872
4043
|
return starting;
|
|
3873
4044
|
}
|
|
3874
4045
|
const current = await this.#readLock();
|
|
3875
|
-
if (
|
|
4046
|
+
if (
|
|
4047
|
+
current &&
|
|
4048
|
+
current.identity === snapshot.identity &&
|
|
4049
|
+
isBrokerLockOwnerAlive(current, hostIdentity.ownerHostId)
|
|
4050
|
+
) {
|
|
4051
|
+
const hostUnqualified = current.ownerHostId === undefined;
|
|
4052
|
+
const foreignHostOwner = !hostUnqualified && current.ownerHostId !== hostIdentity.ownerHostId;
|
|
4053
|
+
let refusal: string;
|
|
4054
|
+
let errorMessage: string;
|
|
4055
|
+
if (hostUnqualified) {
|
|
4056
|
+
refusal = `owner pid ${current.pid} has no host identity; automatic recovery is unsafe for shared agent directories. Stop every broker using this agent directory on all hosts, then follow https://github.com/Yeachan-Heo/gajae-code/blob/dev/docs/sdk.md#recovering-legacy-broker-locks`;
|
|
4057
|
+
errorMessage = `Broker lock owner is host-unqualified (pid ${current.pid}); refusing automatic recovery because its host cannot be verified. Stop every broker using this agent directory on all hosts and follow https://github.com/Yeachan-Heo/gajae-code/blob/dev/docs/sdk.md#recovering-legacy-broker-locks`;
|
|
4058
|
+
} else if (foreignHostOwner) {
|
|
4059
|
+
refusal = `owner pid ${current.pid} is attributed to another host; its liveness cannot be verified here`;
|
|
4060
|
+
errorMessage = `Broker lock owner is attributed to a different host (pid ${current.pid}); refusing automatic recovery because its liveness cannot be verified from this host.`;
|
|
4061
|
+
} else {
|
|
4062
|
+
refusal = `live pid ${current.pid} has published no discovery record`;
|
|
4063
|
+
errorMessage = `Broker lock is held by a live owner (pid ${current.pid})`;
|
|
4064
|
+
}
|
|
3876
4065
|
logger.warn(
|
|
3877
|
-
`sdk broker: lock contention, refusing to start because ${this.#lock} is held by
|
|
4066
|
+
`sdk broker: lock contention, refusing to start because ${this.#lock} is held by ${refusal}`,
|
|
3878
4067
|
);
|
|
3879
|
-
throw new Error(
|
|
4068
|
+
throw new Error(errorMessage);
|
|
3880
4069
|
}
|
|
3881
4070
|
continue;
|
|
3882
4071
|
}
|
|
3883
|
-
await this.#reclaimStaleLock(snapshot);
|
|
4072
|
+
if (await this.#reclaimStaleLock(snapshot)) break;
|
|
3884
4073
|
}
|
|
3885
4074
|
this.#throwIfStartupAborted();
|
|
3886
4075
|
// Only the lock holder reaps, so concurrent brokers cannot race the removal.
|
|
3887
4076
|
await this.#reapLockArtifacts();
|
|
3888
4077
|
try {
|
|
3889
4078
|
this.#throwIfStartupAborted();
|
|
3890
|
-
|
|
4079
|
+
// Recheck under the broker ownership lock: a restart can be committed while
|
|
4080
|
+
// an ordinary startup is waiting to acquire this lock.
|
|
4081
|
+
const restartIntent = await readBrokerRestartIntent(this.settings.agentDir);
|
|
4082
|
+
const activeCommittedIntent =
|
|
4083
|
+
restartIntent?.phase === "committed" && restartIntent.expiresAt > Date.now() ? restartIntent : undefined;
|
|
4084
|
+
if (
|
|
4085
|
+
this.settings.restartRequestId === undefined
|
|
4086
|
+
? activeCommittedIntent !== undefined
|
|
4087
|
+
: activeCommittedIntent?.requestId !== this.settings.restartRequestId
|
|
4088
|
+
)
|
|
4089
|
+
throw new Error("A committed SDK broker restart is active; startup requires its exact request ID.");
|
|
4090
|
+
await this.index.open(
|
|
4091
|
+
this.#startupCheckpointDeadline === undefined
|
|
4092
|
+
? undefined
|
|
4093
|
+
: sessionIndexStartupLockOptions(
|
|
4094
|
+
this.#startupCheckpointDeadline,
|
|
4095
|
+
this.#startupAbortSignal,
|
|
4096
|
+
this.settings.restartRequestId === undefined ? "ordinary" : "authorized-successor",
|
|
4097
|
+
),
|
|
4098
|
+
);
|
|
3891
4099
|
await this.ledger.open();
|
|
3892
4100
|
const brokerIdentityKey = await getBrokerIdentityKey(this.settings.agentDir);
|
|
3893
4101
|
this.#spawnAuthority = new SpawnAuthorityStore(this.settings.agentDir, brokerIdentityKey);
|
|
@@ -3950,6 +4158,8 @@ export class Broker {
|
|
|
3950
4158
|
await this.#startupPrePublicationTestHook?.();
|
|
3951
4159
|
if (this.#startupPrePublicationDelayMs > 0) await Bun.sleep(this.#startupPrePublicationDelayMs);
|
|
3952
4160
|
this.#throwIfStartupAborted();
|
|
4161
|
+
if (!(await this.#hasCanonicalLockOwnership()))
|
|
4162
|
+
throw new Error("SDK broker lock ownership changed before discovery publication.");
|
|
3953
4163
|
this.#publication = await publishBrokerDiscovery(
|
|
3954
4164
|
this.settings.agentDir,
|
|
3955
4165
|
this.discovery,
|
|
@@ -4851,7 +5061,19 @@ export class Broker {
|
|
|
4851
5061
|
}
|
|
4852
5062
|
|
|
4853
5063
|
// Wire requests are readiness-gated by BrokerTransport before reaching this dispatcher.
|
|
4854
|
-
handleRequest(
|
|
5064
|
+
async handleRequest(
|
|
5065
|
+
operation: string,
|
|
5066
|
+
input: Record<string, unknown>,
|
|
5067
|
+
idempotencyKey?: string,
|
|
5068
|
+
): Promise<BrokerResponse> {
|
|
5069
|
+
if (
|
|
5070
|
+
this.#publication !== null &&
|
|
5071
|
+
this.#publicationState === "healthy-owned" &&
|
|
5072
|
+
!(await this.#hasCanonicalLockOwnership())
|
|
5073
|
+
) {
|
|
5074
|
+
this.#fence("suspect-unpublished");
|
|
5075
|
+
return error("unavailable", "broker lock ownership is not held");
|
|
5076
|
+
}
|
|
4855
5077
|
if (operation === "broker.status") return Promise.resolve({ ok: true, result: this.status() });
|
|
4856
5078
|
if (operation === "broker.prepare_restart")
|
|
4857
5079
|
return this.prepareRestart(input as unknown as BrokerRestartPrepareOptions).then(
|
|
@@ -4876,6 +5098,32 @@ export class Broker {
|
|
|
4876
5098
|
this.#admitted.delete(admission);
|
|
4877
5099
|
});
|
|
4878
5100
|
}
|
|
5101
|
+
/**
|
|
5102
|
+
* Session-targeted lifecycle requests may pass terminal legacy rows recorded
|
|
5103
|
+
* for other sessions. A row equal to this key's legacy target identity for its
|
|
5104
|
+
* recorded session proves cross-target key reuse and keeps the fence; close
|
|
5105
|
+
* targets are reconstructed sessionId-only because legacy close rows may lack
|
|
5106
|
+
* endpoint authority.
|
|
5107
|
+
*/
|
|
5108
|
+
async #legacySessionTarget(
|
|
5109
|
+
operation: string,
|
|
5110
|
+
input: Record<string, unknown>,
|
|
5111
|
+
idempotencyKey: string,
|
|
5112
|
+
): Promise<{ sessionId: string; keyReuseIdentities: ReadonlySet<string> } | undefined> {
|
|
5113
|
+
if (!SESSION_TARGETED_LIFECYCLE_OPERATIONS.has(operation)) return undefined;
|
|
5114
|
+
const { sessionId } = lifecycleTarget(operation, input) as { sessionId?: string };
|
|
5115
|
+
if (sessionId === undefined) return undefined;
|
|
5116
|
+
const keyReuseIdentities = new Set<string>();
|
|
5117
|
+
for (const recordedSessionId of this.ledger.terminalLegacySessionIds()) {
|
|
5118
|
+
const recordedTarget = createHash("sha256")
|
|
5119
|
+
.update(canonicalJson(lifecycleTarget(operation, { sessionId: recordedSessionId })))
|
|
5120
|
+
.digest("hex");
|
|
5121
|
+
keyReuseIdentities.add(
|
|
5122
|
+
await deriveLegacyTargetIdentity(this.settings.agentDir, operation, idempotencyKey, recordedTarget),
|
|
5123
|
+
);
|
|
5124
|
+
}
|
|
5125
|
+
return { sessionId, keyReuseIdentities };
|
|
5126
|
+
}
|
|
4879
5127
|
async #handleRequest(
|
|
4880
5128
|
operation: string,
|
|
4881
5129
|
input: Record<string, unknown>,
|
|
@@ -5094,7 +5342,10 @@ export class Broker {
|
|
|
5094
5342
|
} else if (
|
|
5095
5343
|
this.ledger.hasLegacyIdentity(
|
|
5096
5344
|
new Set(this.#spawnAuthority?.claims().map(claim => claim.lifecycleIdentity) ?? []),
|
|
5097
|
-
{
|
|
5345
|
+
{
|
|
5346
|
+
ignoreFinalRows: operation === "session.create",
|
|
5347
|
+
sessionTarget: await this.#legacySessionTarget(operation, input, idempotencyKey),
|
|
5348
|
+
},
|
|
5098
5349
|
)
|
|
5099
5350
|
) {
|
|
5100
5351
|
return error("idempotency_conflict", "legacy lifecycle request has an ambiguous target");
|
|
@@ -5341,6 +5592,15 @@ export function setLockArtifactGraceForTest(broker: Broker, graceMs: number | un
|
|
|
5341
5592
|
else lockArtifactGraceOverridesForTest.set(broker, graceMs);
|
|
5342
5593
|
}
|
|
5343
5594
|
|
|
5595
|
+
/** Test-only barriers for exercising the cross-process broker-lock reclaim fence. */
|
|
5596
|
+
export function setBrokerLockReclaimHooksForTest(
|
|
5597
|
+
broker: Broker,
|
|
5598
|
+
hooks: { beforeQuarantine?: () => Promise<void>; onContended?: () => void } | undefined,
|
|
5599
|
+
): void {
|
|
5600
|
+
if (hooks === undefined) brokerLockReclaimHooksForTest.delete(broker);
|
|
5601
|
+
else brokerLockReclaimHooksForTest.set(broker, hooks);
|
|
5602
|
+
}
|
|
5603
|
+
|
|
5344
5604
|
/** Test-only hook for shortening the bounded liveness deadline. */
|
|
5345
5605
|
export function setLivenessGraceForTest(broker: Broker, graceMs: number | undefined): void {
|
|
5346
5606
|
if (graceMs === undefined) livenessGraceOverridesForTest.delete(broker);
|