@ricsam/r5d-worker 0.0.160 → 0.0.162

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.160",
3
+ "version": "0.0.162",
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.160";
20608
+ if (true) return "0.0.162";
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.160" : "development"}`);
10
+ console.log(`r5d-worker ${true ? "0.0.162" : "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.160" : "development"
18
+ true ? "0.0.162" : "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.160",
3
+ "version": "0.0.162",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,38 @@
1
+ const CONFLICT_PUBLICATION_CODES = /* @__PURE__ */ new Set([
2
+ "conflict",
3
+ "workbench_conflict",
4
+ "workbench_operation_in_progress"
5
+ ]);
6
+ const BLOCKED_PUBLICATION_CODES = /* @__PURE__ */ new Set([
7
+ "secret_or_runtime_path",
8
+ "secret_content",
9
+ "unsafe_file",
10
+ "unsafe_git",
11
+ "unsafe_path",
12
+ "unsafe_tree",
13
+ "too_large"
14
+ ]);
15
+ const PUBLICATION_OBSERVATION_THRESHOLDS = /* @__PURE__ */ new Map([["unsafe_file", 2]]);
16
+ const OPERATION_IN_PROGRESS_CODE = "workbench_operation_in_progress";
17
+ const ABANDONED_OPERATION_OBSERVATIONS = 2;
18
+ const STUCK_OPERATION_OBSERVATIONS = 30;
19
+ function publicationRefusalOutcome(code) {
20
+ if (CONFLICT_PUBLICATION_CODES.has(code)) return "conflict_blocked";
21
+ if (BLOCKED_PUBLICATION_CODES.has(code)) return "large_diff_blocked";
22
+ return "failed";
23
+ }
24
+ function publicationRefusalElects(code, observations, context = {}) {
25
+ if (code === OPERATION_IN_PROGRESS_CODE)
26
+ return context.workbenchHasLiveRun ? observations >= STUCK_OPERATION_OBSERVATIONS : observations >= ABANDONED_OPERATION_OBSERVATIONS;
27
+ return observations >= (PUBLICATION_OBSERVATION_THRESHOLDS.get(code) ?? 1);
28
+ }
29
+ export {
30
+ ABANDONED_OPERATION_OBSERVATIONS,
31
+ BLOCKED_PUBLICATION_CODES,
32
+ CONFLICT_PUBLICATION_CODES,
33
+ OPERATION_IN_PROGRESS_CODE,
34
+ PUBLICATION_OBSERVATION_THRESHOLDS,
35
+ STUCK_OPERATION_OBSERVATIONS,
36
+ publicationRefusalElects,
37
+ publicationRefusalOutcome
38
+ };
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { publicationRefusalElects, publicationRefusalOutcome } from "./publication-refusal.mjs";
2
3
  import { PersonalTcpManager } from "./tcp.mjs";
3
4
  import { createHash, randomBytes, randomUUID } from "node:crypto";
4
5
  import { promises as fs } from "node:fs";
@@ -253,16 +254,6 @@ exec ${quote(executable)} ${quote(cli)} "$@"
253
254
  }
254
255
  }
255
256
  }
256
- const blockedPublicationCodes = /* @__PURE__ */ new Set([
257
- "secret_or_runtime_path",
258
- "secret_content",
259
- "unsafe_file",
260
- "unsafe_git",
261
- "unsafe_path",
262
- "unsafe_tree",
263
- "too_large"
264
- ]);
265
- const conflictPublicationCodes = /* @__PURE__ */ new Set(["conflict", "workbench_conflict"]);
266
257
  const failedPublicationAttempts = /* @__PURE__ */ new Map();
267
258
  let publication = null, closing = false;
268
259
  const synchronize = () => {
@@ -287,13 +278,21 @@ exec ${quote(executable)} ${quote(cli)} "$@"
287
278
  startingHead = status.head;
288
279
  } catch {
289
280
  }
290
- const outcome = conflictPublicationCodes.has(code) ? "conflict_blocked" : blockedPublicationCodes.has(code) ? "large_diff_blocked" : "failed";
281
+ const outcome = publicationRefusalOutcome(code);
291
282
  const fingerprint = canonicalJson({ code, startingHead, outcome });
292
283
  let attempt = failedPublicationAttempts.get(workbenchId);
293
284
  if (!attempt || attempt.fingerprint !== fingerprint) {
294
- attempt = { fingerprint, attemptId: randomUUID() };
285
+ attempt = { fingerprint, attemptId: randomUUID(), observations: 1 };
295
286
  failedPublicationAttempts.set(workbenchId, attempt);
287
+ } else {
288
+ attempt.observations++;
296
289
  }
290
+ let workbenchHasLiveRun = false;
291
+ try {
292
+ workbenchHasLiveRun = await authority.workbenchHasLiveRun({ userId: grant.userId, sessionId });
293
+ } catch {
294
+ }
295
+ if (!publicationRefusalElects(code, attempt.observations, { workbenchHasLiveRun })) continue;
297
296
  try {
298
297
  await options.publicationReport(WorkspacePublicationReport.parse({
299
298
  protocol: 1,
@@ -26,7 +26,8 @@ import {
26
26
  selectedTree,
27
27
  sha256,
28
28
  snapshotTree,
29
- sourceBytes
29
+ sourceBytes,
30
+ WORKBENCH_CONFLICT_CODES
30
31
  } from "./files.mjs";
31
32
  const GITHUB_CREDENTIAL_HELPER = '!f() { if [ "$1" = get ] && [ -n "$R5D_GIT_CREDENTIAL" ]; then git credential-store --file="$R5D_GIT_CREDENTIAL" get; else return 0; fi; }; f';
32
33
  class WorkspaceAuthority {
@@ -417,6 +418,13 @@ class WorkspaceAuthority {
417
418
  async assertAvailable(b, allowBlocked = false) {
418
419
  if (b.state.blocked && !allowBlocked) throw new WorkspaceError(b.state.blocked.code, b.state.blocked.message);
419
420
  }
421
+ /** Whether any run on this physical workbench is still live, across every
422
+ * session sharing it. An in-progress Git operation marker belongs to such a
423
+ * run, so publication defers to it instead of electing an incident over it. */
424
+ async workbenchHasLiveRun(identity) {
425
+ const b = await this.bench(identity);
426
+ return Object.values(b.state.runs).some((run) => !["completed", "cancelled", "rejected_capacity"].includes(run.state));
427
+ }
420
428
  async status(identity) {
421
429
  const b = await this.bench(identity);
422
430
  return this.serial(b, async () => {
@@ -870,7 +878,7 @@ class WorkspaceAuthority {
870
878
  let mode = 420;
871
879
  let retainedBlock = null;
872
880
  const check = async () => {
873
- if (b.state.blocked && !(allowBlockedConflict && (b.state.blocked.code === "conflict" || b.state.blocked.code === "workbench_conflict")))
881
+ if (b.state.blocked && !(allowBlockedConflict && WORKBENCH_CONFLICT_CODES.has(b.state.blocked.code)))
874
882
  throw new WorkspaceError(b.state.blocked.code, b.state.blocked.message);
875
883
  retainedBlock = b.state.blocked;
876
884
  if (!b.state.initialized) throw new WorkspaceError("uninitialized", "Hydrate this workspace before writing files");
@@ -1154,7 +1162,7 @@ class WorkspaceAuthority {
1154
1162
  const b = await this.bench(identity);
1155
1163
  if (b.config.rootProfile === "account") return { head: "", unchanged: true };
1156
1164
  return this.serial(b, async () => {
1157
- const conflictBlocked = b.state.blocked?.code === "conflict" || b.state.blocked?.code === "workbench_conflict";
1165
+ const conflictBlocked = !!b.state.blocked && WORKBENCH_CONFLICT_CODES.has(b.state.blocked.code);
1158
1166
  await this.assertAvailable(b, options.allowBlockedConflict === true && conflictBlocked);
1159
1167
  return this.publishIdle(b, identity, "Destination workspace snapshot", options.allowLargeDiff === true);
1160
1168
  });
@@ -1313,7 +1321,7 @@ class WorkspaceAuthority {
1313
1321
  const b = await this.bench(identity);
1314
1322
  const operation = OperationEnvelope.parse(JSON.parse(canonicalJson(input)));
1315
1323
  return this.serial(b, async () => {
1316
- const conflictBlocked = b.state.blocked?.code === "conflict" || b.state.blocked?.code === "workbench_conflict";
1324
+ const conflictBlocked = !!b.state.blocked && WORKBENCH_CONFLICT_CODES.has(b.state.blocked.code);
1317
1325
  if (!b.state.initialized || b.state.blocked && !(allowBlockedConflict && conflictBlocked))
1318
1326
  throw new WorkspaceError("workbench_blocked", b.state.blocked?.message ?? "Initialize workbench first");
1319
1327
  if (operation.installationId !== this.config.installationId || operation.userId !== identity.userId || operation.sessionId !== identity.sessionId || operation.kind !== "host.shell")
@@ -6,6 +6,20 @@ import { createHash } from "node:crypto";
6
6
  import { safeTreePath, STORAGE_LIMITS } from "./storage-wire.mjs";
7
7
  import { WorkspaceError } from "./contracts.mjs";
8
8
  const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
9
+ const WORKBENCH_OPERATION_MARKERS = [
10
+ "MERGE_HEAD",
11
+ "CHERRY_PICK_HEAD",
12
+ "REVERT_HEAD",
13
+ "REBASE_HEAD",
14
+ "rebase-merge",
15
+ "rebase-apply",
16
+ "index.lock"
17
+ ];
18
+ const WORKBENCH_CONFLICT_CODES = /* @__PURE__ */ new Set([
19
+ "conflict",
20
+ "workbench_conflict",
21
+ "workbench_operation_in_progress"
22
+ ]);
9
23
  async function privateRoot(root, installationId) {
10
24
  if (!path.isAbsolute(root) || path.normalize(root) !== root || path.basename(root) !== installationId || root.split(path.sep).some((p) => [".r5d", "r5d-dev", "legacy", "app-data"].includes(p.toLowerCase())))
11
25
  throw new WorkspaceError("unsafe_root", "Use an independently allocated private new-installation root, never a legacy worktree");
@@ -287,7 +301,7 @@ async function snapshotTree(repo, cwd, canonicalHead, maxBytes = 128 * 1024 * 10
287
301
  if (!gitDirectory.startsWith(worktrees)) throw new WorkspaceError("unsafe_git", "Linked worktree belongs to another repository");
288
302
  await noSymlinkAncestors(gitDirectory);
289
303
  } else if (!metadataStat.isDirectory()) throw new WorkspaceError("unsafe_git", "Invalid worktree metadata");
290
- for (const name of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "REBASE_HEAD", "rebase-merge", "rebase-apply", "index.lock"]) {
304
+ for (const name of WORKBENCH_OPERATION_MARKERS) {
291
305
  if (await fs.lstat(path.join(gitDirectory, name)).then(
292
306
  () => true,
293
307
  (e) => {
@@ -296,7 +310,7 @@ async function snapshotTree(repo, cwd, canonicalHead, maxBytes = 128 * 1024 * 10
296
310
  }
297
311
  ))
298
312
  throw new WorkspaceError(
299
- "workbench_conflict",
313
+ "workbench_operation_in_progress",
300
314
  "Git merge/rebase/index operation remains in progress; preserve and resolve it before publication"
301
315
  );
302
316
  }
@@ -406,6 +420,8 @@ async function snapshotTree(repo, cwd, canonicalHead, maxBytes = 128 * 1024 * 10
406
420
  return (await git(repo, ["write-tree"])).toString().trim();
407
421
  }
408
422
  export {
423
+ WORKBENCH_CONFLICT_CODES,
424
+ WORKBENCH_OPERATION_MARKERS,
409
425
  durableJson,
410
426
  ensureAuthorityGitRepositoryLayout,
411
427
  git,
@@ -0,0 +1,31 @@
1
+ /** Publication refusals, and how long one must persist before it becomes a
2
+ * durable remediation incident.
3
+ *
4
+ * Refusing to publish is cheap and immediate. Electing an incident is not: it
5
+ * interrupts whoever owns the checkout with an automatic remediation session.
6
+ * So a condition an ordinary command produces in passing has to outlive that
7
+ * command before it counts as evidence of a stuck checkout. */
8
+ /** Leaves the checkout holding unpublished conflict state. */
9
+ export declare const CONFLICT_PUBLICATION_CODES: ReadonlySet<string>;
10
+ /** Refused because the source candidate itself is unsafe or oversized. */
11
+ export declare const BLOCKED_PUBLICATION_CODES: ReadonlySet<string>;
12
+ /** Consecutive identical observations required before electing an incident.
13
+ * `unsafe_file`: a package manager or Git command can atomically replace a file
14
+ * between directory enumeration and the bounded single-link read. */
15
+ export declare const PUBLICATION_OBSERVATION_THRESHOLDS: ReadonlyMap<string, number>;
16
+ /** A merge, rebase, revert, cherry-pick or index lock belongs to whatever is
17
+ * running in that checkout. Its duration is the agent's, not ours: resolving a
18
+ * conflicted cherry-pick legitimately takes minutes, so no fixed number of
19
+ * samples is a safe bound. Publication defers while a run is live and elects
20
+ * only once the marker has outlived it. */
21
+ export declare const OPERATION_IN_PROGRESS_CODE = "workbench_operation_in_progress";
22
+ /** Observations required once no run is live — the marker has been abandoned. */
23
+ export declare const ABANDONED_OPERATION_OBSERVATIONS = 2;
24
+ /** Backstop for a run that never reaches a terminal state while holding a
25
+ * marker. At the one-minute publication interval this is roughly half an hour. */
26
+ export declare const STUCK_OPERATION_OBSERVATIONS = 30;
27
+ export declare function publicationRefusalOutcome(code: string): "conflict_blocked" | "large_diff_blocked" | "failed";
28
+ /** True once this refusal has proven durable enough to elect an incident. */
29
+ export declare function publicationRefusalElects(code: string, observations: number, context?: {
30
+ workbenchHasLiveRun?: boolean;
31
+ }): boolean;
@@ -186,6 +186,10 @@ export declare class WorkspaceAuthority {
186
186
  * agents remain ordinary concurrent filesystem writers whose later changes
187
187
  * are observed by a subsequent publication cycle. */
188
188
  private assertAvailable;
189
+ /** Whether any run on this physical workbench is still live, across every
190
+ * session sharing it. An in-progress Git operation marker belongs to such a
191
+ * run, so publication defers to it instead of electing an incident over it. */
192
+ workbenchHasLiveRun(identity: WorkspaceIdentity): Promise<boolean>;
189
193
  status(identity: WorkspaceIdentity): Promise<{
190
194
  files: string[] | null;
191
195
  initialized: boolean;
@@ -1,4 +1,10 @@
1
1
  export declare const sha256: (bytes: Buffer | string) => string;
2
+ /** A Git command is mid-flight. Owned by whoever is running it, and gone as soon
3
+ * as it finishes, so it is never on its own evidence of a stuck checkout. */
4
+ export declare const WORKBENCH_OPERATION_MARKERS: readonly ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "REBASE_HEAD", "rebase-merge", "rebase-apply", "index.lock"];
5
+ /** Publication refusals that leave the checkout holding unpublished conflict
6
+ * state. Remediation is admitted against exactly these. */
7
+ export declare const WORKBENCH_CONFLICT_CODES: ReadonlySet<string>;
2
8
  export declare function privateRoot(root: string, installationId: string): Promise<void>;
3
9
  export declare function noSymlinkAncestors(file: string): Promise<void>;
4
10
  export declare function readRegular(file: string, limit?: number): Promise<Buffer>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.160",
3
+ "version": "0.0.162",
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.160",
24
+ "@ricsam/r5d-api": "^0.0.162",
25
25
  "node-pty": "1.1.0",
26
26
  "zod": "^4.1.13",
27
27
  "picomatch": "^4.0.3"