@gajae-code/coding-agent 0.15.5 → 0.16.0
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 +85 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +120 -0
- package/dist/types/config/file-lock.d.ts +54 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +17 -5
- package/dist/types/config/settings-schema.d.ts +19 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +80 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +67 -1
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +24 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -3
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +671 -39
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +4 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +117 -16
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/launch.ts +86 -18
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +44 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +890 -99
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2795 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1044 -122
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +19 -1
- package/src/config/settings.ts +11 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +592 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +111 -42
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +1321 -147
- package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +66 -4
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +616 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +29 -10
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +51 -6
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +1671 -34
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +217 -46
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +276 -6
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +101 -37
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +581 -43
- package/src/sdk/host/session-runtime.ts +655 -101
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +53 -2
- package/src/sdk/protocol/operation-registry.ts +17 -4
- package/src/sdk/router/session-router.ts +294 -154
- package/src/sdk/session.ts +440 -167
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +1292 -180
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +342 -28
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +1268 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +1028 -109
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +11 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordinator directory scans that must not treat native exact-unlink debris
|
|
3
|
+
* as live JSON, and must not make start/status unreadable on a large dirent pile.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
|
|
8
|
+
class ProjectionScanRaceError extends Error {
|
|
9
|
+
readonly code = "PROJECTION_SCAN_RACED";
|
|
10
|
+
|
|
11
|
+
constructor(message: string) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ProjectionScanRaceError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PROJECTION_SCAN_UNSUPPORTED_CODE = "coordinator_projection_safe_read_unsupported";
|
|
18
|
+
|
|
19
|
+
class ProjectionScanUnsupportedError extends Error {
|
|
20
|
+
readonly code = PROJECTION_SCAN_UNSUPPORTED_CODE;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
super(PROJECTION_SCAN_UNSUPPORTED_CODE);
|
|
24
|
+
this.name = "ProjectionScanUnsupportedError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Test-only platform seam; production always follows the host platform. */
|
|
29
|
+
export const ProjectionScanTestHooks: {
|
|
30
|
+
platform?: NodeJS.Platform;
|
|
31
|
+
} = {};
|
|
32
|
+
|
|
33
|
+
function projectionPlatform(): NodeJS.Platform {
|
|
34
|
+
return ProjectionScanTestHooks.platform ?? process.platform;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Post-filter parse-candidate cap. Exhaustion returns an explicit incomplete result. */
|
|
38
|
+
export const COORDINATOR_JSON_SCAN_CAP = 10_000;
|
|
39
|
+
/** Number of fresh authoritative attempts allowed for a scan that observed churn. */
|
|
40
|
+
export const COORDINATOR_JSON_SCAN_RETRY_ATTEMPTS = 3;
|
|
41
|
+
|
|
42
|
+
export interface ProjectionScanStat {
|
|
43
|
+
size: number | bigint;
|
|
44
|
+
dev?: number | bigint;
|
|
45
|
+
ino?: number | bigint;
|
|
46
|
+
nlink?: number | bigint;
|
|
47
|
+
mtimeNs?: number | bigint;
|
|
48
|
+
ctimeNs?: number | bigint;
|
|
49
|
+
isDirectory?(): boolean;
|
|
50
|
+
isFile(): boolean;
|
|
51
|
+
isSymbolicLink(): boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ProjectionScanFs {
|
|
55
|
+
readdir(dir: string): Promise<string[]>;
|
|
56
|
+
lstat(file: string): Promise<ProjectionScanStat>;
|
|
57
|
+
readFile(file: string, encoding: "utf8", expected?: ProjectionScanStat): Promise<string>;
|
|
58
|
+
/** Optional descriptor-bound reader used by the production filesystem. */
|
|
59
|
+
readFileSafe?: (file: string, encoding: "utf8", expected?: ProjectionScanStat) => Promise<string>;
|
|
60
|
+
/** Optional pinned directory authority used for enumeration and every child operation. */
|
|
61
|
+
openDirectory?: (dir: string) => Promise<ProjectionScanDirectory>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ProjectionScanDirectory {
|
|
65
|
+
readonly stat: ProjectionScanStat;
|
|
66
|
+
readdir(): Promise<string[]>;
|
|
67
|
+
lstat(entry: string): Promise<ProjectionScanStat>;
|
|
68
|
+
readFile(entry: string, encoding: "utf8", expected?: ProjectionScanStat): Promise<string>;
|
|
69
|
+
close(): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ProjectionScanResult {
|
|
73
|
+
values: unknown[];
|
|
74
|
+
parsed: number;
|
|
75
|
+
capped: boolean;
|
|
76
|
+
skippedDebris: number;
|
|
77
|
+
skippedEmpty: number;
|
|
78
|
+
/** Candidates that changed or disappeared after enumeration. */
|
|
79
|
+
raced: number;
|
|
80
|
+
/** True when the scan cannot be authoritative for its caller. */
|
|
81
|
+
incomplete: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read one discovered candidate without ever following a final-component symlink or
|
|
86
|
+
* blocking on a FIFO. POSIX opens are descriptor-bound by O_NOFOLLOW/O_NONBLOCK. Windows
|
|
87
|
+
* has neither flag, so the path is bracketed by lstat/fstat/lstat identity checks and the
|
|
88
|
+
* bytes still come from the opened handle rather than the mutable pathname.
|
|
89
|
+
*/
|
|
90
|
+
function sameProjectionFile(left: ProjectionScanStat, right: ProjectionScanStat): boolean {
|
|
91
|
+
if (!left.isFile() || !right.isFile()) return false;
|
|
92
|
+
if (
|
|
93
|
+
left.dev === undefined ||
|
|
94
|
+
right.dev === undefined ||
|
|
95
|
+
left.ino === undefined ||
|
|
96
|
+
right.ino === undefined ||
|
|
97
|
+
String(left.dev) !== String(right.dev) ||
|
|
98
|
+
String(left.ino) !== String(right.ino) ||
|
|
99
|
+
String(left.size) !== String(right.size)
|
|
100
|
+
)
|
|
101
|
+
return false;
|
|
102
|
+
if (left.nlink !== undefined && right.nlink !== undefined && String(left.nlink) !== String(right.nlink))
|
|
103
|
+
return false;
|
|
104
|
+
if (left.mtimeNs !== undefined && right.mtimeNs !== undefined && String(left.mtimeNs) !== String(right.mtimeNs))
|
|
105
|
+
return false;
|
|
106
|
+
if (left.ctimeNs !== undefined && right.ctimeNs !== undefined && String(left.ctimeNs) !== String(right.ctimeNs))
|
|
107
|
+
return false;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function readProjectionFileSafe(file: string, encoding: "utf8", expected?: ProjectionScanStat): Promise<string> {
|
|
112
|
+
if (encoding !== "utf8") throw new TypeError("Coordinator projection reads require utf8.");
|
|
113
|
+
const platform = projectionPlatform();
|
|
114
|
+
const noFollow = fs.constants.O_NOFOLLOW;
|
|
115
|
+
const nonBlock = fs.constants.O_NONBLOCK;
|
|
116
|
+
if (platform !== "win32" && (typeof noFollow !== "number" || typeof nonBlock !== "number"))
|
|
117
|
+
throw new ProjectionScanUnsupportedError();
|
|
118
|
+
const flags = fs.constants.O_RDONLY | (platform === "win32" ? 0 : (nonBlock as number) | (noFollow as number));
|
|
119
|
+
let before: import("node:fs").BigIntStats | undefined;
|
|
120
|
+
if (platform === "win32") {
|
|
121
|
+
before = await fs.lstat(file, { bigint: true });
|
|
122
|
+
if (before.isSymbolicLink() || !before.isFile())
|
|
123
|
+
throw new ProjectionScanRaceError("candidate is not a regular file");
|
|
124
|
+
}
|
|
125
|
+
let handle: fs.FileHandle | undefined;
|
|
126
|
+
try {
|
|
127
|
+
handle = await fs.open(file, flags);
|
|
128
|
+
const opened = await handle.stat({ bigint: true });
|
|
129
|
+
if (!opened.isFile()) throw new ProjectionScanRaceError("candidate changed to a non-regular file");
|
|
130
|
+
if (expected && !sameProjectionFile(expected, opened))
|
|
131
|
+
throw new ProjectionScanRaceError("candidate changed while opening");
|
|
132
|
+
if (before) {
|
|
133
|
+
if (
|
|
134
|
+
before.dev !== opened.dev ||
|
|
135
|
+
before.ino !== opened.ino ||
|
|
136
|
+
before.nlink !== opened.nlink ||
|
|
137
|
+
before.size !== opened.size ||
|
|
138
|
+
before.mtimeNs !== opened.mtimeNs ||
|
|
139
|
+
before.ctimeNs !== opened.ctimeNs
|
|
140
|
+
)
|
|
141
|
+
throw new ProjectionScanRaceError("candidate changed while opening");
|
|
142
|
+
const relinked = await fs.lstat(file, { bigint: true });
|
|
143
|
+
if (
|
|
144
|
+
relinked.isSymbolicLink() ||
|
|
145
|
+
!relinked.isFile() ||
|
|
146
|
+
relinked.dev !== opened.dev ||
|
|
147
|
+
relinked.ino !== opened.ino ||
|
|
148
|
+
relinked.nlink !== opened.nlink ||
|
|
149
|
+
relinked.size !== opened.size ||
|
|
150
|
+
relinked.mtimeNs !== opened.mtimeNs ||
|
|
151
|
+
relinked.ctimeNs !== opened.ctimeNs
|
|
152
|
+
)
|
|
153
|
+
throw new ProjectionScanRaceError("candidate changed before read");
|
|
154
|
+
}
|
|
155
|
+
return await handle.readFile({ encoding: "utf8" });
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP")
|
|
158
|
+
throw new ProjectionScanRaceError("candidate became a symlink");
|
|
159
|
+
throw error;
|
|
160
|
+
} finally {
|
|
161
|
+
await handle?.close().catch(() => undefined);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sameProjectionRoot(left: ProjectionScanStat, right: ProjectionScanStat): boolean {
|
|
166
|
+
const leftDirectory = left.isDirectory?.();
|
|
167
|
+
const rightDirectory = right.isDirectory?.();
|
|
168
|
+
return (
|
|
169
|
+
leftDirectory !== false &&
|
|
170
|
+
rightDirectory !== false &&
|
|
171
|
+
left.dev !== undefined &&
|
|
172
|
+
right.dev !== undefined &&
|
|
173
|
+
left.ino !== undefined &&
|
|
174
|
+
right.ino !== undefined &&
|
|
175
|
+
String(left.dev) === String(right.dev) &&
|
|
176
|
+
String(left.ino) === String(right.ino)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasProjectionRootIdentity(stat: ProjectionScanStat): boolean {
|
|
181
|
+
return stat.dev !== undefined && stat.ino !== undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function trimProjectionRootPath(dir: string): string {
|
|
185
|
+
const parsedRoot = path.parse(dir).root;
|
|
186
|
+
let trimmed = dir;
|
|
187
|
+
while (trimmed.length > parsedRoot.length && (trimmed.endsWith("/") || trimmed.endsWith("\\")))
|
|
188
|
+
trimmed = trimmed.slice(0, -1);
|
|
189
|
+
while (trimmed.length > parsedRoot.length && (trimmed.endsWith("/.") || trimmed.endsWith("\\."))) {
|
|
190
|
+
trimmed = trimmed.slice(0, -2);
|
|
191
|
+
while (trimmed.length > parsedRoot.length && (trimmed.endsWith("/") || trimmed.endsWith("\\")))
|
|
192
|
+
trimmed = trimmed.slice(0, -1);
|
|
193
|
+
}
|
|
194
|
+
return trimmed || parsedRoot;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Windows has no descriptor-relative `openat` binding in Node. Keep the root pathname
|
|
199
|
+
* authoritative by checking its identity before and after every mutable-path operation.
|
|
200
|
+
*/
|
|
201
|
+
async function openProjectionDirectoryWindowsSafe(dir: string): Promise<ProjectionScanDirectory> {
|
|
202
|
+
const rootPath = trimProjectionRootPath(dir);
|
|
203
|
+
let opened: import("node:fs").BigIntStats;
|
|
204
|
+
try {
|
|
205
|
+
opened = await fs.lstat(rootPath, { bigint: true });
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP")
|
|
208
|
+
throw new ProjectionScanRaceError("scan root became a reparse point");
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
if (opened.isSymbolicLink() || !opened.isDirectory())
|
|
212
|
+
throw new ProjectionScanRaceError("scan root is not a regular directory");
|
|
213
|
+
|
|
214
|
+
const assertRoot = async (): Promise<void> => {
|
|
215
|
+
let settled: import("node:fs").BigIntStats;
|
|
216
|
+
try {
|
|
217
|
+
settled = await fs.lstat(rootPath, { bigint: true });
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
220
|
+
if (code === "ENOENT" || code === "ELOOP")
|
|
221
|
+
throw new ProjectionScanRaceError("scan root changed during enumeration");
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
if (settled.isSymbolicLink() || !settled.isDirectory() || !sameProjectionRoot(opened, settled))
|
|
225
|
+
throw new ProjectionScanRaceError("scan root changed during enumeration");
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const entryPath = (entry: string): string => {
|
|
229
|
+
// Directory entries come from readdir and are therefore single components on
|
|
230
|
+
// Windows. Keep the invariant explicit so an injected or corrupted entry can
|
|
231
|
+
// never turn a child operation into mutable pathname traversal.
|
|
232
|
+
if (
|
|
233
|
+
entry.length === 0 ||
|
|
234
|
+
entry === "." ||
|
|
235
|
+
entry === ".." ||
|
|
236
|
+
entry.includes("/") ||
|
|
237
|
+
entry.includes("\\") ||
|
|
238
|
+
entry.includes(":")
|
|
239
|
+
)
|
|
240
|
+
throw new ProjectionScanRaceError("scan entry is not a single directory component");
|
|
241
|
+
return path.join(rootPath, entry);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const bracketRoot = async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
245
|
+
let value!: T;
|
|
246
|
+
let operationError: unknown;
|
|
247
|
+
let started = false;
|
|
248
|
+
try {
|
|
249
|
+
await assertRoot();
|
|
250
|
+
started = true;
|
|
251
|
+
value = await operation();
|
|
252
|
+
} catch (error) {
|
|
253
|
+
operationError = error;
|
|
254
|
+
}
|
|
255
|
+
if (started) {
|
|
256
|
+
try {
|
|
257
|
+
await assertRoot();
|
|
258
|
+
} catch (error) {
|
|
259
|
+
operationError = error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (operationError !== undefined) {
|
|
263
|
+
const code = (operationError as NodeJS.ErrnoException).code;
|
|
264
|
+
if (code === "ENOENT" || code === "ELOOP")
|
|
265
|
+
throw new ProjectionScanRaceError("scan operation raced with a replacement or reparse point");
|
|
266
|
+
throw operationError;
|
|
267
|
+
}
|
|
268
|
+
return value;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
stat: opened,
|
|
273
|
+
readdir: () => bracketRoot(() => fs.readdir(rootPath)),
|
|
274
|
+
lstat: entry =>
|
|
275
|
+
bracketRoot(async () => {
|
|
276
|
+
const stat = await fs.lstat(entryPath(entry), { bigint: true });
|
|
277
|
+
if (stat.isSymbolicLink()) throw new ProjectionScanRaceError("candidate became a reparse point");
|
|
278
|
+
return stat;
|
|
279
|
+
}),
|
|
280
|
+
readFile: (entry, encoding, expected) =>
|
|
281
|
+
bracketRoot(() => readProjectionFileSafe(entryPath(entry), encoding, expected)),
|
|
282
|
+
close: async () => {},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Open a no-follow directory authority and keep it alive through enumeration, child
|
|
288
|
+
* lstat, and child reads. Node has no `openat` binding, so POSIX uses the proc fd path
|
|
289
|
+
* as the descriptor-relative namespace; a replacement root or parent cannot redirect it.
|
|
290
|
+
*/
|
|
291
|
+
async function openProjectionDirectorySafe(dir: string): Promise<ProjectionScanDirectory> {
|
|
292
|
+
const platform = projectionPlatform();
|
|
293
|
+
if (platform === "win32") return openProjectionDirectoryWindowsSafe(dir);
|
|
294
|
+
if (platform !== "linux") throw new ProjectionScanUnsupportedError();
|
|
295
|
+
const noFollow = fs.constants.O_NOFOLLOW;
|
|
296
|
+
const nonBlock = fs.constants.O_NONBLOCK;
|
|
297
|
+
if (typeof noFollow !== "number" || typeof nonBlock !== "number") throw new ProjectionScanUnsupportedError();
|
|
298
|
+
const directoryFlag = typeof fs.constants.O_DIRECTORY === "number" ? fs.constants.O_DIRECTORY : 0;
|
|
299
|
+
const flags = fs.constants.O_RDONLY | (nonBlock as number) | (noFollow as number) | directoryFlag;
|
|
300
|
+
let handle: fs.FileHandle | undefined;
|
|
301
|
+
try {
|
|
302
|
+
handle = await fs.open(dir, flags);
|
|
303
|
+
const opened = await handle.stat({ bigint: true });
|
|
304
|
+
if (!opened.isDirectory()) throw new ProjectionScanRaceError("scan root is not a directory");
|
|
305
|
+
const pinnedPath = `/proc/self/fd/${handle.fd}`;
|
|
306
|
+
const ownedHandle = handle;
|
|
307
|
+
handle = undefined;
|
|
308
|
+
const assertRoot = async (): Promise<void> => {
|
|
309
|
+
const settled = await ownedHandle.stat({ bigint: true });
|
|
310
|
+
if (!sameProjectionRoot(opened, settled)) throw new ProjectionScanRaceError("scan root descriptor changed");
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
stat: opened,
|
|
314
|
+
readdir: async () => {
|
|
315
|
+
const entries = await fs.readdir(pinnedPath);
|
|
316
|
+
await assertRoot();
|
|
317
|
+
return entries;
|
|
318
|
+
},
|
|
319
|
+
lstat: async entry => {
|
|
320
|
+
const stat = await fs.lstat(path.join(pinnedPath, entry), { bigint: true });
|
|
321
|
+
await assertRoot();
|
|
322
|
+
return stat;
|
|
323
|
+
},
|
|
324
|
+
readFile: async (entry, encoding, expected) => {
|
|
325
|
+
const source = await readProjectionFileSafe(path.join(pinnedPath, entry), encoding, expected);
|
|
326
|
+
await assertRoot();
|
|
327
|
+
return source;
|
|
328
|
+
},
|
|
329
|
+
close: async () => {
|
|
330
|
+
await ownedHandle.close().catch(() => undefined);
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP")
|
|
335
|
+
throw new ProjectionScanRaceError("scan root became a symlink");
|
|
336
|
+
throw error;
|
|
337
|
+
} finally {
|
|
338
|
+
await handle?.close().catch(() => undefined);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const failUnpinnedProjectionOperation = async (): Promise<never> => {
|
|
343
|
+
throw new ProjectionScanUnsupportedError();
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const defaultFs: ProjectionScanFs = {
|
|
347
|
+
// The default implementation never permits an accidental mutable-path fallback;
|
|
348
|
+
// all production operations must go through openProjectionDirectorySafe above.
|
|
349
|
+
readdir: failUnpinnedProjectionOperation,
|
|
350
|
+
lstat: failUnpinnedProjectionOperation,
|
|
351
|
+
readFile: failUnpinnedProjectionOperation,
|
|
352
|
+
readFileSafe: failUnpinnedProjectionOperation,
|
|
353
|
+
openDirectory: openProjectionDirectorySafe,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export function isCoordinatorScanDebrisName(name: string): boolean {
|
|
357
|
+
return name.startsWith(".");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function scanCoordinatorJsonFiles(
|
|
361
|
+
dir: string,
|
|
362
|
+
io: ProjectionScanFs = defaultFs,
|
|
363
|
+
cap: number = COORDINATOR_JSON_SCAN_CAP,
|
|
364
|
+
authority?: ProjectionScanDirectory,
|
|
365
|
+
): Promise<ProjectionScanResult> {
|
|
366
|
+
let rootStat: ProjectionScanStat;
|
|
367
|
+
try {
|
|
368
|
+
rootStat = authority?.stat ?? (await io.lstat(dir));
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
371
|
+
return {
|
|
372
|
+
values: [],
|
|
373
|
+
parsed: 0,
|
|
374
|
+
capped: false,
|
|
375
|
+
skippedDebris: 0,
|
|
376
|
+
skippedEmpty: 0,
|
|
377
|
+
raced: 0,
|
|
378
|
+
incomplete: false,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
if (rootStat.isSymbolicLink() || rootStat.isDirectory?.() === false) {
|
|
384
|
+
return {
|
|
385
|
+
values: [],
|
|
386
|
+
parsed: 0,
|
|
387
|
+
capped: true,
|
|
388
|
+
skippedDebris: 0,
|
|
389
|
+
skippedEmpty: 0,
|
|
390
|
+
raced: 1,
|
|
391
|
+
incomplete: true,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
let entries: string[];
|
|
395
|
+
try {
|
|
396
|
+
entries = authority ? await authority.readdir() : await io.readdir(dir);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
399
|
+
return {
|
|
400
|
+
values: [],
|
|
401
|
+
parsed: 0,
|
|
402
|
+
capped: true,
|
|
403
|
+
skippedDebris: 0,
|
|
404
|
+
skippedEmpty: 0,
|
|
405
|
+
raced: 1,
|
|
406
|
+
incomplete: true,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (error instanceof ProjectionScanRaceError) {
|
|
410
|
+
return {
|
|
411
|
+
values: [],
|
|
412
|
+
parsed: 0,
|
|
413
|
+
capped: true,
|
|
414
|
+
skippedDebris: 0,
|
|
415
|
+
skippedEmpty: 0,
|
|
416
|
+
raced: 1,
|
|
417
|
+
incomplete: true,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let skippedDebris = 0;
|
|
424
|
+
let skippedEmpty = 0;
|
|
425
|
+
let raced = 0;
|
|
426
|
+
const parseCandidates: Array<{ entry: string; stat: ProjectionScanStat }> = [];
|
|
427
|
+
for (const entry of entries) {
|
|
428
|
+
if (!entry.endsWith(".json") || isCoordinatorScanDebrisName(entry)) {
|
|
429
|
+
if (entry.endsWith(".json") && isCoordinatorScanDebrisName(entry)) skippedDebris += 1;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const file = path.join(dir, entry);
|
|
433
|
+
let stat: ProjectionScanStat;
|
|
434
|
+
try {
|
|
435
|
+
stat = authority ? await authority.lstat(entry) : await io.lstat(file);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (
|
|
438
|
+
(error as NodeJS.ErrnoException).code === "ENOENT" ||
|
|
439
|
+
(error as NodeJS.ErrnoException).code === "ELOOP" ||
|
|
440
|
+
error instanceof ProjectionScanRaceError
|
|
441
|
+
) {
|
|
442
|
+
raced += 1;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
448
|
+
// The candidate was a parseable-looking directory entry when enumeration
|
|
449
|
+
// completed, but its type changed before inspection. Treat that as a
|
|
450
|
+
// raced candidate rather than ordinary empty/non-regular debris: callers
|
|
451
|
+
// must not consume a partial projection set.
|
|
452
|
+
raced += 1;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
parseCandidates.push({ entry, stat });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const capped = parseCandidates.length > cap;
|
|
459
|
+
const toParse = capped ? parseCandidates.slice(0, cap) : parseCandidates;
|
|
460
|
+
const values: unknown[] = [];
|
|
461
|
+
for (const candidate of toParse) {
|
|
462
|
+
const { entry, stat } = candidate;
|
|
463
|
+
const file = path.join(dir, entry);
|
|
464
|
+
let source: string;
|
|
465
|
+
try {
|
|
466
|
+
source = authority
|
|
467
|
+
? await authority.readFile(entry, "utf8", stat)
|
|
468
|
+
: await (io.readFileSafe ?? io.readFile)(file, "utf8", stat);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (
|
|
471
|
+
(error as NodeJS.ErrnoException).code === "ENOENT" ||
|
|
472
|
+
(error as NodeJS.ErrnoException).code === "ELOOP" ||
|
|
473
|
+
error instanceof ProjectionScanRaceError
|
|
474
|
+
) {
|
|
475
|
+
raced += 1;
|
|
476
|
+
skippedEmpty += 1;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
values.push(JSON.parse(source));
|
|
483
|
+
} catch (error) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`invalid coordinator projection ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
let finalRoot: ProjectionScanStat;
|
|
490
|
+
try {
|
|
491
|
+
finalRoot = authority?.stat ?? (await io.lstat(dir));
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
494
|
+
return {
|
|
495
|
+
values: [],
|
|
496
|
+
parsed: 0,
|
|
497
|
+
capped: true,
|
|
498
|
+
skippedDebris,
|
|
499
|
+
skippedEmpty,
|
|
500
|
+
raced: raced + 1,
|
|
501
|
+
incomplete: true,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
if (hasProjectionRootIdentity(rootStat) && !sameProjectionRoot(rootStat, finalRoot)) {
|
|
505
|
+
return {
|
|
506
|
+
values: [],
|
|
507
|
+
parsed: 0,
|
|
508
|
+
capped: true,
|
|
509
|
+
skippedDebris,
|
|
510
|
+
skippedEmpty,
|
|
511
|
+
raced: raced + 1,
|
|
512
|
+
incomplete: true,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
values: values.filter(value => value !== null),
|
|
517
|
+
parsed: values.length,
|
|
518
|
+
// Existing authoritative callers already refuse a capped scan. Treat a raced
|
|
519
|
+
// candidate as the same incomplete boundary so they cannot consume a partial
|
|
520
|
+
// projection set while the result still reports the precise race count below.
|
|
521
|
+
capped: capped || raced > 0,
|
|
522
|
+
skippedDebris,
|
|
523
|
+
skippedEmpty,
|
|
524
|
+
raced,
|
|
525
|
+
incomplete: capped || raced > 0,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export async function listCoordinatorJsonFiles(
|
|
530
|
+
dir: string,
|
|
531
|
+
io: ProjectionScanFs = defaultFs,
|
|
532
|
+
cap: number = COORDINATOR_JSON_SCAN_CAP,
|
|
533
|
+
): Promise<ProjectionScanResult> {
|
|
534
|
+
let authority: ProjectionScanDirectory | undefined;
|
|
535
|
+
try {
|
|
536
|
+
authority = io.openDirectory ? await io.openDirectory(dir) : undefined;
|
|
537
|
+
return await scanCoordinatorJsonFiles(dir, io, cap, authority);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (
|
|
540
|
+
error instanceof ProjectionScanRaceError ||
|
|
541
|
+
error instanceof ProjectionScanUnsupportedError ||
|
|
542
|
+
(error as NodeJS.ErrnoException).code === PROJECTION_SCAN_UNSUPPORTED_CODE
|
|
543
|
+
) {
|
|
544
|
+
return {
|
|
545
|
+
values: [],
|
|
546
|
+
parsed: 0,
|
|
547
|
+
capped: true,
|
|
548
|
+
skippedDebris: 0,
|
|
549
|
+
skippedEmpty: 0,
|
|
550
|
+
raced: 1,
|
|
551
|
+
incomplete: true,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
555
|
+
// No authority was returned: this is the lazy first-use shape for an
|
|
556
|
+
// otherwise valid projection namespace, not a partial scan. Once an
|
|
557
|
+
// authority exists, ENOENT is handled by scanCoordinatorJsonFiles as a
|
|
558
|
+
// raced/incomplete result and cannot reach this branch.
|
|
559
|
+
return {
|
|
560
|
+
values: [],
|
|
561
|
+
parsed: 0,
|
|
562
|
+
capped: false,
|
|
563
|
+
skippedDebris: 0,
|
|
564
|
+
skippedEmpty: 0,
|
|
565
|
+
raced: 0,
|
|
566
|
+
incomplete: false,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
throw error;
|
|
570
|
+
} finally {
|
|
571
|
+
await authority?.close().catch(() => undefined);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Retry only scans invalidated by observed directory/file churn. A candidate cap,
|
|
577
|
+
* unsupported authority, parse error, or any other failure remains fail-closed.
|
|
578
|
+
* Each attempt acquires a new authority, so no partial result crosses attempts.
|
|
579
|
+
*/
|
|
580
|
+
export async function listCoordinatorJsonFilesWithRetry(
|
|
581
|
+
dir: string,
|
|
582
|
+
io: ProjectionScanFs = defaultFs,
|
|
583
|
+
cap: number = COORDINATOR_JSON_SCAN_CAP,
|
|
584
|
+
maxAttempts: number = COORDINATOR_JSON_SCAN_RETRY_ATTEMPTS,
|
|
585
|
+
): Promise<ProjectionScanResult> {
|
|
586
|
+
const attempts = Math.max(1, Math.floor(maxAttempts));
|
|
587
|
+
let scan = await listCoordinatorJsonFiles(dir, io, cap);
|
|
588
|
+
for (let attempt = 1; attempt < attempts && scan.incomplete && scan.raced > 0; attempt++) {
|
|
589
|
+
scan = await listCoordinatorJsonFiles(dir, io, cap);
|
|
590
|
+
}
|
|
591
|
+
return scan;
|
|
592
|
+
}
|
|
@@ -1029,7 +1029,7 @@ function assertTransaction(transaction: CoordinatorSessionTransactionV1, namespa
|
|
|
1029
1029
|
const rejectedHistorical = request.phase === "rejected";
|
|
1030
1030
|
if (
|
|
1031
1031
|
rejectedHistorical
|
|
1032
|
-
?
|
|
1032
|
+
? request.safe_receipt?.status !== "rejected"
|
|
1033
1033
|
: question.answer_request_id !== request.request_id ||
|
|
1034
1034
|
question.claim_fence_epoch !== request.claim_fence_epoch
|
|
1035
1035
|
)
|
|
@@ -1083,7 +1083,7 @@ function assertTransaction(transaction: CoordinatorSessionTransactionV1, namespa
|
|
|
1083
1083
|
event.transaction_revision > transaction.revision ||
|
|
1084
1084
|
typeof event.kind !== "string" ||
|
|
1085
1085
|
event.kind.length === 0 ||
|
|
1086
|
-
!
|
|
1086
|
+
!isOutboxEntity(event.entity) ||
|
|
1087
1087
|
typeof event.entity_id !== "string" ||
|
|
1088
1088
|
event.entity_id.length === 0 ||
|
|
1089
1089
|
!isRecord(event.payload) ||
|
|
@@ -1296,7 +1296,7 @@ export async function listCanonicalActiveSessions(
|
|
|
1296
1296
|
paths.registryLock,
|
|
1297
1297
|
async () => {
|
|
1298
1298
|
const registry = await readJson<NamespaceRegistryV1>(paths.registry);
|
|
1299
|
-
if (
|
|
1299
|
+
if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
|
|
1300
1300
|
throw new Error("state_corrupt");
|
|
1301
1301
|
return Object.values(registry.roster ?? {})
|
|
1302
1302
|
.filter(entry => entry.active || entry.dirty)
|
|
@@ -1333,7 +1333,7 @@ export async function readDeliveryDiscoveryCursor(
|
|
|
1333
1333
|
paths.registryLock,
|
|
1334
1334
|
async () => {
|
|
1335
1335
|
const registry = await readJson<NamespaceRegistryV1>(paths.registry);
|
|
1336
|
-
if (
|
|
1336
|
+
if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
|
|
1337
1337
|
throw new Error("state_corrupt");
|
|
1338
1338
|
return registry.delivery_discovery_cursor ?? "";
|
|
1339
1339
|
},
|
|
@@ -1660,7 +1660,7 @@ export async function startCreationRemote(
|
|
|
1660
1660
|
): Promise<CreationRequestV1> {
|
|
1661
1661
|
return await withNamespaceRegistry(paths, async registry => {
|
|
1662
1662
|
const request = registry.creations[keyDigest];
|
|
1663
|
-
if (!request
|
|
1663
|
+
if (!request?.sidecar_verifier) throw new Error("state_corrupt");
|
|
1664
1664
|
if (
|
|
1665
1665
|
request.phase === "claimed" &&
|
|
1666
1666
|
(request.sidecar_verifier.key_id !== expectedVerifier.key_id ||
|
|
@@ -1687,8 +1687,7 @@ export async function reconcileCreationRemoteVerifier(
|
|
|
1687
1687
|
): Promise<CreationRequestV1> {
|
|
1688
1688
|
return await withNamespaceRegistry(paths, async registry => {
|
|
1689
1689
|
const request = registry.creations[keyDigest];
|
|
1690
|
-
if (!request
|
|
1691
|
-
throw new Error("terminal_uncertain");
|
|
1690
|
+
if (!request?.sidecar_verifier || request.phase !== "remote_started") throw new Error("terminal_uncertain");
|
|
1692
1691
|
if (!/^[a-f0-9]{64}$/.test(usedKeyId)) throw new Error("terminal_uncertain");
|
|
1693
1692
|
if (usedKeyId === candidate.key_id) request.sidecar_verifier = candidate;
|
|
1694
1693
|
else if (usedKeyId !== request.sidecar_verifier.key_id) throw new Error("terminal_uncertain");
|
|
@@ -2241,7 +2240,7 @@ export async function enumeratePublicDeliveries(
|
|
|
2241
2240
|
paths.registryLock,
|
|
2242
2241
|
async () => {
|
|
2243
2242
|
const registry = await readJson<NamespaceRegistryV1>(paths.registry);
|
|
2244
|
-
if (
|
|
2243
|
+
if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
|
|
2245
2244
|
throw new Error("state_corrupt");
|
|
2246
2245
|
return [...new Set([...Object.keys(registry.roster ?? {}), ...Object.keys(registry.retained_sessions ?? {})])]
|
|
2247
2246
|
.filter(name => COORDINATOR_SESSION_ID_PATTERN.test(name))
|
|
@@ -2526,7 +2525,7 @@ export async function replaceCreationRetirementIntent(
|
|
|
2526
2525
|
if (request.phase !== "remote_started" && request.phase !== "uncertain" && request.phase !== "retired")
|
|
2527
2526
|
throw new Error("retire_not_allowed");
|
|
2528
2527
|
const staged = request.retirement_intent;
|
|
2529
|
-
if (
|
|
2528
|
+
if (staged?.phase !== "intent" || staged.broker_proof) throw new Error("state_corrupt");
|
|
2530
2529
|
if (staged.retirement_key_digest !== retirementKeyDigest) throw new Error("idempotency_conflict");
|
|
2531
2530
|
assertCreationRetirementProofMatches(request, proof, false);
|
|
2532
2531
|
staged.phase = "pre_effect_rejected";
|
|
@@ -2716,8 +2715,7 @@ export async function removeSessionTransaction(
|
|
|
2716
2715
|
// exporter lease expires instead of deleting an undelivered event.
|
|
2717
2716
|
if (Object.values(transaction.outbox).some(event => event.public_delivery.state !== "acknowledged"))
|
|
2718
2717
|
return false;
|
|
2719
|
-
await
|
|
2720
|
-
await syncCoordinatorDirectory(path.dirname(file));
|
|
2718
|
+
await removeCoordinatorStateFile(file);
|
|
2721
2719
|
delete registry.roster?.[sessionId];
|
|
2722
2720
|
delete registry.retained_sessions?.[sessionId];
|
|
2723
2721
|
return true;
|