@ricsam/r5d-worker 0.0.99 → 0.0.101

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.
@@ -2,6 +2,7 @@
2
2
  import { Database } from "bun:sqlite";
3
3
  import type { WorkerGitIdentity } from "./git-identity";
4
4
  import { configureGitHubRegistryAuthFiles, type PreparedPrivateAuthFileGeneration } from "./registry-auth";
5
+ import { type WorkspaceHydrationTransactionHooks } from "./workspace-git-sync";
5
6
  type WorkerProjectConfig = {
6
7
  projectId: string;
7
8
  checkoutPathSegments: [namespace: string, project: string];
@@ -23,6 +24,11 @@ type WorkerProjectConfig = {
23
24
  baseCommitHash: string;
24
25
  }>;
25
26
  };
27
+ type WorkerAheadOfOriginBranch = {
28
+ projectId: string;
29
+ branchName: string;
30
+ ahead: number;
31
+ };
26
32
  type WorkerLifecycleProgress = {
27
33
  phase: "starting" | "recovering" | "configuring" | "syncing" | "ready" | "failed";
28
34
  operationId: string;
@@ -39,9 +45,28 @@ declare function workerServerHeartbeatAction(input: {
39
45
  timeoutMs: number;
40
46
  watchdogSweepDelayed: boolean;
41
47
  }): "wait" | "start_probe" | "terminate";
48
+ /**
49
+ * A hydration transaction blocks this event loop until it ends, so no pong,
50
+ * progress, or result can leave the process while it runs; on a slow disk
51
+ * that is hours. Declare a hydration heartbeat lease first: the server keeps
52
+ * the control socket open and defers the affected request deadlines while the
53
+ * lease is active instead of reading the silence as a dead worker, which is
54
+ * what turned one slow hydration into a mandatory canonical reset (incident
55
+ * 3bccab03). Bun writes the frame to the socket synchronously; the extra
56
+ * event-loop turn lets control messages that were already queued run before
57
+ * the busy observation that precedes the transaction body.
58
+ */
59
+ declare function createWorkspaceHydrationLeaseHooks(send: (message: Extract<WorkerClientMessage, {
60
+ type: "heartbeat_lease";
61
+ }>) => void, options?: {
62
+ yieldToEventLoop?: () => Promise<void>;
63
+ log?: (line: string) => void;
64
+ now?: () => number;
65
+ }): WorkspaceHydrationTransactionHooks;
42
66
  export declare const workerLifecycleTestHarness: {
43
67
  boundedWorkerLifecycleProgress: typeof boundedWorkerLifecycleProgress;
44
68
  workerServerHeartbeatAction: typeof workerServerHeartbeatAction;
69
+ createWorkspaceHydrationLeaseHooks: typeof createWorkspaceHydrationLeaseHooks;
45
70
  };
46
71
  type WorkspaceSyncTrigger = {
47
72
  type: "connect" | "inbound_head" | "write" | "edit" | "shell_inline" | "process_terminal" | "process_cancel" | "periodic" | "manual" | "remediation" | "remediation_confirm" | "remediation_reset";
@@ -103,6 +128,144 @@ export type WorkerSessionTarget = {
103
128
  ownerUserId: string;
104
129
  rootProfile: "visible_projects" | "canonical_sync";
105
130
  };
131
+ type WorkerClientMessage = {
132
+ type: "hello";
133
+ hostInfo?: Record<string, unknown>;
134
+ startupReady?: boolean;
135
+ } | {
136
+ type: "worker_progress";
137
+ progress: WorkerLifecycleProgress;
138
+ } | {
139
+ type: "heartbeat_lease";
140
+ operationId: string;
141
+ active: boolean;
142
+ /** A hydration transaction may block the worker for hours; the server bounds it separately from preparation leases. */
143
+ kind?: "hydration";
144
+ } | {
145
+ type: "startup_ready";
146
+ } | {
147
+ type: "active_processes";
148
+ processes: Array<{
149
+ runId: string;
150
+ target: WorkerSessionTarget;
151
+ sessionId: string;
152
+ platform: string;
153
+ arch: string;
154
+ mode: "foreground" | "detached";
155
+ pid?: number;
156
+ processGroupId?: number;
157
+ credentialId?: string;
158
+ argv: string[];
159
+ command: string;
160
+ cwd?: string;
161
+ startedAt: string;
162
+ interactive?: boolean;
163
+ workspaceEffect?: "none";
164
+ }>;
165
+ } | {
166
+ type: "pty_opened";
167
+ requestId: string;
168
+ ptyId: string;
169
+ } | {
170
+ type: "pty_output";
171
+ ptyId: string;
172
+ data: string;
173
+ } | {
174
+ type: "pty_exit";
175
+ ptyId: string;
176
+ exitCode: number;
177
+ signal?: number | string;
178
+ } | {
179
+ type: "pty_error";
180
+ ptyId: string;
181
+ requestId?: string;
182
+ error: string;
183
+ } | {
184
+ type: "operation_result";
185
+ requestId: string;
186
+ result?: WorkerOperationResult;
187
+ error?: string;
188
+ } | {
189
+ type: "exec_result";
190
+ requestId: string;
191
+ stdout: string;
192
+ stderr: string;
193
+ exitCode: number;
194
+ durationMs?: number;
195
+ error?: string;
196
+ } | {
197
+ type: "exec_accepted";
198
+ requestId: string;
199
+ runId: string;
200
+ } | {
201
+ type: "exec_started";
202
+ requestId: string;
203
+ runId: string;
204
+ cwd: string;
205
+ pid?: number;
206
+ processGroupId?: number;
207
+ } | {
208
+ type: "exec_start_error";
209
+ requestId: string;
210
+ runId: string;
211
+ error: string;
212
+ } | {
213
+ type: "exec_output";
214
+ runId: string;
215
+ stream: "stdout" | "stderr";
216
+ data: string;
217
+ } | {
218
+ type: "exec_exit";
219
+ runId: string;
220
+ exitCode: number;
221
+ durationMs?: number;
222
+ timedOut?: boolean;
223
+ workspaceEffect?: "none";
224
+ canonicalWorkspaceHead?: string;
225
+ } | {
226
+ type: "exec_error";
227
+ runId: string;
228
+ error: string;
229
+ durationMs?: number;
230
+ workspaceEffect?: "none";
231
+ canonicalWorkspaceHead?: string;
232
+ } | {
233
+ type: "cancel_result";
234
+ requestId: string;
235
+ runId: string;
236
+ cancelled: boolean;
237
+ message?: string;
238
+ } | {
239
+ type: "pong";
240
+ } | {
241
+ type: "workspace_configured";
242
+ requestId: string;
243
+ result: WorkspaceSyncResult;
244
+ pendingCheckouts: Array<{
245
+ projectId: string;
246
+ branchName: string;
247
+ }>;
248
+ aheadOfOriginBranches: WorkerAheadOfOriginBranch[];
249
+ deferredWorkspaceSyncForIncidentId?: string;
250
+ } | {
251
+ type: "workspace_sync_started";
252
+ attemptId: string;
253
+ } | {
254
+ type: "workspace_sync_result";
255
+ requestId?: string;
256
+ result: WorkspaceSyncResult;
257
+ } | {
258
+ type: "session_artifacts_sync_result";
259
+ requestId: string;
260
+ sessionId: string;
261
+ error?: string;
262
+ } | {
263
+ type: "update_clis_result";
264
+ requestId: string;
265
+ installedWorkerVersion?: string;
266
+ installedR5dctlVersion?: string;
267
+ error?: string;
268
+ };
106
269
  type WorkerServerMessage = {
107
270
  type: "connected";
108
271
  label: string;
@@ -371,6 +534,23 @@ type WorkerViewFileBytesResult = {
371
534
  width: number;
372
535
  height: number;
373
536
  };
537
+ type WorkerExecStdinAck = {
538
+ type: "exec_stdin";
539
+ runId: string;
540
+ bytesWritten: number;
541
+ eof: boolean;
542
+ };
543
+ type WorkerCreateProjectBranchResult = {
544
+ type: "create_project_branch";
545
+ branchId: string;
546
+ branchName: string;
547
+ baseCommitHash: string;
548
+ };
549
+ type WorkerDeleteProjectBranchResult = {
550
+ type: "delete_project_branch";
551
+ branchId: string;
552
+ branchName: string;
553
+ };
374
554
  type WorkerCodeEntry = {
375
555
  name: string;
376
556
  type: "file" | "directory";
@@ -389,6 +569,7 @@ type WorkerCodeReadResult = {
389
569
  mtime: string;
390
570
  base64: string;
391
571
  };
572
+ type WorkerOperationResult = WorkerReadFileResult | WorkerWriteFileResult | WorkerEditFileResult | WorkerGrepResult | WorkerFindResult | WorkerLsResult | WorkerViewFileBytesResult | WorkerExecStdinAck | WorkerCreateProjectBranchResult | WorkerDeleteProjectBranchResult | WorkerCodeListResult | WorkerCodeReadResult;
392
573
  declare class WorkspaceSyncAdmissionChangedError extends Error {
393
574
  readonly name = "WorkspaceSyncAdmissionChangedError";
394
575
  }
@@ -12,6 +12,44 @@ export type TreeEntry = {
12
12
  kind: "symlink";
13
13
  mode: number;
14
14
  target: string;
15
+ }
16
+ /**
17
+ * A submodule (D4): the enclosing repository records a commit at this path
18
+ * and never its contents. A leaf for every operation here: nothing beneath
19
+ * it is inspected, copied, captured, removed, or restored.
20
+ */
21
+ | {
22
+ kind: "gitlink";
23
+ objectId: string;
24
+ };
25
+ export type WorkingTreeGitlink = {
26
+ relativePath: string;
27
+ objectId: string;
28
+ };
29
+ /** One index record of a Git checkout, in the form `git update-index --index-info` accepts back. */
30
+ export type WorkingTreeIndexRecord = {
31
+ mode: string;
32
+ objectId: string;
33
+ stage: number;
34
+ relativePath: string;
35
+ };
36
+ /**
37
+ * Index entries a Git-target mirror changes: the records it removes or
38
+ * overwrites (a restore puts them back) and the gitlinks it writes (a restore
39
+ * drops them first).
40
+ */
41
+ export type WorkingTreeIndexChanges = {
42
+ removed: WorkingTreeIndexRecord[];
43
+ written: WorkingTreeGitlink[];
44
+ };
45
+ export type WorkingTreeInspectionOptions = {
46
+ /**
47
+ * Gitlinks the enclosing repository records under an all-mode root, keyed
48
+ * by relative path. Git checks a submodule out as an empty directory, so
49
+ * the filesystem alone cannot show one; the entry replaces whatever is on
50
+ * disk at that path. Ignored in Git mode, where the index is read directly.
51
+ */
52
+ gitlinks?: ReadonlyMap<string, string>;
15
53
  };
16
54
  /**
17
55
  * Destructive native-tree moves cannot silently apply the Git-target filter:
@@ -20,19 +58,14 @@ export type TreeEntry = {
20
58
  * every portable alias must be remediated before the old tree is retired.
21
59
  */
22
60
  export declare function assertWorkingTreeHasNoPortableGitMetadataAliases(root: string): void;
23
- export declare function inspectWorkingTree(root: string, mode: WorkingTreeSourceMode, options?: {
24
- /**
25
- * Leave an uninitialized submodule (index gitlink whose directory is
26
- * empty) out of a Git-mode inspection. Its empty directory is what keeps
27
- * `git status` clean, so a deletion set must never remove it.
28
- */
29
- omitUnpopulatedGitlinks?: boolean;
30
- }): Map<string, TreeEntry>;
61
+ export declare function inspectWorkingTree(root: string, mode: WorkingTreeSourceMode, options?: WorkingTreeInspectionOptions): Map<string, TreeEntry>;
31
62
  export declare function mirrorWorkingTree(input: {
32
63
  sourceRoot: string;
33
64
  targetRoot: string;
34
65
  sourceMode: WorkingTreeSourceMode;
35
66
  deletionMode: WorkingTreeDeletionMode;
67
+ /** Gitlinks the enclosing repository records for an all-mode source (see `inspectWorkingTree`). */
68
+ sourceGitlinks?: ReadonlyMap<string, string>;
36
69
  /**
37
70
  * Defer destination fsync only while building an unpublished private tree.
38
71
  * The caller must durably fsync that complete tree before publishing it or
@@ -41,20 +74,28 @@ export declare function mirrorWorkingTree(input: {
41
74
  durability?: WorkingTreeMirrorDurability;
42
75
  }): {
43
76
  paths: string[];
77
+ gitlinks: WorkingTreeGitlink[];
44
78
  };
45
79
  /** Canonical relative form used by mirror plans and scopes, or null when the value is unsafe. */
46
80
  export declare function normalizeWorkingTreeRelativePath(value: string): string | null;
47
81
  export type WorkingTreeMirrorPlan = {
48
- /** Every target path the mirror creates, rewrites, or verifies, ancestor directories included. */
82
+ /**
83
+ * Every target path the mirror creates, rewrites, or verifies, ancestor
84
+ * directories included. A gitlink is verified as a directory; paths a
85
+ * submodule directory on the target shields are left out.
86
+ */
49
87
  desired: Map<string, TreeEntry>;
50
88
  /**
51
89
  * Target entries the mirror removes or overwrites, as they are before it
52
90
  * runs. A directory entry means the whole subtree goes, ignored files
53
- * included; entries beneath it are folded into it.
91
+ * included; entries beneath it are folded into it. Never a gitlink: a
92
+ * submodule directory stays, and only its index entry can change.
54
93
  */
55
94
  replaced: Map<string, TreeEntry>;
56
95
  /** Desired paths that have no target entry yet. */
57
96
  absent: string[];
97
+ /** Index entries the mirror changes on a Git target; empty for any other target. */
98
+ index: WorkingTreeIndexChanges;
58
99
  };
59
100
  /**
60
101
  * Compute exactly which target entries `mirrorWorkingTree` would remove,
@@ -63,19 +104,23 @@ export type WorkingTreeMirrorPlan = {
63
104
  * A desired path replaces its target entry when the kinds differ, a symlink
64
105
  * points elsewhere, or a file's bytes or mode differ; an equal entry is left
65
106
  * alone, so the plan covers only what the mirror actually touches. A Git
66
- * target's ignored trees appear only where a desired path lands on them.
107
+ * target's ignored trees appear only where a desired path lands on them, and
108
+ * its submodule directories only as index changes.
67
109
  */
68
110
  export declare function planWorkingTreeMirror(input: {
69
111
  sourceRoot: string;
70
112
  targetRoot: string;
71
113
  sourceMode: WorkingTreeSourceMode;
72
114
  deletionMode: WorkingTreeDeletionMode;
115
+ sourceGitlinks?: ReadonlyMap<string, string>;
73
116
  }): WorkingTreeMirrorPlan;
74
117
  export type WorkingTreeMirrorScope = {
75
118
  /** Desired paths that did not exist before the mirror ran; a restore removes them. */
76
119
  absent: string[];
77
120
  /** Directories captured whole; a restore makes them identical to the snapshot again. */
78
121
  trees: string[];
122
+ /** Index entries the mirror changed on a Git target; a restore drops the written gitlinks and puts the removed records back. */
123
+ index?: WorkingTreeIndexChanges;
79
124
  };
80
125
  /**
81
126
  * Copy the entries a mirror plan replaces into `snapshotRoot` at their
@@ -1,4 +1,4 @@
1
- import { type WorkingTreeSourceMode } from "./working-tree-mirror";
1
+ import { type WorkingTreeGitlink, type WorkingTreeSourceMode } from "./working-tree-mirror";
2
2
  export declare const WORKSPACE_GIT_BRANCH = "main";
3
3
  export declare const MAX_WORKSPACE_GIT_DIFF_BYTES: number;
4
4
  export declare const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confirm-large-diff-v1";
@@ -49,6 +49,26 @@ export type WorkspaceGitMount = {
49
49
  * unready state for concurrent visible work.
50
50
  */
51
51
  export declare function workspaceGitMountsForRemediation(mounts: readonly WorkspaceGitMount[]): WorkspaceGitMount[];
52
+ export type WorkspaceHydrationTransactionContext = {
53
+ /** Unique per transaction; pairs the begin and end observations. */
54
+ transactionId: string;
55
+ workspacePath: string;
56
+ /** Mounts the transaction may hydrate or carry across the durability barrier. */
57
+ mountIds: string[];
58
+ };
59
+ /**
60
+ * Observes the synchronous body of a hydration transaction (snapshot,
61
+ * hydration pass, durability barrier, receipt). That body deliberately never
62
+ * yields, so the worker cannot answer control-socket messages while it runs,
63
+ * and on a slow disk it can run for hours. `beforeTransaction` is awaited
64
+ * after the last asynchronous precheck and before the final busy observation,
65
+ * so it may announce the operation and yield once; `afterTransaction` runs
66
+ * after the body committed or rolled back.
67
+ */
68
+ export type WorkspaceHydrationTransactionHooks = {
69
+ beforeTransaction: (context: WorkspaceHydrationTransactionContext) => Promise<void> | void;
70
+ afterTransaction: (context: WorkspaceHydrationTransactionContext) => void;
71
+ };
52
72
  export type WorkspaceGitSyncOutcome = "no_change" | "updated" | "pushed" | "large_diff_blocked" | "conflict_blocked";
53
73
  export type WorkspaceGitSyncResult = {
54
74
  outcome: WorkspaceGitSyncOutcome;
@@ -134,9 +154,19 @@ export declare function ensureWorkspaceGitClone(input: {
134
154
  localHead: string | null;
135
155
  remoteHead: string | null;
136
156
  }>;
137
- declare function mirrorMountsToWorkspace(workspacePath: string, mounts: readonly WorkspaceGitMount[]): void;
157
+ type ProjectedMountGitlinks = {
158
+ mount: WorkspaceGitMount;
159
+ gitlinks: WorkingTreeGitlink[];
160
+ };
161
+ /**
162
+ * Mirror each mount's visible tree into the outer worktree. A submodule
163
+ * arrives as an empty directory plus a gitlink the caller must stage with
164
+ * `stageWorkspaceGitlinks` before `git add -A`, which can never create one.
165
+ */
166
+ declare function mirrorMountsToWorkspace(workspacePath: string, mounts: readonly WorkspaceGitMount[]): ProjectedMountGitlinks[];
138
167
  export declare function hydrateWorkspaceGitMounts(workspacePath: string, mounts: readonly WorkspaceGitMount[], options?: {
139
168
  ignoreBusy?: boolean;
169
+ hydrationHooks?: WorkspaceHydrationTransactionHooks;
140
170
  }): Promise<{
141
171
  hydratedMountIds: string[];
142
172
  skippedMountIds: string[];
@@ -146,6 +176,7 @@ export declare function recoverWorkspaceGitHydration(workspacePath: string, moun
146
176
  deferMountIds?: ReadonlySet<string>;
147
177
  preserveResolutionInProgress?: boolean;
148
178
  preserveStaleBases?: boolean;
179
+ hydrationHooks?: WorkspaceHydrationTransactionHooks;
149
180
  }): Promise<void>;
150
181
  export declare function resetWorkspaceGit(input: {
151
182
  workspacePath: string;
@@ -157,6 +188,7 @@ export declare function resetWorkspaceGit(input: {
157
188
  email: string;
158
189
  };
159
190
  mounts: readonly WorkspaceGitMount[];
191
+ hydrationHooks?: WorkspaceHydrationTransactionHooks;
160
192
  }): Promise<{
161
193
  startingHead: string | null;
162
194
  localHead: string | null;
@@ -165,6 +197,12 @@ export declare function resetWorkspaceGit(input: {
165
197
  activeMountIds: string[];
166
198
  skippedMountIds: string[];
167
199
  }>;
200
+ /**
201
+ * Bytes of the raw `git diff --binary` between two revisions, capped at
202
+ * `limit + 1`. `pathspecs` narrows what is measured; callers pass
203
+ * `:(exclude,literal)<path>` for paths whose text is not publication content,
204
+ * such as a subtree a gitlink replaces (see `pathsReplacedByGitlinks`).
205
+ */
168
206
  declare function diffSizeBytes(workspacePath: string, baseRevision: string | null, headRevision: string, limit: number, pathspecs?: readonly string[]): Promise<number>;
169
207
  export declare function synchronizeWorkspaceGit(input: {
170
208
  attemptId?: string;
@@ -194,6 +232,8 @@ export declare function synchronizeWorkspaceGit(input: {
194
232
  publishedHead: string;
195
233
  activeMountIds: string[];
196
234
  }) => void | Promise<void>;
235
+ /** Announces each synchronous hydration transaction of this cycle; see WorkspaceHydrationTransactionHooks. */
236
+ hydrationHooks?: WorkspaceHydrationTransactionHooks;
197
237
  }): Promise<WorkspaceGitSyncResult>;
198
238
  export declare const workspaceGitSyncTestHarness: {
199
239
  commandArgs: typeof gitCommandArgs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.99",
3
+ "version": "0.0.101",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",