@ricsam/r5d-worker 0.0.163 → 0.0.165

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.163",
3
+ "version": "0.0.165",
4
4
  "type": "commonjs"
5
5
  }
@@ -20605,7 +20605,7 @@ function resolveEntrypointPath(entrypoint) {
20605
20605
  }
20606
20606
  }
20607
20607
  function getR5dctlVersion() {
20608
- if (true) return "0.0.163";
20608
+ if (true) return "0.0.165";
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) {
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.163" : "development"}`);
10
+ console.log(`r5d-worker ${true ? "0.0.165" : "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.163" : "development"
18
+ true ? "0.0.165" : "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.163",
3
+ "version": "0.0.165",
4
4
  "type": "module"
5
5
  }
@@ -103,7 +103,7 @@ async function startPersonalWorker(options, version) {
103
103
  platform: process.platform,
104
104
  arch: process.arch,
105
105
  hostname: os.hostname(),
106
- capabilities: { updateClis: true, durableWorkspace: true, outerWorkspace: true }
106
+ capabilities: { updateClis: true, durableWorkspace: true, outerWorkspace: true, fileWalk: true }
107
107
  });
108
108
  const grant = PersonalWorkerGrant.parse(
109
109
  await request("/api/personal/resources/register", { kind: "worker", ...identity, label: options.label, metadata: metadata() })
@@ -127,7 +127,9 @@ async function startPersonalWorker(options, version) {
127
127
  }
128
128
  },
129
129
  publicationReport: async (report) => {
130
- await request(`${endpoint}/publication`, { instanceId: identity.instanceId, report });
130
+ const response = await request(`${endpoint}/publication`, { instanceId: identity.instanceId, report });
131
+ const result = response?.result;
132
+ return result && typeof result === "object" && typeof result.incidentId === "string" ? { incidentId: result.incidentId } : void 0;
131
133
  }
132
134
  });
133
135
  const ledgerFile = path.join(root, "relay.sqlite");
@@ -261,10 +261,12 @@ exec ${quote(executable)} ${quote(cli)} "$@"
261
261
  }
262
262
  const failedPublicationAttempts = /* @__PURE__ */ new Map();
263
263
  let publication = null, closing = false;
264
+ let pausedForIncident = null;
264
265
  const synchronize = () => {
265
266
  if (publication) return publication;
266
267
  const next = (async () => {
267
268
  const results = [];
269
+ if (pausedForIncident) return [{ workbenchId: "workspace", error: "remediation_paused" }];
268
270
  try {
269
271
  const outer = await authority.synchronizeOuter(grant.userId);
270
272
  failedPublicationAttempts.delete("workspace");
@@ -290,7 +292,7 @@ exec ${quote(executable)} ${quote(cli)} "$@"
290
292
  }
291
293
  if (publicationRefusalElects(code, attempt.observations)) {
292
294
  try {
293
- await options.publicationReport(WorkspacePublicationReport.parse({
295
+ const elected = await options.publicationReport(WorkspacePublicationReport.parse({
294
296
  protocol: 1,
295
297
  attemptId: attempt.attemptId,
296
298
  scope: "workspace",
@@ -300,6 +302,11 @@ exec ${quote(executable)} ${quote(cli)} "$@"
300
302
  error: code,
301
303
  ...paths.length ? { paths } : {}
302
304
  }));
305
+ if (elected?.incidentId && pausedForIncident !== elected.incidentId) {
306
+ pausedForIncident = elected.incidentId;
307
+ process.stderr.write(`[r5d-worker] workspace synchronization paused for remediation ${elected.incidentId}
308
+ `);
309
+ }
303
310
  } catch (reportError) {
304
311
  process.stderr.write(`[r5d-worker] publication report deferred: ${reportError instanceof WorkspaceError ? reportError.code : "publication_report_failed"}
305
312
  `);
@@ -322,7 +329,7 @@ exec ${quote(executable)} ${quote(cli)} "$@"
322
329
  publication = next;
323
330
  void next.then(
324
331
  (results) => {
325
- const deferred = results.filter((result) => result.error && !TRANSIENT_PUBLICATION_CODES.has(result.error));
332
+ const deferred = results.filter((result) => result.error && result.error !== "remediation_paused" && !TRANSIENT_PUBLICATION_CODES.has(result.error));
326
333
  if (deferred.length)
327
334
  process.stderr.write(`[r5d-worker] periodic workspace publication deferred: ${deferred.map((result) => `${result.workbenchId}:${result.error}`).join(", ")}
328
335
  `);
@@ -363,6 +370,7 @@ exec ${quote(executable)} ${quote(cli)} "$@"
363
370
  if (command.method === "git") return authority.inspectGit(identity, { ...command, method: command.action });
364
371
  if (command.method === "fileHistoryDates") return authority.fileHistoryDates(identity, command.path);
365
372
  if (command.method === "directory" || command.method === "raw") return authority.inspectFiles(identity, command);
373
+ if (command.method === "walk") return authority.walkFiles(identity, command);
366
374
  }
367
375
  if (request.kind === "terminal") {
368
376
  await authority.ensureHydrated(identity);
@@ -412,10 +420,18 @@ exec ${quote(executable)} ${quote(cli)} "$@"
412
420
  if (command.method === "commit") return authority.commit(identity, command);
413
421
  if (command.method === "push") return authority.push(identity, command);
414
422
  if (command.method === "reset") return authority.reset(identity, command);
415
- if (command.method === "publish") return authority.publish(identity, {
416
- allowLargeDiff: request.remediation === true && command.allowLargeDiff === true,
417
- allowBlockedConflict: request.remediation === true
418
- });
423
+ if (command.method === "publish") {
424
+ const published = await authority.publish(identity, {
425
+ allowLargeDiff: request.remediation === true && command.allowLargeDiff === true,
426
+ allowBlockedConflict: request.remediation === true
427
+ });
428
+ if (request.remediation === true && pausedForIncident) {
429
+ process.stderr.write(`[r5d-worker] workspace synchronization resumed after remediation ${pausedForIncident}
430
+ `);
431
+ pausedForIncident = null;
432
+ }
433
+ return published;
434
+ }
419
435
  if (command.method === "reconcilePublication") return authority.reconcilePublication(identity);
420
436
  }
421
437
  if (request.kind === "agent") {
@@ -14,6 +14,7 @@ import {
14
14
  import { OuterRepository, OuterSnapshotRefusal, OUTER_BRANCH, OUTER_CONFLICT_CODE } from "./outer.mjs";
15
15
  import { SessionArtifactStore, SessionArtifactChunk, RESERVED_ARTIFACT_ENV } from "./artifacts.mjs";
16
16
  import { WorkspaceFileWrite, WorkspaceFileWriteLookup } from "./file-write.mjs";
17
+ import { walkDirectoryTree } from "./walk.mjs";
17
18
  import { WorkspaceStorageClient } from "./storage-client.mjs";
18
19
  import {
19
20
  durableJson,
@@ -22,7 +23,7 @@ import {
22
23
  gitResult,
23
24
  materializeTree,
24
25
  noSymlinkAncestors,
25
- PLATFORM_COMMIT_EMAIL,
26
+ PLATFORM_COMMIT_EMAILS,
26
27
  privateRoot,
27
28
  PROJECT_COMMIT_IDENTITY,
28
29
  readHostRegular,
@@ -317,6 +318,26 @@ class WorkspaceAuthority {
317
318
  }));
318
319
  return { directory: display, entries };
319
320
  }
321
+ /** Walks a directory tree locally in one operation. Paths are reported
322
+ * relative to the walked directory; the shared ignore list and bounds live
323
+ * in `./walk` so the server's fallback agrees with them. */
324
+ async walkFiles(identity, command) {
325
+ const b = await this.bench(identity);
326
+ const relative = command.hostPath ? "" : command.path.replace(/^\/+/, "").replace(/\/+$/, "");
327
+ if (!command.hostPath && relative) safeTreePath(relative);
328
+ const target = command.hostPath ? this.resolveHostPath(command.path) : path.join(b.config.cwd, relative);
329
+ const display = command.hostPath ? target : `/${relative}`;
330
+ const inspect = command.hostPath ? fs.lstat(target).then((stat) => {
331
+ if (stat.isSymbolicLink()) throw new WorkspaceError("unsafe_path", "Final symlink paths are not inspected");
332
+ }) : noSymlinkAncestors(target);
333
+ await inspect.catch((error) => {
334
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") throw new WorkspaceError("not_found", "File or directory not found");
335
+ throw error;
336
+ });
337
+ if (!(await fs.stat(target)).isDirectory()) throw new WorkspaceError("not_found", "Directory not found");
338
+ const walked = await walkDirectoryTree(target);
339
+ return { directory: display, ...walked };
340
+ }
320
341
  async inspectGit(identity, command) {
321
342
  const b = await this.bench(identity);
322
343
  return this.serial(b, async () => {
@@ -1251,7 +1272,8 @@ class WorkspaceAuthority {
1251
1272
  * GitHub tip. Hydration peels them so a checkout's history is its own. */
1252
1273
  async platformSnapshot(repo, commit) {
1253
1274
  const header = (await git(repo, ["cat-file", "commit", commit])).toString("utf8").split("\n\n")[0] ?? "";
1254
- return /^committer .*<workspace@invalid>/m.test(header.replace(PLATFORM_COMMIT_EMAIL, "workspace@invalid"));
1275
+ const committer = /^committer .*<([^>]+)>/m.exec(header)?.[1];
1276
+ return committer !== void 0 && PLATFORM_COMMIT_EMAILS.includes(committer);
1255
1277
  }
1256
1278
  async firstParent(repo, commit) {
1257
1279
  const parents = (await git(repo, ["rev-list", "--parents", "-n", "1", commit])).toString("utf8").trim().split(/\s+/).slice(1);
@@ -1347,11 +1369,18 @@ class WorkspaceAuthority {
1347
1369
  const storage = await this.options.accountStorage(userId);
1348
1370
  const repositoryId = this.accountRepositoryId(userId);
1349
1371
  if (state.blocked?.code === "publication_unknown") {
1350
- const receipt = await storage.read({ method: "operation.get", lookupId: state.blocked.operationId });
1351
- if (receipt.state !== "completed" || receipt.result?.head !== state.blocked.commit)
1352
- throw new WorkspaceError("publication_unknown", "Original workspace publication not proven completed; preserve state for operator review");
1353
- state.publishedHead = state.blocked.commit;
1354
- state.head = state.blocked.commit;
1372
+ const { operationId: operationId2, commit } = state.blocked;
1373
+ const receipt = await storage.read({ method: "operation.get", lookupId: operationId2 }).catch((error) => {
1374
+ if (error instanceof WorkspaceError && error.code === "not_found") return null;
1375
+ throw error;
1376
+ });
1377
+ if (receipt?.state === "completed" && receipt.result?.head !== commit)
1378
+ throw new WorkspaceError("invalid_state", "Workspace publication receipt names another head; maintenance required");
1379
+ const published = receipt?.state === "completed" || await this.remoteHead(storage, repositoryId, OUTER_BRANCH) === commit;
1380
+ if (published) {
1381
+ state.publishedHead = commit;
1382
+ state.head = commit;
1383
+ }
1355
1384
  state.blocked = null;
1356
1385
  await this.saveOuter(state);
1357
1386
  }
@@ -1466,7 +1495,7 @@ class WorkspaceAuthority {
1466
1495
  if (error instanceof WorkspaceError && error.rejectedBeforeAdmission) {
1467
1496
  state.blocked = null;
1468
1497
  await this.saveOuter(state);
1469
- throw new WorkspaceError("mirror_advanced", "The workspace mirror advanced during publication; retried next cycle", true);
1498
+ throw error.code === "conflict" ? new WorkspaceError("mirror_advanced", "The workspace mirror advanced during publication; retried next cycle", true) : error;
1470
1499
  }
1471
1500
  throw error;
1472
1501
  }
@@ -100,7 +100,8 @@ async function durableJson(file, value) {
100
100
  await dir.close();
101
101
  }
102
102
  }
103
- const PLATFORM_COMMIT_EMAIL = "workspace@invalid";
103
+ const PLATFORM_COMMIT_EMAILS = ["workspace@invalid", "migration@r5d.dev"];
104
+ const PLATFORM_COMMIT_EMAIL = PLATFORM_COMMIT_EMAILS[0];
104
105
  const PROJECT_COMMIT_IDENTITY = {
105
106
  GIT_AUTHOR_NAME: "r5d",
106
107
  GIT_AUTHOR_EMAIL: "workspace@r5d.dev",
@@ -439,6 +440,7 @@ async function snapshotTree(repo, cwd, canonicalHead, maxBytes = 128 * 1024 * 10
439
440
  }
440
441
  export {
441
442
  PLATFORM_COMMIT_EMAIL,
443
+ PLATFORM_COMMIT_EMAILS,
442
444
  PROJECT_COMMIT_IDENTITY,
443
445
  WORKBENCH_CONFLICT_CODES,
444
446
  WORKBENCH_OPERATION_MARKERS,
@@ -0,0 +1,94 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ const WORKSPACE_WALK_IGNORED = /* @__PURE__ */ new Set([
5
+ ".git",
6
+ "node_modules",
7
+ ".next",
8
+ "dist",
9
+ "build",
10
+ "coverage",
11
+ ".cache",
12
+ ".r5d",
13
+ ".r5d-next",
14
+ ".venv",
15
+ "venv",
16
+ "__pycache__",
17
+ ".npm",
18
+ ".pnpm-store",
19
+ ".yarn",
20
+ ".bun",
21
+ ".turbo",
22
+ ".parcel-cache",
23
+ ".mypy_cache",
24
+ ".pytest_cache",
25
+ ".ruff_cache",
26
+ ".gradle",
27
+ ".tox",
28
+ ".svelte-kit",
29
+ ".nuxt",
30
+ ".terraform"
31
+ ]);
32
+ const WORKSPACE_WALK_LIMITS = Object.freeze({
33
+ /** Entries reported before the result is marked truncated. */
34
+ entries: 2e3,
35
+ /** Nesting depth below the walked directory. */
36
+ depth: 32,
37
+ /** Names read from any single directory; a larger directory truncates. */
38
+ directoryEntries: 2e5
39
+ });
40
+ const WorkspaceWalk = z.object({
41
+ method: z.literal("walk"),
42
+ path: z.string().max(4096),
43
+ hostPath: z.boolean().optional()
44
+ }).strict();
45
+ async function walkDirectoryTree(root, options = {}) {
46
+ const ignored = options.ignored ?? WORKSPACE_WALK_IGNORED, limits = options.limits ?? WORKSPACE_WALK_LIMITS;
47
+ const entries = [], queue = [{ relative: "", depth: 0 }];
48
+ let truncated = false;
49
+ while (queue.length) {
50
+ const next = queue.shift();
51
+ const directory = next.relative ? path.join(root, next.relative) : root;
52
+ let names;
53
+ try {
54
+ names = await fs.readdir(directory, { withFileTypes: true });
55
+ } catch (error) {
56
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") continue;
57
+ throw error;
58
+ }
59
+ if (names.length > limits.directoryEntries) {
60
+ truncated = true;
61
+ continue;
62
+ }
63
+ names.sort((a, b) => a.name.localeCompare(b.name));
64
+ for (const entry of names) {
65
+ if (ignored.has(entry.name) || entry.isSymbolicLink() || !(entry.isFile() || entry.isDirectory())) continue;
66
+ if (entries.length >= limits.entries) {
67
+ truncated = true;
68
+ break;
69
+ }
70
+ const relative = next.relative ? path.posix.join(next.relative, entry.name) : entry.name;
71
+ let size = null;
72
+ if (entry.isFile()) {
73
+ try {
74
+ size = (await fs.stat(path.join(directory, entry.name))).size;
75
+ } catch {
76
+ continue;
77
+ }
78
+ }
79
+ entries.push({ path: relative, type: entry.isDirectory() ? "directory" : "file", size });
80
+ if (entry.isDirectory()) {
81
+ if (next.depth < limits.depth) queue.push({ relative, depth: next.depth + 1 });
82
+ else truncated = true;
83
+ }
84
+ }
85
+ if (entries.length >= limits.entries) break;
86
+ }
87
+ return { entries: entries.sort((a, b) => a.path.localeCompare(b.path)), truncated: truncated || queue.length > 0 };
88
+ }
89
+ export {
90
+ WORKSPACE_WALK_IGNORED,
91
+ WORKSPACE_WALK_LIMITS,
92
+ WorkspaceWalk,
93
+ walkDirectoryTree
94
+ };
@@ -111,7 +111,9 @@ export declare function openPersonalWorkerRuntime(options: {
111
111
  publicationIntervalMs?: number;
112
112
  /** Authenticated transport for autonomous publication evidence. Failures are
113
113
  * retried with the same attempt identity on the next publication cycle. */
114
- publicationReport?: (report: z.infer<typeof WorkspacePublicationReport>) => Promise<void>;
114
+ publicationReport?: (report: z.infer<typeof WorkspacePublicationReport>) => Promise<{
115
+ incidentId?: string;
116
+ } | void>;
115
117
  }): Promise<{
116
118
  grant: {
117
119
  [x: string]: unknown;
@@ -3,6 +3,7 @@ import { type PollResult, type RunIdentity } from "../protocol";
3
3
  import { WorkspaceConfig, type ExecutorRoute, type WorkspaceIdentity, type OuterState, type ApprovedWorkbench as ApprovedWorkbenchInput } from "./contracts";
4
4
  import { SessionArtifactChunk } from "./artifacts";
5
5
  import { WorkspaceFileWrite } from "./file-write";
6
+ import { type WorkspaceWalk, type WorkspaceWalkResult } from "./walk";
6
7
  import { WorkspaceStorageClient } from "./storage-client";
7
8
  export interface WorkspaceAuthorityOptions {
8
9
  config: WorkspaceConfig;
@@ -107,6 +108,10 @@ export declare class WorkspaceAuthority {
107
108
  size?: undefined;
108
109
  modifiedAt?: undefined;
109
110
  }>;
111
+ /** Walks a directory tree locally in one operation. Paths are reported
112
+ * relative to the walked directory; the shared ignore list and bounds live
113
+ * in `./walk` so the server's fallback agrees with them. */
114
+ walkFiles(identity: WorkspaceIdentity, command: WorkspaceWalk): Promise<WorkspaceWalkResult>;
110
115
  inspectGit(identity: WorkspaceIdentity, command: {
111
116
  method: "status" | "diff" | "history" | "fileDiff";
112
117
  path?: string;
@@ -26,8 +26,11 @@ export type GitOptions = {
26
26
  * the platform's: the platform identity marks snapshots that hydration peels. */
27
27
  env?: Record<string, string>;
28
28
  };
29
- /** Committer of platform-authored snapshot commits, recognised by hydration. */
30
- export declare const PLATFORM_COMMIT_EMAIL = "workspace@invalid";
29
+ /** Committers of platform-authored history that hydration peels so a checkout's
30
+ * history is its own: per-project snapshots, and the one-time legacy import
31
+ * (`scripts/migration/import-project-files.ts`) that seeded canonical storage. */
32
+ export declare const PLATFORM_COMMIT_EMAILS: readonly string[];
33
+ export declare const PLATFORM_COMMIT_EMAIL: string;
31
34
  /** Identity of commits made in a project's own history on the user's behalf. */
32
35
  export declare const PROJECT_COMMIT_IDENTITY: {
33
36
  readonly GIT_AUTHOR_NAME: "r5d";
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ /** Directory names never descended by find/grep: dependency trees, build
3
+ * output and package-manager caches. The worker walks a checkout locally, so
4
+ * this list decides what a search sees; it is shared with the server so its
5
+ * per-directory fallback (older workers, virtual directories) agrees.
6
+ *
7
+ * Package-manager caches are here deliberately: `npm install` writes a deep
8
+ * hash-sharded `.npm/_cacache` into the checkout whenever its cache resolves
9
+ * there, and before this walk moved to the worker a single find over it cost
10
+ * one network round-trip per directory — enough to exhaust the agent's tool
11
+ * timeout and strand a workspace-remediation run. */
12
+ export declare const WORKSPACE_WALK_IGNORED: ReadonlySet<string>;
13
+ /** Bounds a walk regardless of what a checkout contains. */
14
+ export declare const WORKSPACE_WALK_LIMITS: Readonly<{
15
+ /** Entries reported before the result is marked truncated. */
16
+ entries: 2000;
17
+ /** Nesting depth below the walked directory. */
18
+ depth: 32;
19
+ /** Names read from any single directory; a larger directory truncates. */
20
+ directoryEntries: 200000;
21
+ }>;
22
+ /** Inspect command: walk a directory tree in one worker operation. */
23
+ export declare const WorkspaceWalk: z.ZodObject<{
24
+ method: z.ZodLiteral<"walk">;
25
+ path: z.ZodString;
26
+ hostPath: z.ZodOptional<z.ZodBoolean>;
27
+ }, z.core.$strict>;
28
+ export type WorkspaceWalk = z.infer<typeof WorkspaceWalk>;
29
+ export type WorkspaceWalkEntry = {
30
+ path: string;
31
+ type: "file" | "directory";
32
+ size: number | null;
33
+ };
34
+ export type WorkspaceWalkResult = {
35
+ directory: string;
36
+ entries: WorkspaceWalkEntry[];
37
+ truncated: boolean;
38
+ };
39
+ /** Breadth-first walk of `root`, reporting paths relative to it (POSIX). Names
40
+ * in `ignored` are neither reported nor descended; symlinks and anything that
41
+ * is not a regular file or directory are skipped. The result is sorted by path
42
+ * and bounded by `limits`. */
43
+ export declare function walkDirectoryTree(root: string, options?: {
44
+ ignored?: ReadonlySet<string>;
45
+ limits?: typeof WORKSPACE_WALK_LIMITS;
46
+ }): Promise<{
47
+ entries: WorkspaceWalkEntry[];
48
+ truncated: boolean;
49
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.163",
3
+ "version": "0.0.165",
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.163",
24
+ "@ricsam/r5d-api": "^0.0.165",
25
25
  "node-pty": "1.1.0",
26
26
  "zod": "^4.1.13",
27
27
  "picomatch": "^4.0.3"