@ours.network/fleet 2.0.2 → 2.0.3

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,9 +1,9 @@
1
1
  {
2
- "version": "2.0.2",
3
- "buildId": "aa0a1f047c8f",
4
- "commit": "82c290531c7dc6ce4529fd8d7dabd1b36be52113",
2
+ "version": "2.0.3",
3
+ "buildId": "495dcfb38c4e",
4
+ "commit": "6bc5bab0c0b088acd40d1a79fe5244730dde4708",
5
5
  "dirty": false,
6
- "builtAt": "2026-10-23T08:19:17.510Z",
6
+ "builtAt": "2026-10-23T08:54:42.810Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
@@ -337,6 +337,12 @@ export declare class AcpSession implements AgentSession {
337
337
  interruptPrompt(generation: string, promptId: string): Promise<InterruptOutcome>;
338
338
  interrupt(source?: TurnCancellationSource): Promise<InterruptOutcome>;
339
339
  private cancelActive;
340
+ /**
341
+ * Restart a cancelled adapter whose turn cannot be seen to settle. The child
342
+ * exit releases any steering lease and the runner resumes the context; the
343
+ * returned error is the caller's typed `forced` outcome.
344
+ */
345
+ private forceCancellationRecovery;
340
346
  /**
341
347
  * Do not admit work behind a turn whose adapter may already require restart.
342
348
  * A cooperative adapter settles this promise immediately through runPrompt's
@@ -1012,6 +1012,48 @@ export class AcpSession {
1012
1012
  active.cancellationWait ??= this.awaitCancellationSettlement(active);
1013
1013
  await active.cancellationWait;
1014
1014
  }
1015
+ else if (!active && this.steeringOccupied) {
1016
+ // Steering raced the end of an owned turn and started one only the
1017
+ // adapter knows about. Nothing will report its end, so no interrupt can
1018
+ // see it settle; a resumed adapter restart does end it.
1019
+ throw this.forceCancellationRecovery(undefined, 'steering-started ACP turn cannot report its cancellation');
1020
+ }
1021
+ }
1022
+ /**
1023
+ * Restart a cancelled adapter whose turn cannot be seen to settle. The child
1024
+ * exit releases any steering lease and the runner resumes the context; the
1025
+ * returned error is the caller's typed `forced` outcome.
1026
+ */
1027
+ forceCancellationRecovery(active, reason) {
1028
+ const error = new SessionControlError('control-unavailable', 'ACP adapter restart is in progress after the cancellation deadline', ACP_CANCEL_DEADLINE_EXCEEDED);
1029
+ if (this.cancelRecoveryReason)
1030
+ return error;
1031
+ this.cancelRecoveryReason = ACP_CANCEL_DEADLINE_EXCEEDED;
1032
+ this.readiness = 'failed';
1033
+ this.lastError = `${ACP_CANCEL_DEADLINE_EXCEEDED}: ${reason}`;
1034
+ this.options.log(`[${this.options.name}] ${this.lastError}; restarting adapter with resume`);
1035
+ this.events.emit('error', {
1036
+ turnId: active?.id, origin: active?.origin, status: ACP_CANCEL_DEADLINE_EXCEEDED,
1037
+ text: this.lastError,
1038
+ });
1039
+ this.conversation.appendSafe({
1040
+ kind: 'session.state', sessionGeneration: this.sessionGeneration,
1041
+ acpSessionId: this.sessionId,
1042
+ ...(active ? { promptId: active.id, turnId: active.id } : {}),
1043
+ payload: { status: 'failed', detail: this.lastError },
1044
+ });
1045
+ this.observation.publish();
1046
+ this.child.kill('SIGTERM');
1047
+ const terminateMs = this.options.cancelTerminateGraceMs ?? CANCEL_TERMINATE_GRACE_MS;
1048
+ this.cancelForceKill = setTimeout(() => {
1049
+ if (this.child.exitCode === null) {
1050
+ this.options.log(`[${this.options.name}] ${ACP_CANCEL_DEADLINE_EXCEEDED}: `
1051
+ + `adapter ignored SIGTERM for ${terminateMs}ms; sending SIGKILL`);
1052
+ this.child.kill('SIGKILL');
1053
+ }
1054
+ }, terminateMs);
1055
+ this.cancelForceKill.unref?.();
1056
+ return error;
1015
1057
  }
1016
1058
  /**
1017
1059
  * Do not admit work behind a turn whose adapter may already require restart.
@@ -1036,31 +1078,7 @@ export class AcpSession {
1036
1078
  return;
1037
1079
  }
1038
1080
  active.cancellationDeadlineExceeded = true;
1039
- this.cancelRecoveryReason = ACP_CANCEL_DEADLINE_EXCEEDED;
1040
- this.readiness = 'failed';
1041
- this.lastError = `${ACP_CANCEL_DEADLINE_EXCEEDED}: ACP turn did not settle within ${settleMs}ms`;
1042
- this.options.log(`[${this.options.name}] ${this.lastError}; restarting adapter with resume`);
1043
- this.events.emit('error', {
1044
- turnId: active.id, origin: active.origin, status: ACP_CANCEL_DEADLINE_EXCEEDED,
1045
- text: this.lastError,
1046
- });
1047
- this.conversation.appendSafe({
1048
- kind: 'session.state', sessionGeneration: this.sessionGeneration,
1049
- acpSessionId: this.sessionId,
1050
- promptId: active.id, turnId: active.id,
1051
- payload: { status: 'failed', detail: this.lastError },
1052
- });
1053
- this.child.kill('SIGTERM');
1054
- const terminateMs = this.options.cancelTerminateGraceMs ?? CANCEL_TERMINATE_GRACE_MS;
1055
- this.cancelForceKill = setTimeout(() => {
1056
- if (this.child.exitCode === null) {
1057
- this.options.log(`[${this.options.name}] ${ACP_CANCEL_DEADLINE_EXCEEDED}: `
1058
- + `adapter ignored SIGTERM for ${terminateMs}ms; sending SIGKILL`);
1059
- this.child.kill('SIGKILL');
1060
- }
1061
- }, terminateMs);
1062
- this.cancelForceKill.unref?.();
1063
- reject(new SessionControlError('control-unavailable', 'ACP adapter restart is in progress after the cancellation deadline', ACP_CANCEL_DEADLINE_EXCEEDED));
1081
+ reject(this.forceCancellationRecovery(active, `ACP turn did not settle within ${settleMs}ms`));
1064
1082
  }, settleMs);
1065
1083
  this.cancelEscalation.unref?.();
1066
1084
  });
@@ -121,6 +121,8 @@ export declare class RoleControlServer {
121
121
  private readonly sockets;
122
122
  /** Interrupt idempotency: same command id returns the first receipt. */
123
123
  private readonly interruptCommands;
124
+ /** A Stop retried while its first attempt is settling joins that attempt instead of cancelling the next turn. */
125
+ private readonly interruptsInFlight;
124
126
  private correspondence?;
125
127
  setCorrespondence(value: SupervisorCorrespondence): void;
126
128
  private ownerChannel?;
@@ -118,6 +118,8 @@ export class RoleControlServer {
118
118
  sockets = new Set();
119
119
  /** Interrupt idempotency: same command id returns the first receipt. */
120
120
  interruptCommands = new Map();
121
+ /** A Stop retried while its first attempt is settling joins that attempt instead of cancelling the next turn. */
122
+ interruptsInFlight = new Map();
121
123
  correspondence;
122
124
  setCorrespondence(value) { this.correspondence = value; }
123
125
  ownerChannel;
@@ -602,17 +604,21 @@ export class RoleControlServer {
602
604
  this.write(socket, { version: 1, id: request.id, ok: true, result: existing });
603
605
  return;
604
606
  }
605
- const outcome = await interruptSession(this.session, 'local-console');
606
- const receipt = {
607
- accepted: true, commandId: request.commandId, at: new Date().toISOString(),
608
- ...outcome,
609
- };
610
- this.interruptCommands.set(request.commandId, receipt);
611
- if (this.interruptCommands.size > 200) {
612
- const oldest = this.interruptCommands.keys().next().value;
613
- this.interruptCommands.delete(oldest);
607
+ const commandId = request.commandId;
608
+ let attempt = this.interruptsInFlight.get(commandId);
609
+ if (!attempt) {
610
+ attempt = interruptSession(this.session, 'local-console').then(outcome => {
611
+ const receipt = { accepted: true, commandId, at: new Date().toISOString(), ...outcome };
612
+ this.interruptCommands.set(commandId, receipt);
613
+ if (this.interruptCommands.size > 200) {
614
+ const oldest = this.interruptCommands.keys().next().value;
615
+ this.interruptCommands.delete(oldest);
616
+ }
617
+ return receipt;
618
+ }).finally(() => this.interruptsInFlight.delete(commandId));
619
+ this.interruptsInFlight.set(commandId, attempt);
614
620
  }
615
- this.write(socket, { version: 1, id: request.id, ok: true, result: receipt });
621
+ this.write(socket, { version: 1, id: request.id, ok: true, result: await attempt });
616
622
  return;
617
623
  }
618
624
  case 'respond_permission_v2': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",