@ricsam/r5d-worker 0.0.122 → 0.0.124
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/README.md +6 -3
- package/dist/cjs/command-launcher.cjs +76 -2
- package/dist/cjs/control-command-policy.cjs +177 -0
- package/dist/cjs/main.cjs +541 -247
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-checkout-garbage.cjs +32 -3
- package/dist/cjs/project-workspace-state.cjs +51 -35
- package/dist/cjs/project-worktrees.cjs +79 -34
- package/dist/cjs/recovery-journal-protocol.cjs +56 -0
- package/dist/cjs/recovery-journal-runtime.cjs +133 -0
- package/dist/cjs/recovery-journal-thread.cjs +9 -0
- package/dist/cjs/recovery-journal.cjs +735 -0
- package/dist/cjs/recovery-store.cjs +8 -0
- package/dist/cjs/session-file-mutations.cjs +61 -0
- package/dist/cjs/working-tree-mirror.cjs +1 -0
- package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
- package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
- package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
- package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
- package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
- package/dist/cjs/workspace-git-sync.cjs +275 -201
- package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
- package/dist/mjs/command-launcher.mjs +75 -2
- package/dist/mjs/control-command-policy.mjs +146 -0
- package/dist/mjs/main.mjs +549 -253
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-checkout-garbage.mjs +30 -2
- package/dist/mjs/project-workspace-state.mjs +51 -35
- package/dist/mjs/project-worktrees.mjs +74 -34
- package/dist/mjs/recovery-journal-protocol.mjs +30 -0
- package/dist/mjs/recovery-journal-runtime.mjs +112 -0
- package/dist/mjs/recovery-journal-thread.mjs +8 -0
- package/dist/mjs/recovery-journal.mjs +687 -0
- package/dist/mjs/recovery-store.mjs +8 -0
- package/dist/mjs/session-file-mutations.mjs +37 -0
- package/dist/mjs/working-tree-mirror.mjs +1 -0
- package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
- package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
- package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
- package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
- package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
- package/dist/mjs/workspace-git-sync.mjs +264 -202
- package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
- package/dist/types/command-launcher.d.ts +42 -0
- package/dist/types/control-command-policy.d.ts +53 -0
- package/dist/types/main.d.ts +31 -19
- package/dist/types/project-checkout-garbage.d.ts +19 -2
- package/dist/types/project-workspace-state.d.ts +9 -9
- package/dist/types/project-worktrees.d.ts +49 -5
- package/dist/types/recovery-journal-protocol.d.ts +35 -0
- package/dist/types/recovery-journal-runtime.d.ts +12 -0
- package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
- package/dist/types/recovery-journal-thread.d.ts +1 -0
- package/dist/types/recovery-journal.d.ts +246 -0
- package/dist/types/recovery-store.d.ts +6 -0
- package/dist/types/session-file-mutations.d.ts +22 -0
- package/dist/types/workspace-command-sync-policy.d.ts +22 -7
- package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
- package/dist/types/workspace-filesystem-executor.d.ts +123 -0
- package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
- package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
- package/dist/types/workspace-git-sync.d.ts +113 -7
- package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
- package/package.json +1 -1
- package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
- package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
- package/dist/types/project-snapshot-recovery-runner.d.ts +0 -10
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
class WorkspaceMountHoldAbortedError extends Error {
|
|
2
|
+
constructor(reason) {
|
|
3
|
+
super(reason);
|
|
4
|
+
this.name = "WorkspaceMountHoldAbortedError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
class WorkspaceMountHoldFence {
|
|
8
|
+
holds = /* @__PURE__ */ new Map();
|
|
9
|
+
waiters = /* @__PURE__ */ new Set();
|
|
10
|
+
now;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.now = options.now ?? (() => Date.now());
|
|
13
|
+
}
|
|
14
|
+
/** Take one hold on every listed mount; the returned release is idempotent. */
|
|
15
|
+
hold(mountIds, description) {
|
|
16
|
+
const ids = [...new Set(mountIds)];
|
|
17
|
+
for (const id of ids) {
|
|
18
|
+
const existing = this.holds.get(id);
|
|
19
|
+
if (existing) {
|
|
20
|
+
existing.count += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
this.holds.set(id, { count: 1, description, sinceMs: this.now() });
|
|
24
|
+
}
|
|
25
|
+
let released = false;
|
|
26
|
+
return () => {
|
|
27
|
+
if (released) return;
|
|
28
|
+
released = true;
|
|
29
|
+
for (const id of ids) {
|
|
30
|
+
const existing = this.holds.get(id);
|
|
31
|
+
if (!existing) continue;
|
|
32
|
+
existing.count -= 1;
|
|
33
|
+
if (existing.count === 0) this.holds.delete(id);
|
|
34
|
+
}
|
|
35
|
+
this.wake();
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
isHeld(mountIds) {
|
|
39
|
+
return mountIds.some((id) => this.holds.has(id));
|
|
40
|
+
}
|
|
41
|
+
/** Human-readable holder of the first held mount among `mountIds`, for diagnostics. */
|
|
42
|
+
describeHold(mountIds) {
|
|
43
|
+
for (const id of mountIds) {
|
|
44
|
+
const hold = this.holds.get(id);
|
|
45
|
+
if (hold) return `${hold.description} (held for ${Math.round((this.now() - hold.sinceMs) / 1e3)}s)`;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/** Every held mount id, sorted, for status reporting. */
|
|
50
|
+
heldMountIds() {
|
|
51
|
+
return [...this.holds.keys()].sort();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolves in the same turn as the release that leaves none of `mountIds`
|
|
55
|
+
* held, so the caller can re-check and reserve without an interleaving
|
|
56
|
+
* hold. Resolves immediately when nothing is held. Rejects with
|
|
57
|
+
* WorkspaceMountHoldAbortedError when `signal` aborts first.
|
|
58
|
+
*/
|
|
59
|
+
waitForRelease(mountIds, options = {}) {
|
|
60
|
+
if (options.signal?.aborted) {
|
|
61
|
+
return Promise.reject(new WorkspaceMountHoldAbortedError(abortReason(options.signal)));
|
|
62
|
+
}
|
|
63
|
+
if (!this.isHeld(mountIds)) return Promise.resolve();
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const waiter = { mountIds, resolve, reject, signal: options.signal };
|
|
66
|
+
if (options.signal) {
|
|
67
|
+
const signal = options.signal;
|
|
68
|
+
waiter.onAbort = () => {
|
|
69
|
+
this.waiters.delete(waiter);
|
|
70
|
+
reject(new WorkspaceMountHoldAbortedError(abortReason(signal)));
|
|
71
|
+
};
|
|
72
|
+
signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
73
|
+
}
|
|
74
|
+
this.waiters.add(waiter);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
wake() {
|
|
78
|
+
for (const waiter of [...this.waiters]) {
|
|
79
|
+
if (this.isHeld(waiter.mountIds)) continue;
|
|
80
|
+
this.waiters.delete(waiter);
|
|
81
|
+
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
82
|
+
waiter.resolve();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function abortReason(signal) {
|
|
87
|
+
const reason = signal.reason;
|
|
88
|
+
if (reason instanceof Error) return reason.message;
|
|
89
|
+
if (typeof reason === "string" && reason) return reason;
|
|
90
|
+
return "Workspace mount reservation was aborted";
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
WorkspaceMountHoldAbortedError,
|
|
94
|
+
WorkspaceMountHoldFence
|
|
95
|
+
};
|
|
@@ -2,9 +2,42 @@ export type CommandLaunchRequest = {
|
|
|
2
2
|
id: string;
|
|
3
3
|
kind: "exec" | "exec_start" | "pty_open";
|
|
4
4
|
workspaceEffect?: "none";
|
|
5
|
+
/** An explicitly classified short coordination command; the launcher keeps a bounded lane for these. */
|
|
6
|
+
commandClass?: "control";
|
|
5
7
|
sessionId?: string;
|
|
6
8
|
assertAdmission: () => void;
|
|
7
9
|
};
|
|
10
|
+
export type CommandLaunchLane = "general" | "utility" | "control";
|
|
11
|
+
export type CommandLaunchCapacity = {
|
|
12
|
+
generalSlots: number;
|
|
13
|
+
utilitySlots: number;
|
|
14
|
+
controlSlots: number;
|
|
15
|
+
controlRuntimeMs: number;
|
|
16
|
+
queueTimeoutMs: number;
|
|
17
|
+
};
|
|
18
|
+
export type CommandLaunchLaneReport = {
|
|
19
|
+
/** Null when no launcher bounds this lane (direct execution). */
|
|
20
|
+
slots: number | null;
|
|
21
|
+
active: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
since: string;
|
|
25
|
+
}>;
|
|
26
|
+
queued: Array<{
|
|
27
|
+
id: string;
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
since: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
/** Who holds and who waits for each lane, as the worker sees its own admissions. */
|
|
33
|
+
export type CommandLaunchCapacityReport = {
|
|
34
|
+
/** False for a direct worker: nothing is bounded and nothing can be starved. */
|
|
35
|
+
bounded: boolean;
|
|
36
|
+
controlRuntimeMs: number | null;
|
|
37
|
+
lanes: Record<CommandLaunchLane, CommandLaunchLaneReport>;
|
|
38
|
+
};
|
|
39
|
+
/** The same lane rule the managed launcher applies; kept here so reports match admissions. */
|
|
40
|
+
export declare function commandLaunchLane(request: Pick<CommandLaunchRequest, "kind" | "workspaceEffect" | "commandClass">): CommandLaunchLane;
|
|
8
41
|
export type CommandLaunchLease = {
|
|
9
42
|
wrap(argv: string[]): string[];
|
|
10
43
|
cancel(): Promise<void>;
|
|
@@ -33,6 +66,7 @@ export declare class WorkerCommandLauncher {
|
|
|
33
66
|
private initialized;
|
|
34
67
|
private closePromise?;
|
|
35
68
|
private sequence;
|
|
69
|
+
private capacity?;
|
|
36
70
|
constructor(options?: {
|
|
37
71
|
argv?: string[];
|
|
38
72
|
env?: NodeJS.ProcessEnv;
|
|
@@ -40,9 +74,17 @@ export declare class WorkerCommandLauncher {
|
|
|
40
74
|
onStderr?: (text: string) => void;
|
|
41
75
|
requestTimeoutMs?: number;
|
|
42
76
|
acquisitionTimeoutMs?: number;
|
|
77
|
+
/** Called after any admission changes lane, phase, or is released. */
|
|
78
|
+
onCapacityChange?: () => void;
|
|
43
79
|
});
|
|
44
80
|
initialize(): Promise<void>;
|
|
45
81
|
get activeIds(): string[];
|
|
82
|
+
/** The lane totals the launcher declared at initialization; undefined for a direct worker or an older launcher. */
|
|
83
|
+
get declaredCapacity(): CommandLaunchCapacity | undefined;
|
|
84
|
+
get supportsControlLane(): boolean;
|
|
85
|
+
/** Current admissions per lane, for the worker's capacity report. */
|
|
86
|
+
capacityReport(): CommandLaunchCapacityReport;
|
|
87
|
+
private notifyCapacityChange;
|
|
46
88
|
acquire(request: CommandLaunchRequest): Promise<CommandLaunchLease>;
|
|
47
89
|
cancel(id: string): Promise<void>;
|
|
48
90
|
cancelSession(sessionId: string): Promise<void>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which agent shell invocations may run in the managed launcher's bounded
|
|
3
|
+
* `control` lane: exactly one plain `r5dctl` coordination/diagnosis command.
|
|
4
|
+
*
|
|
5
|
+
* The lane exists so an agent can inspect capacity, message another session,
|
|
6
|
+
* or stop its own runs while every general slot is held by long work
|
|
7
|
+
* (incident 9). It is bounded (small memory, few PIDs, a runtime limit), so
|
|
8
|
+
* only commands that are short HTTP calls to the platform qualify. The
|
|
9
|
+
* classifier fails closed: anything the shell would interpret (operators,
|
|
10
|
+
* substitutions, expansions, quoting the tokenizer cannot prove literal,
|
|
11
|
+
* environment assignments, wrapper programs, paths) is refused and keeps its
|
|
12
|
+
* ordinary general-lane shell semantics. The resulting argv is validated a
|
|
13
|
+
* second time by the worker before it is spawned without a shell.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CONTROL_COMMAND_PROGRAM = "r5dctl";
|
|
16
|
+
/** Worker-side runtime bound for a control run; the launcher enforces its own limit as a backstop. */
|
|
17
|
+
export declare const CONTROL_COMMAND_RUNTIME_MS = 120000;
|
|
18
|
+
export declare const CONTROL_COMMAND_MAX_ARGV = 64;
|
|
19
|
+
export declare const CONTROL_COMMAND_MAX_LENGTH = 4096;
|
|
20
|
+
export type ControlCommandClassification = {
|
|
21
|
+
control: true;
|
|
22
|
+
argv: string[];
|
|
23
|
+
} | {
|
|
24
|
+
control: false;
|
|
25
|
+
reason: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Validate an argv as an allowed r5dctl coordination invocation. Used on the
|
|
29
|
+
* server after tokenizing, and again on the worker before spawning.
|
|
30
|
+
*/
|
|
31
|
+
export declare function assertControlCommandArgv(argv: readonly string[]): void;
|
|
32
|
+
/**
|
|
33
|
+
* Locate the installed CLI through the worker's own, operator-controlled
|
|
34
|
+
* search path. The command's cwd, its environment, and any checkout-local
|
|
35
|
+
* shim play no part, so a project cannot substitute the executable the
|
|
36
|
+
* reserved lane runs.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveControlCommandExecutable(trustedPath: string | undefined, which: (program: string, options: {
|
|
39
|
+
PATH: string;
|
|
40
|
+
}) => string | null, program?: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* The environment a control spawn receives: everything the platform sets for
|
|
43
|
+
* the run (credentials, session identity, declared project values) minus any
|
|
44
|
+
* variable that could load code into the CLI process or redirect which
|
|
45
|
+
* executable runs, with the worker's own PATH restored.
|
|
46
|
+
*/
|
|
47
|
+
export declare function controlCommandEnvironment(environment: NodeJS.ProcessEnv, trustedPath: string | undefined): Record<string, string>;
|
|
48
|
+
/**
|
|
49
|
+
* Classify an agent shell command. Only a plain `r5dctl` coordination
|
|
50
|
+
* invocation becomes a control command; the returned argv is spawned without a
|
|
51
|
+
* shell. Everything else keeps its ordinary shell semantics and lane.
|
|
52
|
+
*/
|
|
53
|
+
export declare function classifyControlCommand(command: string): ControlCommandClassification;
|
package/dist/types/main.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { WORKER_RESUMABLE_PROTOCOL, type WorkerRecoveryClientMessage, type Worke
|
|
|
4
4
|
import { Database } from "bun:sqlite";
|
|
5
5
|
import type { WorkerGitIdentity } from "./git-identity";
|
|
6
6
|
import { configureGitHubRegistryAuthFiles, type PreparedPrivateAuthFileGeneration } from "./registry-auth";
|
|
7
|
+
import { WorkspaceMountHoldFence } from "./workspace-mount-hold-fence";
|
|
7
8
|
import { type WorkspaceHydrationTransactionHooks } from "./workspace-git-sync";
|
|
8
9
|
type WorkerProjectConfig = {
|
|
9
10
|
projectId: string;
|
|
@@ -48,19 +49,22 @@ declare function workerServerHeartbeatAction(input: {
|
|
|
48
49
|
watchdogSweepDelayed: boolean;
|
|
49
50
|
}): "wait" | "start_probe" | "terminate";
|
|
50
51
|
/**
|
|
51
|
-
* A hydration transaction
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* what turned one slow hydration into a mandatory canonical
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
52
|
+
* A hydration transaction runs on the workspace filesystem executor thread
|
|
53
|
+
* and on a stalled disk it can run for a long time; the server bounds its
|
|
54
|
+
* deadlines by a hydration heartbeat lease rather than by the ordinary
|
|
55
|
+
* request timeout. Declare that lease first: the server keeps the control
|
|
56
|
+
* socket open and defers the affected request deadlines while the lease is
|
|
57
|
+
* active, which is what turned one slow hydration into a mandatory canonical
|
|
58
|
+
* reset before the lease existed (incident 3bccab03). The extra event-loop
|
|
59
|
+
* turn lets control messages that were already queued run before the busy
|
|
60
|
+
* observation that precedes the transaction. The hooks also hold every mount
|
|
61
|
+
* a job reads or writes on the process-wide fence, so command admission for
|
|
62
|
+
* those mounts waits until the job ended (incident 10).
|
|
60
63
|
*/
|
|
61
64
|
declare function createWorkspaceHydrationLeaseHooks(send: (message: Extract<WorkerClientMessage, {
|
|
62
65
|
type: "heartbeat_lease";
|
|
63
|
-
}>) => void, options
|
|
66
|
+
}>) => void, options: {
|
|
67
|
+
fence: WorkspaceMountHoldFence;
|
|
64
68
|
yieldToEventLoop?: () => Promise<void>;
|
|
65
69
|
log?: (line: string) => void;
|
|
66
70
|
now?: () => number;
|
|
@@ -69,6 +73,7 @@ export declare const workerLifecycleTestHarness: {
|
|
|
69
73
|
boundedWorkerLifecycleProgress: typeof boundedWorkerLifecycleProgress;
|
|
70
74
|
workerServerHeartbeatAction: typeof workerServerHeartbeatAction;
|
|
71
75
|
createWorkspaceHydrationLeaseHooks: typeof createWorkspaceHydrationLeaseHooks;
|
|
76
|
+
workerCommandHasWorkspaceEffect: typeof workerCommandHasWorkspaceEffect;
|
|
72
77
|
};
|
|
73
78
|
type WorkspaceSyncTrigger = {
|
|
74
79
|
type: "connect" | "inbound_head" | "write" | "edit" | "shell_inline" | "process_terminal" | "process_cancel" | "periodic" | "manual" | "remediation" | "remediation_confirm" | "remediation_reset";
|
|
@@ -137,6 +142,11 @@ export type WorkerSessionTarget = {
|
|
|
137
142
|
rootProfile: "visible_projects" | "canonical_sync";
|
|
138
143
|
};
|
|
139
144
|
type WorkerClientMessage = WorkerRecoveryClientMessage | {
|
|
145
|
+
type: "capacity_report";
|
|
146
|
+
capacity: import("./command-launcher").CommandLaunchCapacityReport & {
|
|
147
|
+
reportedAt: string;
|
|
148
|
+
};
|
|
149
|
+
} | {
|
|
140
150
|
type: "hello";
|
|
141
151
|
resumableProtocol: typeof WORKER_RESUMABLE_PROTOCOL;
|
|
142
152
|
runtimeId: string;
|
|
@@ -172,6 +182,7 @@ type WorkerClientMessage = WorkerRecoveryClientMessage | {
|
|
|
172
182
|
startedAt: string;
|
|
173
183
|
interactive?: boolean;
|
|
174
184
|
workspaceEffect?: "none";
|
|
185
|
+
commandClass?: "control";
|
|
175
186
|
}>;
|
|
176
187
|
} | {
|
|
177
188
|
type: "pty_opened";
|
|
@@ -397,6 +408,7 @@ type WorkerServerMessage = WorkerRecoveryServerMessage | {
|
|
|
397
408
|
timeoutMs?: number;
|
|
398
409
|
interactive?: boolean;
|
|
399
410
|
workspaceEffect?: "none";
|
|
411
|
+
commandClass?: "control";
|
|
400
412
|
} | {
|
|
401
413
|
type: "exec_stdin";
|
|
402
414
|
requestId: string;
|
|
@@ -1010,12 +1022,12 @@ export declare function prepareBuiltInToolPathsForTarget(input: {
|
|
|
1010
1022
|
rootDir: string;
|
|
1011
1023
|
access: "read" | "write";
|
|
1012
1024
|
}): Promise<WorkerBuiltInToolPaths | undefined>;
|
|
1013
|
-
export declare function readWorkerTextFile(branchPath: string, filePath: string, offset?: number, limit?: number, builtInPaths?: WorkerBuiltInToolPaths): WorkerReadFileResult
|
|
1014
|
-
export declare function writeWorkerTextFile(branchPath: string, filePath: string, content: string, builtInPaths?: WorkerBuiltInToolPaths): WorkerWriteFileResult
|
|
1025
|
+
export declare function readWorkerTextFile(branchPath: string, filePath: string, offset?: number, limit?: number, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerReadFileResult>;
|
|
1026
|
+
export declare function writeWorkerTextFile(branchPath: string, filePath: string, content: string, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerWriteFileResult>;
|
|
1015
1027
|
export declare function editWorkerTextFile(branchPath: string, filePath: string, edits: Array<{
|
|
1016
1028
|
oldText: string;
|
|
1017
1029
|
newText: string;
|
|
1018
|
-
}>, builtInPaths?: WorkerBuiltInToolPaths): WorkerEditFileResult
|
|
1030
|
+
}>, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerEditFileResult>;
|
|
1019
1031
|
declare function executeWriteFileOperation(input: {
|
|
1020
1032
|
message: Extract<WorkerServerMessage, {
|
|
1021
1033
|
type: "write";
|
|
@@ -1042,17 +1054,17 @@ export declare function grepWorkerFiles(branchPath: string, input: {
|
|
|
1042
1054
|
glob?: string;
|
|
1043
1055
|
caseSensitive?: boolean;
|
|
1044
1056
|
limit?: number;
|
|
1045
|
-
}, builtInPaths?: WorkerBuiltInToolPaths): WorkerGrepResult
|
|
1057
|
+
}, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerGrepResult>;
|
|
1046
1058
|
export declare function findWorkerFiles(branchPath: string, input: {
|
|
1047
1059
|
pattern?: string;
|
|
1048
1060
|
path?: string;
|
|
1049
1061
|
entryType?: string;
|
|
1050
1062
|
limit?: number;
|
|
1051
|
-
}, builtInPaths?: WorkerBuiltInToolPaths): WorkerFindResult
|
|
1052
|
-
export declare function listWorkerDirectory(branchPath: string, inputPath?: string, inputLimit?: number, builtInPaths?: WorkerBuiltInToolPaths): WorkerLsResult
|
|
1053
|
-
export declare function listWorkerCodeDirectory(branchPath: string, inputPath: string): WorkerCodeListResult
|
|
1054
|
-
export declare function readWorkerCodeFile(branchPath: string, inputPath: string): WorkerCodeReadResult
|
|
1055
|
-
export declare function readWorkerImageFile(branchPath: string, filePath: string, builtInPaths?: WorkerBuiltInToolPaths): WorkerViewFileBytesResult
|
|
1063
|
+
}, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerFindResult>;
|
|
1064
|
+
export declare function listWorkerDirectory(branchPath: string, inputPath?: string, inputLimit?: number, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerLsResult>;
|
|
1065
|
+
export declare function listWorkerCodeDirectory(branchPath: string, inputPath: string): Promise<WorkerCodeListResult>;
|
|
1066
|
+
export declare function readWorkerCodeFile(branchPath: string, inputPath: string): Promise<WorkerCodeReadResult>;
|
|
1067
|
+
export declare function readWorkerImageFile(branchPath: string, filePath: string, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerViewFileBytesResult>;
|
|
1056
1068
|
declare function executeOperation(input: {
|
|
1057
1069
|
message: WorkerOperationServerMessage;
|
|
1058
1070
|
resolvedTarget: ResolvedWorkerSessionTarget;
|
|
@@ -18,7 +18,20 @@ export declare const PROJECT_DELETED_CHECKOUTS_DIRECTORY = ".r5d-deleted";
|
|
|
18
18
|
* later collection walk through it, so staging refuses rather than continue.
|
|
19
19
|
*/
|
|
20
20
|
export declare function stageProjectCheckoutForDeletion(projectRoot: string, branchName: string, checkoutPath: string): string;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Rename a whole project root out of the active namespace, into the
|
|
23
|
+
* namespace's garbage directory, so removing a deleted project never walks
|
|
24
|
+
* its checkouts on the worker event loop. Same guarantees as
|
|
25
|
+
* stageProjectCheckoutForDeletion: one same-filesystem rename, a real garbage
|
|
26
|
+
* directory, collection by the garbage collector later.
|
|
27
|
+
*/
|
|
28
|
+
export declare function stageProjectForDeletion(projectsRoot: string, projectRoot: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* True when `candidate` is lexically a direct entry of a project's garbage
|
|
31
|
+
* directory (`<namespace>/<project>/.r5d-deleted/<entry>`, a staged checkout)
|
|
32
|
+
* or of a namespace's garbage directory (`<namespace>/.r5d-deleted/<entry>`,
|
|
33
|
+
* a staged whole project) under `projectsRoot`.
|
|
34
|
+
*/
|
|
22
35
|
export declare function isStagedProjectCheckoutPath(projectsRoot: string, candidate: string): boolean;
|
|
23
36
|
/**
|
|
24
37
|
* Whether a lexically staged path is safe to remove right now: every ancestor
|
|
@@ -30,7 +43,11 @@ export declare function isStagedProjectCheckoutPath(projectsRoot: string, candid
|
|
|
30
43
|
export declare function ownedStagedPath(projectsRoot: string, candidate: string): {
|
|
31
44
|
kind: "directory" | "link";
|
|
32
45
|
} | null;
|
|
33
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Every owned staged checkout under `<projectsRoot>/<namespace>/<project>/.r5d-deleted/`
|
|
48
|
+
* and every owned staged project under `<projectsRoot>/<namespace>/.r5d-deleted/`;
|
|
49
|
+
* linked ancestors are not followed.
|
|
50
|
+
*/
|
|
34
51
|
export declare function discoverStagedProjectCheckouts(projectsRoot: string): string[];
|
|
35
52
|
/**
|
|
36
53
|
* Delete a staged tree in a child process so a large recursive removal never
|
|
@@ -160,29 +160,29 @@ export declare class ProjectWorkspaceStateStore {
|
|
|
160
160
|
desiredProjects: readonly ProjectWorkspaceDesiredProjectInput[];
|
|
161
161
|
locallyPendingCreatedBranches?: readonly LocallyPendingCreatedProjectBranch[];
|
|
162
162
|
preserveOnlyBranches?: readonly PreserveOnlyProjectBranch[];
|
|
163
|
-
}): ProjectWorkspaceStateView
|
|
163
|
+
}): Promise<ProjectWorkspaceStateView>;
|
|
164
164
|
/** Record a successful enabled reseed without letting disabled config consume it. */
|
|
165
|
-
recordEnabledRepositoryTransition(input: EnabledRepositoryTransition): ProjectWorkspaceStateView
|
|
165
|
+
recordEnabledRepositoryTransition(input: EnabledRepositoryTransition): Promise<ProjectWorkspaceStateView>;
|
|
166
166
|
/** Persist the create intent before invoking Git so a crash cannot orphan an untracked worktree. */
|
|
167
167
|
recordPendingCreatedBranch(input: LocallyPendingCreatedProjectBranch & {
|
|
168
168
|
branchId: string;
|
|
169
|
-
}): ProjectWorkspaceStateView
|
|
169
|
+
}): Promise<ProjectWorkspaceStateView>;
|
|
170
170
|
/** Roll back an intent only when Git failed before creating its branch. */
|
|
171
|
-
rollbackPendingCreatedBranch(input: LocallyPendingCreatedProjectBranch): ProjectWorkspaceStateView
|
|
171
|
+
rollbackPendingCreatedBranch(input: LocallyPendingCreatedProjectBranch): Promise<ProjectWorkspaceStateView>;
|
|
172
172
|
/** Clear a create intent after an authoritative desired config contains it. */
|
|
173
|
-
clearPendingCreatedBranch(input: LocallyPendingCreatedProjectBranch): ProjectWorkspaceStateView
|
|
173
|
+
clearPendingCreatedBranch(input: LocallyPendingCreatedProjectBranch): Promise<ProjectWorkspaceStateView>;
|
|
174
174
|
/** Persist a branch tombstone before mutating its linked worktree or local ref. */
|
|
175
|
-
beginBranchDeletion(input: LocallyPendingCreatedProjectBranch): ProjectWorkspaceStateView
|
|
175
|
+
beginBranchDeletion(input: LocallyPendingCreatedProjectBranch): Promise<ProjectWorkspaceStateView>;
|
|
176
176
|
recordTreePublication(input: {
|
|
177
177
|
tombstoneIds: readonly string[];
|
|
178
178
|
publishedHead: string;
|
|
179
|
-
}): ProjectWorkspaceStateView
|
|
179
|
+
}): Promise<ProjectWorkspaceStateView>;
|
|
180
180
|
recordMirrorRefDeletion(input: {
|
|
181
181
|
tombstoneId: string;
|
|
182
182
|
branchName: string;
|
|
183
|
-
}): ProjectWorkspaceStateView
|
|
183
|
+
}): Promise<ProjectWorkspaceStateView>;
|
|
184
184
|
}
|
|
185
185
|
export declare const projectWorkspaceStateTestHarness: {
|
|
186
|
-
writeEmptyStateAtDurableRootBoundary(stateRoot: string, afterStateRootFsync: () => void): void
|
|
186
|
+
writeEmptyStateAtDurableRootBoundary(stateRoot: string, afterStateRootFsync: () => void): Promise<void>;
|
|
187
187
|
};
|
|
188
188
|
export {};
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { type ProjectMirrorMount, type ProjectMirrorRefsTokens } from "./project-mirror-refs-token";
|
|
2
|
+
import type { WorkspaceFilesystemJobContext } from "./workspace-filesystem-job-types";
|
|
2
3
|
export declare const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
3
4
|
declare const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION: 1;
|
|
5
|
+
/**
|
|
6
|
+
* Distinguishes this control thread's live snapshots from stale ones left by
|
|
7
|
+
* an earlier process that reused the PID. The control thread passes it to
|
|
8
|
+
* every snapshot job, so the executor thread never invents its own.
|
|
9
|
+
*/
|
|
10
|
+
export declare const PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID: `${string}-${string}-${string}-${string}-${string}`;
|
|
4
11
|
/**
|
|
5
12
|
* `project` snapshots cover every configured branch and restore by replacing
|
|
6
13
|
* the whole project root. `branches` snapshots cover only the checkouts one
|
|
7
14
|
* reconciliation rewrites and restore those paths individually, so a rollback
|
|
8
15
|
* never touches a checkout the transaction did not.
|
|
9
16
|
*/
|
|
10
|
-
type ProjectWorktreeSnapshotScope = "project" | "branches";
|
|
11
|
-
type ProjectWorktreeSnapshotManifest = {
|
|
17
|
+
export type ProjectWorktreeSnapshotScope = "project" | "branches";
|
|
18
|
+
export type ProjectWorktreeSnapshotManifest = {
|
|
12
19
|
version: typeof PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION;
|
|
13
20
|
state: "building" | "prepared" | "restoring" | "consumed";
|
|
14
21
|
scope: ProjectWorktreeSnapshotScope;
|
|
@@ -21,6 +28,11 @@ type ProjectWorktreeSnapshotManifest = {
|
|
|
21
28
|
}>;
|
|
22
29
|
createdAt: string;
|
|
23
30
|
};
|
|
31
|
+
export type ProjectWorktreeSnapshot = {
|
|
32
|
+
root: string;
|
|
33
|
+
manifest: ProjectWorktreeSnapshotManifest;
|
|
34
|
+
paths: Map<string, string>;
|
|
35
|
+
};
|
|
24
36
|
export type ProjectWorktreeSnapshotProgress = {
|
|
25
37
|
operation: "snapshot" | "restore";
|
|
26
38
|
phase: "scanning" | "copying" | "moving" | "syncing" | "cleaning";
|
|
@@ -161,10 +173,10 @@ export declare function createOrRetryLinkedProjectBranch(input: {
|
|
|
161
173
|
branchName: string;
|
|
162
174
|
workingTree: ProjectWorktreeWorkingTreeMode;
|
|
163
175
|
pendingRetry: boolean;
|
|
164
|
-
}): {
|
|
176
|
+
}): Promise<{
|
|
165
177
|
branchPath: string;
|
|
166
178
|
baseCommitHash: string;
|
|
167
|
-
}
|
|
179
|
+
}>;
|
|
168
180
|
export type ProjectCheckoutState = "linked" | "missing" | "not_a_worktree";
|
|
169
181
|
export type ProjectCheckoutInventory = {
|
|
170
182
|
/** Branches whose checkout is this project's primary clone or one of its linked worktrees. */
|
|
@@ -216,10 +228,18 @@ export declare function deleteLinkedProjectBranch(input: {
|
|
|
216
228
|
*/
|
|
217
229
|
stagedForCollectionPath?: string;
|
|
218
230
|
};
|
|
231
|
+
/**
|
|
232
|
+
* Retire a whole project: one rename into the namespace garbage directory
|
|
233
|
+
* (the tree, primary Git objects included, is removed by the collector
|
|
234
|
+
* outside every lease), or nothing when the root is already gone.
|
|
235
|
+
*/
|
|
219
236
|
export declare function removeProjectWorktrees(input: {
|
|
237
|
+
projectsRoot: string;
|
|
220
238
|
projectRoot: string;
|
|
221
239
|
branchNames: readonly string[];
|
|
222
|
-
}):
|
|
240
|
+
}): {
|
|
241
|
+
stagedForCollectionPath?: string;
|
|
242
|
+
};
|
|
223
243
|
export declare function deleteProjectMirrorBranch(input: {
|
|
224
244
|
gitDirectory: string;
|
|
225
245
|
branchName: string;
|
|
@@ -335,6 +355,30 @@ export declare function reseedProjectHeadsFromMirror(input: ProjectMirrorHeadUpd
|
|
|
335
355
|
mirrorHead: string | null;
|
|
336
356
|
reseeded: boolean;
|
|
337
357
|
}>>;
|
|
358
|
+
/** Executor job body: `snapshotBranchTrees` with progress relayed to the control thread. */
|
|
359
|
+
export declare function runProjectSnapshotCreateJob(input: {
|
|
360
|
+
projectRoot: string;
|
|
361
|
+
branches: ProjectWorktreeBranch[];
|
|
362
|
+
temporaryRoot: string;
|
|
363
|
+
scope: ProjectWorktreeSnapshotScope;
|
|
364
|
+
ownerSessionId: string;
|
|
365
|
+
}, context: WorkspaceFilesystemJobContext): ProjectWorktreeSnapshot;
|
|
366
|
+
/** Executor job body: mirror captured checkouts back over reconciled worktrees. */
|
|
367
|
+
export declare function runProjectSnapshotApplyJob(input: {
|
|
368
|
+
mirrors: Array<{
|
|
369
|
+
snapshotPath: string;
|
|
370
|
+
targetPath: string;
|
|
371
|
+
}>;
|
|
372
|
+
}): null;
|
|
373
|
+
/** Executor job body: `persistReconciledProjectAndConsumeSnapshot`. */
|
|
374
|
+
export declare function runProjectSnapshotPersistJob(input: {
|
|
375
|
+
snapshot: ProjectWorktreeSnapshot;
|
|
376
|
+
durabilityPaths: string[] | null;
|
|
377
|
+
}): null;
|
|
378
|
+
/** Executor job body: `rollbackProjectWorktreeSnapshot`. */
|
|
379
|
+
export declare function runProjectSnapshotRollbackJob(input: {
|
|
380
|
+
snapshot: ProjectWorktreeSnapshot;
|
|
381
|
+
}): null;
|
|
338
382
|
export declare const projectWorktreesTestHarness: {
|
|
339
383
|
commandArgs: typeof gitCommandArgs;
|
|
340
384
|
configureRepository: typeof configureRepository;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Messages between the control thread and the recovery journal thread. */
|
|
2
|
+
export declare const WORKER_RECOVERY_JOURNAL_OPS: readonly ["admit", "readmitUnknownBranchDeletion", "record", "unknown", "unknownRun", "acknowledgeResult", "acknowledgeOutput", "acknowledgeTerminal", "cancelSession", "isRequestCancelled", "operations", "recoveryState", "terminals", "outputReplayBegin", "outputReplayNext", "outputReplayEnd"];
|
|
3
|
+
export type WorkerRecoveryJournalOp = (typeof WORKER_RECOVERY_JOURNAL_OPS)[number];
|
|
4
|
+
export declare function isWorkerRecoveryJournalOp(value: unknown): value is WorkerRecoveryJournalOp;
|
|
5
|
+
export type WorkerRecoveryJournalEntry = {
|
|
6
|
+
seq: number;
|
|
7
|
+
op: WorkerRecoveryJournalOp;
|
|
8
|
+
args: unknown[];
|
|
9
|
+
};
|
|
10
|
+
export type WorkerRecoveryJournalBatchRequest = {
|
|
11
|
+
type: "batch";
|
|
12
|
+
id: number;
|
|
13
|
+
entries: WorkerRecoveryJournalEntry[];
|
|
14
|
+
};
|
|
15
|
+
export type WorkerRecoveryJournalSerializedError = {
|
|
16
|
+
name: string;
|
|
17
|
+
message: string;
|
|
18
|
+
stack?: string;
|
|
19
|
+
};
|
|
20
|
+
export type WorkerRecoveryJournalThreadReply = {
|
|
21
|
+
type: "ready";
|
|
22
|
+
ledgerId: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: "batch_result";
|
|
25
|
+
id: number;
|
|
26
|
+
results: Array<{
|
|
27
|
+
seq: number;
|
|
28
|
+
value: unknown;
|
|
29
|
+
}>;
|
|
30
|
+
} | {
|
|
31
|
+
type: "batch_failure";
|
|
32
|
+
id: number;
|
|
33
|
+
error: WorkerRecoveryJournalSerializedError;
|
|
34
|
+
};
|
|
35
|
+
export declare function serializeWorkerRecoveryJournalError(error: unknown): WorkerRecoveryJournalSerializedError;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MessagePort } from "node:worker_threads";
|
|
2
|
+
/**
|
|
3
|
+
* The recovery journal thread's loop. It owns the one WorkerRecoveryStore
|
|
4
|
+
* handle of the process and applies the control thread's entries strictly in
|
|
5
|
+
* sequence, one durable commit per batch. Everything the store guarantees
|
|
6
|
+
* (acceptance before effects, offsets, terminal lengths, unknown gating,
|
|
7
|
+
* acknowledgement retention, cancel snapshots) is unchanged; only the thread
|
|
8
|
+
* running it moved. `beforeBatch` exists for the stall fixture used by tests.
|
|
9
|
+
*/
|
|
10
|
+
export declare function serveRecoveryJournal(port: MessagePort, filename: string, hooks?: {
|
|
11
|
+
beforeBatch?: () => void;
|
|
12
|
+
}): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|