@yu_robotics/remote-cli 1.6.133 → 1.6.137

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 (31) hide show
  1. package/README.md +50 -2
  2. package/dist/client/MessageHandler.d.ts +2 -0
  3. package/dist/client/MessageHandler.d.ts.map +1 -1
  4. package/dist/client/MessageHandler.js +60 -6
  5. package/dist/client/MessageHandler.js.map +1 -1
  6. package/dist/client/WebSocketClient.d.ts.map +1 -1
  7. package/dist/client/WebSocketClient.js +1 -0
  8. package/dist/client/WebSocketClient.js.map +1 -1
  9. package/dist/delegation/DelegatedWorkerLaneCleanup.d.ts +2 -0
  10. package/dist/delegation/DelegatedWorkerLaneCleanup.d.ts.map +1 -1
  11. package/dist/delegation/DelegatedWorkerLaneCleanup.js +13 -0
  12. package/dist/delegation/DelegatedWorkerLaneCleanup.js.map +1 -1
  13. package/dist/delegation/DelegatedWorkerSessionStore.d.ts +4 -0
  14. package/dist/delegation/DelegatedWorkerSessionStore.d.ts.map +1 -1
  15. package/dist/delegation/DelegatedWorkerSessionStore.js +32 -1
  16. package/dist/delegation/DelegatedWorkerSessionStore.js.map +1 -1
  17. package/dist/delegation/DelegatedWorkspaceManager.d.ts +30 -0
  18. package/dist/delegation/DelegatedWorkspaceManager.d.ts.map +1 -1
  19. package/dist/delegation/DelegatedWorkspaceManager.js +469 -23
  20. package/dist/delegation/DelegatedWorkspaceManager.js.map +1 -1
  21. package/dist/delegation/DelegationManager.d.ts +10 -0
  22. package/dist/delegation/DelegationManager.d.ts.map +1 -1
  23. package/dist/delegation/DelegationManager.js +138 -6
  24. package/dist/delegation/DelegationManager.js.map +1 -1
  25. package/dist/maintenance/ReleaseNotes.js +2 -2
  26. package/dist/maintenance/ReleaseNotes.js.map +1 -1
  27. package/dist/maintenance/ReleaseOverview.js +1 -1
  28. package/dist/maintenance/release-notes.json +1 -1
  29. package/dist/types/index.d.ts +5 -0
  30. package/dist/types/index.d.ts.map +1 -1
  31. package/package.json +1 -1
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.DelegatedWorkspaceManager = void 0;
7
7
  const crypto_1 = require("crypto");
8
+ const fs_1 = require("fs");
8
9
  const promises_1 = __importDefault(require("fs/promises"));
9
10
  const os_1 = __importDefault(require("os"));
10
11
  const path_1 = __importDefault(require("path"));
@@ -18,6 +19,7 @@ function inside(root, child) {
18
19
  /** Worktrees isolate checkout files, not process permissions. Artifacts are never expired with task diagnostics. */
19
20
  class DelegatedWorkspaceManager {
20
21
  directory;
22
+ static operations = new Map();
21
23
  constructor(directory = path_1.default.join(os_1.default.homedir(), '.remote-cli', 'delegation-workspaces')) {
22
24
  this.directory = directory;
23
25
  }
@@ -68,6 +70,8 @@ class DelegatedWorkspaceManager {
68
70
  const record = await this.read('lanes', id);
69
71
  if (!record || record.blocked || record.pendingTaskId)
70
72
  blocked.add(id);
73
+ else
74
+ this.assertLifecycle(record);
71
75
  }
72
76
  catch {
73
77
  blocked.add(id);
@@ -76,6 +80,9 @@ class DelegatedWorkspaceManager {
76
80
  return blocked;
77
81
  }
78
82
  async prepare(lane, source, input, taskId) {
83
+ return this.withLane(lane.id, () => this.prepareLocked(lane, source, input, taskId));
84
+ }
85
+ async prepareLocked(lane, source, input, taskId) {
79
86
  this.assertId(taskId);
80
87
  await this.initialize();
81
88
  await this.validateSource(source);
@@ -85,14 +92,22 @@ class DelegatedWorkspaceManager {
85
92
  }
86
93
  const directory = this.worktreeDirectory(source, lane.id);
87
94
  const taskRef = `refs/remote-cli/tasks/${taskId}`;
88
- const previous = await this.read('lanes', lane.id);
95
+ let previous = await this.read('lanes', lane.id);
89
96
  if (previous) {
90
- await this.validateWorkspace(previous);
91
- if (previous.threadId !== lane.threadId || previous.workspaceGeneration !== lane.workspaceGeneration
97
+ await this.validateWorkspaceRecord(previous);
98
+ if (previous.laneId !== lane.id || previous.threadId !== lane.threadId || previous.workspaceGeneration !== lane.workspaceGeneration
92
99
  || previous.source.cwd !== source.cwd || previous.blocked || previous.pendingTaskId) {
93
100
  throw new Error('Worker workspace has unresolved artifacts or a changed identity');
94
101
  }
102
+ previous = await this.resolveReclamation(previous);
103
+ }
104
+ if (previous && previous.checkoutState !== 'reclaimed') {
105
+ await this.validateWorkspace(previous);
95
106
  await this.assertClean(previous.directory);
107
+ const indexEntries = (await (0, GitCheckpoint_1.runGit)(previous.directory, ['ls-files', '-v', '-z'])).toString('utf8').split('\0').filter(Boolean);
108
+ if (indexEntries.some(entry => !entry.startsWith('H '))) {
109
+ throw new Error('Worker checkout has hidden index state; manual recovery is required');
110
+ }
96
111
  // Never reset unknown files, including ignored files that checkout could overwrite.
97
112
  await (0, GitCheckpoint_1.runGit)(directory, ['checkout', '--detach', '--no-overwrite-ignore', input.commit]);
98
113
  }
@@ -108,18 +123,30 @@ class DelegatedWorkspaceManager {
108
123
  await this.safeDirectory(path_1.default.dirname(directory));
109
124
  // Anchor the baseline before creating any checkout, even if setup later fails.
110
125
  await (0, GitCheckpoint_1.runGit)(source.root, ['update-ref', this.inputRef(taskId), input.commit]);
111
- await (0, GitCheckpoint_1.runGit)(source.root, ['worktree', 'add', '--detach', directory, input.commit]);
126
+ await this.withRepository(source, async () => {
127
+ if (await this.registration(source, directory))
128
+ throw new Error('Worker path is still registered; manual recovery is required');
129
+ await (0, GitCheckpoint_1.runGit)(source.root, ['worktree', 'add', '--detach', directory, input.commit]);
130
+ });
112
131
  }
113
132
  await (0, GitCheckpoint_1.runGit)(source.root, ['update-ref', this.inputRef(taskId), input.commit]);
114
133
  await (0, GitCheckpoint_1.runGit)(source.root, ['update-ref', taskRef, input.commit]);
115
134
  const workspace = { laneId: lane.id, threadId: lane.threadId,
116
135
  workspaceGeneration: lane.workspaceGeneration, source, directory,
117
- cwd: path_1.default.join(directory, source.relativeCwd), taskId, taskRef, input };
136
+ cwd: path_1.default.join(directory, source.relativeCwd), taskId, taskRef, input, checkoutState: 'present' };
118
137
  await this.write('lanes', lane.id, workspace);
119
138
  await this.validateWorkspace(workspace);
120
139
  return workspace;
121
140
  }
122
141
  async collect(workspace, successful) {
142
+ return this.withLane(workspace.laneId, () => this.collectLocked(workspace, successful));
143
+ }
144
+ async collectLocked(workspace, successful) {
145
+ const record = await this.read('lanes', workspace.laneId);
146
+ if (!record || record.taskId !== workspace.taskId || record.blocked
147
+ || record.checkoutState && record.checkoutState !== 'present') {
148
+ throw new Error('Worker workspace task changed before collection');
149
+ }
123
150
  await this.validateWorkspace(workspace);
124
151
  const checkpoint = await (0, GitCheckpoint_1.captureCheckpoint)(workspace.directory);
125
152
  const output = await (0, GitCheckpoint_1.createCheckpointCommit)(workspace.directory, checkpoint.tree, workspace.input.commit, `Remote CLI worker artifact ${workspace.taskId}`);
@@ -148,20 +175,56 @@ class DelegatedWorkspaceManager {
148
175
  async integrate(owner, taskId, action, expectedRevision) {
149
176
  this.assertId(taskId);
150
177
  const artifact = await this.read('artifacts', taskId);
178
+ if (!artifact)
179
+ throw new Error('Worker artifact not found');
180
+ const result = await this.withLane(artifact.laneId, () => this.integrateLocked(owner, taskId, action, expectedRevision));
181
+ if (action === 'apply' || action === 'inspect' && result.deliveredAtHead) {
182
+ await this.reclaim(taskId);
183
+ }
184
+ return result;
185
+ }
186
+ async integrateLocked(owner, taskId, action, expectedRevision) {
187
+ this.assertId(taskId);
188
+ let artifact = await this.read('artifacts', taskId);
151
189
  if (!artifact)
152
190
  throw new Error('Worker artifact not found');
153
191
  await this.validateArtifact(artifact, owner);
154
192
  owner.checkActive?.();
155
- const view = await this.view(artifact);
156
- const current = await (0, GitCheckpoint_1.captureCheckpoint)(artifact.source.root);
157
- if (action === 'inspect')
158
- return { ...view, revision: current.revision,
159
- deliveryDirectory: artifact.source.cwd, integration: 'Explicit apply or retain is required for pending changes.' };
160
- if (action !== 'apply' && action !== 'retain')
193
+ if (action !== 'inspect' && action !== 'apply' && action !== 'retain') {
161
194
  throw new Error('Artifact action must be inspect, apply, or retain');
162
- if (typeof expectedRevision !== 'string' || expectedRevision !== current.revision) {
195
+ }
196
+ const current = await (0, GitCheckpoint_1.captureCheckpoint)(artifact.source.root);
197
+ if (action !== 'inspect' && (typeof expectedRevision !== 'string' || expectedRevision !== current.revision)) {
163
198
  throw new Error('Delivery workspace changed; inspect the artifact again before integrating');
164
199
  }
200
+ if (action !== 'retain') {
201
+ try {
202
+ artifact = await this.recognizeHistoricalDelivery(artifact, owner);
203
+ }
204
+ catch {
205
+ console.warn('[Delegation] Historical delivery could not be verified; the artifact was preserved');
206
+ // The receipt may already be durable even if clearing its marker failed.
207
+ // Never apply stale pending state over a subsequently reverted delivery.
208
+ try {
209
+ const persisted = await this.read('artifacts', taskId);
210
+ if (!persisted)
211
+ throw new Error('Worker delivery receipt is missing');
212
+ await this.validateArtifact(persisted, owner);
213
+ artifact = persisted;
214
+ }
215
+ catch {
216
+ if (action === 'apply')
217
+ throw new Error('Worker delivery state is unavailable; inspect again before applying');
218
+ }
219
+ }
220
+ }
221
+ owner.checkActive?.();
222
+ const view = await this.view(artifact);
223
+ if (action === 'inspect')
224
+ return { ...view, revision: current.revision,
225
+ deliveryDirectory: artifact.source.cwd, integration: artifact.deliveredAtHead
226
+ ? 'Historically delivered. Later delivery-branch changes do not reopen this artifact.'
227
+ : 'Explicit apply or retain is required for pending changes.' };
165
228
  if (artifact.disposition !== 'pending')
166
229
  return view;
167
230
  if (action === 'retain') {
@@ -177,15 +240,357 @@ class DelegatedWorkspaceManager {
177
240
  await this.assertSelectedScope(artifact);
178
241
  return this.applyArtifact(artifact, current, owner);
179
242
  }
243
+ /** Reconcile only the caller's exited, ready lanes. Admission never removes checkouts. */
244
+ async reconcileHistoricalDelivery(owner, lanes, reclaim = false) {
245
+ const delivered = [];
246
+ for (const lane of lanes) {
247
+ if (lane.state !== 'ready' || lane.threadId !== owner.threadId || lane.workingDirectory !== owner.cwd
248
+ || lane.workspaceGeneration !== owner.workspaceGeneration)
249
+ continue;
250
+ owner.checkActive?.();
251
+ try {
252
+ const view = await this.withLane(lane.id, async () => {
253
+ const workspace = await this.read('lanes', lane.id);
254
+ if (!workspace || workspace.laneId !== lane.id || workspace.threadId !== owner.threadId
255
+ || workspace.source.cwd !== owner.cwd || workspace.workspaceGeneration !== owner.workspaceGeneration
256
+ || workspace.blocked || workspace.pendingTaskId !== workspace.taskId)
257
+ return undefined;
258
+ const artifact = await this.read('artifacts', workspace.taskId);
259
+ if (!artifact || artifact.successful !== true || artifact.disposition === 'retained' || artifact.recovery)
260
+ return undefined;
261
+ const confirmed = await this.recognizeHistoricalDelivery(artifact, owner);
262
+ return confirmed.disposition === 'applied' ? this.view(confirmed) : undefined;
263
+ });
264
+ if (view) {
265
+ delivered.push(view);
266
+ if (reclaim) {
267
+ owner.checkActive?.();
268
+ await this.reclaim(view.taskId);
269
+ }
270
+ }
271
+ }
272
+ catch {
273
+ console.warn('[Delegation] Historical delivery could not be verified; the worker lane was preserved');
274
+ }
275
+ }
276
+ return delivered;
277
+ }
278
+ async recognizeHistoricalDelivery(artifact, owner) {
279
+ if (artifact.successful !== true || artifact.disposition === 'retained' || artifact.recovery)
280
+ return artifact;
281
+ const workspace = await this.read('lanes', artifact.laneId);
282
+ if (!workspace || workspace.taskId !== artifact.taskId || workspace.laneId !== artifact.laneId
283
+ || workspace.threadId !== owner.threadId || workspace.source.cwd !== owner.cwd
284
+ || workspace.workspaceGeneration !== owner.workspaceGeneration || workspace.blocked)
285
+ return artifact;
286
+ if (artifact.disposition === 'applied' && !workspace.pendingTaskId)
287
+ return artifact;
288
+ if (workspace.pendingTaskId !== artifact.taskId || workspace.checkoutState && workspace.checkoutState !== 'present')
289
+ return artifact;
290
+ await this.validateWorkspaceRecord(workspace);
291
+ await this.validateArtifact(artifact, owner);
292
+ if (workspace.input.commit !== artifact.input.commit || workspace.input.tree !== artifact.input.tree
293
+ || !GitCheckpoint_1.GIT_OID.test(artifact.originalHead)
294
+ || await (0, GitCheckpoint_1.gitText)(workspace.source.root, ['rev-parse', workspace.taskRef]) !== artifact.output
295
+ || await (0, GitCheckpoint_1.gitText)(workspace.source.root, ['rev-parse', `refs/remote-cli/worker-history/${artifact.taskId}`]) !== artifact.originalHead) {
296
+ throw new Error('Worker delivery references changed');
297
+ }
298
+ owner.checkActive?.();
299
+ if (artifact.disposition === 'applied') {
300
+ // A crash after the durable receipt must not strand its pending-lane marker.
301
+ await this.resolveArtifact(artifact, 'applied', artifact.integratedTree);
302
+ return artifact;
303
+ }
304
+ const head = await (0, GitCheckpoint_1.gitText)(artifact.source.root, ['rev-parse', '--verify', 'HEAD']);
305
+ if (!GitCheckpoint_1.GIT_OID.test(head))
306
+ throw new Error('Delivery HEAD is unavailable');
307
+ try {
308
+ await (0, GitCheckpoint_1.runGit)(artifact.source.root, ['--no-replace-objects', 'merge-base', '--is-ancestor', artifact.output, head]);
309
+ }
310
+ catch (error) {
311
+ if (error instanceof GitCheckpoint_1.GitCommandError && error.code === 1)
312
+ return artifact;
313
+ throw error;
314
+ }
315
+ if (await (0, GitCheckpoint_1.gitText)(artifact.source.root, ['rev-parse', '--verify', 'HEAD']) !== head) {
316
+ throw new Error('Delivery HEAD changed during historical verification');
317
+ }
318
+ owner.checkActive?.();
319
+ const confirmed = { ...artifact, disposition: 'applied', deliveredAtHead: head };
320
+ // Record historical handoff, not current-byte inclusion or an integration tree.
321
+ await this.resolveArtifact(confirmed, 'applied');
322
+ return confirmed;
323
+ }
180
324
  async describe(taskId) {
181
325
  const artifact = await this.read('artifacts', taskId);
182
326
  return artifact ? this.view(artifact) : undefined;
183
327
  }
328
+ /** Closeout needs a fresh, owned receipt, not a cached result or an absent record. */
329
+ async inspectCloseout(owner, taskId) {
330
+ const saved = await this.read('artifacts', taskId);
331
+ if (!saved)
332
+ throw new Error('Worker artifact is unavailable');
333
+ return this.withLane(saved.laneId, async () => {
334
+ const artifact = await this.read('artifacts', taskId);
335
+ if (!artifact || artifact.taskId !== taskId || artifact.laneId !== saved.laneId
336
+ || typeof artifact.successful !== 'boolean')
337
+ throw new Error('Invalid worker artifact receipt');
338
+ await this.validateArtifact(artifact, owner);
339
+ owner.checkActive?.();
340
+ const workspace = await this.read('lanes', artifact.laneId);
341
+ if (artifact.disposition !== 'retained' && workspace?.taskId === taskId && workspace.blocked) {
342
+ throw new Error('Worker artifact collection requires recovery');
343
+ }
344
+ // Failed workers may have a no-change receipt, but cannot claim an applied patch.
345
+ if (artifact.disposition === 'applied' && !artifact.successful && artifact.tree !== artifact.input.tree) {
346
+ throw new Error('Unsuccessful worker changes have no delivery receipt');
347
+ }
348
+ const view = await this.view(artifact);
349
+ owner.checkActive?.();
350
+ return view;
351
+ });
352
+ }
184
353
  /** Removing a conversation does not authorize deleting its files or pending artifacts. */
185
354
  async preserveLane(laneId) {
355
+ await this.withLane(laneId, async () => {
356
+ const workspace = await this.read('lanes', laneId);
357
+ if (workspace)
358
+ await this.write('lanes', laneId, { ...workspace, blocked: true });
359
+ });
360
+ }
361
+ /** Called only after worker exit or a durable delivery receipt; never races a cleanup deadline. */
362
+ async reclaim(taskId) {
363
+ try {
364
+ this.assertId(taskId);
365
+ const artifact = await this.read('artifacts', taskId);
366
+ if (!artifact)
367
+ return false;
368
+ return await this.withLane(artifact.laneId, () => this.reclaimLocked(taskId, artifact.laneId));
369
+ }
370
+ catch {
371
+ // Physical cleanup is optional. Do not turn a delivered result into a failed task.
372
+ console.warn('[Delegation] Worker checkout retained; automatic reclamation could not be verified');
373
+ return false;
374
+ }
375
+ }
376
+ async reclaimLocked(taskId, laneId) {
186
377
  const workspace = await this.read('lanes', laneId);
187
- if (workspace)
188
- await this.write('lanes', laneId, { ...workspace, blocked: true });
378
+ const artifact = await this.read('artifacts', taskId);
379
+ if (!workspace || workspace.taskId !== taskId || !artifact || workspace.blocked || workspace.pendingTaskId
380
+ || artifact.successful !== true || artifact.disposition !== 'applied' || artifact.recovery)
381
+ return false;
382
+ await this.validateReclamationArtifact(workspace, artifact);
383
+ if (workspace.checkoutState === 'reclaimed')
384
+ return false;
385
+ await this.verifyCheckout(workspace, artifact);
386
+ const intent = { ...workspace, checkoutState: 'reclaiming',
387
+ reclamation: { taskId, output: artifact.output, tree: artifact.tree } };
388
+ await this.write('lanes', laneId, intent);
389
+ // Full byte verification holds only the lane guard, not other lanes' metadata admission.
390
+ await this.verifyCheckout(intent, artifact);
391
+ return this.withRepository(workspace.source, async () => {
392
+ const latest = await this.read('lanes', laneId);
393
+ if (!latest || JSON.stringify(latest) !== JSON.stringify(intent))
394
+ throw new Error('Worker reclamation intent changed');
395
+ const registered = await this.registration(workspace.source, workspace.directory);
396
+ if (!registered || registered.locked || registered.head !== artifact.output) {
397
+ throw new Error('Worker registration changed before removal');
398
+ }
399
+ await (0, GitCheckpoint_1.runGit)(workspace.source.root, ['worktree', 'remove', workspace.directory]);
400
+ if (await this.pathExists(workspace.directory) || await this.registration(workspace.source, workspace.directory)) {
401
+ throw new Error('Worker checkout removal was not confirmed');
402
+ }
403
+ await this.write('lanes', laneId, { ...intent, checkoutState: 'reclaimed' });
404
+ return true;
405
+ });
406
+ }
407
+ async validateReclamationArtifact(workspace, artifact) {
408
+ await this.validateWorkspaceRecord(workspace);
409
+ await this.validateArtifact(artifact, { threadId: workspace.threadId, cwd: workspace.source.cwd,
410
+ workspaceGeneration: workspace.workspaceGeneration });
411
+ if (artifact.laneId !== workspace.laneId || artifact.taskId !== workspace.taskId
412
+ || artifact.input.commit !== workspace.input.commit || artifact.input.tree !== workspace.input.tree
413
+ || artifact.successful !== true || artifact.disposition !== 'applied' || artifact.recovery
414
+ || workspace.blocked || workspace.pendingTaskId
415
+ || !GitCheckpoint_1.GIT_OID.test(artifact.originalHead)
416
+ || await (0, GitCheckpoint_1.gitText)(workspace.source.root, ['rev-parse', workspace.taskRef]) !== artifact.output
417
+ || await (0, GitCheckpoint_1.gitText)(workspace.source.root, ['rev-parse', `refs/remote-cli/worker-history/${artifact.taskId}`]) !== artifact.originalHead) {
418
+ throw new Error('Worker checkout has no matching delivery receipt');
419
+ }
420
+ if (workspace.reclamation && (workspace.reclamation.taskId !== artifact.taskId
421
+ || workspace.reclamation.output !== artifact.output || workspace.reclamation.tree !== artifact.tree)) {
422
+ throw new Error('Worker reclamation receipt changed');
423
+ }
424
+ }
425
+ async resolveReclamation(workspace) {
426
+ if (!workspace.checkoutState || workspace.checkoutState === 'present')
427
+ return workspace;
428
+ // The caller holds the lane guard. This resolver does not mutate Git worktree metadata.
429
+ const artifact = await this.read('artifacts', workspace.taskId);
430
+ if (!artifact)
431
+ throw new Error('Worker reclamation artifact is missing');
432
+ await this.validateReclamationArtifact(workspace, artifact);
433
+ const exists = await this.pathExists(workspace.directory);
434
+ const registered = await this.registration(workspace.source, workspace.directory);
435
+ if (!exists && !registered) {
436
+ const reclaimed = { ...workspace, checkoutState: 'reclaimed' };
437
+ await this.write('lanes', workspace.laneId, reclaimed);
438
+ return reclaimed;
439
+ }
440
+ if (workspace.checkoutState === 'reclaiming' && exists && registered) {
441
+ // A fully intact checkout can cancel an interrupted intent. Partial/foreign paths cannot.
442
+ await this.verifyCheckout(workspace, artifact);
443
+ const present = { ...workspace, checkoutState: 'present', reclamation: undefined };
444
+ await this.write('lanes', workspace.laneId, present);
445
+ return present;
446
+ }
447
+ throw new Error('Reclaimed worker path is occupied or partially registered; manual recovery is required');
448
+ }
449
+ async verifyCheckout(workspace, artifact) {
450
+ await this.validateWorkspace(workspace);
451
+ const registered = await this.registration(workspace.source, workspace.directory);
452
+ if (!registered || registered.locked || registered.head !== artifact.output
453
+ || await (0, GitCheckpoint_1.gitText)(workspace.directory, ['branch', '--show-current'])
454
+ || await (0, GitCheckpoint_1.gitText)(workspace.directory, ['rev-parse', 'HEAD']) !== artifact.output
455
+ || (await (0, GitCheckpoint_1.runGit)(workspace.directory, ['ls-files', '-u', '-z'])).length) {
456
+ throw new Error('Worker checkout is locked, unresolved, or no longer at its saved output');
457
+ }
458
+ await this.assertClean(workspace.directory);
459
+ // Git status and a copied index can hide changed bytes behind assume-unchanged,
460
+ // skip-worktree, or stat-cache flags. Hash actual bytes without filters or index metadata.
461
+ const files = new Map();
462
+ const directories = new Set(['']);
463
+ const tree = await (0, GitCheckpoint_1.runGit)(workspace.source.root, ['ls-tree', '-r', '-z', '--full-tree', artifact.tree]);
464
+ if (!Buffer.from(tree.toString('utf8')).equals(tree))
465
+ throw new Error('Non-UTF-8 checkout paths require manual cleanup');
466
+ for (const entry of tree.toString('utf8').split('\0').filter(Boolean)) {
467
+ const match = /^(100644|100755|120000) blob ([a-f0-9]{40}|[a-f0-9]{64})\t(.+)$/s.exec(entry);
468
+ if (!match)
469
+ throw new Error('Unsupported checkout tree entry');
470
+ const [, mode, oid, name] = match;
471
+ if (!inside(workspace.directory, path_1.default.join(workspace.directory, name)) || name.split('/').includes('.git')
472
+ || files.has(name))
473
+ throw new Error('Invalid checkout tree path');
474
+ files.set(name, { mode, oid });
475
+ let parent = path_1.default.posix.dirname(name);
476
+ while (parent !== '.') {
477
+ directories.add(parent);
478
+ parent = path_1.default.posix.dirname(parent);
479
+ }
480
+ if (files.size > GitCheckpoint_1.CHECKPOINT_LIMITS.files)
481
+ throw new Error('Checkout verification file limit exceeded');
482
+ }
483
+ let bytes = 0;
484
+ let entries = 0;
485
+ const buffer = Buffer.alloc(64 * 1024);
486
+ const walk = async (relative) => {
487
+ const absolute = path_1.default.join(workspace.directory, relative);
488
+ if (await promises_1.default.realpath(absolute) !== absolute)
489
+ throw new Error('Checkout directory changed during verification');
490
+ for (const name of await promises_1.default.readdir(absolute)) {
491
+ if (++entries > GitCheckpoint_1.CHECKPOINT_LIMITS.files * 2)
492
+ throw new Error('Checkout verification entry limit exceeded');
493
+ const child = relative ? `${relative}/${name}` : name;
494
+ const file = path_1.default.join(workspace.directory, child);
495
+ const stat = await promises_1.default.lstat(file);
496
+ if (child === '.git' && stat.isFile() && !stat.isSymbolicLink())
497
+ continue;
498
+ if (stat.isDirectory() && directories.has(child)) {
499
+ await walk(child);
500
+ continue;
501
+ }
502
+ const expected = files.get(child);
503
+ if (!expected || (expected.mode === '120000' ? !stat.isSymbolicLink() : !stat.isFile())) {
504
+ throw new Error('Checkout contains unknown, ignored, or unsupported files');
505
+ }
506
+ if (process.platform !== 'win32' && expected.mode !== '120000'
507
+ && Boolean(stat.mode & 0o111) !== (expected.mode === '100755'))
508
+ throw new Error('Checkout file mode changed');
509
+ bytes += stat.size;
510
+ if (bytes > GitCheckpoint_1.CHECKPOINT_LIMITS.bytes)
511
+ throw new Error('Checkout verification byte limit exceeded');
512
+ const hash = (0, crypto_1.createHash)(expected.oid.length === 64 ? 'sha256' : 'sha1');
513
+ hash.update(`blob ${stat.size}\0`);
514
+ if (stat.isSymbolicLink())
515
+ hash.update(await promises_1.default.readlink(file, { encoding: 'buffer' }));
516
+ else {
517
+ const handle = await promises_1.default.open(file, fs_1.constants.O_RDONLY | (fs_1.constants.O_NOFOLLOW ?? 0));
518
+ try {
519
+ const opened = await handle.stat();
520
+ if (!opened.isFile() || opened.dev !== stat.dev || opened.ino !== stat.ino || opened.size !== stat.size) {
521
+ throw new Error('Checkout file changed during verification');
522
+ }
523
+ let read = 0;
524
+ while (true) {
525
+ const chunk = await handle.read(buffer, 0, buffer.length, null);
526
+ if (!chunk.bytesRead)
527
+ break;
528
+ read += chunk.bytesRead;
529
+ if (read > stat.size)
530
+ throw new Error('Checkout file grew during verification');
531
+ hash.update(buffer.subarray(0, chunk.bytesRead));
532
+ }
533
+ const after = await handle.stat();
534
+ if (read !== stat.size || after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs) {
535
+ throw new Error('Checkout file changed during verification');
536
+ }
537
+ }
538
+ finally {
539
+ await handle.close();
540
+ }
541
+ }
542
+ if (hash.digest('hex') !== expected.oid)
543
+ throw new Error('Checkout bytes differ from the saved output');
544
+ files.delete(child);
545
+ }
546
+ };
547
+ await walk('');
548
+ if (files.size)
549
+ throw new Error('Saved checkout files are missing');
550
+ }
551
+ async registration(source, directory) {
552
+ let selected = false;
553
+ let result;
554
+ // Git 2.34 has no worktree-list -z option. Decode its quoted paths rather than
555
+ // weakening ownership checks or raising the existing Git requirement.
556
+ const output = await (0, GitCheckpoint_1.runGit)(source.root, ['-c', 'core.quotePath=false', 'worktree', 'list', '--porcelain']);
557
+ if (!Buffer.from(output.toString('utf8')).equals(output))
558
+ throw new Error('Unsupported Git worktree path encoding');
559
+ for (const field of output.toString('utf8').split('\n')) {
560
+ if (field.startsWith('worktree ')) {
561
+ const encoded = field.slice(9);
562
+ let decoded;
563
+ try {
564
+ decoded = encoded.startsWith('"') ? JSON.parse(encoded) : encoded;
565
+ }
566
+ catch {
567
+ throw new Error('Unsupported Git worktree path encoding; manual recovery is required');
568
+ }
569
+ if (typeof decoded !== 'string')
570
+ throw new Error('Invalid Git worktree path');
571
+ selected = decoded === directory;
572
+ if (selected)
573
+ result = { locked: false };
574
+ }
575
+ else if (selected && result) {
576
+ if (field.startsWith('HEAD '))
577
+ result.head = field.slice(5);
578
+ if (field === 'locked' || field.startsWith('locked '))
579
+ result.locked = true;
580
+ }
581
+ }
582
+ return result;
583
+ }
584
+ async pathExists(directory) {
585
+ try {
586
+ await promises_1.default.lstat(directory);
587
+ return true;
588
+ }
589
+ catch (error) {
590
+ if (error.code === 'ENOENT')
591
+ return false;
592
+ throw error;
593
+ }
189
594
  }
190
595
  async applyArtifact(artifact, current, owner) {
191
596
  // Compatible with Git 2.34: merge in an owned scratch checkout, never in the user's worktree.
@@ -194,7 +599,7 @@ class DelegatedWorkspaceManager {
194
599
  let created = false;
195
600
  let preserve = false;
196
601
  try {
197
- await (0, GitCheckpoint_1.runGit)(artifact.source.root, ['worktree', 'add', '--detach', integrationDirectory, wrapper]);
602
+ await this.withRepository(artifact.source, () => (0, GitCheckpoint_1.runGit)(artifact.source.root, ['worktree', 'add', '--detach', integrationDirectory, wrapper]));
198
603
  created = true;
199
604
  try {
200
605
  await (0, GitCheckpoint_1.runGit)(integrationDirectory, ['-c', 'user.name=Remote CLI', '-c', 'user.email=remote-cli@example.com',
@@ -244,7 +649,7 @@ class DelegatedWorkspaceManager {
244
649
  finally {
245
650
  if (created && !preserve) {
246
651
  // This directory contains only captured integration input and a reproducible merge result.
247
- await (0, GitCheckpoint_1.runGit)(artifact.source.root, ['worktree', 'remove', '--force', integrationDirectory])
652
+ await this.withRepository(artifact.source, () => (0, GitCheckpoint_1.runGit)(artifact.source.root, ['worktree', 'remove', '--force', integrationDirectory]))
248
653
  .catch(() => console.warn('[Delegation] Integration scratch worktree retained for manual cleanup'));
249
654
  }
250
655
  else if (!created) {
@@ -267,6 +672,7 @@ class DelegatedWorkspaceManager {
267
672
  return { taskId: artifact.taskId, disposition: artifact.disposition, successful: artifact.successful,
268
673
  baseCommit: artifact.input.commit, outputCommit: artifact.output,
269
674
  changedFiles: files.slice(0, 60), filesTruncated: files.length > 60,
675
+ ...(artifact.deliveredAtHead ? { deliveredAtHead: artifact.deliveredAtHead } : {}),
270
676
  ...(artifact.recovery ? { recovery: artifact.recovery } : {}) };
271
677
  }
272
678
  async assertSelectedScope(artifact) {
@@ -282,8 +688,10 @@ class DelegatedWorkspaceManager {
282
688
  || artifact.threadId !== owner.threadId || artifact.source.cwd !== owner.cwd
283
689
  || artifact.workspaceGeneration !== owner.workspaceGeneration || !GitCheckpoint_1.GIT_OID.test(artifact.output)
284
690
  || !GitCheckpoint_1.GIT_OID.test(artifact.tree) || !GitCheckpoint_1.GIT_OID.test(artifact.input.commit) || !GitCheckpoint_1.GIT_OID.test(artifact.input.tree)
285
- || !['pending', 'applied', 'retained'].includes(artifact.disposition))
691
+ || !['pending', 'applied', 'retained'].includes(artifact.disposition)
692
+ || artifact.deliveredAtHead !== undefined && (!GitCheckpoint_1.GIT_OID.test(artifact.deliveredAtHead) || artifact.disposition !== 'applied')) {
286
693
  throw new Error('Invalid or foreign worker artifact');
694
+ }
287
695
  await this.validateSource(artifact.source);
288
696
  if (await (0, GitCheckpoint_1.gitText)(artifact.source.root, ['rev-parse', this.outputRef(artifact.taskId)]) !== artifact.output
289
697
  || await (0, GitCheckpoint_1.gitText)(artifact.source.root, ['rev-parse', this.inputRef(artifact.taskId)]) !== artifact.input.commit
@@ -299,21 +707,37 @@ class DelegatedWorkspaceManager {
299
707
  throw new Error('Source repository identity changed');
300
708
  }
301
709
  async validateWorkspace(workspace) {
710
+ await this.validateWorkspaceRecord(workspace);
711
+ if (workspace.checkoutState === 'reclaimed' || !await this.pathExists(workspace.directory)) {
712
+ throw new Error('Owned worker checkout is missing; an explicit reclamation receipt is required');
713
+ }
714
+ if (await promises_1.default.realpath(workspace.directory) !== workspace.directory
715
+ || await (0, GitCheckpoint_1.gitText)(workspace.directory, ['rev-parse', '--show-toplevel']) !== workspace.directory
716
+ || await promises_1.default.realpath(path_1.default.resolve(workspace.directory, await (0, GitCheckpoint_1.gitText)(workspace.directory, ['rev-parse', '--git-common-dir']))) !== workspace.source.commonDirectory) {
717
+ throw new Error('Owned worker worktree identity changed');
718
+ }
719
+ if (!inside(workspace.directory, await promises_1.default.realpath(workspace.cwd)))
720
+ throw new Error('Worker directory escapes its worktree');
721
+ }
722
+ assertLifecycle(workspace) {
723
+ const state = workspace.checkoutState ?? 'present';
724
+ if (!['present', 'reclaiming', 'reclaimed'].includes(state)
725
+ || (state === 'present' ? workspace.reclamation !== undefined : !workspace.reclamation)) {
726
+ throw new Error('Invalid worker checkout lifecycle');
727
+ }
728
+ }
729
+ async validateWorkspaceRecord(workspace) {
302
730
  this.assertId(workspace.laneId);
303
731
  this.assertId(workspace.taskId);
732
+ this.assertLifecycle(workspace);
304
733
  if (workspace.taskRef !== `refs/remote-cli/tasks/${workspace.taskId}` || !GitCheckpoint_1.GIT_OID.test(workspace.input.commit)) {
305
734
  throw new Error('Owned worker task reference is invalid');
306
735
  }
307
736
  await this.validateSource(workspace.source);
308
737
  if (workspace.directory !== this.worktreeDirectory(workspace.source, workspace.laneId)
309
- || workspace.cwd !== path_1.default.join(workspace.directory, workspace.source.relativeCwd)
310
- || await promises_1.default.realpath(workspace.directory) !== workspace.directory
311
- || await (0, GitCheckpoint_1.gitText)(workspace.directory, ['rev-parse', '--show-toplevel']) !== workspace.directory
312
- || await promises_1.default.realpath(path_1.default.resolve(workspace.directory, await (0, GitCheckpoint_1.gitText)(workspace.directory, ['rev-parse', '--git-common-dir']))) !== workspace.source.commonDirectory) {
738
+ || workspace.cwd !== path_1.default.join(workspace.directory, workspace.source.relativeCwd)) {
313
739
  throw new Error('Owned worker worktree identity changed');
314
740
  }
315
- if (!inside(workspace.directory, await promises_1.default.realpath(workspace.cwd)))
316
- throw new Error('Worker directory escapes its worktree');
317
741
  }
318
742
  async assertClean(directory) {
319
743
  if ((await (0, GitCheckpoint_1.runGit)(directory, ['status', '--porcelain=v1', '-z', '--untracked-files=all'])).length) {
@@ -329,6 +753,28 @@ class DelegatedWorkspaceManager {
329
753
  outputRef(taskId) { this.assertId(taskId); return `refs/remote-cli/artifacts/${taskId}`; }
330
754
  assertId(id) { if (!UUID.test(id))
331
755
  throw new Error('Invalid worker workspace ID'); }
756
+ withLane(laneId, operation) {
757
+ this.assertId(laneId);
758
+ return this.serialize(`lane:${path_1.default.resolve(this.directory)}:${laneId}`, operation);
759
+ }
760
+ withRepository(source, operation) {
761
+ return this.serialize(`repository:${source.commonDirectory}`, operation);
762
+ }
763
+ async serialize(key, operation) {
764
+ const previous = DelegatedWorkspaceManager.operations.get(key) ?? Promise.resolve();
765
+ let release;
766
+ const current = new Promise(resolve => { release = resolve; });
767
+ DelegatedWorkspaceManager.operations.set(key, current);
768
+ await previous;
769
+ try {
770
+ return await operation();
771
+ }
772
+ finally {
773
+ release();
774
+ if (DelegatedWorkspaceManager.operations.get(key) === current)
775
+ DelegatedWorkspaceManager.operations.delete(key);
776
+ }
777
+ }
332
778
  async initialize() {
333
779
  await this.safeDirectory(this.directory);
334
780
  await Promise.all(['lanes', 'artifacts', 'trees'].map(name => this.safeDirectory(path_1.default.join(this.directory, name))));