@ours.network/fleet 1.0.1 → 1.0.2
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 +14 -7
- package/dist/briefing.d.ts +2 -1
- package/dist/briefing.js +47 -86
- package/dist/build-info.json +4 -4
- package/dist/config.d.ts +8 -6
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +18 -6
- package/dist/harness/claude-code.js +1 -1
- package/dist/harness/types.d.ts +3 -3
- package/dist/rooms-tasks/cli.js +8 -19
- package/dist/rooms-tasks/close.js +3 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +3 -0
- package/dist/rooms-tasks/cowork-adapter.js +3 -1
- package/dist/rooms-tasks/member-startup.d.ts +5 -38
- package/dist/rooms-tasks/member-startup.js +3 -119
- package/dist/rooms-tasks/provision.js +194 -409
- package/dist/rooms-tasks/room-state.d.ts +1 -2
- package/dist/rooms-tasks/room-state.js +0 -9
- package/dist/rooms-tasks/types.d.ts +4 -20
- package/dist/session/acp.d.ts +4 -11
- package/dist/session/acp.js +7 -12
- package/dist/spawn.d.ts +3 -3
- package/dist/spawn.js +6 -14
- package/package.json +1 -1
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { activateTask, updateTaskMembers, failTask, blockTask, unblockTask, getTask, } from './task-state.js';
|
|
4
|
+
import { advanceSaga, setSagaError, updateMemberSeats, updateMemberStartup, activateRoom, getRoomRecord, } from './room-state.js';
|
|
5
|
+
import { activateTask, updateTaskMembers, blockTask, unblockTask, getTask, } from './task-state.js';
|
|
7
6
|
import { spawnTemp } from '../spawn.js';
|
|
8
7
|
import { findRole } from '../config.js';
|
|
9
8
|
import { closeManagedRoom } from './close.js';
|
|
10
|
-
import {
|
|
9
|
+
import { buildRoomMemberTask, sha256Text } from './member-startup.js';
|
|
11
10
|
import { agentDir } from '../paths.js';
|
|
12
11
|
import { readProvenance } from '../creation.js';
|
|
13
12
|
import { readTempSupervisor, secureStoppedTempArchive, tempArchiveForCreationAction, tempSupervisorLiveness, } from '../temp-lifecycle.js';
|
|
@@ -35,61 +34,6 @@ function expandMembers(template, prefix) {
|
|
|
35
34
|
}
|
|
36
35
|
return result;
|
|
37
36
|
}
|
|
38
|
-
async function createMemberIdentity(name, bio) {
|
|
39
|
-
const client = await attachOursClient({
|
|
40
|
-
env: process.env,
|
|
41
|
-
leaseToken: `ours-fleet-member-create-${process.pid}-${randomUUID()}`,
|
|
42
|
-
clientPid: process.pid,
|
|
43
|
-
});
|
|
44
|
-
try {
|
|
45
|
-
const result = await client.createIdentity({
|
|
46
|
-
name,
|
|
47
|
-
bio,
|
|
48
|
-
exposeLocal: true,
|
|
49
|
-
localAutoAccept: true,
|
|
50
|
-
});
|
|
51
|
-
return { cid: result.info.cid, client };
|
|
52
|
-
}
|
|
53
|
-
finally {
|
|
54
|
-
await client.releaseLease();
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
async function removeMemberIdentity(name) {
|
|
58
|
-
try {
|
|
59
|
-
const client = await attachOursClient({
|
|
60
|
-
env: process.env,
|
|
61
|
-
leaseToken: `ours-fleet-member-cleanup-${process.pid}-${randomUUID()}`,
|
|
62
|
-
clientPid: process.pid,
|
|
63
|
-
});
|
|
64
|
-
try {
|
|
65
|
-
await client.removeIdentity({ name });
|
|
66
|
-
}
|
|
67
|
-
finally {
|
|
68
|
-
await client.releaseLease();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
catch { /* best-effort cleanup */ }
|
|
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
|
-
}
|
|
93
37
|
function settingsFor(member, cfg) {
|
|
94
38
|
let refRole;
|
|
95
39
|
try {
|
|
@@ -103,66 +47,57 @@ function settingsFor(member, cfg) {
|
|
|
103
47
|
persona: member.overrides?.persona ?? refRole?.persona,
|
|
104
48
|
};
|
|
105
49
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
throw new Error(`Cowork room ${roomId} is missing during briefing verification`);
|
|
126
|
-
for (const [role, expected] of definitions) {
|
|
127
|
-
const current = room.role_briefings[role];
|
|
128
|
-
if (!current || current.text !== expected.text
|
|
129
|
-
|| sha256Text(current.text) !== expected.sha256
|
|
130
|
-
|| (expected.version !== undefined && current.version !== expected.version)) {
|
|
131
|
-
throw new Error(`Cowork current role briefing drift for ${role}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
function startupMission(gate) {
|
|
136
|
-
return [
|
|
137
|
-
'Complete the dedicated room startup gate in briefing.md before readiness.',
|
|
138
|
-
`Expected room ${gate.room_id} (${gate.room_identity_cid}), role ${gate.briefing_role},`,
|
|
139
|
-
`briefing version ${gate.briefing_version}, sha256 ${gate.briefing_sha256}.`,
|
|
140
|
-
].join(' ');
|
|
50
|
+
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid) {
|
|
51
|
+
return buildRoomMemberTask({
|
|
52
|
+
taskId: input.taskId,
|
|
53
|
+
roomId: input.roomId,
|
|
54
|
+
roomIdentityCid,
|
|
55
|
+
ownerSeatCid,
|
|
56
|
+
goal: input.goal,
|
|
57
|
+
brief: input.brief,
|
|
58
|
+
contract: input.template.contract,
|
|
59
|
+
member: {
|
|
60
|
+
role_name: member.name,
|
|
61
|
+
cowork_role: member.coworkRole,
|
|
62
|
+
persona: settings.persona,
|
|
63
|
+
},
|
|
64
|
+
roster: members.map(candidate => ({
|
|
65
|
+
role_name: candidate.name,
|
|
66
|
+
cowork_role: candidate.coworkRole,
|
|
67
|
+
})),
|
|
68
|
+
});
|
|
141
69
|
}
|
|
142
|
-
function launchMatches(dir, member, actionId,
|
|
70
|
+
function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid, expectedInviteId) {
|
|
143
71
|
const provenance = readProvenance(dir);
|
|
144
72
|
if (provenance?.creationActionId !== actionId || provenance.role !== member.name)
|
|
145
73
|
return false;
|
|
146
74
|
try {
|
|
147
75
|
const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
|
|
76
|
+
const startup = role.roomMemberStartup;
|
|
148
77
|
return role.identity === member.name
|
|
149
78
|
&& typeof role.mission === 'string'
|
|
150
|
-
&& sha256Text(role.mission) ===
|
|
151
|
-
&&
|
|
79
|
+
&& sha256Text(role.mission) === taskSha
|
|
80
|
+
&& startup?.room_id === roomId
|
|
81
|
+
&& startup.room_identity_cid === roomIdentityCid
|
|
82
|
+
&& startup.identity_name === member.name
|
|
83
|
+
&& startup.role === member.coworkRole
|
|
84
|
+
&& startup.task === role.mission
|
|
85
|
+
&& (expectedInviteId === undefined || startup.invite_id === expectedInviteId)
|
|
86
|
+
&& typeof startup.invite === 'string'
|
|
87
|
+
&& startup.invite.length > 0;
|
|
152
88
|
}
|
|
153
89
|
catch {
|
|
154
90
|
return false;
|
|
155
91
|
}
|
|
156
92
|
}
|
|
157
|
-
async function
|
|
158
|
-
const { provision, member,
|
|
93
|
+
async function retainRunningLaunch(input) {
|
|
94
|
+
const { provision, member, task, roomIdentityCid } = input;
|
|
159
95
|
let seat = getRoomRecord(provision.roomId).member_seats
|
|
160
96
|
.find(candidate => candidate.role_name === member.name);
|
|
161
|
-
const mission = startupMission(gate);
|
|
162
|
-
const missionSha = sha256Text(mission);
|
|
163
97
|
const dir = agentDir(member.name, true);
|
|
98
|
+
const taskSha = sha256Text(task);
|
|
164
99
|
if ((seat.launch?.state === 'intent' || seat.launch?.state === 'launched') && existsSync(dir)) {
|
|
165
|
-
if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id,
|
|
100
|
+
if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
166
101
|
throw new Error(`existing launch for ${member.name} does not match its durable intent`);
|
|
167
102
|
}
|
|
168
103
|
const supervisor = readTempSupervisor(dir);
|
|
@@ -176,204 +111,141 @@ async function ensureLaunch(input) {
|
|
|
176
111
|
...seat.launch, state: 'launched', launch_id: supervisor.launchId,
|
|
177
112
|
updated_at: new Date().toISOString(),
|
|
178
113
|
} });
|
|
179
|
-
return;
|
|
114
|
+
return true;
|
|
180
115
|
}
|
|
181
116
|
await secureStoppedTempArchive(member.name, supervisor.launchId);
|
|
182
117
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
183
118
|
...seat.launch, state: 'stopped', launch_id: supervisor.launchId,
|
|
184
119
|
updated_at: new Date().toISOString(),
|
|
185
120
|
} });
|
|
186
|
-
|
|
187
|
-
.find(candidate => candidate.role_name === member.name);
|
|
121
|
+
return false;
|
|
188
122
|
}
|
|
189
|
-
|
|
123
|
+
if (seat.launch?.state === 'launched' && !existsSync(dir)) {
|
|
190
124
|
if (!seat.launch.launch_id || !seat.launch.action_id)
|
|
191
125
|
throw new Error(`missing durable launch identity for disappeared ${member.name}`);
|
|
192
126
|
const archive = await secureStoppedTempArchive(member.name, seat.launch.launch_id);
|
|
193
|
-
if (!launchMatches(archive, member, seat.launch.action_id,
|
|
127
|
+
if (!launchMatches(archive, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
194
128
|
throw new Error(`archive for disappeared ${member.name} does not match its durable intent`);
|
|
129
|
+
}
|
|
195
130
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
196
131
|
...seat.launch, state: 'stopped', updated_at: new Date().toISOString(),
|
|
197
132
|
} });
|
|
198
|
-
|
|
199
|
-
.find(candidate => candidate.role_name === member.name);
|
|
133
|
+
return false;
|
|
200
134
|
}
|
|
201
|
-
|
|
135
|
+
if (seat.launch?.state === 'intent' && !existsSync(dir)) {
|
|
202
136
|
if (!seat.launch.action_id)
|
|
203
137
|
throw new Error(`missing action ID for disappeared launch intent ${member.name}`);
|
|
204
138
|
const archive = tempArchiveForCreationAction(member.name, seat.launch.action_id);
|
|
205
|
-
if (!archive || !launchMatches(archive.path, member, seat.launch.action_id,
|
|
139
|
+
if (!archive || !launchMatches(archive.path, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
206
140
|
throw new Error(`launch intent for ${member.name} has no exact live or terminated archive evidence`);
|
|
207
141
|
}
|
|
208
142
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
209
143
|
...seat.launch, state: 'stopped', launch_id: archive.launchId,
|
|
210
144
|
updated_at: new Date().toISOString(),
|
|
211
145
|
} });
|
|
212
|
-
seat = getRoomRecord(provision.roomId).member_seats
|
|
213
|
-
.find(candidate => candidate.role_name === member.name);
|
|
214
146
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
async function launchMember(input) {
|
|
150
|
+
const { provision, member, settings, startup } = input;
|
|
151
|
+
const seat = getRoomRecord(provision.roomId).member_seats
|
|
152
|
+
.find(candidate => candidate.role_name === member.name);
|
|
153
|
+
const actionId = randomUUID();
|
|
154
|
+
const attempt = (seat.launch?.attempt ?? 0) + 1;
|
|
155
|
+
const taskSha = sha256Text(startup.task);
|
|
219
156
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
220
|
-
state: 'intent', attempt, action_id: actionId, mission_sha256:
|
|
157
|
+
state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
|
|
221
158
|
updated_at: new Date().toISOString(),
|
|
222
159
|
} });
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
160
|
+
try {
|
|
161
|
+
const launchedDir = await spawnTemp({
|
|
162
|
+
name: member.name,
|
|
163
|
+
temp: true,
|
|
164
|
+
identity: member.name,
|
|
165
|
+
mission: startup.task,
|
|
166
|
+
model: settings.model,
|
|
167
|
+
harness: settings.harness,
|
|
168
|
+
cwd: settings.cwd,
|
|
169
|
+
surface: 'agent',
|
|
170
|
+
creationActionId: actionId,
|
|
171
|
+
roomMemberStartup: startup,
|
|
172
|
+
}, provision.binPath);
|
|
173
|
+
const supervisor = readTempSupervisor(launchedDir);
|
|
174
|
+
if (!supervisor || supervisor.role !== member.name || !launchMatches(launchedDir, member, actionId, taskSha, provision.roomId, startup.room_identity_cid, startup.invite_id)) {
|
|
175
|
+
throw new Error(`new launch for ${member.name} did not persist matching provenance`);
|
|
176
|
+
}
|
|
177
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
178
|
+
state: 'launched', attempt, action_id: actionId, mission_sha256: taskSha,
|
|
179
|
+
launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
|
|
180
|
+
} });
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
184
|
+
state: 'failed', attempt, action_id: actionId, mission_sha256: taskSha,
|
|
185
|
+
updated_at: new Date().toISOString(),
|
|
186
|
+
error: error instanceof Error ? error.message : String(error),
|
|
187
|
+
} });
|
|
188
|
+
throw error;
|
|
232
189
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
190
|
+
}
|
|
191
|
+
function exactCoworkSeat(observed, member, expectedInviteId) {
|
|
192
|
+
const named = observed.filter(seat => seat.display_name === member.name && seat.seat_state !== 'removed');
|
|
193
|
+
if (named.length > 1)
|
|
194
|
+
throw new Error(`Cowork has multiple live seats named ${member.name}`);
|
|
195
|
+
const seat = named[0];
|
|
196
|
+
if (!seat)
|
|
197
|
+
return undefined;
|
|
198
|
+
if (seat.role !== member.coworkRole)
|
|
199
|
+
throw new Error(`Cowork seat ${member.name} has role ${seat.role}; expected ${member.coworkRole}`);
|
|
200
|
+
if (expectedInviteId && seat.invite_id !== expectedInviteId) {
|
|
201
|
+
throw new Error(`Cowork seat ${member.name} used invite ${seat.invite_id}; expected ${expectedInviteId}`);
|
|
202
|
+
}
|
|
203
|
+
return seat;
|
|
204
|
+
}
|
|
205
|
+
function reconcileMemberSeats(roomId, members, observed) {
|
|
206
|
+
const current = getRoomRecord(roomId);
|
|
207
|
+
let complete = true;
|
|
208
|
+
const seats = current.member_seats.map(seat => {
|
|
209
|
+
const member = members.find(candidate => candidate.name === seat.role_name);
|
|
210
|
+
const found = exactCoworkSeat(observed, member, seat.invite_id);
|
|
211
|
+
if (!found || found.seat_state !== 'active') {
|
|
212
|
+
complete = false;
|
|
213
|
+
return seat;
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
...seat,
|
|
217
|
+
identity_cid: found.identity_cid,
|
|
218
|
+
invite_id: found.invite_id,
|
|
219
|
+
seat_state: 'active',
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
updateMemberSeats(roomId, seats);
|
|
223
|
+
return { complete, seats };
|
|
237
224
|
}
|
|
238
225
|
export async function provisionMembers(input) {
|
|
239
|
-
const { cfg, cowork, roomId, taskId, template
|
|
226
|
+
const { cfg, cowork, roomId, taskId, template } = input;
|
|
240
227
|
const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
|
|
241
|
-
const
|
|
242
|
-
const createdIdentities = [];
|
|
243
|
-
const members = [];
|
|
228
|
+
const members = expandMembers(template, prefix);
|
|
244
229
|
const existing = getRoomRecord(roomId);
|
|
245
230
|
if (!existing?.room_identity_cid)
|
|
246
231
|
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
247
232
|
const roomIdentityCid = existing.room_identity_cid;
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (resuming) {
|
|
253
|
-
for (const planned of plan) {
|
|
254
|
-
const seat = persistedSeats.find(s => s.role_name === planned.name);
|
|
255
|
-
members.push({ ...planned, cid: seat.identity_cid });
|
|
256
|
-
}
|
|
257
|
-
seats = persistedSeats.map(seat => ({
|
|
258
|
-
...seat,
|
|
259
|
-
launch: seat.launch ?? {
|
|
260
|
-
state: 'pending', attempt: 0, updated_at: existing.created_at,
|
|
261
|
-
},
|
|
262
|
-
briefing: seat.briefing ?? {
|
|
263
|
-
role: seat.cowork_role, state: 'pending', rejected_ack_count: 0,
|
|
264
|
-
},
|
|
265
|
-
}));
|
|
266
|
-
updateMemberSeats(roomId, seats);
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
// create_members saga phase
|
|
233
|
+
const ownerSeatCid = existing.owner_seat_cid ?? null;
|
|
234
|
+
const persistedNames = new Set(existing.member_seats.map(seat => seat.role_name));
|
|
235
|
+
const resuming = members.length > 0
|
|
236
|
+
&& members.every(member => persistedNames.has(member.name));
|
|
237
|
+
if (!resuming) {
|
|
270
238
|
advanceSaga(roomId, 'create_members', 3);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
createdIdentities.push(planned.name);
|
|
276
|
-
members.push({ ...planned, cid });
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
for (const name of createdIdentities)
|
|
281
|
-
await removeMemberIdentity(name);
|
|
282
|
-
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Member identity creation failed. Retry with `task recover`.', 'member_failed');
|
|
283
|
-
if (taskId)
|
|
284
|
-
failTask(taskId, error instanceof Error ? error.message : String(error));
|
|
285
|
-
throw error;
|
|
286
|
-
}
|
|
287
|
-
seats = members.map(m => ({
|
|
288
|
-
role_name: m.name,
|
|
289
|
-
identity_cid: m.cid,
|
|
290
|
-
slot: m.slot,
|
|
291
|
-
cowork_role: m.coworkRole,
|
|
239
|
+
updateMemberSeats(roomId, members.map(member => ({
|
|
240
|
+
role_name: member.name,
|
|
241
|
+
slot: member.slot,
|
|
242
|
+
cowork_role: member.coworkRole,
|
|
292
243
|
seat_state: 'pending',
|
|
293
244
|
launch: { state: 'pending', attempt: 0, updated_at: new Date().toISOString() },
|
|
294
|
-
|
|
295
|
-
role: m.coworkRole, state: 'pending', rejected_ack_count: 0,
|
|
296
|
-
},
|
|
297
|
-
}));
|
|
298
|
-
updateMemberSeats(roomId, seats);
|
|
299
|
-
if (taskId) {
|
|
300
|
-
const taskMembers = members.map(m => ({
|
|
301
|
-
name: m.name,
|
|
302
|
-
identity_cid: m.cid,
|
|
303
|
-
slot: m.slot,
|
|
304
|
-
cowork_role: m.coworkRole,
|
|
305
|
-
}));
|
|
306
|
-
updateTaskMembers(taskId, taskMembers);
|
|
307
|
-
}
|
|
245
|
+
})));
|
|
308
246
|
}
|
|
309
247
|
const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg)]));
|
|
310
|
-
const
|
|
311
|
-
role_name: member.name, cowork_role: member.coworkRole, identity_cid: member.cid,
|
|
312
|
-
}));
|
|
313
|
-
const definitions = new Map();
|
|
314
|
-
for (const member of members) {
|
|
315
|
-
const durable = getRoomRecord(roomId).role_briefings?.[member.coworkRole];
|
|
316
|
-
const text = durable?.text ?? buildRoomMemberCharter({
|
|
317
|
-
taskId, roomId, roomIdentityCid: existing.room_identity_cid,
|
|
318
|
-
ownerSeatCid: existing.owner_seat_cid ?? null,
|
|
319
|
-
goal: input.goal, brief: input.brief, contract: template.contract,
|
|
320
|
-
member: { ...roster.find(role => role.role_name === member.name),
|
|
321
|
-
persona: settings.get(member.name)?.persona },
|
|
322
|
-
roster,
|
|
323
|
-
});
|
|
324
|
-
const sha256 = sha256Text(text);
|
|
325
|
-
if (durable && durable.sha256 !== sha256)
|
|
326
|
-
throw new Error(`persisted role briefing hash mismatch for ${member.coworkRole}`);
|
|
327
|
-
const known = definitions.get(member.coworkRole);
|
|
328
|
-
if (known && known.text !== text) {
|
|
329
|
-
throw new Error(`members sharing Cowork role ${member.coworkRole} produced different briefing bytes`);
|
|
330
|
-
}
|
|
331
|
-
definitions.set(member.coworkRole, { text, sha256 });
|
|
332
|
-
}
|
|
333
|
-
advanceSaga(roomId, 'configure_briefings', 4);
|
|
334
|
-
const remoteRoom = await cowork.getRoom(roomId);
|
|
335
|
-
if (!remoteRoom)
|
|
336
|
-
throw new Error(`Cowork room ${roomId} is missing during briefing configuration`);
|
|
337
|
-
for (const [role, definition] of definitions) {
|
|
338
|
-
const current = getRoomRecord(roomId).role_briefings?.[role];
|
|
339
|
-
const attempt = (current?.attempts ?? 0) + 1;
|
|
340
|
-
updateRoomRoleBriefing(roomId, role, {
|
|
341
|
-
role, ...definition, ...(current?.version ? { version: current.version } : {}),
|
|
342
|
-
state: 'pending', attempts: attempt,
|
|
343
|
-
updated_at: new Date().toISOString(),
|
|
344
|
-
});
|
|
345
|
-
try {
|
|
346
|
-
const remote = remoteRoom.role_briefings[role];
|
|
347
|
-
if (remote && (remote.text !== definition.text
|
|
348
|
-
|| sha256Text(remote.text) !== definition.sha256
|
|
349
|
-
|| (current?.version !== undefined && remote.version !== current.version))) {
|
|
350
|
-
throw new Error(`Cowork current role briefing drift for ${role}`);
|
|
351
|
-
}
|
|
352
|
-
if (!remote && current?.version !== undefined)
|
|
353
|
-
throw new Error(`Cowork current role briefing is missing for ${role}`);
|
|
354
|
-
const configured = remote ?? await cowork.setRoleBriefing(roomId, {
|
|
355
|
-
role, text: definition.text,
|
|
356
|
-
});
|
|
357
|
-
if (configured.text !== definition.text)
|
|
358
|
-
throw new Error(`Cowork returned different briefing bytes for role ${role}`);
|
|
359
|
-
updateRoomRoleBriefing(roomId, role, {
|
|
360
|
-
role, ...definition, version: configured.version, state: 'configured',
|
|
361
|
-
attempts: attempt, updated_at: configured.updated_at,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
catch (error) {
|
|
365
|
-
updateRoomRoleBriefing(roomId, role, {
|
|
366
|
-
role, ...definition, ...(current?.version ? { version: current.version } : {}),
|
|
367
|
-
state: 'failed', attempts: attempt,
|
|
368
|
-
updated_at: new Date().toISOString(),
|
|
369
|
-
last_error: error instanceof Error ? error.message : String(error),
|
|
370
|
-
});
|
|
371
|
-
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Role briefing configuration failed. Retry task or room recover.', 'member_failed');
|
|
372
|
-
if (taskId)
|
|
373
|
-
blockTask(taskId, error instanceof Error ? error.message : String(error));
|
|
374
|
-
throw error;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
248
|
+
const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid)]));
|
|
377
249
|
const policy = {
|
|
378
250
|
timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
|
|
379
251
|
initialDelayMs: input.startupWait?.initialDelayMs ?? 250,
|
|
@@ -381,178 +253,91 @@ export async function provisionMembers(input) {
|
|
|
381
253
|
now: input.startupWait?.now ?? Date.now,
|
|
382
254
|
sleep: input.startupWait?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
383
255
|
};
|
|
384
|
-
advanceSaga(roomId, 'join_role_groups',
|
|
256
|
+
advanceSaga(roomId, 'join_role_groups', 4);
|
|
385
257
|
try {
|
|
386
|
-
const
|
|
258
|
+
const initialRoom = await cowork.recoverRoom(roomId);
|
|
259
|
+
reconcileMemberSeats(roomId, members, initialRoom.seats);
|
|
387
260
|
for (const member of members) {
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
for (const member of members.filter(candidate => !admitted.has(candidate.cid))) {
|
|
397
|
-
roleGroups.set(member.coworkRole, [
|
|
398
|
-
...(roleGroups.get(member.coworkRole) ?? []), member,
|
|
399
|
-
]);
|
|
400
|
-
}
|
|
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}`);
|
|
261
|
+
const task = tasks.get(member.name);
|
|
262
|
+
const currentSeat = getRoomRecord(roomId).member_seats
|
|
263
|
+
.find(seat => seat.role_name === member.name);
|
|
264
|
+
if (currentSeat.seat_state === 'active') {
|
|
265
|
+
if (!await retainRunningLaunch({
|
|
266
|
+
provision: input, member, task, roomIdentityCid,
|
|
267
|
+
})) {
|
|
268
|
+
throw new Error(`active Cowork seat ${member.name} has no matching live Fleet launch`);
|
|
434
269
|
}
|
|
435
|
-
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (await retainRunningLaunch({ provision: input, member, task, roomIdentityCid }))
|
|
273
|
+
continue;
|
|
274
|
+
const issued = await cowork.issueInvite(roomId, {
|
|
275
|
+
mode: 'one_time', role: member.coworkRole, min_accepts: 1,
|
|
436
276
|
});
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
277
|
+
const seats = getRoomRecord(roomId).member_seats.map(seat => seat.role_name === member.name ? { ...seat, invite_id: issued.invite_id } : seat);
|
|
278
|
+
updateMemberSeats(roomId, seats);
|
|
279
|
+
try {
|
|
280
|
+
await launchMember({
|
|
281
|
+
provision: input,
|
|
282
|
+
member,
|
|
283
|
+
settings: settings.get(member.name),
|
|
284
|
+
startup: {
|
|
285
|
+
room_id: roomId,
|
|
286
|
+
room_identity_cid: roomIdentityCid,
|
|
287
|
+
identity_name: member.name,
|
|
288
|
+
invite_id: issued.invite_id,
|
|
289
|
+
invite: issued.invite,
|
|
290
|
+
role: member.coworkRole,
|
|
291
|
+
task,
|
|
292
|
+
owner_seat_cid: ownerSeatCid,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
await cowork.revokeInvite(roomId, issued.invite_id).catch(() => { });
|
|
298
|
+
throw error;
|
|
441
299
|
}
|
|
442
|
-
await policy.sleep(delay);
|
|
443
|
-
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
444
300
|
}
|
|
445
301
|
}
|
|
446
302
|
catch (error) {
|
|
447
|
-
|
|
303
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
304
|
+
setSagaError(roomId, reason, 'Member invite or launch failed. Retry with `task recover`.', 'member_failed');
|
|
448
305
|
if (taskId)
|
|
449
|
-
blockTask(taskId,
|
|
450
|
-
throw error;
|
|
451
|
-
}
|
|
452
|
-
if (taskId && getTask(taskId).blocked)
|
|
453
|
-
unblockTask(taskId);
|
|
454
|
-
advanceSaga(roomId, 'wait_seats', 6);
|
|
455
|
-
const coworkSeats = await cowork.getSeats(roomId);
|
|
456
|
-
const memberCids = new Set(members.map(m => m.cid));
|
|
457
|
-
const allActive = [...memberCids].every(cid => coworkSeats.some(s => s.identity_cid === cid
|
|
458
|
-
&& s.role === members.find(member => member.cid === cid)?.coworkRole
|
|
459
|
-
&& s.seat_state === 'active'));
|
|
460
|
-
if (!allActive) {
|
|
461
|
-
advanceSaga(roomId, 'wait_seats', 6, 'waiting_seats');
|
|
462
|
-
// Seats are pending — return record for caller to retry later
|
|
463
|
-
return getRoomRecord(roomId);
|
|
464
|
-
}
|
|
465
|
-
const activeSeats = seats.map(s => ({ ...s, seat_state: 'active' }));
|
|
466
|
-
updateMemberSeats(roomId, activeSeats);
|
|
467
|
-
advanceSaga(roomId, 'launch_work', 7);
|
|
468
|
-
try {
|
|
469
|
-
for (const member of members) {
|
|
470
|
-
const definition = getRoomRecord(roomId).role_briefings?.[member.coworkRole];
|
|
471
|
-
if (!definition?.version)
|
|
472
|
-
throw new Error(`role ${member.coworkRole} briefing is not configured`);
|
|
473
|
-
await ensureLaunch({
|
|
474
|
-
provision: input, member, settings: settings.get(member.name),
|
|
475
|
-
gate: {
|
|
476
|
-
room_id: roomId, room_identity_cid: existing.room_identity_cid,
|
|
477
|
-
briefing_role: member.coworkRole, briefing_version: definition.version,
|
|
478
|
-
briefing_sha256: definition.sha256,
|
|
479
|
-
owner_seat_cid: existing.owner_seat_cid ?? null,
|
|
480
|
-
},
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
catch (error) {
|
|
485
|
-
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Member launch failed. Some agents may be running. Retry with `task recover`.', 'member_failed');
|
|
306
|
+
blockTask(taskId, reason);
|
|
486
307
|
throw error;
|
|
487
308
|
}
|
|
309
|
+
advanceSaga(roomId, 'wait_seats', 5);
|
|
488
310
|
const deadline = policy.now() + policy.timeoutMs;
|
|
489
311
|
let delay = policy.initialDelayMs;
|
|
490
312
|
for (;;) {
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
for (const member of members) {
|
|
495
|
-
const currentRoom = getRoomRecord(roomId);
|
|
496
|
-
const definition = currentRoom.role_briefings[member.coworkRole];
|
|
497
|
-
const previous = currentRoom.member_seats
|
|
498
|
-
.find(seat => seat.role_name === member.name).briefing;
|
|
499
|
-
const result = reconcileBriefingHistory(history.records, {
|
|
500
|
-
roomId, roomIdentityCid: existing.room_identity_cid,
|
|
501
|
-
ownerSeatCid: existing.owner_seat_cid ?? null,
|
|
502
|
-
memberCid: member.cid, role: member.coworkRole,
|
|
503
|
-
version: definition.version, sha256: definition.sha256,
|
|
504
|
-
}, previous);
|
|
505
|
-
if (result.drift) {
|
|
506
|
-
setSagaError(roomId, result.drift, 'Cowork role briefing history drifted; inspect current room briefing state.', 'uncertain');
|
|
507
|
-
if (taskId)
|
|
508
|
-
blockTask(taskId, result.drift);
|
|
509
|
-
throw new Error(result.drift);
|
|
510
|
-
}
|
|
511
|
-
updateMemberStartup(roomId, member.name, { briefing: result.briefing });
|
|
512
|
-
allAcknowledged &&= result.briefing.state === 'acknowledged';
|
|
513
|
-
relayFailed ||= result.briefing.state === 'relay_failed';
|
|
514
|
-
}
|
|
515
|
-
updateRoomHistoryCursor(roomId, history.cursor);
|
|
516
|
-
if (!history.exhausted) {
|
|
517
|
-
const reason = 'Cowork history backlog exceeded 20,000 raw records in one pass; '
|
|
518
|
-
+ 'cursor progress was preserved, retry recovery to continue.';
|
|
519
|
-
setSagaError(roomId, reason, reason, 'uncertain');
|
|
520
|
-
throw new Error(reason);
|
|
521
|
-
}
|
|
522
|
-
if (relayFailed) {
|
|
523
|
-
const reason = 'Cowork recorded terminal send_failed for a role briefing; '
|
|
524
|
-
+ 'same-version redelivery requires Cowork support or seat replacement.';
|
|
525
|
-
advanceSaga(roomId, 'wait_briefing_acks', 8, 'briefing_delivery_failed');
|
|
526
|
-
setSagaError(roomId, reason, reason, 'briefing_delivery_failed');
|
|
527
|
-
if (taskId)
|
|
528
|
-
blockTask(taskId, reason);
|
|
529
|
-
return getRoomRecord(roomId);
|
|
530
|
-
}
|
|
531
|
-
if (allAcknowledged)
|
|
313
|
+
const remote = await cowork.recoverRoom(roomId);
|
|
314
|
+
const reconciled = reconcileMemberSeats(roomId, members, remote.seats);
|
|
315
|
+
if (reconciled.complete)
|
|
532
316
|
break;
|
|
533
317
|
if (policy.now() >= deadline) {
|
|
534
|
-
advanceSaga(roomId, '
|
|
318
|
+
advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
|
|
535
319
|
return getRoomRecord(roomId);
|
|
536
320
|
}
|
|
537
|
-
advanceSaga(roomId, '
|
|
321
|
+
advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
|
|
538
322
|
await policy.sleep(delay);
|
|
539
323
|
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
540
324
|
}
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
325
|
+
if (taskId) {
|
|
326
|
+
const taskMembers = getRoomRecord(roomId).member_seats.map(seat => {
|
|
327
|
+
if (!seat.identity_cid)
|
|
328
|
+
throw new Error(`active room member ${seat.role_name} has no authenticated identity CID`);
|
|
329
|
+
return {
|
|
330
|
+
name: seat.role_name,
|
|
331
|
+
identity_cid: seat.identity_cid,
|
|
332
|
+
slot: seat.slot,
|
|
333
|
+
cowork_role: seat.cowork_role,
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
updateTaskMembers(taskId, taskMembers);
|
|
337
|
+
if (getTask(taskId).blocked)
|
|
338
|
+
unblockTask(taskId);
|
|
554
339
|
}
|
|
555
|
-
advanceSaga(roomId, 'activate',
|
|
340
|
+
advanceSaga(roomId, 'activate', 6);
|
|
556
341
|
const record = activateRoom(roomId);
|
|
557
342
|
if (taskId)
|
|
558
343
|
activateTask(taskId);
|