@ricsam/r5d-worker 0.0.124 → 0.0.125

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.
package/README.md CHANGED
@@ -89,7 +89,7 @@ The launcher owns admission, OS isolation, budgets, and diagnostic evidence. Its
89
89
 
90
90
  Requests must be processed concurrently so cancellation and cleanup cannot wait behind queued admission. Controller RPCs are bounded (10 seconds, or 90 seconds for acquisition); external policy must reject excessive queue waits within that bound. Controller loss or protocol failure stops command admission, preserves uncertain outcomes for recovery, reaps ordinary children, and restarts the runtime through its supervisor. It never falls back to direct spawning. Shutdown closes controller stdin; the controller must reap its jobs before exiting.
91
91
 
92
- Cancellation is checked again immediately before spawn. Transient disconnects preserve commands and terminals; expiry of the five-minute execution lease cancels agent work and closes terminals. File operations, communication recovery, and cancellation do not enter launcher admission. Internal worker maintenance subprocesses are also outside this hook. The worker reports its own view of every lane (holders and queue, by run id) to the server with `capacity_report` whenever an admission changes; the platform exposes it through `r5dctl ps list` and `r5dctl workspace status`. Deployment-specific limits and service protection are documented in [Worker deployment resources](../../docs/worker-deployment-resources.md).
92
+ Cancellation is checked again immediately before spawn. A `cancel` carrying `scope: "unstarted"` is the server giving up on a start whose budget expired: it revokes only a launch that has not spawned (capacity queue, mount hold, preparation) and leaves a running process alone; `cancel_result.outcome` reports `unstarted`, `started`, `stopped`, or `unknown` (no record of the run; the cancellation is remembered so a later arrival is refused). Transient disconnects preserve commands and terminals; expiry of the five-minute execution lease cancels agent work and closes terminals. File operations, communication recovery, and cancellation do not enter launcher admission. Internal worker maintenance subprocesses are also outside this hook. The worker reports its own view of every lane (holders and queue, by run id) to the server with `capacity_report` whenever an admission changes; the platform exposes it through `r5dctl ps list` and `r5dctl workspace status`. Deployment-specific limits and service protection are documented in [Worker deployment resources](../../docs/worker-deployment-resources.md).
93
93
 
94
94
  When the connected `r5d-browser` requests a port forward, the worker opens each relayed connection only to `127.0.0.1` on the requested worker port. Browser-side and worker-side ports may differ. The worker never opens a public listener, and a disconnected worker leaves the browser's long-lived mapping unavailable until the same worker label reconnects.
95
95
 
@@ -245,7 +245,9 @@ class WorkerCommandLauncher {
245
245
  if (!argv.length) throw new Error("Command argv must not be empty");
246
246
  return [...prefix, ...argv];
247
247
  },
248
- cancel: () => this.cancel(request.id),
248
+ cancel: async () => {
249
+ await this.cancel(request.id);
250
+ },
249
251
  diagnose: async () => {
250
252
  this.assertHealthy();
251
253
  if (!this.child) return void 0;
@@ -261,9 +263,10 @@ class WorkerCommandLauncher {
261
263
  release: () => this.releaseRecord(record)
262
264
  };
263
265
  }
266
+ /** Revoke a launch. Returns whether this launcher still held it (queued or acquired, not yet released). */
264
267
  async cancel(id) {
265
268
  const record = this.launches.get(id);
266
- if (!record || record.released) return;
269
+ if (!record || record.released) return false;
267
270
  record.revoked = true;
268
271
  record.rejectAdmission(new CommandLaunchCancelledError());
269
272
  if (this.child) {
@@ -273,6 +276,7 @@ class WorkerCommandLauncher {
273
276
  throw this.fail(error instanceof Error ? error : new Error(String(error)));
274
277
  }
275
278
  }
279
+ return true;
276
280
  }
277
281
  async cancelSession(sessionId) {
278
282
  await this.cancelMatching((_id, ownerSessionId) => ownerSessionId === sessionId);
package/dist/cjs/main.cjs CHANGED
@@ -325,9 +325,10 @@ function unregisterPendingReservation(key, controller) {
325
325
  }
326
326
  function abortPendingReservation(key, reason) {
327
327
  const pending = pendingReservationAborts.get(key);
328
- if (!pending) return;
328
+ if (!pending) return false;
329
329
  pendingReservationAborts.delete(key);
330
330
  pending.controller.abort(new import_workspace_mount_hold_fence.WorkspaceMountHoldAbortedError(reason));
331
+ return true;
331
332
  }
332
333
  function abortPendingReservations(reason, sessionId) {
333
334
  for (const [key, pending] of [...pendingReservationAborts]) {
@@ -3191,32 +3192,38 @@ async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
3191
3192
  );
3192
3193
  }
3193
3194
  }
3194
- async function cancelProcessRun(runId) {
3195
+ async function cancelProcessRun(runId, scope) {
3196
+ const spawned = activeProcesses.get(runId);
3197
+ if (scope === "unstarted" && spawned) return { cancelled: false, message: `${spawned.command} already started`, outcome: "started" };
3198
+ if (!spawned) cancelledProcessRuns.add(runId);
3199
+ const revocation = workerCommandLauncher?.cancel(runId);
3195
3200
  let resourceCancellationError;
3201
+ let launchRevoked = false;
3196
3202
  try {
3197
- await workerCommandLauncher?.cancel(runId);
3203
+ launchRevoked = await revocation ?? false;
3198
3204
  } catch (error) {
3199
3205
  resourceCancellationError = error instanceof Error ? error.message : String(error);
3200
3206
  }
3201
- const active = activeProcesses.get(runId);
3202
3207
  let cancelled = resourceCancellationError === void 0;
3203
3208
  let cancelMessage;
3204
- if (active) {
3209
+ let outcome;
3210
+ if (spawned) {
3211
+ outcome = "stopped";
3205
3212
  try {
3206
- closeProcessStdin(active);
3207
- await (0, import_process_tree.terminateProcessTree)(active.process);
3208
- cancelMessage = `Stopped ${active.command}`;
3213
+ closeProcessStdin(spawned);
3214
+ await (0, import_process_tree.terminateProcessTree)(spawned.process);
3215
+ cancelMessage = `Stopped ${spawned.command}`;
3209
3216
  } catch (error) {
3210
3217
  cancelled = false;
3211
- cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
3218
+ cancelMessage = `Failed to stop ${spawned.command}: ${error instanceof Error ? error.message : String(error)}`;
3212
3219
  }
3213
3220
  } else {
3214
- cancelledProcessRuns.add(runId);
3215
- abortPendingReservation(`run:${runId}`, "Cancellation queued before command start");
3221
+ const reservationAborted = abortPendingReservation(`run:${runId}`, "Cancellation queued before command start");
3222
+ outcome = launchRevoked || reservationAborted ? "unstarted" : "unknown";
3216
3223
  cancelMessage = "Cancellation queued before command start";
3217
3224
  }
3218
3225
  if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
3219
- return { cancelled, message: cancelMessage };
3226
+ return { cancelled, message: cancelMessage, outcome };
3220
3227
  }
3221
3228
  function closeProcessStdin(active) {
3222
3229
  if (!active?.stdin) {
@@ -6168,7 +6175,7 @@ async function startWorker(options, projectRuntime = {
6168
6175
  const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" ? message.requestId : void 0;
6169
6176
  if (message.type === "cancel") {
6170
6177
  const admission = recoveryJournal.admit({ ...message, requestId: message.requestId });
6171
- const cancelled = await cancelProcessRun(message.runId);
6178
+ const cancelled = await cancelProcessRun(message.runId, message.scope);
6172
6179
  let admitted;
6173
6180
  try {
6174
6181
  admitted = await admission;
@@ -6194,7 +6201,8 @@ async function startWorker(options, projectRuntime = {
6194
6201
  requestId: message.requestId,
6195
6202
  runId: message.runId,
6196
6203
  cancelled: cancelled.cancelled,
6197
- message: cancelled.message
6204
+ message: cancelled.message,
6205
+ outcome: cancelled.outcome
6198
6206
  })
6199
6207
  );
6200
6208
  return;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.124",
3
+ "version": "0.0.125",
4
4
  "type": "commonjs"
5
5
  }
@@ -216,7 +216,9 @@ class WorkerCommandLauncher {
216
216
  if (!argv.length) throw new Error("Command argv must not be empty");
217
217
  return [...prefix, ...argv];
218
218
  },
219
- cancel: () => this.cancel(request.id),
219
+ cancel: async () => {
220
+ await this.cancel(request.id);
221
+ },
220
222
  diagnose: async () => {
221
223
  this.assertHealthy();
222
224
  if (!this.child) return void 0;
@@ -232,9 +234,10 @@ class WorkerCommandLauncher {
232
234
  release: () => this.releaseRecord(record)
233
235
  };
234
236
  }
237
+ /** Revoke a launch. Returns whether this launcher still held it (queued or acquired, not yet released). */
235
238
  async cancel(id) {
236
239
  const record = this.launches.get(id);
237
- if (!record || record.released) return;
240
+ if (!record || record.released) return false;
238
241
  record.revoked = true;
239
242
  record.rejectAdmission(new CommandLaunchCancelledError());
240
243
  if (this.child) {
@@ -244,6 +247,7 @@ class WorkerCommandLauncher {
244
247
  throw this.fail(error instanceof Error ? error : new Error(String(error)));
245
248
  }
246
249
  }
250
+ return true;
247
251
  }
248
252
  async cancelSession(sessionId) {
249
253
  await this.cancelMatching((_id, ownerSessionId) => ownerSessionId === sessionId);
package/dist/mjs/main.mjs CHANGED
@@ -346,9 +346,10 @@ function unregisterPendingReservation(key, controller) {
346
346
  }
347
347
  function abortPendingReservation(key, reason) {
348
348
  const pending = pendingReservationAborts.get(key);
349
- if (!pending) return;
349
+ if (!pending) return false;
350
350
  pendingReservationAborts.delete(key);
351
351
  pending.controller.abort(new WorkspaceMountHoldAbortedError(reason));
352
+ return true;
352
353
  }
353
354
  function abortPendingReservations(reason, sessionId) {
354
355
  for (const [key, pending] of [...pendingReservationAborts]) {
@@ -3212,32 +3213,38 @@ async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
3212
3213
  );
3213
3214
  }
3214
3215
  }
3215
- async function cancelProcessRun(runId) {
3216
+ async function cancelProcessRun(runId, scope) {
3217
+ const spawned = activeProcesses.get(runId);
3218
+ if (scope === "unstarted" && spawned) return { cancelled: false, message: `${spawned.command} already started`, outcome: "started" };
3219
+ if (!spawned) cancelledProcessRuns.add(runId);
3220
+ const revocation = workerCommandLauncher?.cancel(runId);
3216
3221
  let resourceCancellationError;
3222
+ let launchRevoked = false;
3217
3223
  try {
3218
- await workerCommandLauncher?.cancel(runId);
3224
+ launchRevoked = await revocation ?? false;
3219
3225
  } catch (error) {
3220
3226
  resourceCancellationError = error instanceof Error ? error.message : String(error);
3221
3227
  }
3222
- const active = activeProcesses.get(runId);
3223
3228
  let cancelled = resourceCancellationError === void 0;
3224
3229
  let cancelMessage;
3225
- if (active) {
3230
+ let outcome;
3231
+ if (spawned) {
3232
+ outcome = "stopped";
3226
3233
  try {
3227
- closeProcessStdin(active);
3228
- await terminateProcessTree(active.process);
3229
- cancelMessage = `Stopped ${active.command}`;
3234
+ closeProcessStdin(spawned);
3235
+ await terminateProcessTree(spawned.process);
3236
+ cancelMessage = `Stopped ${spawned.command}`;
3230
3237
  } catch (error) {
3231
3238
  cancelled = false;
3232
- cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
3239
+ cancelMessage = `Failed to stop ${spawned.command}: ${error instanceof Error ? error.message : String(error)}`;
3233
3240
  }
3234
3241
  } else {
3235
- cancelledProcessRuns.add(runId);
3236
- abortPendingReservation(`run:${runId}`, "Cancellation queued before command start");
3242
+ const reservationAborted = abortPendingReservation(`run:${runId}`, "Cancellation queued before command start");
3243
+ outcome = launchRevoked || reservationAborted ? "unstarted" : "unknown";
3237
3244
  cancelMessage = "Cancellation queued before command start";
3238
3245
  }
3239
3246
  if (resourceCancellationError) cancelMessage += `; job resource termination failed: ${resourceCancellationError}`;
3240
- return { cancelled, message: cancelMessage };
3247
+ return { cancelled, message: cancelMessage, outcome };
3241
3248
  }
3242
3249
  function closeProcessStdin(active) {
3243
3250
  if (!active?.stdin) {
@@ -6189,7 +6196,7 @@ async function startWorker(options, projectRuntime = {
6189
6196
  const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" ? message.requestId : void 0;
6190
6197
  if (message.type === "cancel") {
6191
6198
  const admission = recoveryJournal.admit({ ...message, requestId: message.requestId });
6192
- const cancelled = await cancelProcessRun(message.runId);
6199
+ const cancelled = await cancelProcessRun(message.runId, message.scope);
6193
6200
  let admitted;
6194
6201
  try {
6195
6202
  admitted = await admission;
@@ -6215,7 +6222,8 @@ async function startWorker(options, projectRuntime = {
6215
6222
  requestId: message.requestId,
6216
6223
  runId: message.runId,
6217
6224
  cancelled: cancelled.cancelled,
6218
- message: cancelled.message
6225
+ message: cancelled.message,
6226
+ outcome: cancelled.outcome
6219
6227
  })
6220
6228
  );
6221
6229
  return;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.124",
3
+ "version": "0.0.125",
4
4
  "type": "module"
5
5
  }
@@ -86,7 +86,8 @@ export declare class WorkerCommandLauncher {
86
86
  capacityReport(): CommandLaunchCapacityReport;
87
87
  private notifyCapacityChange;
88
88
  acquire(request: CommandLaunchRequest): Promise<CommandLaunchLease>;
89
- cancel(id: string): Promise<void>;
89
+ /** Revoke a launch. Returns whether this launcher still held it (queued or acquired, not yet released). */
90
+ cancel(id: string): Promise<boolean>;
90
91
  cancelSession(sessionId: string): Promise<void>;
91
92
  cancelMatching(matches: (id: string, sessionId?: string) => boolean): Promise<void>;
92
93
  close(): Promise<void>;
@@ -141,6 +141,8 @@ export type WorkerSessionTarget = {
141
141
  ownerUserId: string;
142
142
  rootProfile: "visible_projects" | "canonical_sync";
143
143
  };
144
+ type WorkerCancelScope = "unstarted";
145
+ type WorkerCancelOutcome = "unstarted" | "started" | "stopped" | "unknown";
144
146
  type WorkerClientMessage = WorkerRecoveryClientMessage | {
145
147
  type: "capacity_report";
146
148
  capacity: import("./command-launcher").CommandLaunchCapacityReport & {
@@ -262,6 +264,8 @@ type WorkerClientMessage = WorkerRecoveryClientMessage | {
262
264
  runId: string;
263
265
  cancelled: boolean;
264
266
  message?: string;
267
+ /** `unstarted`: a launch that had not spawned was revoked; `started`: a spawned process was left alone; `stopped`; `unknown`: no record, cancellation recorded. */
268
+ outcome?: WorkerCancelOutcome;
265
269
  } | {
266
270
  type: "pong";
267
271
  } | {
@@ -533,6 +537,8 @@ type WorkerServerMessage = WorkerRecoveryServerMessage | {
533
537
  type: "cancel";
534
538
  requestId: string;
535
539
  runId: string;
540
+ /** `unstarted`: the server's start-deadline abandonment; never touch a spawned process. */
541
+ scope?: WorkerCancelScope;
536
542
  } | {
537
543
  type: "ping";
538
544
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.124",
3
+ "version": "0.0.125",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",