@ours.network/fleet 1.0.0 → 1.0.1
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
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.0.1",
|
|
3
|
+
"buildId": "bb9ae2d6a815",
|
|
4
|
+
"commit": "52468879ecc67b45ddeb632f195ac8ee895a8b1d",
|
|
5
5
|
"dirty": false,
|
|
6
|
-
"builtAt": "2026-08-
|
|
6
|
+
"builtAt": "2026-08-25T07:16:56.275Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|
|
@@ -100,6 +100,14 @@ async function retireMember(roomId, seat, deps) {
|
|
|
100
100
|
let current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
101
101
|
let retirement = current.retirement;
|
|
102
102
|
if (!retirement) {
|
|
103
|
+
if (current.launch?.state === 'pending' && current.launch.attempt === 0) {
|
|
104
|
+
if (existsSync(agentDir(current.role_name, true))) {
|
|
105
|
+
throw new Error(`never-launched room member '${current.role_name}' unexpectedly has Fleet temp state`);
|
|
106
|
+
}
|
|
107
|
+
await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
|
|
108
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', 'never-launched');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
103
111
|
const ownership = await (deps.inspectMember ?? inspectMember)(current);
|
|
104
112
|
room = advanceMemberRetirement(roomId, current.role_name, 'stop_requested', ownership.launchId);
|
|
105
113
|
current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
@@ -17,6 +17,7 @@ export interface CoworkInviteAcceptResult {
|
|
|
17
17
|
}
|
|
18
18
|
export interface CoworkInviteResult {
|
|
19
19
|
invite: string;
|
|
20
|
+
invite_id: string;
|
|
20
21
|
min_accepts: number;
|
|
21
22
|
}
|
|
22
23
|
export interface CoworkSeatInfo {
|
|
@@ -63,6 +64,7 @@ export interface CoworkAdapter {
|
|
|
63
64
|
role: string;
|
|
64
65
|
min_accepts: number;
|
|
65
66
|
}): Promise<CoworkInviteResult>;
|
|
67
|
+
revokeInvite(roomId: string, inviteId: string): Promise<void>;
|
|
66
68
|
setRoleBriefing(roomId: string, opts: {
|
|
67
69
|
role: string;
|
|
68
70
|
text: string;
|
|
@@ -318,10 +318,14 @@ export function createCoworkAdapter(options = {}) {
|
|
|
318
318
|
const invite = object(receipt.invite, 'room.invite', 'receipt.invite');
|
|
319
319
|
return {
|
|
320
320
|
invite: string(receipt.blob, 'room.invite', 'receipt.blob'),
|
|
321
|
+
invite_id: string(invite.invite_id, 'room.invite', 'receipt.invite.invite_id'),
|
|
321
322
|
min_accepts: typeof invite.min_accepts === 'number'
|
|
322
323
|
? invite.min_accepts : opts.min_accepts,
|
|
323
324
|
};
|
|
324
325
|
},
|
|
326
|
+
async revokeInvite(roomId, inviteId) {
|
|
327
|
+
await call('room.revoke', { room_id: roomId, invite_id: inviteId });
|
|
328
|
+
},
|
|
325
329
|
async setRoleBriefing(roomId, opts) {
|
|
326
330
|
const result = object(await call('room.briefing.role.set', {
|
|
327
331
|
room_id: roomId, role: opts.role, text: opts.text,
|
|
@@ -70,6 +70,26 @@ async function removeMemberIdentity(name) {
|
|
|
70
70
|
}
|
|
71
71
|
catch { /* best-effort cleanup */ }
|
|
72
72
|
}
|
|
73
|
+
async function redeemRoomInviteAsMember(member, invite, roomIdentityCid) {
|
|
74
|
+
const client = await attachOursClient({
|
|
75
|
+
env: process.env,
|
|
76
|
+
leaseToken: `ours-fleet-member-admit-${process.pid}-${randomUUID()}`,
|
|
77
|
+
clientPid: process.pid,
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
const bound = await client.chooseIdentity({ name: member.name, force: false });
|
|
81
|
+
if (bound.cid.toLowerCase() !== member.cid.toLowerCase()) {
|
|
82
|
+
throw new Error(`member ${member.name} identity CID mismatch: expected ${member.cid}, found ${bound.cid}`);
|
|
83
|
+
}
|
|
84
|
+
const added = await client.addContact({ invite });
|
|
85
|
+
if (added.cid.toLowerCase() !== roomIdentityCid.toLowerCase()) {
|
|
86
|
+
throw new Error(`member ${member.name} invite resolved to ${added.cid}; expected room ${roomIdentityCid}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await client.releaseLease().catch(() => { });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
73
93
|
function settingsFor(member, cfg) {
|
|
74
94
|
let refRole;
|
|
75
95
|
try {
|
|
@@ -224,6 +244,7 @@ export async function provisionMembers(input) {
|
|
|
224
244
|
const existing = getRoomRecord(roomId);
|
|
225
245
|
if (!existing?.room_identity_cid)
|
|
226
246
|
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
247
|
+
const roomIdentityCid = existing.room_identity_cid;
|
|
227
248
|
const persistedSeats = existing?.member_seats ?? [];
|
|
228
249
|
const resuming = plan.length > 0
|
|
229
250
|
&& plan.every(p => persistedSeats.some(s => s.role_name === p.name));
|
|
@@ -353,6 +374,13 @@ export async function provisionMembers(input) {
|
|
|
353
374
|
throw error;
|
|
354
375
|
}
|
|
355
376
|
}
|
|
377
|
+
const policy = {
|
|
378
|
+
timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
|
|
379
|
+
initialDelayMs: input.startupWait?.initialDelayMs ?? 250,
|
|
380
|
+
maxDelayMs: input.startupWait?.maxDelayMs ?? 2_000,
|
|
381
|
+
now: input.startupWait?.now ?? Date.now,
|
|
382
|
+
sleep: input.startupWait?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
383
|
+
};
|
|
356
384
|
advanceSaga(roomId, 'join_role_groups', 5);
|
|
357
385
|
try {
|
|
358
386
|
const observedSeats = await cowork.getSeats(roomId);
|
|
@@ -370,15 +398,49 @@ export async function provisionMembers(input) {
|
|
|
370
398
|
...(roleGroups.get(member.coworkRole) ?? []), member,
|
|
371
399
|
]);
|
|
372
400
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
401
|
+
const issuance = await Promise.allSettled([...roleGroups].map(async ([coworkRole, group]) => ({
|
|
402
|
+
coworkRole,
|
|
403
|
+
group,
|
|
404
|
+
...await cowork.issueInvite(roomId, { role: coworkRole, min_accepts: group.length }),
|
|
405
|
+
})));
|
|
406
|
+
const admissions = issuance.flatMap(result => result.status === 'fulfilled' ? [result.value] : []);
|
|
407
|
+
const issuanceFailure = issuance.find(result => result.status === 'rejected');
|
|
408
|
+
if (issuanceFailure?.status === 'rejected') {
|
|
409
|
+
const cleanup = await Promise.allSettled(admissions.map(admission => cowork.revokeInvite(roomId, admission.invite_id)));
|
|
410
|
+
const cleanupFailures = cleanup.filter(result => result.status === 'rejected');
|
|
411
|
+
if (cleanupFailures.length > 0) {
|
|
412
|
+
throw new AggregateError([issuanceFailure.reason, ...cleanupFailures.map(result => result.reason)], 'role descriptor issuance failed and one or more issued descriptors could not be revoked');
|
|
413
|
+
}
|
|
414
|
+
throw issuanceFailure.reason;
|
|
415
|
+
}
|
|
416
|
+
const redemptions = await Promise.allSettled(admissions.flatMap(({ group, invite }) => group.map(member => redeemRoomInviteAsMember(member, invite, roomIdentityCid))));
|
|
417
|
+
const redemptionFailure = redemptions.find(result => result.status === 'rejected');
|
|
418
|
+
if (redemptionFailure?.status === 'rejected') {
|
|
419
|
+
const cleanup = await Promise.allSettled(admissions.map(admission => cowork.revokeInvite(roomId, admission.invite_id)));
|
|
420
|
+
const cleanupFailures = cleanup.filter(result => result.status === 'rejected');
|
|
421
|
+
if (cleanupFailures.length > 0) {
|
|
422
|
+
throw new AggregateError([redemptionFailure.reason, ...cleanupFailures.map(result => result.reason)], 'member invite redemption failed and one or more issued descriptors could not be revoked');
|
|
423
|
+
}
|
|
424
|
+
throw redemptionFailure.reason;
|
|
425
|
+
}
|
|
426
|
+
const deadline = policy.now() + policy.timeoutMs;
|
|
427
|
+
let delay = policy.initialDelayMs;
|
|
428
|
+
for (;;) {
|
|
429
|
+
const reconciled = await cowork.recoverRoom(roomId);
|
|
430
|
+
const waiting = members.some(member => {
|
|
431
|
+
const seat = reconciled.seats.find(candidate => candidate.identity_cid === member.cid && candidate.seat_state !== 'removed');
|
|
432
|
+
if (seat && seat.role !== member.coworkRole) {
|
|
433
|
+
throw new Error(`Cowork seat ${member.cid} has role ${seat.role}; expected ${member.coworkRole}`);
|
|
434
|
+
}
|
|
435
|
+
return !seat;
|
|
376
436
|
});
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
437
|
+
if (!waiting)
|
|
438
|
+
break;
|
|
439
|
+
if (policy.now() >= deadline) {
|
|
440
|
+
throw new Error('Cowork did not authenticate every required room seat after invite redemption');
|
|
381
441
|
}
|
|
442
|
+
await policy.sleep(delay);
|
|
443
|
+
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
382
444
|
}
|
|
383
445
|
}
|
|
384
446
|
catch (error) {
|
|
@@ -423,13 +485,6 @@ export async function provisionMembers(input) {
|
|
|
423
485
|
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Member launch failed. Some agents may be running. Retry with `task recover`.', 'member_failed');
|
|
424
486
|
throw error;
|
|
425
487
|
}
|
|
426
|
-
const policy = {
|
|
427
|
-
timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
|
|
428
|
-
initialDelayMs: input.startupWait?.initialDelayMs ?? 250,
|
|
429
|
-
maxDelayMs: input.startupWait?.maxDelayMs ?? 2_000,
|
|
430
|
-
now: input.startupWait?.now ?? Date.now,
|
|
431
|
-
sleep: input.startupWait?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
432
|
-
};
|
|
433
488
|
const deadline = policy.now() + policy.timeoutMs;
|
|
434
489
|
let delay = policy.initialDelayMs;
|
|
435
490
|
for (;;) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux or ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|