@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.
Files changed (67) hide show
  1. package/README.md +6 -3
  2. package/dist/cjs/command-launcher.cjs +76 -2
  3. package/dist/cjs/control-command-policy.cjs +177 -0
  4. package/dist/cjs/main.cjs +541 -247
  5. package/dist/cjs/package.json +1 -1
  6. package/dist/cjs/project-checkout-garbage.cjs +32 -3
  7. package/dist/cjs/project-workspace-state.cjs +51 -35
  8. package/dist/cjs/project-worktrees.cjs +79 -34
  9. package/dist/cjs/recovery-journal-protocol.cjs +56 -0
  10. package/dist/cjs/recovery-journal-runtime.cjs +133 -0
  11. package/dist/cjs/recovery-journal-thread.cjs +9 -0
  12. package/dist/cjs/recovery-journal.cjs +735 -0
  13. package/dist/cjs/recovery-store.cjs +8 -0
  14. package/dist/cjs/session-file-mutations.cjs +61 -0
  15. package/dist/cjs/working-tree-mirror.cjs +1 -0
  16. package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
  17. package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
  18. package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
  19. package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
  20. package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
  21. package/dist/cjs/workspace-git-sync.cjs +275 -201
  22. package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
  23. package/dist/mjs/command-launcher.mjs +75 -2
  24. package/dist/mjs/control-command-policy.mjs +146 -0
  25. package/dist/mjs/main.mjs +549 -253
  26. package/dist/mjs/package.json +1 -1
  27. package/dist/mjs/project-checkout-garbage.mjs +30 -2
  28. package/dist/mjs/project-workspace-state.mjs +51 -35
  29. package/dist/mjs/project-worktrees.mjs +74 -34
  30. package/dist/mjs/recovery-journal-protocol.mjs +30 -0
  31. package/dist/mjs/recovery-journal-runtime.mjs +112 -0
  32. package/dist/mjs/recovery-journal-thread.mjs +8 -0
  33. package/dist/mjs/recovery-journal.mjs +687 -0
  34. package/dist/mjs/recovery-store.mjs +8 -0
  35. package/dist/mjs/session-file-mutations.mjs +37 -0
  36. package/dist/mjs/working-tree-mirror.mjs +1 -0
  37. package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
  38. package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
  39. package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
  40. package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
  41. package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
  42. package/dist/mjs/workspace-git-sync.mjs +264 -202
  43. package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
  44. package/dist/types/command-launcher.d.ts +42 -0
  45. package/dist/types/control-command-policy.d.ts +53 -0
  46. package/dist/types/main.d.ts +31 -19
  47. package/dist/types/project-checkout-garbage.d.ts +19 -2
  48. package/dist/types/project-workspace-state.d.ts +9 -9
  49. package/dist/types/project-worktrees.d.ts +49 -5
  50. package/dist/types/recovery-journal-protocol.d.ts +35 -0
  51. package/dist/types/recovery-journal-runtime.d.ts +12 -0
  52. package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
  53. package/dist/types/recovery-journal-thread.d.ts +1 -0
  54. package/dist/types/recovery-journal.d.ts +246 -0
  55. package/dist/types/recovery-store.d.ts +6 -0
  56. package/dist/types/session-file-mutations.d.ts +22 -0
  57. package/dist/types/workspace-command-sync-policy.d.ts +22 -7
  58. package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
  59. package/dist/types/workspace-filesystem-executor.d.ts +123 -0
  60. package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
  61. package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
  62. package/dist/types/workspace-git-sync.d.ts +113 -7
  63. package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
  64. package/package.json +1 -1
  65. package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
  66. package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
  67. package/dist/types/project-snapshot-recovery-runner.d.ts +0 -10
@@ -0,0 +1,246 @@
1
+ import type { WorkerOperationRecovery, WorkerOutputOffsets } from "./recovery-protocol";
2
+ /**
3
+ * The control-thread side of the worker's durable recovery journal.
4
+ *
5
+ * The SQLite store (WAL, `synchronous=FULL`) lives on the journal thread;
6
+ * this facade owns one ordered channel of store operations and the ordered
7
+ * delivery of the messages those operations make durable. Its invariants:
8
+ *
9
+ * - order: every entry is applied in append order, one durable commit per
10
+ * batch, so an `unknown` appended after a `record` applies after it, and an
11
+ * `admit` of a later request commits after the `record` of an earlier one;
12
+ * - commit before send: a keyed message is written to the socket only after
13
+ * the batch holding its `record` committed, in append order, so a terminal
14
+ * never leaves before the output recorded ahead of it and a response is
15
+ * never acknowledged before it is durable;
16
+ * - commit before effects: `admit` resolves after its commit; handlers run
17
+ * after it;
18
+ * - bounded: everything the facade retains (entries waiting for the thread,
19
+ * entries in the batch being committed, and committed messages not yet
20
+ * drained to the socket) is charged to one general budget of entries and
21
+ * bytes, reserved atomically before anything is queued and released only
22
+ * when the item is actually done; unknown marks, acknowledgements and
23
+ * handshake reads use a separate reserved control budget. Output producers
24
+ * and admissions wait for budget in bounded waiting rooms; a result that
25
+ * cannot be buffered is never sent early: its operation is marked unknown,
26
+ * which is exactly what a crash would have left;
27
+ * - fail closed: a thread exit, a failed batch, or an exhausted control budget
28
+ * fails every pending and future entry and reports once; the store is never
29
+ * re-opened in-process.
30
+ */
31
+ type Message = {
32
+ type: string;
33
+ [key: string]: unknown;
34
+ };
35
+ export declare const WORKER_RECOVERY_JOURNAL_MAX_BATCH_ENTRIES = 256;
36
+ export declare const WORKER_RECOVERY_JOURNAL_MAX_PENDING_ENTRIES = 4096;
37
+ export declare const WORKER_RECOVERY_JOURNAL_MAX_PENDING_BYTES: number;
38
+ export declare const WORKER_RECOVERY_JOURNAL_MAX_CONTROL_ENTRIES = 2048;
39
+ export declare const WORKER_RECOVERY_JOURNAL_MAX_CONTROL_BYTES: number;
40
+ export declare const WORKER_RECOVERY_JOURNAL_MAX_ENTRY_BYTES: number;
41
+ export declare const WORKER_RECOVERY_JOURNAL_OUTPUT_CHUNK_CHARACTERS: number;
42
+ export declare const WORKER_RECOVERY_JOURNAL_MAX_WAITING_ADMISSIONS = 512;
43
+ export declare const WORKER_RECOVERY_JOURNAL_MAX_WAITING_OUTPUT = 1024;
44
+ export declare const WORKER_RECOVERY_JOURNAL_MAX_WAITING_BYTES: number;
45
+ export declare const WORKER_RECOVERY_JOURNAL_REPLAY_PAGE_FRAMES = 32;
46
+ export declare class WorkerRecoveryJournalError extends Error {
47
+ readonly name: string;
48
+ }
49
+ /** The journal is intact but its bounded waiting room is full; the caller must not queue more. */
50
+ export declare class WorkerRecoveryJournalBusyError extends WorkerRecoveryJournalError {
51
+ readonly name = "WorkerRecoveryJournalBusyError";
52
+ }
53
+ export type WorkerRecoveryJournalAdmission = {
54
+ admission: "new" | "duplicate" | "unknown" | "conflict";
55
+ response: Message | undefined;
56
+ cancelled: boolean;
57
+ };
58
+ export type WorkerRecoveryJournalRecoveryState = {
59
+ operations: WorkerOperationRecovery[];
60
+ cursors: WorkerOutputOffsets[];
61
+ responses: Message[];
62
+ terminals: Message[];
63
+ };
64
+ export type WorkerRecoveryJournalStatus = {
65
+ /** Entries waiting for the thread plus entries in the batch being committed. */
66
+ pendingEntries: number;
67
+ /** General budget in use: pending entries and undelivered messages, in entries and bytes. */
68
+ retainedEntries: number;
69
+ retainedBytes: number;
70
+ controlEntries: number;
71
+ controlBytes: number;
72
+ waitingAdmissions: number;
73
+ waitingOutput: number;
74
+ /** Payload bytes held by requests and output chunks waiting for budget. */
75
+ waitingBytes: number;
76
+ /** Bytes of store replies (handshake state, output pages) the caller has not consumed yet. */
77
+ retainedReplyBytes: number;
78
+ committedSeq: number;
79
+ appendedSeq: number;
80
+ undelivered: number;
81
+ failed: string | null;
82
+ };
83
+ export type WorkerRecoveryJournalOptions = {
84
+ filename: string;
85
+ threadModulePath?: string;
86
+ /** Extra `workerData` for the thread module (test fixtures only). */
87
+ threadWorkerData?: Record<string, unknown>;
88
+ log?: (line: string) => void;
89
+ maxBatchEntries?: number;
90
+ maxPendingEntries?: number;
91
+ maxPendingBytes?: number;
92
+ maxControlEntries?: number;
93
+ maxControlBytes?: number;
94
+ maxEntryBytes?: number;
95
+ maxWaitingAdmissions?: number;
96
+ maxWaitingOutput?: number;
97
+ maxWaitingBytes?: number;
98
+ };
99
+ export declare class WorkerRecoveryJournal {
100
+ readonly ledgerId: string;
101
+ private readonly worker;
102
+ private readonly log;
103
+ private readonly maxBatchEntries;
104
+ private readonly maxEntryBytes;
105
+ private readonly maxWaitingAdmissions;
106
+ private readonly maxWaitingOutput;
107
+ private readonly maxWaitingBytes;
108
+ private waitingBytes;
109
+ private retainedReplyBytes;
110
+ private readonly general;
111
+ private readonly control;
112
+ private nextSeq;
113
+ private committedSeq;
114
+ private readonly pending;
115
+ private inFlight;
116
+ private nextBatchId;
117
+ private readonly waiters;
118
+ private readonly outbound;
119
+ private deliveryPaused;
120
+ private replayCoveredSeq;
121
+ private failure;
122
+ private closed;
123
+ private readonly pendingAdmissions;
124
+ private readonly activeOperations;
125
+ private readonly cancelledRequestIds;
126
+ private readonly unknownRequestIds;
127
+ private readonly unknownRunIds;
128
+ private readonly coalescedControl;
129
+ private readonly flushWaiters;
130
+ /** Delivers one committed message in order; returns false when the transport declined it (the pumps re-send). */
131
+ deliver: ((message: Message) => boolean) | undefined;
132
+ /** Invoked once when the journal fails closed. */
133
+ onFailure: ((error: Error) => void) | undefined;
134
+ private constructor();
135
+ /** Start the journal thread, open the store there (its crash recovery runs there) and return once it is ready. */
136
+ static open(options: WorkerRecoveryJournalOptions): Promise<WorkerRecoveryJournal>;
137
+ status(): WorkerRecoveryJournalStatus;
138
+ /**
139
+ * Durably admit an inbound operation; resolves after the commit. Waits for
140
+ * general budget in a bounded waiting room; when that room is full the
141
+ * admission is refused at once (WorkerRecoveryJournalBusyError) so the
142
+ * message is neither acknowledged nor retained.
143
+ */
144
+ admit(message: Message & {
145
+ requestId: string;
146
+ }): Promise<WorkerRecoveryJournalAdmission>;
147
+ /**
148
+ * Re-admit an unknown branch deletion. Like `admit`, the request is a
149
+ * pending admission from before the await, so a session cancellation that
150
+ * lands while the disk is slow snapshots it and the handler's recheck after
151
+ * the await refuses to rerun the deletion.
152
+ */
153
+ readmitUnknownBranchDeletion(requestId: string, sessionId: string | undefined): Promise<boolean>;
154
+ /**
155
+ * Queue an outbound message. Keyed messages are recorded (when the store
156
+ * records their type) and delivered in order after their commit; other
157
+ * messages are delivered at once. Returns false when the message was not
158
+ * queued: its operation is already unknown, or the budget cannot hold it,
159
+ * in which case the operation is marked unknown so it is neither
160
+ * acknowledged early nor sent later.
161
+ */
162
+ send(message: Message): boolean;
163
+ /**
164
+ * Queue an output frame, split into bounded records, waiting for budget so
165
+ * a stalled journal throttles the producer (and through the pipe, the
166
+ * child) instead of growing memory.
167
+ */
168
+ sendOutput(frame: Message & {
169
+ type: "exec_output";
170
+ runId: string;
171
+ stream: "stdout" | "stderr";
172
+ data: string;
173
+ }): Promise<void>;
174
+ /**
175
+ * Reserve general budget atomically, waiting in the bounded room of `kind`
176
+ * when it does not fit now. The waiting room is bounded in count and in
177
+ * payload bytes (the message a waiting admission holds is charged before it
178
+ * waits), and an entry that could never fit the general budget is refused
179
+ * at once rather than parked at the head of the queue.
180
+ */
181
+ private reserveGeneral;
182
+ /** Reserve for the head waiter inside this synchronous call, so no continuation can outrun the accounting. */
183
+ private wakeWaiters;
184
+ private enqueueRecord;
185
+ private refuse;
186
+ private trackRecorded;
187
+ /**
188
+ * Mark an operation unknown: never sent again (undelivered messages for it
189
+ * are dropped now, and the durable mark keeps replay from resurrecting it).
190
+ * Coalesced per id; uses the reserved control budget.
191
+ */
192
+ unknown(requestId: string): void;
193
+ unknownRun(runId: string): void;
194
+ acknowledgeResult(requestId: string): void;
195
+ /** Monotonic per stream: a later acknowledgement replaces a pending earlier one. */
196
+ acknowledgeOutput(runId: string, stream: "stdout" | "stderr", offset: number): void;
197
+ acknowledgeTerminal(runId: string): void;
198
+ /**
199
+ * Snapshot every admitted and pending operation of the session as cancelled
200
+ * (the handlers check `isRequestCancelled` at their fences) and append the
201
+ * durable cancellation, which the thread applies after every admission
202
+ * appended before it. Resolves after the commit.
203
+ */
204
+ cancelSession(sessionId: string): Promise<void>;
205
+ isRequestCancelled(requestId: string): boolean;
206
+ /** Every operation admitted or awaiting admission in this process that has not completed, for the runtime fence and lease expiry. */
207
+ activeAndPendingOperations(): WorkerOperationRecovery[];
208
+ /**
209
+ * The handshake read. One entry, so it sees exactly the entries appended
210
+ * before it and none after: the replay boundary. Callers pause delivery
211
+ * before appending it and resume after the handshake, so every keyed
212
+ * message committed before the mark is replayed once (from the store) and
213
+ * nothing committed after it can enter the replay of responses, operations
214
+ * or terminals; output pages that follow may re-send frames the pump would
215
+ * re-send anyway, which the server's offset cursors absorb.
216
+ */
217
+ recoveryState(requests: string[]): Promise<WorkerRecoveryJournalRecoveryState>;
218
+ private retainedStateBytes;
219
+ /** Output replay, one bounded page at a time; a page is accounted until the consumer asks for the next one. */
220
+ outputReplay(offsets: WorkerOutputOffsets[] | undefined, pageFrames?: number): AsyncGenerator<Message[]>;
221
+ terminals(): Promise<Message[]>;
222
+ /** Delivery of committed keyed messages is held (in order) while a handshake replays from the store. */
223
+ pauseDelivery(): void;
224
+ resumeDelivery(): void;
225
+ /** Resolves once every entry appended so far is committed (or the journal failed). */
226
+ flush(): Promise<void>;
227
+ close(): void;
228
+ private assertOpen;
229
+ /** Control entries are tiny and idempotent per key: a pending one is updated in place instead of queued again. */
230
+ private appendControl;
231
+ /** A control-budget entry whose result the caller awaits; rejects when the budget is exhausted. */
232
+ private appendControlOrThrow;
233
+ private lastAppended;
234
+ /** Queue an entry whose budget was already reserved from `budget`. */
235
+ private append;
236
+ private dispatch;
237
+ private onReply;
238
+ private dropOutbound;
239
+ private releaseOutbound;
240
+ private deliverReady;
241
+ private fail;
242
+ private failEntries;
243
+ }
244
+ /** The packaged thread module lives next to this module with the same extension (.ts in source, .cjs/.mjs in the package). */
245
+ export declare function workerRecoveryJournalThreadModulePath(): string;
246
+ export {};
@@ -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
+ };