@ricsam/r5d-worker 0.0.123 → 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.
Files changed (60) hide show
  1. package/dist/cjs/main.cjs +488 -238
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/cjs/project-checkout-garbage.cjs +32 -3
  4. package/dist/cjs/project-workspace-state.cjs +51 -35
  5. package/dist/cjs/project-worktrees.cjs +79 -34
  6. package/dist/cjs/recovery-journal-protocol.cjs +56 -0
  7. package/dist/cjs/recovery-journal-runtime.cjs +133 -0
  8. package/dist/cjs/recovery-journal-thread.cjs +9 -0
  9. package/dist/cjs/recovery-journal.cjs +735 -0
  10. package/dist/cjs/recovery-store.cjs +8 -0
  11. package/dist/cjs/session-file-mutations.cjs +61 -0
  12. package/dist/cjs/working-tree-mirror.cjs +1 -0
  13. package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
  14. package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
  15. package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
  16. package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
  17. package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
  18. package/dist/cjs/workspace-git-sync.cjs +275 -201
  19. package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
  20. package/dist/mjs/main.mjs +491 -244
  21. package/dist/mjs/package.json +1 -1
  22. package/dist/mjs/project-checkout-garbage.mjs +30 -2
  23. package/dist/mjs/project-workspace-state.mjs +51 -35
  24. package/dist/mjs/project-worktrees.mjs +74 -34
  25. package/dist/mjs/recovery-journal-protocol.mjs +30 -0
  26. package/dist/mjs/recovery-journal-runtime.mjs +112 -0
  27. package/dist/mjs/recovery-journal-thread.mjs +8 -0
  28. package/dist/mjs/recovery-journal.mjs +687 -0
  29. package/dist/mjs/recovery-store.mjs +8 -0
  30. package/dist/mjs/session-file-mutations.mjs +37 -0
  31. package/dist/mjs/working-tree-mirror.mjs +1 -0
  32. package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
  33. package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
  34. package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
  35. package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
  36. package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
  37. package/dist/mjs/workspace-git-sync.mjs +264 -202
  38. package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
  39. package/dist/types/main.d.ts +24 -19
  40. package/dist/types/project-checkout-garbage.d.ts +19 -2
  41. package/dist/types/project-workspace-state.d.ts +9 -9
  42. package/dist/types/project-worktrees.d.ts +49 -5
  43. package/dist/types/recovery-journal-protocol.d.ts +35 -0
  44. package/dist/types/recovery-journal-runtime.d.ts +12 -0
  45. package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
  46. package/dist/types/recovery-journal-thread.d.ts +1 -0
  47. package/dist/types/recovery-journal.d.ts +246 -0
  48. package/dist/types/recovery-store.d.ts +6 -0
  49. package/dist/types/session-file-mutations.d.ts +22 -0
  50. package/dist/types/workspace-command-sync-policy.d.ts +22 -7
  51. package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
  52. package/dist/types/workspace-filesystem-executor.d.ts +123 -0
  53. package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
  54. package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
  55. package/dist/types/workspace-git-sync.d.ts +113 -7
  56. package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
  57. package/package.json +1 -1
  58. package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
  59. package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
  60. package/dist/types/project-snapshot-recovery-runner.d.ts +0 -10
@@ -11,6 +11,12 @@ export declare class WorkerRecoveryStore {
11
11
  readonly ledgerId: string;
12
12
  constructor(filename: string);
13
13
  close(): void;
14
+ /**
15
+ * Run several store operations as one durable commit (one FULL fsync).
16
+ * Operations that open their own transaction nest as savepoints. A throw
17
+ * rolls the whole group back; callers treat that as a journal failure.
18
+ */
19
+ batch<T>(operations: () => T): T;
14
20
  private row;
15
21
  admit(message: Message & {
16
22
  requestId: string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * File mutations (write, edit, plan updates) run on the asynchronous
3
+ * filesystem API, so a write can still be in flight when a session is
4
+ * stopped. A Stop acknowledges cleanup only after every mutation that had
5
+ * already started for the session has actually settled; mutations that were
6
+ * still queued re-check the session's cancellation immediately before their
7
+ * first write and never start. The tracker records the former; the admission
8
+ * fence inside each mutation's queue callback provides the latter.
9
+ */
10
+ export declare class SessionFileMutationTracker {
11
+ private readonly inFlight;
12
+ /** Register a started mutation; it is forgotten when it settles, however it settles. */
13
+ track<T>(sessionId: string, mutation: Promise<T>): Promise<T>;
14
+ count(sessionId: string): number;
15
+ /**
16
+ * Resolves once every mutation tracked for the session at the time of the
17
+ * call has settled. Mutations started later are not joined: the session's
18
+ * cancellation marker, recorded before this is awaited, makes their
19
+ * admission recheck refuse them.
20
+ */
21
+ settled(sessionId: string): Promise<void>;
22
+ }
@@ -1,5 +1,7 @@
1
- type WorkspaceCommandTarget = {
1
+ export type WorkspaceCommandTarget = {
2
2
  type: "project";
3
+ projectId: string;
4
+ branchName: string;
3
5
  } | {
4
6
  type: "workspace";
5
7
  rootProfile: "visible_projects" | "canonical_sync";
@@ -8,16 +10,29 @@ type WorkspaceCommandMutationCoordinator = {
8
10
  runMutation<T>(operation: () => Promise<T> | T): Promise<T>;
9
11
  acquireMutation(): Promise<() => void>;
10
12
  };
11
- type WorkspaceSyncCompletionBarrier = {
13
+ export type WorkspaceSyncCompletionBarrier = {
12
14
  afterCurrent(): Promise<void>;
15
+ /**
16
+ * Null when no workspace filesystem job holds the target's mount right now;
17
+ * otherwise a promise that settles once the current holders released it
18
+ * (rejecting with WorkspaceMountHoldAbortedError when `signal` aborts
19
+ * first). Workspace targets have no mount and always get null.
20
+ */
21
+ mountHold(target: WorkspaceCommandTarget, signal?: AbortSignal): Promise<void> | null;
13
22
  };
14
23
  /**
15
- * A project command may reserve immediately: per-mount hydration rechecks its
16
- * busy predicate immediately before touching visible bytes. A visible-projects
17
- * workspace command spans every project, so it keeps the whole-cycle barrier.
18
- * Canonical remediation is serialized separately by the mutation gate.
24
+ * A project command may reserve as soon as no workspace filesystem job holds
25
+ * its mount: per-mount hydration and projection observe busy-ness before they
26
+ * dispatch a job and hold the mount until the job ended, so a reservation
27
+ * taken here is never interleaved with a job reading or writing the mount. A
28
+ * visible-projects workspace command spans every project, so it keeps the
29
+ * whole-cycle barrier. Canonical remediation is serialized separately by the
30
+ * mutation gate. Commands without a workspace effect (incident-9 control
31
+ * commands among them) never come here and never wait.
19
32
  */
20
- export declare function reserveWorkspaceCommandAfterCurrentSync(target: WorkspaceCommandTarget, coordinator: WorkspaceSyncCompletionBarrier, reserve: () => void): Promise<void>;
33
+ export declare function reserveWorkspaceCommandAfterCurrentSync(target: WorkspaceCommandTarget, coordinator: WorkspaceSyncCompletionBarrier, reserve: () => void, options?: {
34
+ signal?: AbortSignal;
35
+ }): Promise<void>;
21
36
  /**
22
37
  * Commands in visible checkouts behave like independent Git clients and may
23
38
  * overlap periodic synchronization. Canonical remediation commands mutate the
@@ -0,0 +1,123 @@
1
+ import { type WorkspaceFilesystemJobInput, type WorkspaceFilesystemJobKind, type WorkspaceFilesystemJobProgress, type WorkspaceFilesystemJobResult } from "./workspace-filesystem-job-types";
2
+ /**
3
+ * Bounded executor for the worker's workspace filesystem transactions.
4
+ *
5
+ * One thread runs one job at a time; the control thread only ever awaits.
6
+ * The point is the incident this was written for: a single `pread64` or
7
+ * `pwrite64` blocked in the kernel on a stalled network disk used to freeze
8
+ * the worker's only event loop for minutes, so no heartbeat, cancel or Stop
9
+ * could run. On the executor thread that same blocked syscall stalls exactly
10
+ * one job while the control thread keeps serving the socket.
11
+ *
12
+ * A thread rather than a helper process because a thread cannot outlive the
13
+ * process: when the worker dies, its in-flight syscall completes and nothing
14
+ * else is written afterwards, which is the crash model every recovery path
15
+ * already handles. Nothing is ever abandoned: a running job's promise settles
16
+ * only with the thread's reply or the thread's exit, so the gate its caller
17
+ * holds is held for the job's true duration. Only jobs that have not started
18
+ * can fail on the queue-wait bound.
19
+ *
20
+ * A thread that exits while a job is running leaves an unknown partial
21
+ * mutation behind. The executor then fails closed: the job and everything
22
+ * queued fail, no further job is accepted, and `onFailure` lets the worker
23
+ * take its established restart path, whose startup recovery (manifests,
24
+ * receipts, snapshot recovery) runs before any ordinary mutation. No
25
+ * replacement thread is started over that state in this process.
26
+ */
27
+ export declare const WORKSPACE_FILESYSTEM_EXECUTOR_MAX_QUEUED = 32;
28
+ export declare const WORKSPACE_FILESYSTEM_EXECUTOR_QUEUE_TIMEOUT_MS: number;
29
+ export declare const WORKSPACE_FILESYSTEM_EXECUTOR_STALL_LOG_INTERVAL_MS = 30000;
30
+ export declare class WorkspaceFilesystemExecutorError extends Error {
31
+ readonly name = "WorkspaceFilesystemExecutorError";
32
+ }
33
+ /** The executor thread exited (crashed or was terminated) while a job was running. */
34
+ export declare class WorkspaceFilesystemExecutorThreadExitedError extends WorkspaceFilesystemExecutorError {
35
+ readonly exitCode: number | null;
36
+ readonly description: string;
37
+ constructor(exitCode: number | null, description: string, cause?: unknown);
38
+ }
39
+ export type WorkspaceFilesystemExecutorStatus = {
40
+ running: {
41
+ kind: WorkspaceFilesystemJobKind;
42
+ description: string;
43
+ startedAtMs: number;
44
+ ageMs: number;
45
+ } | null;
46
+ queued: number;
47
+ /** Set once the thread exited under a running job; every job is then rejected until the worker restarts. */
48
+ failed: string | null;
49
+ };
50
+ export type WorkspaceFilesystemExecutorOptions = {
51
+ /** Module the thread executes; must resolve next to the packaged executor module. */
52
+ threadModulePath: string;
53
+ log?: (line: string) => void;
54
+ now?: () => number;
55
+ maxQueued?: number;
56
+ queueTimeoutMs?: number;
57
+ stallLogIntervalMs?: number;
58
+ };
59
+ export declare class WorkspaceFilesystemExecutor {
60
+ private readonly threadModulePath;
61
+ private readonly log;
62
+ private readonly now;
63
+ private readonly maxQueued;
64
+ private readonly queueTimeoutMs;
65
+ private readonly stallLogIntervalMs;
66
+ private worker;
67
+ private readonly queue;
68
+ private running;
69
+ private nextJobId;
70
+ private failed;
71
+ private closed;
72
+ /** Invoked once when the executor fails closed (thread exit under a running job). */
73
+ onFailure: ((error: WorkspaceFilesystemExecutorThreadExitedError) => void) | undefined;
74
+ constructor(options: WorkspaceFilesystemExecutorOptions);
75
+ /**
76
+ * Run one job to completion on the executor thread. Resolves with the
77
+ * job's result; rejects with the job's own error (properties preserved),
78
+ * with a queue-wait or capacity error when the job never started, or with
79
+ * WorkspaceFilesystemExecutorThreadExitedError when the thread died under it.
80
+ */
81
+ run<K extends WorkspaceFilesystemJobKind>(kind: K, input: WorkspaceFilesystemJobInput<K>, options?: {
82
+ onProgress?: (progress: WorkspaceFilesystemJobProgress) => void;
83
+ }): Promise<WorkspaceFilesystemJobResult<K>>;
84
+ status(): WorkspaceFilesystemExecutorStatus;
85
+ /**
86
+ * Stop accepting jobs, fail everything still queued and ask the thread to
87
+ * stop. A running job settles when the thread actually exits; a thread
88
+ * blocked in the kernel only exits with the process, which is the point.
89
+ */
90
+ close(): void;
91
+ private describeRunning;
92
+ private expireQueued;
93
+ private dispatch;
94
+ private settleRunning;
95
+ private ensureWorker;
96
+ private lastThreadError;
97
+ private onReply;
98
+ private onExit;
99
+ }
100
+ /** The packaged thread module lives next to this module with the same extension (.ts in source, .cjs/.mjs in the package). */
101
+ export declare function workspaceFilesystemExecutorThreadModulePath(): string;
102
+ /** What the engine modules dispatch through; the thread executor in production. */
103
+ export type WorkspaceFilesystemJobRunner = Pick<WorkspaceFilesystemExecutor, "run">;
104
+ /**
105
+ * The worker process's executor. Like the workspace mutation gate it is
106
+ * process-global: in-process reconnects share it, so a job started by a
107
+ * previous connection generation is still the one running when the next
108
+ * generation's configuration queues behind it.
109
+ */
110
+ export declare function workspaceFilesystemExecutor(): WorkspaceFilesystemJobRunner;
111
+ /** The worker runtime's fail-closed hook: installed once on the process executor (created if needed). */
112
+ export declare function installWorkspaceFilesystemExecutorFailureHandler(handler: (error: WorkspaceFilesystemExecutorThreadExitedError) => void): void;
113
+ /**
114
+ * Test seam. Engine tests that inject faults or count syscalls by replacing
115
+ * `node:fs` functions in their own thread cannot observe a body running on
116
+ * the executor thread; inside `runJobsInline` the same operation table runs
117
+ * on the calling thread instead. The boundary itself is tested with the real
118
+ * thread in workspace-filesystem-executor.test.ts. Never used in production.
119
+ */
120
+ export declare const workspaceFilesystemExecutorTestHarness: {
121
+ runJobsInline<T>(body: () => Promise<T>): Promise<T>;
122
+ processExecutor(): WorkspaceFilesystemExecutor | null;
123
+ };
@@ -0,0 +1,246 @@
1
+ import type { ProjectWorktreeBranch, ProjectWorktreeSnapshot, ProjectWorktreeSnapshotProgress, ProjectWorktreeSnapshotScope, ProjectWorktreeWorkingTreeMode } from "./project-worktrees";
2
+ import type { WorkingTreeGitlink } from "./working-tree-mirror";
3
+ import type { WorkspaceGitMountData, WorkspaceHydrationReceipt } from "./workspace-git-sync";
4
+ import type { WorkspaceMergeProjectionMount, WorkspaceMergeProjectionResult } from "./workspace-merge-projection";
5
+ import type { ProjectCheckoutPathMoveBranch } from "./workspace-path-move";
6
+ /**
7
+ * The closed set of filesystem transactions the worker runs on its executor
8
+ * thread. Every input is plain data (paths, mount descriptors, receipts); the
9
+ * busy predicates, mutation tokens, hooks and gates that decide *whether* a
10
+ * job may run stay on the control thread. Every body is the synchronous code
11
+ * that used to run on the control thread, unchanged in its write and fsync
12
+ * ordering. A job kind outside this table is rejected by both threads.
13
+ */
14
+ export type WorkspaceFilesystemJobTable = {
15
+ /** `recoverWorkspaceCheckoutDurability`: rebuild an interrupted outer checkout boundary. */
16
+ checkout_durability_recovery: {
17
+ input: {
18
+ workspacePath: string;
19
+ preserveResolutionInProgress: boolean;
20
+ };
21
+ result: null;
22
+ };
23
+ /** Checkout durability recovery, pending hydration transaction restore/completion, basis-ref repair. */
24
+ hydration_recovery: {
25
+ input: {
26
+ workspacePath: string;
27
+ preserveResolutionInProgress: boolean;
28
+ };
29
+ result: null;
30
+ };
31
+ /** The complete hydration transaction: snapshot, hydrate, fsync, commit marker, receipt; restore on failure. */
32
+ hydration_transaction: {
33
+ input: {
34
+ workspacePath: string;
35
+ hydrationMounts: WorkspaceGitMountData[];
36
+ advancedDurabilityMounts: WorkspaceGitMountData[];
37
+ targetReceipt: WorkspaceHydrationReceipt | null;
38
+ };
39
+ result: {
40
+ hydratedMountIds: string[];
41
+ };
42
+ };
43
+ /** Pre-clone hydration of visible mounts without a transaction (no outer Git directory yet). */
44
+ hydration_raw: {
45
+ input: {
46
+ workspacePath: string;
47
+ mounts: WorkspaceGitMountData[];
48
+ };
49
+ result: {
50
+ hydratedMountIds: string[];
51
+ };
52
+ };
53
+ /** Recover the durable checkout boundary and record the transition that follows. */
54
+ checkout_transition_begin: {
55
+ input: {
56
+ workspacePath: string;
57
+ };
58
+ result: null;
59
+ };
60
+ /** Make the outer checkout durable and record its head as the durable boundary. */
61
+ checkout_transition_complete: {
62
+ input: {
63
+ workspacePath: string;
64
+ };
65
+ result: null;
66
+ };
67
+ /** Durability barrier over the outer checkout tree without changing the record. */
68
+ outer_checkout_fsync: {
69
+ input: {
70
+ workspacePath: string;
71
+ };
72
+ result: null;
73
+ };
74
+ /** Three-way merge projection of one stale mount (ours snapshot + merge-tree). */
75
+ projection_merge: {
76
+ input: {
77
+ workspacePath: string;
78
+ mount: WorkspaceMergeProjectionMount;
79
+ basisHead: string;
80
+ currentHead: string;
81
+ attemptId: string;
82
+ };
83
+ result: WorkspaceMergeProjectionResult;
84
+ };
85
+ /** Byte mirror of one visible mount into the outer checkout. */
86
+ projection_mirror: {
87
+ input: {
88
+ workspacePath: string;
89
+ mount: WorkspaceGitMountData;
90
+ };
91
+ result: {
92
+ gitlinks: WorkingTreeGitlink[];
93
+ };
94
+ };
95
+ /** Outer-tree edits after the mirrors: tombstone removal, merged subtree materialization, deferred restores. */
96
+ projection_outer_apply: {
97
+ input: {
98
+ workspacePath: string;
99
+ removeMounts: WorkspaceGitMountData[];
100
+ materialize: Array<{
101
+ workspaceRelativePath: string;
102
+ resultTree: string;
103
+ }>;
104
+ restoreFromHead: WorkspaceGitMountData[];
105
+ };
106
+ result: null;
107
+ };
108
+ /** Persist projected visible mounts and publish the projection receipt. */
109
+ projection_receipt: {
110
+ input: {
111
+ workspacePath: string;
112
+ fsyncMounts: WorkspaceGitMountData[];
113
+ receipt: WorkspaceHydrationReceipt;
114
+ };
115
+ result: null;
116
+ };
117
+ /** `snapshotBranchTrees`: copy every checkout a reconciliation may rewrite. */
118
+ project_snapshot_create: {
119
+ input: {
120
+ projectRoot: string;
121
+ branches: ProjectWorktreeBranch[];
122
+ temporaryRoot: string;
123
+ scope: ProjectWorktreeSnapshotScope;
124
+ ownerSessionId: string;
125
+ };
126
+ result: ProjectWorktreeSnapshot;
127
+ };
128
+ /** Mirror captured checkouts back over the reconciled worktrees. */
129
+ project_snapshot_apply: {
130
+ input: {
131
+ mirrors: Array<{
132
+ snapshotPath: string;
133
+ targetPath: string;
134
+ }>;
135
+ };
136
+ result: null;
137
+ };
138
+ /** `persistReconciledProjectAndConsumeSnapshot`. */
139
+ project_snapshot_persist: {
140
+ input: {
141
+ snapshot: ProjectWorktreeSnapshot;
142
+ durabilityPaths: string[] | null;
143
+ };
144
+ result: null;
145
+ };
146
+ /** `rollbackProjectWorktreeSnapshot`. */
147
+ project_snapshot_rollback: {
148
+ input: {
149
+ snapshot: ProjectWorktreeSnapshot;
150
+ };
151
+ result: null;
152
+ };
153
+ /** `recoverStaleProjectWorktreeSnapshots` at startup. */
154
+ project_snapshots_recover: {
155
+ input: {
156
+ projectsRoot: string;
157
+ temporaryRoot: string;
158
+ currentProcessId: number;
159
+ currentOwnerSessionId: string;
160
+ };
161
+ result: {
162
+ restored: string[];
163
+ removed: string[];
164
+ failed: Array<{
165
+ path: string;
166
+ error: string;
167
+ }>;
168
+ };
169
+ };
170
+ /** `createLinkedProjectBranch`: branch ref, linked worktree, index seed and working-tree copy. */
171
+ project_branch_create: {
172
+ input: {
173
+ projectRoot: string;
174
+ sourceBranchName: string;
175
+ branchName: string;
176
+ workingTree: ProjectWorktreeWorkingTreeMode;
177
+ };
178
+ result: {
179
+ branchPath: string;
180
+ baseCommitHash: string;
181
+ };
182
+ };
183
+ /** `preserveProjectCheckoutPathMove`. */
184
+ project_checkout_path_move: {
185
+ input: {
186
+ oldProjectRoot: string;
187
+ newProjectRoot: string;
188
+ projectsDurabilityRoot: string;
189
+ outerWorkspaceRoot: string;
190
+ branches: ProjectCheckoutPathMoveBranch[];
191
+ };
192
+ result: {
193
+ preservedBranchNames: string[];
194
+ };
195
+ };
196
+ };
197
+ export type WorkspaceFilesystemJobKind = keyof WorkspaceFilesystemJobTable;
198
+ export type WorkspaceFilesystemJobInput<K extends WorkspaceFilesystemJobKind> = WorkspaceFilesystemJobTable[K]["input"];
199
+ export type WorkspaceFilesystemJobResult<K extends WorkspaceFilesystemJobKind> = WorkspaceFilesystemJobTable[K]["result"];
200
+ export type WorkspaceFilesystemJobProgress = ProjectWorktreeSnapshotProgress;
201
+ export type WorkspaceFilesystemJobContext = {
202
+ progress: (progress: WorkspaceFilesystemJobProgress) => void;
203
+ };
204
+ export type WorkspaceFilesystemJobOperations = {
205
+ [K in WorkspaceFilesystemJobKind]: (input: WorkspaceFilesystemJobInput<K>, context: WorkspaceFilesystemJobContext) => WorkspaceFilesystemJobResult<K>;
206
+ };
207
+ export declare const WORKSPACE_FILESYSTEM_JOB_KINDS: readonly WorkspaceFilesystemJobKind[];
208
+ export declare function isWorkspaceFilesystemJobKind(value: unknown): value is WorkspaceFilesystemJobKind;
209
+ /** Short, path-bearing description used in stall diagnostics and errors. */
210
+ export declare function describeWorkspaceFilesystemJob<K extends WorkspaceFilesystemJobKind>(kind: K, input: WorkspaceFilesystemJobInput<K>): string;
211
+ /** Errors cross the thread boundary as data; the properties callers inspect are preserved. */
212
+ export type WorkspaceFilesystemSerializedError = {
213
+ name: string;
214
+ message: string;
215
+ stack?: string;
216
+ code?: string;
217
+ branchMayExist?: true;
218
+ cleanupFailures?: string[];
219
+ errors?: WorkspaceFilesystemSerializedError[];
220
+ cause?: WorkspaceFilesystemSerializedError;
221
+ };
222
+ export declare function serializeWorkspaceFilesystemError(error: unknown, depth?: number): WorkspaceFilesystemSerializedError;
223
+ export declare function restoreWorkspaceFilesystemError(serialized: WorkspaceFilesystemSerializedError): Error;
224
+ /** Control thread → executor thread. */
225
+ export type WorkspaceFilesystemThreadRequest = {
226
+ type: "run";
227
+ id: number;
228
+ kind: WorkspaceFilesystemJobKind;
229
+ input: unknown;
230
+ /** The control thread's environment at dispatch; the thread runs the job under exactly this. */
231
+ environment: Record<string, string>;
232
+ };
233
+ /** Executor thread → control thread. */
234
+ export type WorkspaceFilesystemThreadReply = {
235
+ type: "progress";
236
+ id: number;
237
+ progress: WorkspaceFilesystemJobProgress;
238
+ } | {
239
+ type: "result";
240
+ id: number;
241
+ result: unknown;
242
+ } | {
243
+ type: "failure";
244
+ id: number;
245
+ error: WorkspaceFilesystemSerializedError;
246
+ };
@@ -0,0 +1,7 @@
1
+ import type { WorkspaceFilesystemJobOperations } from "./workspace-filesystem-job-types";
2
+ /**
3
+ * The operation table of the workspace filesystem executor thread. It is the
4
+ * only thing the thread can run; every entry is an existing synchronous
5
+ * transaction body. Imported by the thread entry module only.
6
+ */
7
+ export declare const workspaceFilesystemJobOperations: WorkspaceFilesystemJobOperations;
@@ -5,6 +5,27 @@ export declare const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confi
5
5
  export declare const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
6
6
  export declare const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
7
7
  export declare const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
8
+ type WorkspaceHydrationMountBasis = {
9
+ id: string;
10
+ incarnationKey: string;
11
+ sourcePath: string;
12
+ durabilityRootPath: string;
13
+ workspaceRelativePath: string;
14
+ sourceMode: WorkingTreeSourceMode;
15
+ hydrateDeletionMode: "all" | "git";
16
+ preserveLocalOnInitialOuterAbsence: boolean;
17
+ preserveLocalOnHydrationBasisChange: boolean;
18
+ deleteWhenSourceMissing: boolean;
19
+ };
20
+ type WorkspaceHydrationMountReceipt = WorkspaceHydrationMountBasis & {
21
+ head: string;
22
+ };
23
+ export type WorkspaceHydrationReceipt = {
24
+ version: 3;
25
+ mounts: WorkspaceHydrationMountReceipt[];
26
+ /** Non-enumerable read-compat marker used only to finish a v1/v2 crash transition. */
27
+ legacyHead?: string;
28
+ };
8
29
  export type WorkspaceGitMount = {
9
30
  id: string;
10
31
  /** Durable logical incarnation (for example the project-branch row id). */
@@ -49,6 +70,15 @@ export type WorkspaceGitMount = {
49
70
  /** Activity-only fence used before checkout readiness is established on reconnect. */
50
71
  busyForRecovery?: () => boolean;
51
72
  };
73
+ /**
74
+ * The data of a mount, without its control-thread predicates. This is what a
75
+ * workspace filesystem job receives: a job never evaluates `busy()` or
76
+ * `mutationToken()`; the control thread evaluated them before dispatch and
77
+ * holds the mount (see WorkspaceHydrationTransactionHooks.holdMounts) until
78
+ * the job ends.
79
+ */
80
+ export type WorkspaceGitMountData = Pick<WorkspaceGitMount, "id" | "hydrationIncarnationKey" | "sourcePath" | "durabilityRootPath" | "workspaceRelativePath" | "sourceMode" | "hydrateDeletionMode" | "preserveLocalOnInitialOuterAbsence" | "preserveLocalOnHydrationBasisChange" | "initialPublicationSourceRelativePath" | "deleteWhenSourceMissing" | "lastProjectedMutationToken">;
81
+ export declare function workspaceGitMountData(mount: WorkspaceGitMount): WorkspaceGitMountData;
52
82
  /**
53
83
  * Incident-bound configuration keeps project checkouts unready while the
54
84
  * canonical remediation owns the workspace. The guarded remediation sync is
@@ -65,17 +95,23 @@ export type WorkspaceHydrationTransactionContext = {
65
95
  mountIds: string[];
66
96
  };
67
97
  /**
68
- * Observes the synchronous body of a hydration transaction (snapshot,
69
- * hydration pass, durability barrier, receipt). That body deliberately never
70
- * yields, so the worker cannot answer control-socket messages while it runs,
71
- * and on a slow disk it can run for hours. `beforeTransaction` is awaited
72
- * after the last asynchronous precheck and before the final busy observation,
73
- * so it may announce the operation and yield once; `afterTransaction` runs
74
- * after the body committed or rolled back.
98
+ * Observes a hydration transaction (snapshot, hydration pass, durability
99
+ * barrier, receipt) and fences the mounts any workspace filesystem job reads
100
+ * or writes. The body runs on the executor thread and on a stalled disk it
101
+ * can run for a long time. `beforeTransaction` is awaited after the last
102
+ * asynchronous precheck and before the final busy observation, so it may
103
+ * announce the operation and yield once; `afterTransaction` runs after the
104
+ * body committed or rolled back. `holdMounts` is called synchronously right
105
+ * after the busy observation that selected the mounts, before the job is
106
+ * dispatched, and its release is called when the job's result or failure
107
+ * arrived: while held, command admission for those mounts waits (see
108
+ * WorkspaceMountHoldFence), which replaces the guarantee the un-yielding body
109
+ * used to give.
75
110
  */
76
111
  export type WorkspaceHydrationTransactionHooks = {
77
112
  beforeTransaction: (context: WorkspaceHydrationTransactionContext) => Promise<void> | void;
78
113
  afterTransaction: (context: WorkspaceHydrationTransactionContext) => void;
114
+ holdMounts: (mountIds: readonly string[], description: string) => () => void;
79
115
  };
80
116
  export type WorkspaceGitSyncOutcome = "no_change" | "updated" | "pushed" | "large_diff_blocked" | "conflict_blocked";
81
117
  export type WorkspaceGitSyncResult = {
@@ -126,6 +162,23 @@ export declare function workspaceGitHydrationIsCurrent(workspacePath: string, mo
126
162
  * Configuration-time recovery keeps the strict predicate.
127
163
  */
128
164
  export declare function workspaceGitHydrationIsRecoverable(workspacePath: string, mounts?: readonly WorkspaceGitMount[]): boolean;
165
+ /** Executor job body: recover the durable boundary, then record the transition that follows. */
166
+ export declare function runCheckoutTransitionBeginJob(input: {
167
+ workspacePath: string;
168
+ }): null;
169
+ /** Executor job body: make the outer checkout durable and record its head as the durable boundary. */
170
+ export declare function runCheckoutTransitionCompleteJob(input: {
171
+ workspacePath: string;
172
+ }): null;
173
+ /** Executor job body: `recoverWorkspaceCheckoutDurability`. */
174
+ export declare function runCheckoutDurabilityRecoveryJob(input: {
175
+ workspacePath: string;
176
+ preserveResolutionInProgress: boolean;
177
+ }): null;
178
+ /** Executor job body: the outer-tree durability barrier alone. */
179
+ export declare function runOuterCheckoutFsyncJob(input: {
180
+ workspacePath: string;
181
+ }): null;
129
182
  declare function configureWorkspaceRepository(input: {
130
183
  workspacePath: string;
131
184
  remoteUrl: string;
@@ -179,6 +232,36 @@ type ProjectedMountGitlinks = {
179
232
  * `stageWorkspaceGitlinks` before `git add -A`, which can never create one.
180
233
  */
181
234
  declare function mirrorMountsToWorkspace(workspacePath: string, mounts: readonly WorkspaceGitMount[]): ProjectedMountGitlinks[];
235
+ /**
236
+ * Executor job body: the complete hydration transaction. Snapshot exactly
237
+ * what hydration replaces, hydrate, cross the durability barrier, write the
238
+ * commit marker, publish the receipt, remove the transaction; on failure
239
+ * restore the snapshot unless the visible tree is already durable. The write
240
+ * and fsync ordering is the one the control thread used to run inline.
241
+ */
242
+ export declare function runHydrationTransactionJob(input: {
243
+ workspacePath: string;
244
+ hydrationMounts: readonly WorkspaceGitMountData[];
245
+ advancedDurabilityMounts: readonly WorkspaceGitMountData[];
246
+ targetReceipt: WorkspaceHydrationReceipt | null;
247
+ }): {
248
+ hydratedMountIds: string[];
249
+ };
250
+ /** Executor job body: hydrate visible mounts before an outer clone exists (no transaction to record). */
251
+ export declare function runHydrationRawJob(input: {
252
+ workspacePath: string;
253
+ mounts: readonly WorkspaceGitMountData[];
254
+ }): {
255
+ hydratedMountIds: string[];
256
+ };
257
+ /**
258
+ * Executor job body: checkout durability recovery, pending hydration
259
+ * transaction restore or completion, and basis-ref repair, in that order.
260
+ */
261
+ export declare function runHydrationRecoveryJob(input: {
262
+ workspacePath: string;
263
+ preserveResolutionInProgress: boolean;
264
+ }): null;
182
265
  export declare function hydrateWorkspaceGitMounts(workspacePath: string, mounts: readonly WorkspaceGitMount[], options?: {
183
266
  ignoreBusy?: boolean;
184
267
  hydrationHooks?: WorkspaceHydrationTransactionHooks;
@@ -257,6 +340,29 @@ export type SynchronizeWorkspaceGitInput = {
257
340
  hydrationHooks?: WorkspaceHydrationTransactionHooks;
258
341
  };
259
342
  export declare function synchronizeWorkspaceGit(input: SynchronizeWorkspaceGitInput): Promise<WorkspaceGitSyncResult>;
343
+ /** Executor job body: byte-mirror one visible mount into the outer checkout. */
344
+ export declare function runProjectionMirrorJob(input: {
345
+ workspacePath: string;
346
+ mount: WorkspaceGitMountData;
347
+ }): {
348
+ gitlinks: WorkingTreeGitlink[];
349
+ };
350
+ /** Executor job body: outer-tree edits that follow the mirrors, in the cycle's order. */
351
+ export declare function runProjectionOuterApplyJob(input: {
352
+ workspacePath: string;
353
+ removeMounts: readonly WorkspaceGitMountData[];
354
+ materialize: ReadonlyArray<{
355
+ workspaceRelativePath: string;
356
+ resultTree: string;
357
+ }>;
358
+ restoreFromHead: readonly WorkspaceGitMountData[];
359
+ }): null;
360
+ /** Executor job body: persist projected visible mounts, then publish the projection receipt. */
361
+ export declare function runProjectionReceiptJob(input: {
362
+ workspacePath: string;
363
+ fsyncMounts: readonly WorkspaceGitMountData[];
364
+ receipt: WorkspaceHydrationReceipt;
365
+ }): null;
260
366
  export declare const workspaceGitSyncTestHarness: {
261
367
  commandArgs: typeof gitCommandArgs;
262
368
  workspaceCloneCommandArgs: typeof workspaceCloneCommandArgs;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Tracks the mounts whose bytes a workspace filesystem job is reading or
3
+ * writing on the executor thread. The sync engine takes a hold immediately
4
+ * after the busy observation that selected a mount, in the same synchronous
5
+ * stretch, and releases it when the job's result or failure arrives. Command
6
+ * admission waits on the fence before reserving a mount, so no tracked
7
+ * process, PTY or file operation can start in a mount while a job owns it,
8
+ * and no job can start on a mount a command already reserved (that mount was
9
+ * busy at the observation). Holds are counted per mount; overlapping holds
10
+ * (a recovery pass and a projection of the same mount cannot overlap under the
11
+ * mutation gate, but counting keeps the fence correct regardless) release in
12
+ * any order.
13
+ */
14
+ export type WorkspaceMountHoldRelease = () => void;
15
+ export declare class WorkspaceMountHoldAbortedError extends Error {
16
+ constructor(reason: string);
17
+ }
18
+ export declare class WorkspaceMountHoldFence {
19
+ private readonly holds;
20
+ private readonly waiters;
21
+ private readonly now;
22
+ constructor(options?: {
23
+ now?: () => number;
24
+ });
25
+ /** Take one hold on every listed mount; the returned release is idempotent. */
26
+ hold(mountIds: readonly string[], description: string): WorkspaceMountHoldRelease;
27
+ isHeld(mountIds: readonly string[]): boolean;
28
+ /** Human-readable holder of the first held mount among `mountIds`, for diagnostics. */
29
+ describeHold(mountIds: readonly string[]): string | null;
30
+ /** Every held mount id, sorted, for status reporting. */
31
+ heldMountIds(): string[];
32
+ /**
33
+ * Resolves in the same turn as the release that leaves none of `mountIds`
34
+ * held, so the caller can re-check and reserve without an interleaving
35
+ * hold. Resolves immediately when nothing is held. Rejects with
36
+ * WorkspaceMountHoldAbortedError when `signal` aborts first.
37
+ */
38
+ waitForRelease(mountIds: readonly string[], options?: {
39
+ signal?: AbortSignal;
40
+ }): Promise<void>;
41
+ private wake;
42
+ }