@ricsam/r5d-worker 0.0.169 → 0.0.171

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.169",
3
+ "version": "0.0.171",
4
4
  "type": "commonjs"
5
5
  }
@@ -20296,7 +20296,7 @@ var SESSION_RUN_HELP_TEXT = [
20296
20296
  ' r5dctl -p <project> session start --worker <label> --worktree <branch> --model <tier> [-f|--follow|--watch] [--tools] [--parent-session <session-id>] "<prompt>"',
20297
20297
  ' r5dctl -p <project> session start --worker <label> --new-worktree <branch> --source worktree:<branch> [--working-tree carry|clean] --model <tier> [-f|--follow|--watch] [--tools] [--parent-session <session-id>] "<prompt>"',
20298
20298
  " r5dctl session status <session-id>",
20299
- ' r5dctl session prompt --worker <label> --mode <mode> --model <tier> [--delivery queue|steer|force] [--sender <name>] <session-id> "<prompt>"',
20299
+ ' r5dctl session prompt --worker <label> --mode <plan|agent> --model <tier> [--delivery queue|steer|force] [--sender <name>] <session-id> "<prompt>"',
20300
20300
  " r5dctl session stop <session-id>",
20301
20301
  " r5dctl session set-worker <session-id> --worker <label>",
20302
20302
  " r5dctl session process-log <run-id> [--tail <n>] [--grep <pattern>]",
@@ -20321,7 +20321,7 @@ var SESSION_RUN_HELP_TEXT = [
20321
20321
  "The reserved topic `checkout` declares tracked-file writes in that checkout: claim it before running the command, hold it until the",
20322
20322
  "write is done, and release it. A running session native to that branch holds `checkout` implicitly, so the call names who to message.",
20323
20323
  "Commands started while a cross-scope `checkout` claim is held also fence the claimed checkout on the bound worker (the fence line says",
20324
- "whether the worker supports it). checkout path prints where a project branch is checked out on the session's bound worker.",
20324
+ "whether the worker supports it). checkout path prints the absolute checkout path of a project branch on the session's bound worker, one bare line (use --json for the checkout and worker too).",
20325
20325
  "Agent shells act as their own session; user credentials select the acting session with -s or R5D_SESSION_ID.",
20326
20326
  "A new worktree starts with the source checkout's uncommitted changes (--working-tree carry, the default);",
20327
20327
  "--working-tree clean checks out the source branch's current commit only. Start and status print the mode, never a commit hash.",
@@ -20465,7 +20465,7 @@ var SHARED_HELP_ENTRIES = [
20465
20465
  },
20466
20466
  {
20467
20467
  section: "sessions",
20468
- usage: 'session prompt --worker <label> --mode <mode> --model <tier> [--delivery queue|steer|force] [--sender <name>] <session-id> "<prompt>"',
20468
+ usage: 'session prompt --worker <label> --mode <plan|agent> --model <tier> [--delivery queue|steer|force] [--sender <name>] <session-id> "<prompt>"',
20469
20469
  description: "Prompt a session: steer after the next tool result (default), queue a deferred follow-up, or force in place of the current tool round. An idle unfinished session may require explicit continuation or confirmation of interrupted tools. Blocked delivery reports the saved message/queue ID and reason; inspect the existing queue in the session UI instead of sending duplicates. The recipient sees the verified sender session id; --sender only adds a display title."
20470
20470
  },
20471
20471
  {
@@ -20500,12 +20500,12 @@ var SHARED_HELP_ENTRIES = [
20500
20500
  },
20501
20501
  {
20502
20502
  section: "sessions",
20503
- usage: '-s <session-id> answer-questions --worker <label> --mode <mode> --model <tier> -a1 "<answer>" ...',
20503
+ usage: '-s <session-id> answer-questions --worker <label> --mode <plan|agent> --model <tier> -a1 "<answer>" ...',
20504
20504
  description: "Answer pending questions in order."
20505
20505
  },
20506
20506
  {
20507
20507
  section: "sessions",
20508
- usage: "-s <session-id> answer-env-request --worker <label> --mode <mode> --model <tier> [-e KEY=value] [--context <text>]",
20508
+ usage: "-s <session-id> answer-env-request --worker <label> --mode <plan|agent> --model <tier> [-e KEY=value] [--context <text>]",
20509
20509
  description: "Answer a pending environment variable request with values, context, or both."
20510
20510
  },
20511
20511
  {
@@ -20605,7 +20605,7 @@ function resolveEntrypointPath(entrypoint) {
20605
20605
  }
20606
20606
  }
20607
20607
  function getR5dctlVersion() {
20608
- if (true) return "0.0.169";
20608
+ if (true) return "0.0.171";
20609
20609
  const entrypoint = process.argv[1] ? resolveEntrypointPath(process.argv[1]) : null;
20610
20610
  let current = entrypoint ? import_node_path2.default.dirname(entrypoint) : process.cwd();
20611
20611
  for (let index = 0; index < 12; index += 1) {
@@ -20762,7 +20762,7 @@ function requireModel(value) {
20762
20762
  return model;
20763
20763
  }
20764
20764
  function requireSessionMode(value) {
20765
- const mode = requireValue(value?.trim(), "--mode <mode> is required");
20765
+ const mode = requireValue(value?.trim(), "--mode <plan|agent> is required");
20766
20766
  if (!R5DCTL_SESSION_MODES.has(mode)) {
20767
20767
  throw new Error("Invalid --mode. Expected one of: plan, agent");
20768
20768
  }
@@ -21263,9 +21263,8 @@ ${rows.join("\n")}
21263
21263
  `;
21264
21264
  }
21265
21265
  function renderCheckoutPath(response) {
21266
- return [`Checkout: ${response.projectPath}#${response.branchName}`, `Worker: ${response.worker}`, `Path: ${response.path}`, ""].join(
21267
- "\n"
21268
- );
21266
+ return `${response.path}
21267
+ `;
21269
21268
  }
21270
21269
  function assertAuthLoginArgs(args) {
21271
21270
  const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
@@ -21504,7 +21503,7 @@ function parseAnswerFlags(args) {
21504
21503
  return ordered.map(([, value]) => value);
21505
21504
  }
21506
21505
  function requireChatMode(value) {
21507
- const mode = requireValue(value?.trim(), "--mode <mode> is required");
21506
+ const mode = requireValue(value?.trim(), "--mode <plan|agent> is required");
21508
21507
  if (!CHAT_MODES.has(mode)) throw new Error(`Invalid --mode: ${mode}`);
21509
21508
  return mode;
21510
21509
  }
package/dist/mjs/main.mjs CHANGED
@@ -7,7 +7,7 @@ import { startManagerRpc } from "./runtime/releases/rpc-main.mjs";
7
7
  import { ManagerRpcConfig } from "./runtime/releases/rpc-protocol.mjs";
8
8
  const args = process.argv.slice(2);
9
9
  if (args.includes("--version")) {
10
- console.log(`r5d-worker ${true ? "0.0.169" : "development"}`);
10
+ console.log(`r5d-worker ${true ? "0.0.171" : "development"}`);
11
11
  } else if (!args.length || args.includes("--help")) {
12
12
  console.log(
13
13
  "Usage: r5d-worker start --label <label> [--root <dir>] [--base-url <url>] [--token <worker-token>]\n r5d-worker executor /absolute/private/config.json\n r5d-worker manager /absolute/private/config.json\n\nRun independently supervised durable runtime services. Provision configuration with r5dinfra."
@@ -15,7 +15,7 @@ if (args.includes("--version")) {
15
15
  } else if (args[0] === "start") {
16
16
  const runtime = await startPersonalWorker(
17
17
  parsePersonalWorkerOptions(args.slice(1)),
18
- true ? "0.0.169" : "development"
18
+ true ? "0.0.171" : "development"
19
19
  );
20
20
  console.log(`Worker connected: ${runtime.resourceId}`);
21
21
  let closing = false;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.169",
3
+ "version": "0.0.171",
4
4
  "type": "module"
5
5
  }
@@ -20,6 +20,7 @@ import {
20
20
  } from "./protocol.mjs";
21
21
  import { ExecutorStore, privateDirectory } from "./storage.mjs";
22
22
  import { AdapterGrantRegistry, digest } from "./releases/grants.mjs";
23
+ import { hostEnvironment } from "./host-environment.mjs";
23
24
  const tokenHash = (token) => createHash("sha256").update(token).digest("hex");
24
25
  class HostExecutor {
25
26
  constructor(config, nativePty) {
@@ -219,7 +220,7 @@ class HostExecutor {
219
220
  const secrets = join(this.config.stateDir, "secrets", tokenHash(operation.operationId));
220
221
  try {
221
222
  privateDirectory(secrets);
222
- const env = { PATH: "/usr/local/bin:/usr/bin:/bin", LANG: "C.UTF-8", HOME: cwd, ...payload.env };
223
+ const env = { ...hostEnvironment(), ...payload.env };
223
224
  for (const credential of credentials) {
224
225
  Object.assign(env, credential.env);
225
226
  for (const [name, content] of Object.entries(credential.files)) {
@@ -0,0 +1,10 @@
1
+ function hostEnvironment() {
2
+ const env = {};
3
+ for (const [name, value] of Object.entries(process.env)) if (typeof value === "string") env[name] = value;
4
+ if (!env.PATH) env.PATH = "/usr/local/bin:/usr/bin:/bin";
5
+ if (!env.LANG && !env.LC_ALL) env.LANG = "C.UTF-8";
6
+ return env;
7
+ }
8
+ export {
9
+ hostEnvironment
10
+ };
@@ -1,6 +1,7 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { ExecutorError } from "./protocol.mjs";
4
+ import { hostEnvironment } from "./host-environment.mjs";
4
5
  const nodeExecutable = typeof R5D_PACKAGED_NODE !== "undefined" && R5D_PACKAGED_NODE ? fileURLToPath(new URL("./bin/node", import.meta.url)) : Bun.which("node") ?? "node";
5
6
  function preflightNativePty() {
6
7
  if (process.platform !== "darwin") return;
@@ -19,7 +20,7 @@ class PtySession {
19
20
  ready;
20
21
  constructor(input) {
21
22
  this.child = spawn(nodeExecutable, [fileURLToPath(new URL("./pty-host.mjs", import.meta.url))], {
22
- env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: input.cwd },
23
+ env: hostEnvironment(),
23
24
  stdio: "pipe"
24
25
  });
25
26
  this.ready = new Promise((resolve, reject) => {
@@ -1,12 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { ExecutorError, isStartNonAdmissionCode, MAX_FRAME_BYTES } from "../protocol.mjs";
4
+ import { hostEnvironment } from "../host-environment.mjs";
4
5
  class AdapterChild {
5
6
  constructor(runtimePath, entrypoint, configPath, home, timeoutMs = 1e4) {
6
7
  this.timeoutMs = timeoutMs;
7
8
  this.process = spawn(runtimePath, [entrypoint, configPath], {
8
9
  cwd: home,
9
- env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: home, LANG: "C.UTF-8" },
10
+ env: hostEnvironment(),
10
11
  stdio: "pipe"
11
12
  });
12
13
  let buffer = Buffer.alloc(0);
@@ -11,7 +11,7 @@ import {
11
11
  WorkspaceConfig,
12
12
  WorkspaceError
13
13
  } from "./contracts.mjs";
14
- import { OuterRepository, OuterSnapshotRefusal, OUTER_BRANCH, OUTER_CONFLICT_CODE } from "./outer.mjs";
14
+ import { OuterRepository, OuterSnapshotRefusal, OUTER_BRANCH, OUTER_CONFLICT_CODE, OUTER_PUBLISH_ATTEMPTS } from "./outer.mjs";
15
15
  import { SessionArtifactStore, SessionArtifactChunk, RESERVED_ARTIFACT_ENV } from "./artifacts.mjs";
16
16
  import { WorkspaceFileWrite, WorkspaceFileWriteLookup } from "./file-write.mjs";
17
17
  import { walkDirectoryTree } from "./walk.mjs";
@@ -1379,8 +1379,8 @@ class WorkspaceAuthority {
1379
1379
  const storage = await this.options.accountStorage(userId);
1380
1380
  const repositoryId = this.accountRepositoryId(userId);
1381
1381
  if (state.blocked?.code === "publication_unknown") {
1382
- const { operationId: operationId2, commit } = state.blocked;
1383
- const receipt = await storage.read({ method: "operation.get", lookupId: operationId2 }).catch((error) => {
1382
+ const { operationId, commit } = state.blocked;
1383
+ const receipt = await storage.read({ method: "operation.get", lookupId: operationId }).catch((error) => {
1384
1384
  if (error instanceof WorkspaceError && error.code === "not_found") return null;
1385
1385
  throw error;
1386
1386
  });
@@ -1395,17 +1395,17 @@ class WorkspaceAuthority {
1395
1395
  await this.saveOuter(state);
1396
1396
  }
1397
1397
  if (!state.initialized) {
1398
- const remote2 = await this.remoteHead(storage, repositoryId, OUTER_BRANCH, true);
1399
- if (remote2) {
1400
- if (!await repo.has(remote2)) await repo.unbundle(await this.downloadBundle(storage, repositoryId));
1401
- if (!await repo.isLegacyBootstrap(remote2)) await repo.checkout(remote2);
1402
- await repo.setHead(remote2);
1398
+ const remote = await this.remoteHead(storage, repositoryId, OUTER_BRANCH, true);
1399
+ if (remote) {
1400
+ if (!await repo.has(remote)) await repo.unbundle(await this.downloadBundle(storage, repositoryId));
1401
+ if (!await repo.isLegacyBootstrap(remote)) await repo.checkout(remote);
1402
+ await repo.setHead(remote);
1403
1403
  }
1404
1404
  state.initialized = true;
1405
- state.publishedHead = remote2;
1406
- state.head = remote2;
1405
+ state.publishedHead = remote;
1406
+ state.head = remote;
1407
1407
  await this.saveOuter(state);
1408
- if (options.bootstrapOnly) return { head: remote2, unchanged: true };
1408
+ if (options.bootstrapOnly) return { head: remote, unchanged: true };
1409
1409
  } else if (options.bootstrapOnly) return { head: state.head, unchanged: true };
1410
1410
  if (state.conflict && !options.resolveConflict)
1411
1411
  throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, "Workspace synchronization conflict awaits resolution", state.conflict.paths, true);
@@ -1423,11 +1423,11 @@ class WorkspaceAuthority {
1423
1423
  if (state.conflict) {
1424
1424
  const marker = /^(<{7}|={7}|>{7})( |$)/m;
1425
1425
  for (const file of state.conflict.paths) {
1426
- const bytes2 = await readRegular(path.join(repo.workTree, file)).catch((error) => {
1426
+ const bytes = await readRegular(path.join(repo.workTree, file)).catch((error) => {
1427
1427
  if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
1428
1428
  throw error;
1429
1429
  });
1430
- if (bytes2 && marker.test(bytes2.toString("utf8")))
1430
+ if (bytes && marker.test(bytes.toString("utf8")))
1431
1431
  throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, "Conflict markers remain; resolve them before confirming", [file], true);
1432
1432
  }
1433
1433
  } else if (state.blocked && state.blocked.code !== "publication_unknown") {
@@ -1435,14 +1435,22 @@ class WorkspaceAuthority {
1435
1435
  await this.saveOuter(state);
1436
1436
  }
1437
1437
  const base = state.publishedHead;
1438
- const baseTree = base ? await repo.treeOf(base) : await repo.emptyTree();
1438
+ const carried = state.head && state.head !== base ? (await repo.parents(state.head)).slice(1).filter((parent) => parent !== base) : [];
1439
1439
  let local;
1440
+ let integratedRemote = null;
1440
1441
  if (state.conflict) {
1441
1442
  local = await repo.commit(snapshot.tree, [state.conflict.ours, state.conflict.theirs], "Resolve workspace synchronization conflict");
1443
+ integratedRemote = state.conflict.theirs;
1442
1444
  state.conflict = null;
1443
1445
  state.blocked = null;
1444
- } else if (base && snapshot.tree === baseTree) local = base;
1445
- else local = await repo.commit(snapshot.tree, base ? [base] : [], "Workspace snapshot");
1446
+ } else if (state.head && state.head !== base && snapshot.tree === await repo.treeOf(state.head)) {
1447
+ local = state.head;
1448
+ integratedRemote = carried.at(-1) ?? null;
1449
+ } else if (base && !carried.length && snapshot.tree === await repo.treeOf(base)) local = base;
1450
+ else {
1451
+ local = await repo.commit(snapshot.tree, [...base ? [base] : [], ...carried], carried.length ? "Merge workspace" : "Workspace snapshot");
1452
+ integratedRemote = carried.at(-1) ?? null;
1453
+ }
1446
1454
  if (state.head !== local) {
1447
1455
  state.head = local;
1448
1456
  await repo.setHead(local);
@@ -1453,66 +1461,71 @@ class WorkspaceAuthority {
1453
1461
  throw new WorkspaceError("conflict", "The required workspace head is not integrated; retry the transfer");
1454
1462
  return options.requireHead ? { integrated: options.requireHead } : {};
1455
1463
  };
1456
- const remote = await this.remoteHead(storage, repositoryId, OUTER_BRANCH, true);
1457
1464
  let currentTree = snapshot.tree;
1458
- if (remote && remote !== base) {
1459
- if (!await repo.has(remote)) await repo.unbundle(await this.downloadBundle(storage, repositoryId));
1460
- if (local === base) {
1461
- await repo.apply(currentTree, await repo.treeOf(remote));
1462
- state.publishedHead = remote;
1463
- state.head = remote;
1464
- await repo.setHead(remote);
1465
+ for (let attempt = 1; ; attempt++) {
1466
+ const remote = await this.remoteHead(storage, repositoryId, OUTER_BRANCH, true);
1467
+ if (remote && remote !== local && remote !== integratedRemote) {
1468
+ if (!await repo.has(remote)) await repo.unbundle(await this.downloadBundle(storage, repositoryId));
1469
+ if (local === base) {
1470
+ await repo.apply(currentTree, await repo.treeOf(remote));
1471
+ state.publishedHead = remote;
1472
+ state.head = remote;
1473
+ await repo.setHead(remote);
1474
+ await this.saveOuter(state);
1475
+ return { head: remote, unchanged: false, ...await integrated(remote) };
1476
+ }
1477
+ const merge = await repo.merge(local, remote);
1478
+ if (merge.conflicts.length) {
1479
+ await repo.apply(currentTree, merge.tree);
1480
+ state.conflict = { ours: local, theirs: remote, tree: merge.tree, paths: merge.conflicts };
1481
+ state.blocked = { code: OUTER_CONFLICT_CODE, message: "The workspace diverged from another worker; conflict markers were written to the paths named", paths: merge.conflicts };
1482
+ await this.saveOuter(state);
1483
+ throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, state.blocked.message, merge.conflicts, true);
1484
+ }
1485
+ await repo.apply(currentTree, merge.tree);
1486
+ currentTree = merge.tree;
1487
+ local = await repo.commit(merge.tree, [local, remote], "Merge workspace");
1488
+ integratedRemote = remote;
1489
+ state.head = local;
1490
+ await repo.setHead(local);
1465
1491
  await this.saveOuter(state);
1466
- return { head: remote, unchanged: false, ...await integrated(remote) };
1467
1492
  }
1468
- const merge = await repo.merge(local, remote);
1469
- if (merge.conflicts.length) {
1470
- await repo.apply(currentTree, merge.tree);
1471
- state.conflict = { ours: local, theirs: remote, tree: merge.tree, paths: merge.conflicts };
1472
- state.blocked = { code: OUTER_CONFLICT_CODE, message: "The workspace diverged from another worker; conflict markers were written to the paths named", paths: merge.conflicts };
1493
+ if (local === remote) {
1494
+ state.publishedHead = local;
1473
1495
  await this.saveOuter(state);
1474
- throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, state.blocked.message, merge.conflicts, true);
1496
+ return { head: local, unchanged: true, ...await integrated(local) };
1475
1497
  }
1476
- await repo.apply(currentTree, merge.tree);
1477
- currentTree = merge.tree;
1478
- local = await repo.commit(merge.tree, [local, remote], "Merge workspace");
1479
- state.head = local;
1480
- await repo.setHead(local);
1498
+ const operationId = `ws-${randomUUID()}`, blobId = `ws-${randomUUID()}`;
1499
+ const bytes = await repo.bundle(remote && await repo.has(remote) ? remote : null);
1500
+ state.blocked = { code: "publication_unknown", operationId, commit: local, message: "Workspace publication is in progress/unknown. Inspect the original storage operation; never retry under a new ID." };
1481
1501
  await this.saveOuter(state);
1482
- }
1483
- if (local === remote) {
1502
+ try {
1503
+ await this.uploadBundle(storage, bytes, blobId, operationId);
1504
+ const result = await storage.mutate({
1505
+ method: "repository.publish",
1506
+ operationId,
1507
+ repositoryId,
1508
+ branch: OUTER_BRANCH,
1509
+ expectedHead: remote,
1510
+ commit: local,
1511
+ bundleId: blobId
1512
+ });
1513
+ if (result.head !== local) throw new WorkspaceError("invalid_response", "Workspace publication receipt head mismatch; inspect original operation");
1514
+ } catch (error) {
1515
+ if (error instanceof WorkspaceError && error.rejectedBeforeAdmission) {
1516
+ state.blocked = null;
1517
+ await this.saveOuter(state);
1518
+ if (error.code !== "conflict") throw error;
1519
+ if (attempt < OUTER_PUBLISH_ATTEMPTS) continue;
1520
+ throw new WorkspaceError("mirror_advanced", "The workspace mirror advanced during publication; retried next cycle", true);
1521
+ }
1522
+ throw error;
1523
+ }
1484
1524
  state.publishedHead = local;
1525
+ state.blocked = null;
1485
1526
  await this.saveOuter(state);
1486
- return { head: local, unchanged: true, ...await integrated(local) };
1487
- }
1488
- const operationId = `ws-${randomUUID()}`, blobId = `ws-${randomUUID()}`;
1489
- const bytes = await repo.bundle(remote && await repo.has(remote) ? remote : null);
1490
- state.blocked = { code: "publication_unknown", operationId, commit: local, message: "Workspace publication is in progress/unknown. Inspect the original storage operation; never retry under a new ID." };
1491
- await this.saveOuter(state);
1492
- try {
1493
- await this.uploadBundle(storage, bytes, blobId, operationId);
1494
- const result = await storage.mutate({
1495
- method: "repository.publish",
1496
- operationId,
1497
- repositoryId,
1498
- branch: OUTER_BRANCH,
1499
- expectedHead: remote,
1500
- commit: local,
1501
- bundleId: blobId
1502
- });
1503
- if (result.head !== local) throw new WorkspaceError("invalid_response", "Workspace publication receipt head mismatch; inspect original operation");
1504
- } catch (error) {
1505
- if (error instanceof WorkspaceError && error.rejectedBeforeAdmission) {
1506
- state.blocked = null;
1507
- await this.saveOuter(state);
1508
- throw error.code === "conflict" ? new WorkspaceError("mirror_advanced", "The workspace mirror advanced during publication; retried next cycle", true) : error;
1509
- }
1510
- throw error;
1527
+ return { head: local, unchanged: false, ...await integrated(local) };
1511
1528
  }
1512
- state.publishedHead = local;
1513
- state.blocked = null;
1514
- await this.saveOuter(state);
1515
- return { head: local, unchanged: false, ...await integrated(local) };
1516
1529
  }
1517
1530
  /** Push a project checkout's own HEAD to its mirror. Last push wins: no merge
1518
1531
  * is attempted, and a rewind or rewrite replaces the mirror as readily as a
@@ -1587,7 +1600,7 @@ class WorkspaceAuthority {
1587
1600
  const mirror = await this.remoteHead(storage, b.config.repositoryId, b.config.branch);
1588
1601
  if (mirror && !await this.hasCommit(b.repo, mirror)) await this.unbundleInto(b, await this.downloadBundle(storage, b.config.repositoryId));
1589
1602
  let visible = mirror;
1590
- while (visible && await this.platformSnapshot(b.repo, visible)) {
1603
+ while (visible && visible !== b.config.baseCommitHash && await this.platformSnapshot(b.repo, visible)) {
1591
1604
  const parent = await this.firstParent(b.repo, visible);
1592
1605
  if (!parent) break;
1593
1606
  visible = parent;
@@ -9,6 +9,7 @@ const OUTER_REF = `refs/heads/${OUTER_BRANCH}`;
9
9
  const OUTER_SNAPSHOT_LIMIT_BYTES = 5 * 1024 * 1024;
10
10
  const OUTER_GIT_TIMEOUT_MS = 10 * 6e4;
11
11
  const OUTER_CONFLICT_CODE = "sync_conflict";
12
+ const OUTER_PUBLISH_ATTEMPTS = 3;
12
13
  const LEGACY_ACCOUNT_README = "# Account workspace\n";
13
14
  const OUTER_EXCLUDES = [
14
15
  "/artifacts/",
@@ -100,6 +101,10 @@ class OuterRepository {
100
101
  async emptyTree() {
101
102
  return GitOid.parse((await this.run(["hash-object", "-t", "tree", "--stdin"], "")).toString("utf8").trim());
102
103
  }
104
+ async parents(commit) {
105
+ const line = (await this.run(["rev-list", "--parents", "-n", "1", GitOid.parse(commit)])).toString("utf8").trim();
106
+ return line.split(/\s+/).slice(1).map((oid) => GitOid.parse(oid));
107
+ }
103
108
  async isAncestor(ancestor, descendant) {
104
109
  const { code } = await this.result(["merge-base", "--is-ancestor", GitOid.parse(ancestor), GitOid.parse(descendant)]);
105
110
  if (code !== 0 && code !== 1) throw new WorkspaceError("git_failed", "Outer repository ancestry is unreadable");
@@ -308,6 +313,7 @@ export {
308
313
  OUTER_CONFLICT_CODE,
309
314
  OUTER_EXCLUDES,
310
315
  OUTER_GIT_TIMEOUT_MS,
316
+ OUTER_PUBLISH_ATTEMPTS,
311
317
  OUTER_REF,
312
318
  OUTER_SNAPSHOT_LIMIT_BYTES,
313
319
  OuterRepository,
@@ -0,0 +1,12 @@
1
+ /** The environment every process the worker spawns for the user runs with.
2
+ *
3
+ * It is the host account's own environment, as the worker itself was started
4
+ * with — HOME, PATH, agent sockets, configs — with only a UTF-8 locale added
5
+ * when the host set none. The worker is a machine boundary, not a sandbox:
6
+ * shells and agents behave exactly like the host user, and isolation is the
7
+ * operator's decision (a dedicated account or machine), never an environment
8
+ * variable. Re-pointing HOME at the checkout was tried on 2026-09-16/17 and
9
+ * abandoned: tools that ignore HOME confused more than it isolated, and the
10
+ * caches that honoured it landed inside checkouts, where the publication guard
11
+ * measured them and two workers conflicted on them. */
12
+ export declare function hostEnvironment(): Record<string, string>;
@@ -11,6 +11,9 @@ export declare const OUTER_SNAPSHOT_LIMIT_BYTES: number;
11
11
  * is sized for one project. */
12
12
  export declare const OUTER_GIT_TIMEOUT_MS: number;
13
13
  export declare const OUTER_CONFLICT_CODE = "sync_conflict";
14
+ /** Publications one cycle attempts while the mirror keeps moving underneath
15
+ * them; each attempt integrates what the mirror gained before trying again. */
16
+ export declare const OUTER_PUBLISH_ATTEMPTS = 3;
14
17
  /** The bootstrap tree a legacy account repository carries. It is never
15
18
  * materialized: the workspace root is the user's, not a README's. */
16
19
  export declare const LEGACY_ACCOUNT_README = "# Account workspace\n";
@@ -63,6 +66,7 @@ export declare class OuterRepository {
63
66
  has(object: string): Promise<boolean>;
64
67
  treeOf(commit: string): Promise<string>;
65
68
  emptyTree(): Promise<string>;
69
+ parents(commit: string): Promise<string[]>;
66
70
  isAncestor(ancestor: string, descendant: string): Promise<boolean>;
67
71
  listTree(tree: string): Promise<OuterTreeEntry[]>;
68
72
  /** Git refuses to walk into a nested repository until the outer index holds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.169",
3
+ "version": "0.0.171",
4
4
  "type": "module",
5
5
  "main": "./dist/mjs/main.mjs",
6
6
  "module": "./dist/mjs/main.mjs",
@@ -21,7 +21,7 @@
21
21
  "r5d-worker": "dist/mjs/main.mjs"
22
22
  },
23
23
  "dependencies": {
24
- "@ricsam/r5d-api": "^0.0.169",
24
+ "@ricsam/r5d-api": "^0.0.171",
25
25
  "node-pty": "1.1.0",
26
26
  "zod": "^4.1.13",
27
27
  "picomatch": "^4.0.3"