@ours.network/fleet 2.0.1 → 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.
- package/dist/build-info.json +4 -4
- package/dist/session/acp.d.ts +14 -0
- package/dist/session/acp.js +69 -25
- package/dist/session/control.d.ts +2 -0
- package/dist/session/control.js +16 -10
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.0.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "2.0.3",
|
|
3
|
+
"buildId": "495dcfb38c4e",
|
|
4
|
+
"commit": "6bc5bab0c0b088acd40d1a79fe5244730dde4708",
|
|
5
5
|
"dirty": false,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-23T08:54:42.810Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"service.refresh-v1",
|
|
9
9
|
"managed-cli.codex-reasoning-effort-v1",
|
package/dist/session/acp.d.ts
CHANGED
|
@@ -264,6 +264,14 @@ export declare class AcpSession implements AgentSession {
|
|
|
264
264
|
* session/cancel and never resolves a pending permission.
|
|
265
265
|
*/
|
|
266
266
|
submitPromptAfterTool(text: string, options?: SubmitPromptOptions): Promise<TurnResult>;
|
|
267
|
+
/**
|
|
268
|
+
* Start a wake as a turn this client owns when none is running. Steering an
|
|
269
|
+
* idle adapter starts a turn only the adapter knows about: its stopReason
|
|
270
|
+
* never reaches Fleet, so an interrupt could cancel it but never see it
|
|
271
|
+
* settle, and the role stayed `running` for the whole occupancy lease. Like
|
|
272
|
+
* a steered start, the caller learns only that the turn began.
|
|
273
|
+
*/
|
|
274
|
+
private startOwnedWake;
|
|
267
275
|
/**
|
|
268
276
|
* Accept responsibility for a prompt, then return. The turn itself may run
|
|
269
277
|
* for minutes behind other queued turns; making an interactive caller wait
|
|
@@ -329,6 +337,12 @@ export declare class AcpSession implements AgentSession {
|
|
|
329
337
|
interruptPrompt(generation: string, promptId: string): Promise<InterruptOutcome>;
|
|
330
338
|
interrupt(source?: TurnCancellationSource): Promise<InterruptOutcome>;
|
|
331
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;
|
|
332
346
|
/**
|
|
333
347
|
* Do not admit work behind a turn whose adapter may already require restart.
|
|
334
348
|
* A cooperative adapter settles this promise immediately through runPrompt's
|
package/dist/session/acp.js
CHANGED
|
@@ -658,6 +658,16 @@ export class AcpSession {
|
|
|
658
658
|
return this.submitPrompt(text, { ...options, interrupt: false, steer: false });
|
|
659
659
|
if (this.stallRecoveryClaimed)
|
|
660
660
|
return this.submitPrompt(text, { ...options, interrupt: false, steer: false });
|
|
661
|
+
if (!this.activeTurn) {
|
|
662
|
+
try {
|
|
663
|
+
return (await this.startOwnedWake(text, options)).result;
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
if (error instanceof SessionControlError)
|
|
667
|
+
return turnResult(false, 'failed', error.message);
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
661
671
|
const steered = await this.steerPrompt(text);
|
|
662
672
|
if (steered.accepted || steered.detail !== 'ACP steering failed'
|
|
663
673
|
|| this.closing || !this.isAlive())
|
|
@@ -715,6 +725,18 @@ export class AcpSession {
|
|
|
715
725
|
safeBoundary: { state, waitedMs, activeToolCount: remainingToolCount },
|
|
716
726
|
};
|
|
717
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* Start a wake as a turn this client owns when none is running. Steering an
|
|
730
|
+
* idle adapter starts a turn only the adapter knows about: its stopReason
|
|
731
|
+
* never reaches Fleet, so an interrupt could cancel it but never see it
|
|
732
|
+
* settle, and the role stayed `running` for the whole occupancy lease. Like
|
|
733
|
+
* a steered start, the caller learns only that the turn began.
|
|
734
|
+
*/
|
|
735
|
+
async startOwnedWake(text, options) {
|
|
736
|
+
const queued = await this.queuePrompt(text, { ...options, interrupt: false, steer: false });
|
|
737
|
+
void queued.completion.catch(() => undefined);
|
|
738
|
+
return { queued, result: turnResult(true, 'inconclusive', 'startedNewTurn') };
|
|
739
|
+
}
|
|
718
740
|
/**
|
|
719
741
|
* Accept responsibility for a prompt, then return. The turn itself may run
|
|
720
742
|
* for minutes behind other queued turns; making an interactive caller wait
|
|
@@ -737,6 +759,10 @@ export class AcpSession {
|
|
|
737
759
|
// live turn, the extension starts one and acknowledges `startedNewTurn`
|
|
738
760
|
// immediately; a normal session/prompt would keep the monitor blocked until
|
|
739
761
|
// the entire wake-triggered turn terminated.
|
|
762
|
+
if (options.steer && this.steeringSupported && !this.activeTurn) {
|
|
763
|
+
const { queued, result } = await this.startOwnedWake(text, options);
|
|
764
|
+
return { ...queued, completion: Promise.resolve(result), ...(delivery ? { delivery } : {}) };
|
|
765
|
+
}
|
|
740
766
|
if (options.steer && this.steeringSupported) {
|
|
741
767
|
const promptId = randomUUID();
|
|
742
768
|
return {
|
|
@@ -986,6 +1012,48 @@ export class AcpSession {
|
|
|
986
1012
|
active.cancellationWait ??= this.awaitCancellationSettlement(active);
|
|
987
1013
|
await active.cancellationWait;
|
|
988
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;
|
|
989
1057
|
}
|
|
990
1058
|
/**
|
|
991
1059
|
* Do not admit work behind a turn whose adapter may already require restart.
|
|
@@ -1010,31 +1078,7 @@ export class AcpSession {
|
|
|
1010
1078
|
return;
|
|
1011
1079
|
}
|
|
1012
1080
|
active.cancellationDeadlineExceeded = true;
|
|
1013
|
-
this.
|
|
1014
|
-
this.readiness = 'failed';
|
|
1015
|
-
this.lastError = `${ACP_CANCEL_DEADLINE_EXCEEDED}: ACP turn did not settle within ${settleMs}ms`;
|
|
1016
|
-
this.options.log(`[${this.options.name}] ${this.lastError}; restarting adapter with resume`);
|
|
1017
|
-
this.events.emit('error', {
|
|
1018
|
-
turnId: active.id, origin: active.origin, status: ACP_CANCEL_DEADLINE_EXCEEDED,
|
|
1019
|
-
text: this.lastError,
|
|
1020
|
-
});
|
|
1021
|
-
this.conversation.appendSafe({
|
|
1022
|
-
kind: 'session.state', sessionGeneration: this.sessionGeneration,
|
|
1023
|
-
acpSessionId: this.sessionId,
|
|
1024
|
-
promptId: active.id, turnId: active.id,
|
|
1025
|
-
payload: { status: 'failed', detail: this.lastError },
|
|
1026
|
-
});
|
|
1027
|
-
this.child.kill('SIGTERM');
|
|
1028
|
-
const terminateMs = this.options.cancelTerminateGraceMs ?? CANCEL_TERMINATE_GRACE_MS;
|
|
1029
|
-
this.cancelForceKill = setTimeout(() => {
|
|
1030
|
-
if (this.child.exitCode === null) {
|
|
1031
|
-
this.options.log(`[${this.options.name}] ${ACP_CANCEL_DEADLINE_EXCEEDED}: `
|
|
1032
|
-
+ `adapter ignored SIGTERM for ${terminateMs}ms; sending SIGKILL`);
|
|
1033
|
-
this.child.kill('SIGKILL');
|
|
1034
|
-
}
|
|
1035
|
-
}, terminateMs);
|
|
1036
|
-
this.cancelForceKill.unref?.();
|
|
1037
|
-
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`));
|
|
1038
1082
|
}, settleMs);
|
|
1039
1083
|
this.cancelEscalation.unref?.();
|
|
1040
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?;
|
package/dist/session/control.js
CHANGED
|
@@ -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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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:
|
|
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.
|
|
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",
|