@ours.network/fleet 1.1.0-nightly.5 → 1.1.0-nightly.7
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/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/fleet-query-service.js +13 -1
- package/dist/application/role-creation-service.js +11 -0
- package/dist/briefing.js +6 -2
- package/dist/build-info.json +4 -4
- package/dist/cli.js +95 -8
- package/dist/daemon-recovery.d.ts +93 -0
- package/dist/daemon-recovery.js +328 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +15 -5
- package/dist/doctor.js +14 -0
- package/dist/fleet-command-audit.d.ts +131 -0
- package/dist/fleet-command-audit.js +375 -0
- package/dist/fleet-proxy.d.ts +2 -0
- package/dist/owner-channel/channel.d.ts +56 -0
- package/dist/owner-channel/channel.js +251 -17
- package/dist/rooms-tasks/cli.js +121 -18
- package/dist/rooms-tasks/external-worker.d.ts +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/runner.d.ts +9 -2
- package/dist/runner.js +81 -29
- package/dist/session/control.d.ts +14 -1
- package/dist/session/control.js +35 -0
- package/dist/temp-lifecycle.d.ts +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { mkdir, readFile, readdir, rm } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { canonicalCid } from '../config.js';
|
|
6
|
+
import { DAEMON_RECOVERY_DEADLINE_MS } from '../daemon-recovery.js';
|
|
6
7
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
7
8
|
import { TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
8
9
|
import { RoleLifecycleService } from '../application/role-command-service.js';
|
|
@@ -14,6 +15,7 @@ import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, } from '../session/t
|
|
|
14
15
|
import { VERSION } from '../version.js';
|
|
15
16
|
import { renderMarkdownFailure, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
|
|
16
17
|
import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
|
|
18
|
+
import { FleetCommandAuditStore, renderFleetAuditInvocation, renderFleetAuditOutcome, } from '../fleet-command-audit.js';
|
|
17
19
|
import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
|
|
18
20
|
import { ownerNotices, } from './notices.js';
|
|
19
21
|
import { DuplicateSendError, OwnerAuthorizationState, OwnerChannelState, OwnerConversationState, } from './state.js';
|
|
@@ -21,6 +23,24 @@ import { OwnerTaskState, ownerTaskAuditId, ownerTaskDigest, } from './tasks.js';
|
|
|
21
23
|
import { AttachmentRecoveryState, admitAttachments, cleanupAttachmentRoot, parseIncomingAttachments, parseRetrievedAttachments, prepareAttachmentDirectory, recoveredAttachment, removeRequestDirectory, safeField, validateAttachmentSelection, validateAttachmentRelaySelection, writeRecoveredAttachment, } from './attachments.js';
|
|
22
24
|
import { acquireOwnerBinderLease, OWNER_BIND_HANDOFF_TIMEOUT_MS, } from './binder.js';
|
|
23
25
|
import { MessageRecoveryState } from './message-recovery.js';
|
|
26
|
+
export const OWNER_RECOVERY_QUEUE_CAPACITY = 32;
|
|
27
|
+
export const OWNER_RECOVERY_DEGRADED = 'OWNER_RECOVERY_DEGRADED';
|
|
28
|
+
export class OwnerRecoveryDegradedError extends Error {
|
|
29
|
+
code = OWNER_RECOVERY_DEGRADED;
|
|
30
|
+
constructor() {
|
|
31
|
+
super('owner channel recovery is degraded');
|
|
32
|
+
this.name = 'OwnerRecoveryDegradedError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class OwnerRecoveryTimeoutError extends Error {
|
|
36
|
+
stage;
|
|
37
|
+
code = OWNER_RECOVERY_DEGRADED;
|
|
38
|
+
constructor(stage) {
|
|
39
|
+
super(`owner channel recovery timed out at ${stage}`);
|
|
40
|
+
this.stage = stage;
|
|
41
|
+
this.name = 'OwnerRecoveryTimeoutError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
24
44
|
const OWNER_UPDATE_MIN_INTERVAL_MS = 5_000;
|
|
25
45
|
const OWNER_UPDATE_MAX_COUNT = 20;
|
|
26
46
|
const OWNER_UPDATE_MAX_CHARS = 280;
|
|
@@ -76,11 +96,22 @@ export class OwnerChannel {
|
|
|
76
96
|
completionTasks = new Set();
|
|
77
97
|
activeRequests = new Map();
|
|
78
98
|
managementTail = Promise.resolve();
|
|
99
|
+
recoveryEpoch;
|
|
100
|
+
recoveryTask;
|
|
101
|
+
recoveryToken = 0;
|
|
102
|
+
recoveryQueued = 0;
|
|
103
|
+
watchGeneration = 0;
|
|
104
|
+
/** A timed-out client operation that must settle before replacement is safe. */
|
|
105
|
+
recoveryQuiescence;
|
|
106
|
+
releaseSafe = true;
|
|
79
107
|
ready = false;
|
|
108
|
+
startedOnce = false;
|
|
109
|
+
shutdownDegraded = false;
|
|
80
110
|
binder;
|
|
81
111
|
binderOwnedInternally = false;
|
|
82
112
|
fleetOps;
|
|
83
113
|
prepareRestart;
|
|
114
|
+
commandAudits;
|
|
84
115
|
constructor(options) {
|
|
85
116
|
this.options = options;
|
|
86
117
|
this.client = options.client ?? new OursSdkClient(options.env, line => options.log(`[${options.role}] owner channel ${line}`));
|
|
@@ -96,6 +127,7 @@ export class OwnerChannel {
|
|
|
96
127
|
await lifecycle.prepareRestart({ roleIds: [role], mode });
|
|
97
128
|
});
|
|
98
129
|
this.state = new OwnerChannelState(join(options.stateDir, '.owner-channel-state.json'));
|
|
130
|
+
this.commandAudits = new FleetCommandAuditStore(join(options.stateDir, '.owner-channel-command-audit.json'));
|
|
99
131
|
this.authorizations = new OwnerAuthorizationState(join(options.stateDir, '.owner-channel-owners.json'), options.config.owners);
|
|
100
132
|
this.conversations = new OwnerConversationState(join(options.stateDir, '.owner-channel-conversations.json'));
|
|
101
133
|
this.tasks = new OwnerTaskState(join(options.stateDir, '.owner-channel-tasks.json'));
|
|
@@ -124,6 +156,7 @@ export class OwnerChannel {
|
|
|
124
156
|
}
|
|
125
157
|
async start() {
|
|
126
158
|
this.stopping = false;
|
|
159
|
+
this.shutdownDegraded = false;
|
|
127
160
|
this.binderOwnedInternally = !this.options.binderLease;
|
|
128
161
|
this.binder = this.options.binderLease ?? await acquireOwnerBinderLease(this.options.stateDir, this.options.role, this.options.config.identity, this.options.binderDeps);
|
|
129
162
|
try {
|
|
@@ -164,11 +197,12 @@ export class OwnerChannel {
|
|
|
164
197
|
this.attachmentRecovery.cleanup(Date.now(), this.attachmentConfig.retention_ms);
|
|
165
198
|
void cleanupAttachmentRoot(this.attachmentRoot, Date.now(), this.attachmentConfig.retention_ms).catch(error => this.logError('attachment crash cleanup failed', error));
|
|
166
199
|
}
|
|
200
|
+
this.startedOnce = true;
|
|
167
201
|
this.ready = true;
|
|
168
202
|
// Do not make role startup wait for an old owner request to finish a turn.
|
|
169
203
|
// watchLoop itself drains before every establishment, including this first
|
|
170
204
|
// one, so there is no drain-to-tip race.
|
|
171
|
-
this.watchTask = this.watchLoop();
|
|
205
|
+
this.watchTask = this.watchLoop(++this.watchGeneration);
|
|
172
206
|
}
|
|
173
207
|
drain() {
|
|
174
208
|
this.drainRequested = true;
|
|
@@ -183,28 +217,50 @@ export class OwnerChannel {
|
|
|
183
217
|
return this.drainTask;
|
|
184
218
|
}
|
|
185
219
|
async close() {
|
|
220
|
+
if (this.recoveryTask)
|
|
221
|
+
this.shutdownDegraded = true;
|
|
186
222
|
this.stopping = true;
|
|
187
223
|
this.ready = false;
|
|
188
|
-
this.
|
|
189
|
-
|
|
224
|
+
this.recoveryToken++;
|
|
225
|
+
this.watchGeneration++;
|
|
226
|
+
const staleWatch = this.watchTask;
|
|
190
227
|
this.watchTask = undefined;
|
|
191
|
-
|
|
228
|
+
this.watchAbort?.abort();
|
|
229
|
+
const now = this.options.recoveryDeps?.now ?? (() => Date.now());
|
|
230
|
+
const deadlineAt = now()
|
|
231
|
+
+ (this.options.recoveryDeps?.deadlineMs ?? DAEMON_RECOVERY_DEADLINE_MS);
|
|
232
|
+
let disposed = false;
|
|
192
233
|
try {
|
|
193
|
-
await this.
|
|
234
|
+
await this.recoveryStage('shutdown_quiescence', Promise.all([
|
|
235
|
+
staleWatch?.catch(error => this.logError('watch shutdown failed', error)),
|
|
236
|
+
this.drainTask?.catch(error => this.logError('drain shutdown failed', error)),
|
|
237
|
+
this.managementTail.then(() => undefined),
|
|
238
|
+
this.recoveryQuiescence,
|
|
239
|
+
]), deadlineAt);
|
|
240
|
+
await this.recoveryStage('shutdown_close', this.client.close(), deadlineAt);
|
|
241
|
+
disposed = true;
|
|
242
|
+
this.writeShutdownState('closed', 'OWNER_SHUTDOWN_CLOSED', now());
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
this.shutdownDegraded = true;
|
|
246
|
+
this.releaseSafe = false;
|
|
247
|
+
const stage = error instanceof OwnerRecoveryTimeoutError ? error.stage : 'shutdown_unknown';
|
|
248
|
+
this.writeShutdownState('degraded', `OWNER_${stage.toUpperCase()}_TIMEOUT`, now());
|
|
249
|
+
this.options.log(`[${this.options.role}] owner shutdown degraded stage=${stage}`);
|
|
194
250
|
}
|
|
195
251
|
finally {
|
|
196
|
-
if (this.binderOwnedInternally)
|
|
252
|
+
if (disposed && this.binderOwnedInternally)
|
|
197
253
|
this.binder?.release();
|
|
198
|
-
|
|
254
|
+
if (disposed)
|
|
255
|
+
this.binder = undefined;
|
|
199
256
|
}
|
|
200
257
|
}
|
|
258
|
+
binderReleaseSafe() { return this.releaseSafe; }
|
|
201
259
|
manage(request) {
|
|
202
|
-
|
|
203
|
-
this.managementTail = run.then(() => undefined, () => undefined);
|
|
204
|
-
return run;
|
|
260
|
+
return this.queueManagement(() => this.manageNow(request));
|
|
205
261
|
}
|
|
206
262
|
notifyFleetSpawn(event) {
|
|
207
|
-
|
|
263
|
+
return this.queueManagement(async () => {
|
|
208
264
|
if (!this.ready || this.stopping)
|
|
209
265
|
throw new Error('owner-channel MCP client is unavailable');
|
|
210
266
|
const model = event.model ? `, model ${event.model}` : '';
|
|
@@ -220,9 +276,187 @@ export class OwnerChannel {
|
|
|
220
276
|
await this.sendProactiveMessage(`🧑💻 ${event.caller} spawned ${event.lifetime} agent ${event.role} `
|
|
221
277
|
+ `(${event.harness}/${event.session}${model}; ${monitor}${permission}).${inherited}`, `fleet-spawn\0${event.creationActionId}`, 0);
|
|
222
278
|
});
|
|
279
|
+
}
|
|
280
|
+
beginFleetCommandAudit(requestId, argv) {
|
|
281
|
+
const run = this.managementTail.then(async () => {
|
|
282
|
+
let attempt = this.commandAudits.begin(requestId, this.options.role, argv);
|
|
283
|
+
if (attempt.invocation === 'delivered')
|
|
284
|
+
return attempt;
|
|
285
|
+
if (attempt.invocation === 'uncertain')
|
|
286
|
+
throw new Error('fleet command invocation delivery is uncertain; execution is denied');
|
|
287
|
+
if (!this.ready || this.stopping) {
|
|
288
|
+
this.commandAudits.invocation(attempt.correlationId, this.options.role, 'uncertain');
|
|
289
|
+
this.options.log(`[${this.options.role}] fleet command invocation ${attempt.correlationId} `
|
|
290
|
+
+ 'delivery unavailable; execution denied');
|
|
291
|
+
throw new Error('owner-channel MCP client is unavailable; invocation recorded uncertain');
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
await this.sendProactiveMessage(renderFleetAuditInvocation(attempt), `fleet-command-invocation\0${attempt.correlationId}`, 0);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
this.commandAudits.invocation(attempt.correlationId, this.options.role, 'uncertain');
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
attempt = this.commandAudits.invocation(attempt.correlationId, this.options.role, 'delivered');
|
|
301
|
+
return attempt;
|
|
302
|
+
});
|
|
303
|
+
this.managementTail = run.then(() => undefined, () => undefined);
|
|
304
|
+
return run;
|
|
305
|
+
}
|
|
306
|
+
finishFleetCommandAudit(input) {
|
|
307
|
+
const run = this.managementTail.then(async () => {
|
|
308
|
+
let attempt = this.commandAudits.finish(input.correlationId, this.options.role, {
|
|
309
|
+
class: input.class, effect: input.effect,
|
|
310
|
+
...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
|
|
311
|
+
...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
|
|
312
|
+
...(input.presentation ? { presentation: input.presentation } : {}),
|
|
313
|
+
});
|
|
314
|
+
if (attempt.outcome?.delivery === 'delivered')
|
|
315
|
+
return attempt;
|
|
316
|
+
if (attempt.outcome?.delivery === 'uncertain')
|
|
317
|
+
throw new Error(`fleet command outcome delivery is uncertain after execution; effect status=${attempt.outcome.effect}`);
|
|
318
|
+
if (!this.ready || this.stopping) {
|
|
319
|
+
this.commandAudits.outcome(attempt.correlationId, this.options.role, 'uncertain');
|
|
320
|
+
this.options.log(`[${this.options.role}] fleet command outcome ${attempt.correlationId} `
|
|
321
|
+
+ `delivery unavailable after execution; effect status=${attempt.outcome?.effect}`);
|
|
322
|
+
throw new Error(`owner-channel unavailable after execution; effect status=${attempt.outcome?.effect}`);
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
await this.sendProactiveMessage(renderFleetAuditOutcome(attempt), `fleet-command-outcome\0${attempt.correlationId}`, 0);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
this.commandAudits.outcome(attempt.correlationId, this.options.role, 'uncertain');
|
|
329
|
+
throw new Error(`fleet command outcome delivery is uncertain after execution; effect status=${attempt.outcome?.effect}`, { cause: error });
|
|
330
|
+
}
|
|
331
|
+
attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, 'delivered');
|
|
332
|
+
return attempt;
|
|
333
|
+
});
|
|
223
334
|
this.managementTail = run.then(() => undefined, () => undefined);
|
|
224
335
|
return run;
|
|
225
336
|
}
|
|
337
|
+
recover(epoch) {
|
|
338
|
+
if (this.stopping)
|
|
339
|
+
return Promise.reject(new Error('owner channel is stopping'));
|
|
340
|
+
if (this.recoveryEpoch === epoch && this.recoveryTask)
|
|
341
|
+
return this.recoveryTask;
|
|
342
|
+
this.recoveryEpoch = epoch;
|
|
343
|
+
this.ready = false;
|
|
344
|
+
const token = ++this.recoveryToken;
|
|
345
|
+
const watchGeneration = ++this.watchGeneration;
|
|
346
|
+
const staleWatch = this.watchTask;
|
|
347
|
+
this.watchTask = undefined;
|
|
348
|
+
this.watchAbort?.abort();
|
|
349
|
+
const superseded = () => this.stopping || token !== this.recoveryToken;
|
|
350
|
+
const now = this.options.recoveryDeps?.now ?? (() => Date.now());
|
|
351
|
+
const deadlineAt = now()
|
|
352
|
+
+ (this.options.recoveryDeps?.deadlineMs ?? DAEMON_RECOVERY_DEADLINE_MS);
|
|
353
|
+
const run = this.managementTail.then(async () => {
|
|
354
|
+
if (superseded())
|
|
355
|
+
throw new Error('owner recovery epoch superseded');
|
|
356
|
+
if (this.recoveryQuiescence) {
|
|
357
|
+
const debt = this.recoveryQuiescence;
|
|
358
|
+
await this.recoveryStage('prior_quiescence', debt, deadlineAt);
|
|
359
|
+
if (this.recoveryQuiescence === debt)
|
|
360
|
+
this.recoveryQuiescence = undefined;
|
|
361
|
+
}
|
|
362
|
+
// Quiesce every stale-client reader before replacing its transport. A
|
|
363
|
+
// watch can be inside the shared drain task when its abort lands.
|
|
364
|
+
await this.recoveryStage('stale_quiescence', Promise.all([
|
|
365
|
+
staleWatch?.catch(error => this.logError('stale watch shutdown failed', error)),
|
|
366
|
+
this.drainTask?.catch(error => this.logError('stale drain shutdown failed', error)),
|
|
367
|
+
]), deadlineAt);
|
|
368
|
+
if (superseded())
|
|
369
|
+
throw new Error('owner recovery epoch superseded');
|
|
370
|
+
await this.recoveryStage('close', this.client.close(), deadlineAt);
|
|
371
|
+
if (superseded())
|
|
372
|
+
throw new Error('owner recovery epoch superseded');
|
|
373
|
+
try {
|
|
374
|
+
await this.recoveryStage('start', this.client.start(), deadlineAt);
|
|
375
|
+
if (superseded())
|
|
376
|
+
throw new Error('owner recovery epoch superseded');
|
|
377
|
+
await this.recoveryStage('bind', this.client.bindIdentity(this.options.config.identity), deadlineAt);
|
|
378
|
+
if (superseded())
|
|
379
|
+
throw new Error('owner recovery epoch superseded');
|
|
380
|
+
// Journal-aware drain is the only operation allowed to mark owner mail
|
|
381
|
+
// read. The final listing is a typed, read-only channel probe.
|
|
382
|
+
await this.recoveryStage('drain', this.drain(), deadlineAt);
|
|
383
|
+
await this.recoveryStage('list', this.client.listIncomingMessages(), deadlineAt);
|
|
384
|
+
if (superseded())
|
|
385
|
+
throw new Error('owner recovery epoch superseded');
|
|
386
|
+
this.ready = true;
|
|
387
|
+
this.watchTask = this.watchLoop(watchGeneration);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
this.ready = false;
|
|
391
|
+
if (token === this.recoveryToken)
|
|
392
|
+
this.recoveryToken++;
|
|
393
|
+
// A timed-out operation may still be using this client. Never overlap
|
|
394
|
+
// its disposal; the recorded quiescence debt gates the next retry.
|
|
395
|
+
if (!this.recoveryQuiescence) {
|
|
396
|
+
try {
|
|
397
|
+
await this.recoveryStage('close_after_failure', this.client.close(), deadlineAt);
|
|
398
|
+
}
|
|
399
|
+
catch (closeError) {
|
|
400
|
+
this.logError('recovery client close failed', closeError);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
this.managementTail = run.then(() => undefined, () => undefined);
|
|
407
|
+
const task = run.finally(() => {
|
|
408
|
+
if (this.recoveryTask === task)
|
|
409
|
+
this.recoveryTask = undefined;
|
|
410
|
+
});
|
|
411
|
+
this.recoveryTask = task;
|
|
412
|
+
return task;
|
|
413
|
+
}
|
|
414
|
+
recoveryStage(stage, operation, deadlineAt) {
|
|
415
|
+
const now = this.options.recoveryDeps?.now ?? (() => Date.now());
|
|
416
|
+
const remaining = Math.max(0, deadlineAt - now());
|
|
417
|
+
const setTimer = this.options.recoveryDeps?.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
418
|
+
const clearTimer = this.options.recoveryDeps?.clearTimer ?? (timer => clearTimeout(timer));
|
|
419
|
+
let timer;
|
|
420
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
421
|
+
timer = setTimer(() => reject(new OwnerRecoveryTimeoutError(stage)), remaining);
|
|
422
|
+
});
|
|
423
|
+
return Promise.race([operation, timeout]).catch(error => {
|
|
424
|
+
if (error instanceof OwnerRecoveryTimeoutError) {
|
|
425
|
+
const debt = operation.then(() => undefined, () => undefined);
|
|
426
|
+
this.recoveryQuiescence = debt;
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
429
|
+
}).finally(() => clearTimer(timer));
|
|
430
|
+
}
|
|
431
|
+
writeShutdownState(state, reason, at) {
|
|
432
|
+
try {
|
|
433
|
+
replaceFileAtomically(join(this.options.stateDir, '.owner-channel-shutdown.json'), JSON.stringify({ version: 1, state, reason, at: new Date(at).toISOString() }, null, 2) + '\n');
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
this.logError('shutdown status write failed', error);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
queueManagement(operation) {
|
|
440
|
+
const deferred = this.recoveryTask !== undefined && !this.ready;
|
|
441
|
+
if (!this.ready && !deferred)
|
|
442
|
+
return Promise.reject(!this.startedOnce || (this.stopping && !this.shutdownDegraded)
|
|
443
|
+
? new Error('owner-channel MCP client is unavailable')
|
|
444
|
+
: new OwnerRecoveryDegradedError());
|
|
445
|
+
if (deferred && this.recoveryQueued >= OWNER_RECOVERY_QUEUE_CAPACITY)
|
|
446
|
+
return Promise.reject(new Error('owner recovery queue is full'));
|
|
447
|
+
if (deferred)
|
|
448
|
+
this.recoveryQueued++;
|
|
449
|
+
const run = this.managementTail.then(() => {
|
|
450
|
+
if (!this.ready || this.stopping)
|
|
451
|
+
throw new OwnerRecoveryDegradedError();
|
|
452
|
+
return operation();
|
|
453
|
+
});
|
|
454
|
+
this.managementTail = run.then(() => undefined, () => undefined);
|
|
455
|
+
return run.finally(() => {
|
|
456
|
+
if (deferred)
|
|
457
|
+
this.recoveryQueued = Math.max(0, this.recoveryQueued - 1);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
226
460
|
async manageNow(request) {
|
|
227
461
|
if (!this.ready || this.stopping)
|
|
228
462
|
throw new Error('owner-channel MCP client is unavailable');
|
|
@@ -1876,7 +2110,7 @@ export class OwnerChannel {
|
|
|
1876
2110
|
}
|
|
1877
2111
|
return undefined;
|
|
1878
2112
|
}
|
|
1879
|
-
async watchLoop() {
|
|
2113
|
+
async watchLoop(generation) {
|
|
1880
2114
|
const sleep = this.options.binderDeps?.sleep
|
|
1881
2115
|
?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
1882
2116
|
const restored = this.readWatchState();
|
|
@@ -1885,7 +2119,7 @@ export class OwnerChannel {
|
|
|
1885
2119
|
state = this.writeWatchState(state, 'OWNER_WATCH_STATE_RECOVERED');
|
|
1886
2120
|
let delayMs = 1_000;
|
|
1887
2121
|
let attempts = 0;
|
|
1888
|
-
while (!this.stopping) {
|
|
2122
|
+
while (!this.stopping && generation === this.watchGeneration) {
|
|
1889
2123
|
const ctrl = new AbortController();
|
|
1890
2124
|
this.watchAbort = ctrl;
|
|
1891
2125
|
try {
|
|
@@ -1895,12 +2129,12 @@ export class OwnerChannel {
|
|
|
1895
2129
|
// fall into a gap. Persistent history plus fleet's body-free claim
|
|
1896
2130
|
// journal is authoritative; durable wire-ID dedupe makes hints harmless.
|
|
1897
2131
|
await this.drain();
|
|
1898
|
-
if (this.stopping)
|
|
2132
|
+
if (this.stopping || generation !== this.watchGeneration)
|
|
1899
2133
|
return;
|
|
1900
2134
|
state = this.writeWatchState(state, 'OWNER_WATCH_CONNECTING', { reconnected: attempts > 0 });
|
|
1901
2135
|
attempts++;
|
|
1902
2136
|
for await (const _event of this.client.watchNotifications(this.options.config.identity, { since: 0, signal: ctrl.signal })) {
|
|
1903
|
-
if (this.stopping)
|
|
2137
|
+
if (this.stopping || generation !== this.watchGeneration)
|
|
1904
2138
|
return;
|
|
1905
2139
|
state = this.writeWatchState(state, 'OWNER_WATCH_CONNECTED', { resetFailures: true });
|
|
1906
2140
|
delayMs = 1_000;
|
|
@@ -1909,11 +2143,11 @@ export class OwnerChannel {
|
|
|
1909
2143
|
// recorded before a managed request is dispatched.
|
|
1910
2144
|
await this.drain();
|
|
1911
2145
|
}
|
|
1912
|
-
if (!this.stopping)
|
|
2146
|
+
if (!this.stopping && generation === this.watchGeneration)
|
|
1913
2147
|
throw new Error('ours SDK notification stream ended');
|
|
1914
2148
|
}
|
|
1915
2149
|
catch (error) {
|
|
1916
|
-
if (this.stopping)
|
|
2150
|
+
if (this.stopping || generation !== this.watchGeneration)
|
|
1917
2151
|
return;
|
|
1918
2152
|
// SDK 2 deliberately hides transport status behind its typed stream.
|
|
1919
2153
|
// Do not parse error prose to rediscover it: every failure follows the
|