@ricsam/r5d-worker 0.0.164 → 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.164",
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.164";
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.164" : "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.164" : "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.164",
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,
@@ -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 () => {
@@ -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;
@@ -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.164",
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.164",
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"