@ricsam/r5d-worker 0.0.179 → 0.0.181

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.179",
3
+ "version": "0.0.181",
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.179";
20608
+ if (true) return "0.0.181";
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.179" : "development"}`);
10
+ console.log(`r5d-worker ${true ? "0.0.181" : "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>] [--initial-sync merge|reset]\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.179" : "development"
18
+ true ? "0.0.181" : "development"
19
19
  );
20
20
  console.log(`Worker connected: ${runtime.resourceId}`);
21
21
  let closing = false;
@@ -24,7 +24,12 @@ if (args.includes("--version")) {
24
24
  closing = true;
25
25
  void runtime.close().then(
26
26
  () => process.exit(0),
27
- () => process.exit(1)
27
+ (error) => {
28
+ const code = error?.code;
29
+ process.stderr.write(`[r5d-worker] stop did not complete cleanly: ${typeof code === "string" ? code : error instanceof Error ? error.message : String(error)}
30
+ `);
31
+ process.exit(1);
32
+ }
28
33
  );
29
34
  };
30
35
  process.once("SIGTERM", close);
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.179",
3
+ "version": "0.0.181",
4
4
  "type": "module"
5
5
  }
@@ -254,11 +254,17 @@ async function startPersonalWorker(options, version) {
254
254
  root,
255
255
  async close() {
256
256
  stopped = true;
257
- await terminalStream.close();
258
- await loop;
259
- await scheduler.drain();
260
- await runtime.close();
261
- ledger.close();
257
+ try {
258
+ await terminalStream.close();
259
+ await loop;
260
+ await scheduler.drain();
261
+ } finally {
262
+ try {
263
+ await runtime.close();
264
+ } finally {
265
+ ledger.close();
266
+ }
267
+ }
262
268
  }
263
269
  };
264
270
  }
@@ -509,10 +509,18 @@ exec ${quote(executable)} ${quote(cli)} "$@"
509
509
  async close() {
510
510
  closing = true;
511
511
  clearInterval(publicationTimer);
512
- await publication;
513
- await tcp.close();
514
- await authority.close();
515
- await daemon.close();
512
+ try {
513
+ await publication;
514
+ } catch (error) {
515
+ process.stderr.write(`[r5d-worker] final publication cycle failed: ${error instanceof WorkspaceError ? error.code : "publication_failed"}
516
+ `);
517
+ }
518
+ try {
519
+ await tcp.close();
520
+ await authority.close();
521
+ } finally {
522
+ await daemon.close();
523
+ }
516
524
  }
517
525
  };
518
526
  } catch (error) {
@@ -327,7 +327,10 @@ class HostExecutor {
327
327
  if (pty) {
328
328
  pty.onData((data) => this.output(run2, "stdout", Buffer.from(data)));
329
329
  pty.onExit(({ exitCode, signal }) => {
330
- if (exitCode === -1) receipt2.reason = "pty_helper_lost";
330
+ if (exitCode === -1) {
331
+ receipt2.reason = "pty_helper_lost";
332
+ run2.cancelled = true;
333
+ }
331
334
  onExit(exitCode, signal ? String(signal) : null);
332
335
  });
333
336
  } else {
@@ -467,7 +470,7 @@ class HostExecutor {
467
470
  if (run.finishing) return run.finishing;
468
471
  run.finishing = (async () => {
469
472
  clearTimeout(run.timer);
470
- let cleanupFailed = run.receipt.reason === "pty_helper_lost";
473
+ let cleanupFailed = false;
471
474
  try {
472
475
  if (run.process.pid) await terminateProcessTree(run.process, { graceMs: 200, forceKillWaitMs: 1e3 });
473
476
  await Promise.race([
@@ -10,6 +10,10 @@ function preflightNativePty() {
10
10
  }
11
11
  class PtySession {
12
12
  pid = 0;
13
+ /** The helper process that owns the terminal, for tests that end it the way a stop signal would. */
14
+ get helperPid() {
15
+ return this.child.pid ?? 0;
16
+ }
13
17
  sequence = 0;
14
18
  pending = /* @__PURE__ */ new Map();
15
19
  child;
@@ -2031,6 +2031,7 @@ class WorkspaceAuthority {
2031
2031
  await Promise.allSettled([...this.actions]);
2032
2032
  await Promise.allSettled([...this.queues.values()]);
2033
2033
  for (const b of this.benches.values()) await this.refresh(b);
2034
+ await this.drainLiveRuns();
2034
2035
  if ([...this.benches.values()].some(
2035
2036
  (b) => b.state.blocked || Object.values(b.state.runs).some((r) => !["completed", "cancelled", "rejected_capacity"].includes(r.state))
2036
2037
  ))
@@ -2043,6 +2044,32 @@ class WorkspaceAuthority {
2043
2044
  await fs.rm(path.join(this.config.root, ".sync-lock"), { recursive: true });
2044
2045
  this.closed = true;
2045
2046
  }
2047
+ /** A stop ends what the keeper tracks. Live runs (an open shell, an agent
2048
+ * command) are cancelled through the executor and waited for, the way a
2049
+ * supervisor drain would, so that an orderly stop leaves cancelled receipts
2050
+ * behind rather than a lock nobody can release: the process is going away
2051
+ * either way. Outcomes that stay unknown after the drain still retain the
2052
+ * lock for review. */
2053
+ async drainLiveRuns() {
2054
+ const live = (b) => Object.entries(b.state.runs).filter(([, run]) => !["completed", "cancelled", "rejected_capacity", "unknown"].includes(run.state));
2055
+ if (![...this.benches.values()].some((b) => live(b).length)) return;
2056
+ for (const b of this.benches.values()) {
2057
+ for (const [operationId, run] of live(b)) {
2058
+ try {
2059
+ const identity = { userId: b.config.userId, sessionId: run.sessionId ?? b.config.sessionId };
2060
+ const route = await this.route(b, identity);
2061
+ await route.client.cancel({ ...identity, operationId, fence: route.workerFence }, `drain-${this.lockId.slice(0, 8)}-${sha256(operationId).slice(0, 16)}`);
2062
+ } catch {
2063
+ }
2064
+ }
2065
+ }
2066
+ const deadline = Date.now() + 15e3;
2067
+ for (; ; ) {
2068
+ for (const b of this.benches.values()) await this.refresh(b);
2069
+ if (![...this.benches.values()].some((b) => live(b).length) || Date.now() >= deadline) return;
2070
+ await new Promise((resolve) => setTimeout(resolve, 200));
2071
+ }
2072
+ }
2046
2073
  }
2047
2074
  export {
2048
2075
  WorkspaceAuthority
@@ -2,6 +2,8 @@ export declare function preflightNativePty(): void;
2
2
  /** Node native FD keeper is a daemon child; replacing adapters cannot signal it. */
3
3
  export declare class PtySession {
4
4
  pid: number;
5
+ /** The helper process that owns the terminal, for tests that end it the way a stop signal would. */
6
+ get helperPid(): number;
5
7
  private sequence;
6
8
  private readonly pending;
7
9
  private readonly child;
@@ -500,4 +500,11 @@ export declare class WorkspaceAuthority {
500
500
  cancel(identity: WorkspaceIdentity, run: RunIdentity, actionId: string): Promise<import("..").ActionReceipt>;
501
501
  close(): Promise<void>;
502
502
  private finishClose;
503
+ /** A stop ends what the keeper tracks. Live runs (an open shell, an agent
504
+ * command) are cancelled through the executor and waited for, the way a
505
+ * supervisor drain would, so that an orderly stop leaves cancelled receipts
506
+ * behind rather than a lock nobody can release: the process is going away
507
+ * either way. Outcomes that stay unknown after the drain still retain the
508
+ * lock for review. */
509
+ private drainLiveRuns;
503
510
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.179",
3
+ "version": "0.0.181",
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.179",
24
+ "@ricsam/r5d-api": "^0.0.181",
25
25
  "node-pty": "1.1.0",
26
26
  "zod": "^4.1.13",
27
27
  "picomatch": "^4.0.3"