@ours.network/fleet 1.0.0 → 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 +11 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
- package/dist/rooms-tasks/cowork-adapter.js +7 -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 +202 -362
- 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,41 +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
37
|
function settingsFor(member, cfg) {
|
|
74
38
|
let refRole;
|
|
75
39
|
try {
|
|
@@ -83,66 +47,57 @@ function settingsFor(member, cfg) {
|
|
|
83
47
|
persona: member.overrides?.persona ?? refRole?.persona,
|
|
84
48
|
};
|
|
85
49
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
throw new Error(`Cowork room ${roomId} is missing during briefing verification`);
|
|
106
|
-
for (const [role, expected] of definitions) {
|
|
107
|
-
const current = room.role_briefings[role];
|
|
108
|
-
if (!current || current.text !== expected.text
|
|
109
|
-
|| sha256Text(current.text) !== expected.sha256
|
|
110
|
-
|| (expected.version !== undefined && current.version !== expected.version)) {
|
|
111
|
-
throw new Error(`Cowork current role briefing drift for ${role}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function startupMission(gate) {
|
|
116
|
-
return [
|
|
117
|
-
'Complete the dedicated room startup gate in briefing.md before readiness.',
|
|
118
|
-
`Expected room ${gate.room_id} (${gate.room_identity_cid}), role ${gate.briefing_role},`,
|
|
119
|
-
`briefing version ${gate.briefing_version}, sha256 ${gate.briefing_sha256}.`,
|
|
120
|
-
].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
|
+
});
|
|
121
69
|
}
|
|
122
|
-
function launchMatches(dir, member, actionId,
|
|
70
|
+
function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid, expectedInviteId) {
|
|
123
71
|
const provenance = readProvenance(dir);
|
|
124
72
|
if (provenance?.creationActionId !== actionId || provenance.role !== member.name)
|
|
125
73
|
return false;
|
|
126
74
|
try {
|
|
127
75
|
const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
|
|
76
|
+
const startup = role.roomMemberStartup;
|
|
128
77
|
return role.identity === member.name
|
|
129
78
|
&& typeof role.mission === 'string'
|
|
130
|
-
&& sha256Text(role.mission) ===
|
|
131
|
-
&&
|
|
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;
|
|
132
88
|
}
|
|
133
89
|
catch {
|
|
134
90
|
return false;
|
|
135
91
|
}
|
|
136
92
|
}
|
|
137
|
-
async function
|
|
138
|
-
const { provision, member,
|
|
93
|
+
async function retainRunningLaunch(input) {
|
|
94
|
+
const { provision, member, task, roomIdentityCid } = input;
|
|
139
95
|
let seat = getRoomRecord(provision.roomId).member_seats
|
|
140
96
|
.find(candidate => candidate.role_name === member.name);
|
|
141
|
-
const mission = startupMission(gate);
|
|
142
|
-
const missionSha = sha256Text(mission);
|
|
143
97
|
const dir = agentDir(member.name, true);
|
|
98
|
+
const taskSha = sha256Text(task);
|
|
144
99
|
if ((seat.launch?.state === 'intent' || seat.launch?.state === 'launched') && existsSync(dir)) {
|
|
145
|
-
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)) {
|
|
146
101
|
throw new Error(`existing launch for ${member.name} does not match its durable intent`);
|
|
147
102
|
}
|
|
148
103
|
const supervisor = readTempSupervisor(dir);
|
|
@@ -156,348 +111,233 @@ async function ensureLaunch(input) {
|
|
|
156
111
|
...seat.launch, state: 'launched', launch_id: supervisor.launchId,
|
|
157
112
|
updated_at: new Date().toISOString(),
|
|
158
113
|
} });
|
|
159
|
-
return;
|
|
114
|
+
return true;
|
|
160
115
|
}
|
|
161
116
|
await secureStoppedTempArchive(member.name, supervisor.launchId);
|
|
162
117
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
163
118
|
...seat.launch, state: 'stopped', launch_id: supervisor.launchId,
|
|
164
119
|
updated_at: new Date().toISOString(),
|
|
165
120
|
} });
|
|
166
|
-
|
|
167
|
-
.find(candidate => candidate.role_name === member.name);
|
|
121
|
+
return false;
|
|
168
122
|
}
|
|
169
|
-
|
|
123
|
+
if (seat.launch?.state === 'launched' && !existsSync(dir)) {
|
|
170
124
|
if (!seat.launch.launch_id || !seat.launch.action_id)
|
|
171
125
|
throw new Error(`missing durable launch identity for disappeared ${member.name}`);
|
|
172
126
|
const archive = await secureStoppedTempArchive(member.name, seat.launch.launch_id);
|
|
173
|
-
if (!launchMatches(archive, member, seat.launch.action_id,
|
|
127
|
+
if (!launchMatches(archive, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
174
128
|
throw new Error(`archive for disappeared ${member.name} does not match its durable intent`);
|
|
129
|
+
}
|
|
175
130
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
176
131
|
...seat.launch, state: 'stopped', updated_at: new Date().toISOString(),
|
|
177
132
|
} });
|
|
178
|
-
|
|
179
|
-
.find(candidate => candidate.role_name === member.name);
|
|
133
|
+
return false;
|
|
180
134
|
}
|
|
181
|
-
|
|
135
|
+
if (seat.launch?.state === 'intent' && !existsSync(dir)) {
|
|
182
136
|
if (!seat.launch.action_id)
|
|
183
137
|
throw new Error(`missing action ID for disappeared launch intent ${member.name}`);
|
|
184
138
|
const archive = tempArchiveForCreationAction(member.name, seat.launch.action_id);
|
|
185
|
-
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)) {
|
|
186
140
|
throw new Error(`launch intent for ${member.name} has no exact live or terminated archive evidence`);
|
|
187
141
|
}
|
|
188
142
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
189
143
|
...seat.launch, state: 'stopped', launch_id: archive.launchId,
|
|
190
144
|
updated_at: new Date().toISOString(),
|
|
191
145
|
} });
|
|
192
|
-
seat = getRoomRecord(provision.roomId).member_seats
|
|
193
|
-
.find(candidate => candidate.role_name === member.name);
|
|
194
146
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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);
|
|
199
156
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
200
|
-
state: 'intent', attempt, action_id: actionId, mission_sha256:
|
|
157
|
+
state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
|
|
201
158
|
updated_at: new Date().toISOString(),
|
|
202
159
|
} });
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
} });
|
|
212
181
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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;
|
|
189
|
+
}
|
|
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 };
|
|
217
224
|
}
|
|
218
225
|
export async function provisionMembers(input) {
|
|
219
|
-
const { cfg, cowork, roomId, taskId, template
|
|
226
|
+
const { cfg, cowork, roomId, taskId, template } = input;
|
|
220
227
|
const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
|
|
221
|
-
const
|
|
222
|
-
const createdIdentities = [];
|
|
223
|
-
const members = [];
|
|
228
|
+
const members = expandMembers(template, prefix);
|
|
224
229
|
const existing = getRoomRecord(roomId);
|
|
225
230
|
if (!existing?.room_identity_cid)
|
|
226
231
|
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const seat = persistedSeats.find(s => s.role_name === planned.name);
|
|
234
|
-
members.push({ ...planned, cid: seat.identity_cid });
|
|
235
|
-
}
|
|
236
|
-
seats = persistedSeats.map(seat => ({
|
|
237
|
-
...seat,
|
|
238
|
-
launch: seat.launch ?? {
|
|
239
|
-
state: 'pending', attempt: 0, updated_at: existing.created_at,
|
|
240
|
-
},
|
|
241
|
-
briefing: seat.briefing ?? {
|
|
242
|
-
role: seat.cowork_role, state: 'pending', rejected_ack_count: 0,
|
|
243
|
-
},
|
|
244
|
-
}));
|
|
245
|
-
updateMemberSeats(roomId, seats);
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
// create_members saga phase
|
|
232
|
+
const roomIdentityCid = existing.room_identity_cid;
|
|
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) {
|
|
249
238
|
advanceSaga(roomId, 'create_members', 3);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
createdIdentities.push(planned.name);
|
|
255
|
-
members.push({ ...planned, cid });
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
for (const name of createdIdentities)
|
|
260
|
-
await removeMemberIdentity(name);
|
|
261
|
-
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Member identity creation failed. Retry with `task recover`.', 'member_failed');
|
|
262
|
-
if (taskId)
|
|
263
|
-
failTask(taskId, error instanceof Error ? error.message : String(error));
|
|
264
|
-
throw error;
|
|
265
|
-
}
|
|
266
|
-
seats = members.map(m => ({
|
|
267
|
-
role_name: m.name,
|
|
268
|
-
identity_cid: m.cid,
|
|
269
|
-
slot: m.slot,
|
|
270
|
-
cowork_role: m.coworkRole,
|
|
239
|
+
updateMemberSeats(roomId, members.map(member => ({
|
|
240
|
+
role_name: member.name,
|
|
241
|
+
slot: member.slot,
|
|
242
|
+
cowork_role: member.coworkRole,
|
|
271
243
|
seat_state: 'pending',
|
|
272
244
|
launch: { state: 'pending', attempt: 0, updated_at: new Date().toISOString() },
|
|
273
|
-
|
|
274
|
-
role: m.coworkRole, state: 'pending', rejected_ack_count: 0,
|
|
275
|
-
},
|
|
276
|
-
}));
|
|
277
|
-
updateMemberSeats(roomId, seats);
|
|
278
|
-
if (taskId) {
|
|
279
|
-
const taskMembers = members.map(m => ({
|
|
280
|
-
name: m.name,
|
|
281
|
-
identity_cid: m.cid,
|
|
282
|
-
slot: m.slot,
|
|
283
|
-
cowork_role: m.coworkRole,
|
|
284
|
-
}));
|
|
285
|
-
updateTaskMembers(taskId, taskMembers);
|
|
286
|
-
}
|
|
245
|
+
})));
|
|
287
246
|
}
|
|
288
247
|
const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg)]));
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
goal: input.goal, brief: input.brief, contract: template.contract,
|
|
299
|
-
member: { ...roster.find(role => role.role_name === member.name),
|
|
300
|
-
persona: settings.get(member.name)?.persona },
|
|
301
|
-
roster,
|
|
302
|
-
});
|
|
303
|
-
const sha256 = sha256Text(text);
|
|
304
|
-
if (durable && durable.sha256 !== sha256)
|
|
305
|
-
throw new Error(`persisted role briefing hash mismatch for ${member.coworkRole}`);
|
|
306
|
-
const known = definitions.get(member.coworkRole);
|
|
307
|
-
if (known && known.text !== text) {
|
|
308
|
-
throw new Error(`members sharing Cowork role ${member.coworkRole} produced different briefing bytes`);
|
|
309
|
-
}
|
|
310
|
-
definitions.set(member.coworkRole, { text, sha256 });
|
|
311
|
-
}
|
|
312
|
-
advanceSaga(roomId, 'configure_briefings', 4);
|
|
313
|
-
const remoteRoom = await cowork.getRoom(roomId);
|
|
314
|
-
if (!remoteRoom)
|
|
315
|
-
throw new Error(`Cowork room ${roomId} is missing during briefing configuration`);
|
|
316
|
-
for (const [role, definition] of definitions) {
|
|
317
|
-
const current = getRoomRecord(roomId).role_briefings?.[role];
|
|
318
|
-
const attempt = (current?.attempts ?? 0) + 1;
|
|
319
|
-
updateRoomRoleBriefing(roomId, role, {
|
|
320
|
-
role, ...definition, ...(current?.version ? { version: current.version } : {}),
|
|
321
|
-
state: 'pending', attempts: attempt,
|
|
322
|
-
updated_at: new Date().toISOString(),
|
|
323
|
-
});
|
|
324
|
-
try {
|
|
325
|
-
const remote = remoteRoom.role_briefings[role];
|
|
326
|
-
if (remote && (remote.text !== definition.text
|
|
327
|
-
|| sha256Text(remote.text) !== definition.sha256
|
|
328
|
-
|| (current?.version !== undefined && remote.version !== current.version))) {
|
|
329
|
-
throw new Error(`Cowork current role briefing drift for ${role}`);
|
|
330
|
-
}
|
|
331
|
-
if (!remote && current?.version !== undefined)
|
|
332
|
-
throw new Error(`Cowork current role briefing is missing for ${role}`);
|
|
333
|
-
const configured = remote ?? await cowork.setRoleBriefing(roomId, {
|
|
334
|
-
role, text: definition.text,
|
|
335
|
-
});
|
|
336
|
-
if (configured.text !== definition.text)
|
|
337
|
-
throw new Error(`Cowork returned different briefing bytes for role ${role}`);
|
|
338
|
-
updateRoomRoleBriefing(roomId, role, {
|
|
339
|
-
role, ...definition, version: configured.version, state: 'configured',
|
|
340
|
-
attempts: attempt, updated_at: configured.updated_at,
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
catch (error) {
|
|
344
|
-
updateRoomRoleBriefing(roomId, role, {
|
|
345
|
-
role, ...definition, ...(current?.version ? { version: current.version } : {}),
|
|
346
|
-
state: 'failed', attempts: attempt,
|
|
347
|
-
updated_at: new Date().toISOString(),
|
|
348
|
-
last_error: error instanceof Error ? error.message : String(error),
|
|
349
|
-
});
|
|
350
|
-
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Role briefing configuration failed. Retry task or room recover.', 'member_failed');
|
|
351
|
-
if (taskId)
|
|
352
|
-
blockTask(taskId, error instanceof Error ? error.message : String(error));
|
|
353
|
-
throw error;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
advanceSaga(roomId, 'join_role_groups', 5);
|
|
248
|
+
const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid)]));
|
|
249
|
+
const policy = {
|
|
250
|
+
timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
|
|
251
|
+
initialDelayMs: input.startupWait?.initialDelayMs ?? 250,
|
|
252
|
+
maxDelayMs: input.startupWait?.maxDelayMs ?? 2_000,
|
|
253
|
+
now: input.startupWait?.now ?? Date.now,
|
|
254
|
+
sleep: input.startupWait?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
255
|
+
};
|
|
256
|
+
advanceSaga(roomId, 'join_role_groups', 4);
|
|
357
257
|
try {
|
|
358
|
-
const
|
|
258
|
+
const initialRoom = await cowork.recoverRoom(roomId);
|
|
259
|
+
reconcileMemberSeats(roomId, members, initialRoom.seats);
|
|
359
260
|
for (const member of members) {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
role: coworkRole, min_accepts: group.length,
|
|
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`);
|
|
269
|
+
}
|
|
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,
|
|
376
276
|
});
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
+
},
|
|
380
294
|
});
|
|
381
295
|
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
await cowork.revokeInvite(roomId, issued.invite_id).catch(() => { });
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
382
300
|
}
|
|
383
301
|
}
|
|
384
302
|
catch (error) {
|
|
385
|
-
|
|
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');
|
|
386
305
|
if (taskId)
|
|
387
|
-
blockTask(taskId,
|
|
388
|
-
throw error;
|
|
389
|
-
}
|
|
390
|
-
if (taskId && getTask(taskId).blocked)
|
|
391
|
-
unblockTask(taskId);
|
|
392
|
-
advanceSaga(roomId, 'wait_seats', 6);
|
|
393
|
-
const coworkSeats = await cowork.getSeats(roomId);
|
|
394
|
-
const memberCids = new Set(members.map(m => m.cid));
|
|
395
|
-
const allActive = [...memberCids].every(cid => coworkSeats.some(s => s.identity_cid === cid
|
|
396
|
-
&& s.role === members.find(member => member.cid === cid)?.coworkRole
|
|
397
|
-
&& s.seat_state === 'active'));
|
|
398
|
-
if (!allActive) {
|
|
399
|
-
advanceSaga(roomId, 'wait_seats', 6, 'waiting_seats');
|
|
400
|
-
// Seats are pending — return record for caller to retry later
|
|
401
|
-
return getRoomRecord(roomId);
|
|
402
|
-
}
|
|
403
|
-
const activeSeats = seats.map(s => ({ ...s, seat_state: 'active' }));
|
|
404
|
-
updateMemberSeats(roomId, activeSeats);
|
|
405
|
-
advanceSaga(roomId, 'launch_work', 7);
|
|
406
|
-
try {
|
|
407
|
-
for (const member of members) {
|
|
408
|
-
const definition = getRoomRecord(roomId).role_briefings?.[member.coworkRole];
|
|
409
|
-
if (!definition?.version)
|
|
410
|
-
throw new Error(`role ${member.coworkRole} briefing is not configured`);
|
|
411
|
-
await ensureLaunch({
|
|
412
|
-
provision: input, member, settings: settings.get(member.name),
|
|
413
|
-
gate: {
|
|
414
|
-
room_id: roomId, room_identity_cid: existing.room_identity_cid,
|
|
415
|
-
briefing_role: member.coworkRole, briefing_version: definition.version,
|
|
416
|
-
briefing_sha256: definition.sha256,
|
|
417
|
-
owner_seat_cid: existing.owner_seat_cid ?? null,
|
|
418
|
-
},
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
catch (error) {
|
|
423
|
-
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);
|
|
424
307
|
throw error;
|
|
425
308
|
}
|
|
426
|
-
|
|
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
|
-
};
|
|
309
|
+
advanceSaga(roomId, 'wait_seats', 5);
|
|
433
310
|
const deadline = policy.now() + policy.timeoutMs;
|
|
434
311
|
let delay = policy.initialDelayMs;
|
|
435
312
|
for (;;) {
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
for (const member of members) {
|
|
440
|
-
const currentRoom = getRoomRecord(roomId);
|
|
441
|
-
const definition = currentRoom.role_briefings[member.coworkRole];
|
|
442
|
-
const previous = currentRoom.member_seats
|
|
443
|
-
.find(seat => seat.role_name === member.name).briefing;
|
|
444
|
-
const result = reconcileBriefingHistory(history.records, {
|
|
445
|
-
roomId, roomIdentityCid: existing.room_identity_cid,
|
|
446
|
-
ownerSeatCid: existing.owner_seat_cid ?? null,
|
|
447
|
-
memberCid: member.cid, role: member.coworkRole,
|
|
448
|
-
version: definition.version, sha256: definition.sha256,
|
|
449
|
-
}, previous);
|
|
450
|
-
if (result.drift) {
|
|
451
|
-
setSagaError(roomId, result.drift, 'Cowork role briefing history drifted; inspect current room briefing state.', 'uncertain');
|
|
452
|
-
if (taskId)
|
|
453
|
-
blockTask(taskId, result.drift);
|
|
454
|
-
throw new Error(result.drift);
|
|
455
|
-
}
|
|
456
|
-
updateMemberStartup(roomId, member.name, { briefing: result.briefing });
|
|
457
|
-
allAcknowledged &&= result.briefing.state === 'acknowledged';
|
|
458
|
-
relayFailed ||= result.briefing.state === 'relay_failed';
|
|
459
|
-
}
|
|
460
|
-
updateRoomHistoryCursor(roomId, history.cursor);
|
|
461
|
-
if (!history.exhausted) {
|
|
462
|
-
const reason = 'Cowork history backlog exceeded 20,000 raw records in one pass; '
|
|
463
|
-
+ 'cursor progress was preserved, retry recovery to continue.';
|
|
464
|
-
setSagaError(roomId, reason, reason, 'uncertain');
|
|
465
|
-
throw new Error(reason);
|
|
466
|
-
}
|
|
467
|
-
if (relayFailed) {
|
|
468
|
-
const reason = 'Cowork recorded terminal send_failed for a role briefing; '
|
|
469
|
-
+ 'same-version redelivery requires Cowork support or seat replacement.';
|
|
470
|
-
advanceSaga(roomId, 'wait_briefing_acks', 8, 'briefing_delivery_failed');
|
|
471
|
-
setSagaError(roomId, reason, reason, 'briefing_delivery_failed');
|
|
472
|
-
if (taskId)
|
|
473
|
-
blockTask(taskId, reason);
|
|
474
|
-
return getRoomRecord(roomId);
|
|
475
|
-
}
|
|
476
|
-
if (allAcknowledged)
|
|
313
|
+
const remote = await cowork.recoverRoom(roomId);
|
|
314
|
+
const reconciled = reconcileMemberSeats(roomId, members, remote.seats);
|
|
315
|
+
if (reconciled.complete)
|
|
477
316
|
break;
|
|
478
317
|
if (policy.now() >= deadline) {
|
|
479
|
-
advanceSaga(roomId, '
|
|
318
|
+
advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
|
|
480
319
|
return getRoomRecord(roomId);
|
|
481
320
|
}
|
|
482
|
-
advanceSaga(roomId, '
|
|
321
|
+
advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
|
|
483
322
|
await policy.sleep(delay);
|
|
484
323
|
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
485
324
|
}
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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);
|
|
499
339
|
}
|
|
500
|
-
advanceSaga(roomId, 'activate',
|
|
340
|
+
advanceSaga(roomId, 'activate', 6);
|
|
501
341
|
const record = activateRoom(roomId);
|
|
502
342
|
if (taskId)
|
|
503
343
|
activateTask(taskId);
|