@ours.network/fleet 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -11
- 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 +22 -9
- package/dist/harness/claude-code.js +1 -1
- package/dist/harness/codex-app-server-proxy.d.ts +1 -1
- package/dist/harness/codex-app-server-proxy.js +23 -9
- package/dist/harness/types.d.ts +5 -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 +261 -413
- 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 +6 -20
- package/dist/runner.js +9 -11
- package/dist/session/acp.d.ts +9 -11
- package/dist/session/acp.js +20 -9
- package/dist/spawn.d.ts +3 -3
- package/dist/spawn.js +6 -14
- package/package.json +1 -1
|
@@ -1,13 +1,15 @@
|
|
|
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';
|
|
8
|
+
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
|
|
9
|
+
import { controlRequest } from '../session/control.js';
|
|
10
|
+
import { SessionControlError } from '../session/types.js';
|
|
9
11
|
import { closeManagedRoom } from './close.js';
|
|
10
|
-
import {
|
|
12
|
+
import { buildRoomMemberTask, sha256Text } from './member-startup.js';
|
|
11
13
|
import { agentDir } from '../paths.js';
|
|
12
14
|
import { readProvenance } from '../creation.js';
|
|
13
15
|
import { readTempSupervisor, secureStoppedTempArchive, tempArchiveForCreationAction, tempSupervisorLiveness, } from '../temp-lifecycle.js';
|
|
@@ -19,6 +21,28 @@ export function getBinPath() {
|
|
|
19
21
|
return process.argv[1];
|
|
20
22
|
}
|
|
21
23
|
}
|
|
24
|
+
async function spawnRoomMember(options, binPath) {
|
|
25
|
+
const stateDir = process.env[FLEET_PROXY_STATE_DIR_ENV];
|
|
26
|
+
if (!stateDir)
|
|
27
|
+
return {
|
|
28
|
+
statePath: await spawnTemp(options, binPath),
|
|
29
|
+
creationActionId: options.creationActionId,
|
|
30
|
+
};
|
|
31
|
+
const response = await controlRequest(stateDir, { command: 'fleet_spawn', spawn: options }, 10 * 60_000);
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new SessionControlError(response.kind ?? 'backend', response.error ?? 'managed room member spawn failed');
|
|
34
|
+
}
|
|
35
|
+
const result = response.result;
|
|
36
|
+
const expectedCaller = process.env[FLEET_PROXY_CALLER_ENV];
|
|
37
|
+
if (expectedCaller && result.caller !== expectedCaller) {
|
|
38
|
+
throw new Error(`fleet proxy caller mismatch: expected '${expectedCaller}', got '${result.caller}'`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
statePath: result.statePath,
|
|
42
|
+
creationActionId: result.creationActionId,
|
|
43
|
+
callerRole: result.caller,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
22
46
|
function shortId(id) { return id.slice(0, 8); }
|
|
23
47
|
function expandMembers(template, prefix) {
|
|
24
48
|
const result = [];
|
|
@@ -35,135 +59,90 @@ function expandMembers(template, prefix) {
|
|
|
35
59
|
}
|
|
36
60
|
return result;
|
|
37
61
|
}
|
|
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
62
|
function settingsFor(member, cfg) {
|
|
94
63
|
let refRole;
|
|
95
64
|
try {
|
|
96
65
|
refRole = findRole(cfg, member.roleRef);
|
|
97
66
|
}
|
|
98
67
|
catch { /* no ref role */ }
|
|
68
|
+
const permissions = member.overrides?.permissions;
|
|
99
69
|
return {
|
|
100
70
|
model: member.overrides?.model ?? refRole?.model,
|
|
101
71
|
harness: member.overrides?.harness ?? refRole?.harness,
|
|
102
72
|
cwd: member.overrides?.cwd ?? refRole?.cwd,
|
|
103
73
|
persona: member.overrides?.persona ?? refRole?.persona,
|
|
74
|
+
approval: permissions?.approval,
|
|
75
|
+
filesystem: permissions?.filesystem,
|
|
76
|
+
unattended: permissions?.unattended,
|
|
104
77
|
};
|
|
105
78
|
}
|
|
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(' ');
|
|
79
|
+
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid) {
|
|
80
|
+
return buildRoomMemberTask({
|
|
81
|
+
taskId: input.taskId,
|
|
82
|
+
roomId: input.roomId,
|
|
83
|
+
roomIdentityCid,
|
|
84
|
+
ownerSeatCid,
|
|
85
|
+
goal: input.goal,
|
|
86
|
+
brief: input.brief,
|
|
87
|
+
contract: input.template.contract,
|
|
88
|
+
member: {
|
|
89
|
+
role_name: member.name,
|
|
90
|
+
cowork_role: member.coworkRole,
|
|
91
|
+
persona: settings.persona,
|
|
92
|
+
},
|
|
93
|
+
roster: members.map(candidate => ({
|
|
94
|
+
role_name: candidate.name,
|
|
95
|
+
cowork_role: candidate.coworkRole,
|
|
96
|
+
})),
|
|
97
|
+
});
|
|
141
98
|
}
|
|
142
|
-
function launchMatches(dir, member, actionId,
|
|
99
|
+
function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid, expectedInviteId) {
|
|
143
100
|
const provenance = readProvenance(dir);
|
|
144
101
|
if (provenance?.creationActionId !== actionId || provenance.role !== member.name)
|
|
145
102
|
return false;
|
|
146
103
|
try {
|
|
147
104
|
const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
|
|
105
|
+
const startup = role.roomMemberStartup;
|
|
148
106
|
return role.identity === member.name
|
|
149
107
|
&& typeof role.mission === 'string'
|
|
150
|
-
&& sha256Text(role.mission) ===
|
|
151
|
-
&&
|
|
108
|
+
&& sha256Text(role.mission) === taskSha
|
|
109
|
+
&& startup?.room_id === roomId
|
|
110
|
+
&& startup.room_identity_cid === roomIdentityCid
|
|
111
|
+
&& startup.identity_name === member.name
|
|
112
|
+
&& startup.role === member.coworkRole
|
|
113
|
+
&& startup.task === role.mission
|
|
114
|
+
&& (expectedInviteId === undefined || startup.invite_id === expectedInviteId)
|
|
115
|
+
&& typeof startup.invite === 'string'
|
|
116
|
+
&& startup.invite.length > 0;
|
|
152
117
|
}
|
|
153
118
|
catch {
|
|
154
119
|
return false;
|
|
155
120
|
}
|
|
156
121
|
}
|
|
157
|
-
async function
|
|
158
|
-
const { provision, member,
|
|
122
|
+
async function retainRunningLaunch(input) {
|
|
123
|
+
const { provision, member, task, roomIdentityCid } = input;
|
|
159
124
|
let seat = getRoomRecord(provision.roomId).member_seats
|
|
160
125
|
.find(candidate => candidate.role_name === member.name);
|
|
161
|
-
const mission = startupMission(gate);
|
|
162
|
-
const missionSha = sha256Text(mission);
|
|
163
126
|
const dir = agentDir(member.name, true);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
127
|
+
const taskSha = sha256Text(task);
|
|
128
|
+
if ((seat.launch?.state === 'intent' || seat.launch?.state === 'launched'
|
|
129
|
+
|| seat.launch?.state === 'failed') && existsSync(dir)) {
|
|
130
|
+
if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
131
|
+
const provenance = readProvenance(dir);
|
|
132
|
+
const adoptable = (seat.launch.state === 'intent' || seat.launch.state === 'failed')
|
|
133
|
+
&& Boolean(seat.launch.caller_role)
|
|
134
|
+
&& provenance?.surface === 'agent'
|
|
135
|
+
&& provenance.callerRole === seat.launch.caller_role
|
|
136
|
+
&& typeof provenance.creationActionId === 'string'
|
|
137
|
+
&& launchMatches(dir, member, provenance.creationActionId, taskSha, provision.roomId, roomIdentityCid, seat.invite_id);
|
|
138
|
+
if (!adoptable)
|
|
139
|
+
throw new Error(`existing launch for ${member.name} does not match its durable intent`);
|
|
140
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
141
|
+
...seat.launch, state: 'intent', action_id: provenance.creationActionId,
|
|
142
|
+
updated_at: new Date().toISOString(),
|
|
143
|
+
} });
|
|
144
|
+
seat = getRoomRecord(provision.roomId).member_seats
|
|
145
|
+
.find(candidate => candidate.role_name === member.name);
|
|
167
146
|
}
|
|
168
147
|
const supervisor = readTempSupervisor(dir);
|
|
169
148
|
if (!supervisor || supervisor.role !== member.name)
|
|
@@ -171,209 +150,165 @@ async function ensureLaunch(input) {
|
|
|
171
150
|
const live = await tempSupervisorLiveness(dir);
|
|
172
151
|
if (live === 'unknown')
|
|
173
152
|
throw new Error(`existing launch for ${member.name} has unknown liveness`);
|
|
153
|
+
const retainedLaunch = seat.launch;
|
|
174
154
|
if (live === 'running') {
|
|
175
155
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
176
|
-
...
|
|
156
|
+
...retainedLaunch, state: 'launched', launch_id: supervisor.launchId,
|
|
177
157
|
updated_at: new Date().toISOString(),
|
|
178
158
|
} });
|
|
179
|
-
return;
|
|
159
|
+
return true;
|
|
180
160
|
}
|
|
181
161
|
await secureStoppedTempArchive(member.name, supervisor.launchId);
|
|
182
162
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
183
|
-
...
|
|
163
|
+
...retainedLaunch, state: 'stopped', launch_id: supervisor.launchId,
|
|
184
164
|
updated_at: new Date().toISOString(),
|
|
185
165
|
} });
|
|
186
|
-
|
|
187
|
-
.find(candidate => candidate.role_name === member.name);
|
|
166
|
+
return false;
|
|
188
167
|
}
|
|
189
|
-
|
|
168
|
+
if (seat.launch?.state === 'launched' && !existsSync(dir)) {
|
|
190
169
|
if (!seat.launch.launch_id || !seat.launch.action_id)
|
|
191
170
|
throw new Error(`missing durable launch identity for disappeared ${member.name}`);
|
|
192
171
|
const archive = await secureStoppedTempArchive(member.name, seat.launch.launch_id);
|
|
193
|
-
if (!launchMatches(archive, member, seat.launch.action_id,
|
|
172
|
+
if (!launchMatches(archive, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
194
173
|
throw new Error(`archive for disappeared ${member.name} does not match its durable intent`);
|
|
174
|
+
}
|
|
195
175
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
196
176
|
...seat.launch, state: 'stopped', updated_at: new Date().toISOString(),
|
|
197
177
|
} });
|
|
198
|
-
|
|
199
|
-
.find(candidate => candidate.role_name === member.name);
|
|
178
|
+
return false;
|
|
200
179
|
}
|
|
201
|
-
|
|
180
|
+
if (seat.launch?.state === 'intent' && !existsSync(dir)) {
|
|
202
181
|
if (!seat.launch.action_id)
|
|
203
182
|
throw new Error(`missing action ID for disappeared launch intent ${member.name}`);
|
|
204
183
|
const archive = tempArchiveForCreationAction(member.name, seat.launch.action_id);
|
|
205
|
-
if (!archive || !launchMatches(archive.path, member, seat.launch.action_id,
|
|
184
|
+
if (!archive || !launchMatches(archive.path, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
206
185
|
throw new Error(`launch intent for ${member.name} has no exact live or terminated archive evidence`);
|
|
207
186
|
}
|
|
208
187
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
209
188
|
...seat.launch, state: 'stopped', launch_id: archive.launchId,
|
|
210
189
|
updated_at: new Date().toISOString(),
|
|
211
190
|
} });
|
|
212
|
-
seat = getRoomRecord(provision.roomId).member_seats
|
|
213
|
-
.find(candidate => candidate.role_name === member.name);
|
|
214
191
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
async function launchMember(input) {
|
|
195
|
+
const { provision, member, settings, startup } = input;
|
|
196
|
+
const seat = getRoomRecord(provision.roomId).member_seats
|
|
197
|
+
.find(candidate => candidate.role_name === member.name);
|
|
198
|
+
const actionId = randomUUID();
|
|
199
|
+
let effectiveActionId = actionId;
|
|
200
|
+
const attempt = (seat.launch?.attempt ?? 0) + 1;
|
|
201
|
+
const taskSha = sha256Text(startup.task);
|
|
202
|
+
const proxyCaller = process.env[FLEET_PROXY_STATE_DIR_ENV]
|
|
203
|
+
? process.env[FLEET_PROXY_CALLER_ENV] : undefined;
|
|
219
204
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
220
|
-
state: 'intent', attempt, action_id: actionId, mission_sha256:
|
|
205
|
+
state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
|
|
206
|
+
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
221
207
|
updated_at: new Date().toISOString(),
|
|
222
208
|
} });
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
209
|
+
try {
|
|
210
|
+
const launched = await spawnRoomMember({
|
|
211
|
+
name: member.name,
|
|
212
|
+
temp: true,
|
|
213
|
+
identity: member.name,
|
|
214
|
+
mission: startup.task,
|
|
215
|
+
model: settings.model,
|
|
216
|
+
harness: settings.harness,
|
|
217
|
+
cwd: settings.cwd,
|
|
218
|
+
approval: settings.approval,
|
|
219
|
+
filesystem: settings.filesystem,
|
|
220
|
+
unattended: settings.unattended,
|
|
221
|
+
surface: 'agent',
|
|
222
|
+
creationActionId: actionId,
|
|
223
|
+
roomMemberStartup: startup,
|
|
224
|
+
}, provision.binPath);
|
|
225
|
+
const launchedDir = launched.statePath;
|
|
226
|
+
effectiveActionId = launched.creationActionId;
|
|
227
|
+
if (launched.creationActionId !== actionId) {
|
|
228
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
229
|
+
state: 'intent', attempt, action_id: launched.creationActionId,
|
|
230
|
+
mission_sha256: taskSha, updated_at: new Date().toISOString(),
|
|
231
|
+
...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
|
|
232
|
+
} });
|
|
233
|
+
}
|
|
234
|
+
const supervisor = readTempSupervisor(launchedDir);
|
|
235
|
+
if (!supervisor || supervisor.role !== member.name || !launchMatches(launchedDir, member, launched.creationActionId, taskSha, provision.roomId, startup.room_identity_cid, startup.invite_id)) {
|
|
236
|
+
throw new Error(`new launch for ${member.name} did not persist matching provenance`);
|
|
237
|
+
}
|
|
238
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
239
|
+
state: 'launched', attempt, action_id: launched.creationActionId, mission_sha256: taskSha,
|
|
240
|
+
...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
|
|
241
|
+
launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
|
|
242
|
+
} });
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
246
|
+
state: 'failed', attempt, action_id: effectiveActionId, mission_sha256: taskSha,
|
|
247
|
+
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
248
|
+
updated_at: new Date().toISOString(),
|
|
249
|
+
error: error instanceof Error ? error.message : String(error),
|
|
250
|
+
} });
|
|
251
|
+
throw error;
|
|
232
252
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
253
|
+
}
|
|
254
|
+
function exactCoworkSeat(observed, member, expectedInviteId) {
|
|
255
|
+
const named = observed.filter(seat => seat.display_name === member.name && seat.seat_state !== 'removed');
|
|
256
|
+
if (named.length > 1)
|
|
257
|
+
throw new Error(`Cowork has multiple live seats named ${member.name}`);
|
|
258
|
+
const seat = named[0];
|
|
259
|
+
if (!seat)
|
|
260
|
+
return undefined;
|
|
261
|
+
if (seat.role !== member.coworkRole)
|
|
262
|
+
throw new Error(`Cowork seat ${member.name} has role ${seat.role}; expected ${member.coworkRole}`);
|
|
263
|
+
if (expectedInviteId && seat.invite_id !== expectedInviteId) {
|
|
264
|
+
throw new Error(`Cowork seat ${member.name} used invite ${seat.invite_id}; expected ${expectedInviteId}`);
|
|
265
|
+
}
|
|
266
|
+
return seat;
|
|
267
|
+
}
|
|
268
|
+
function reconcileMemberSeats(roomId, members, observed) {
|
|
269
|
+
const current = getRoomRecord(roomId);
|
|
270
|
+
let complete = true;
|
|
271
|
+
const seats = current.member_seats.map(seat => {
|
|
272
|
+
const member = members.find(candidate => candidate.name === seat.role_name);
|
|
273
|
+
const found = exactCoworkSeat(observed, member, seat.invite_id);
|
|
274
|
+
if (!found || found.seat_state !== 'active') {
|
|
275
|
+
complete = false;
|
|
276
|
+
return seat;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
...seat,
|
|
280
|
+
identity_cid: found.identity_cid,
|
|
281
|
+
invite_id: found.invite_id,
|
|
282
|
+
seat_state: 'active',
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
updateMemberSeats(roomId, seats);
|
|
286
|
+
return { complete, seats };
|
|
237
287
|
}
|
|
238
288
|
export async function provisionMembers(input) {
|
|
239
|
-
const { cfg, cowork, roomId, taskId, template
|
|
289
|
+
const { cfg, cowork, roomId, taskId, template } = input;
|
|
240
290
|
const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
|
|
241
|
-
const
|
|
242
|
-
const createdIdentities = [];
|
|
243
|
-
const members = [];
|
|
291
|
+
const members = expandMembers(template, prefix);
|
|
244
292
|
const existing = getRoomRecord(roomId);
|
|
245
293
|
if (!existing?.room_identity_cid)
|
|
246
294
|
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
247
295
|
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
|
|
296
|
+
const ownerSeatCid = existing.owner_seat_cid ?? null;
|
|
297
|
+
const persistedNames = new Set(existing.member_seats.map(seat => seat.role_name));
|
|
298
|
+
const resuming = members.length > 0
|
|
299
|
+
&& members.every(member => persistedNames.has(member.name));
|
|
300
|
+
if (!resuming) {
|
|
270
301
|
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,
|
|
302
|
+
updateMemberSeats(roomId, members.map(member => ({
|
|
303
|
+
role_name: member.name,
|
|
304
|
+
slot: member.slot,
|
|
305
|
+
cowork_role: member.coworkRole,
|
|
292
306
|
seat_state: 'pending',
|
|
293
307
|
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
|
-
}
|
|
308
|
+
})));
|
|
308
309
|
}
|
|
309
310
|
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
|
-
}
|
|
311
|
+
const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid)]));
|
|
377
312
|
const policy = {
|
|
378
313
|
timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
|
|
379
314
|
initialDelayMs: input.startupWait?.initialDelayMs ?? 250,
|
|
@@ -381,178 +316,91 @@ export async function provisionMembers(input) {
|
|
|
381
316
|
now: input.startupWait?.now ?? Date.now,
|
|
382
317
|
sleep: input.startupWait?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
383
318
|
};
|
|
384
|
-
advanceSaga(roomId, 'join_role_groups',
|
|
319
|
+
advanceSaga(roomId, 'join_role_groups', 4);
|
|
385
320
|
try {
|
|
386
|
-
const
|
|
321
|
+
const initialRoom = await cowork.recoverRoom(roomId);
|
|
322
|
+
reconcileMemberSeats(roomId, members, initialRoom.seats);
|
|
387
323
|
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}`);
|
|
324
|
+
const task = tasks.get(member.name);
|
|
325
|
+
const currentSeat = getRoomRecord(roomId).member_seats
|
|
326
|
+
.find(seat => seat.role_name === member.name);
|
|
327
|
+
if (currentSeat.seat_state === 'active') {
|
|
328
|
+
if (!await retainRunningLaunch({
|
|
329
|
+
provision: input, member, task, roomIdentityCid,
|
|
330
|
+
})) {
|
|
331
|
+
throw new Error(`active Cowork seat ${member.name} has no matching live Fleet launch`);
|
|
434
332
|
}
|
|
435
|
-
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (await retainRunningLaunch({ provision: input, member, task, roomIdentityCid }))
|
|
336
|
+
continue;
|
|
337
|
+
const issued = await cowork.issueInvite(roomId, {
|
|
338
|
+
mode: 'one_time', role: member.coworkRole, min_accepts: 1,
|
|
436
339
|
});
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
340
|
+
const seats = getRoomRecord(roomId).member_seats.map(seat => seat.role_name === member.name ? { ...seat, invite_id: issued.invite_id } : seat);
|
|
341
|
+
updateMemberSeats(roomId, seats);
|
|
342
|
+
try {
|
|
343
|
+
await launchMember({
|
|
344
|
+
provision: input,
|
|
345
|
+
member,
|
|
346
|
+
settings: settings.get(member.name),
|
|
347
|
+
startup: {
|
|
348
|
+
room_id: roomId,
|
|
349
|
+
room_identity_cid: roomIdentityCid,
|
|
350
|
+
identity_name: member.name,
|
|
351
|
+
invite_id: issued.invite_id,
|
|
352
|
+
invite: issued.invite,
|
|
353
|
+
role: member.coworkRole,
|
|
354
|
+
task,
|
|
355
|
+
owner_seat_cid: ownerSeatCid,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
await cowork.revokeInvite(roomId, issued.invite_id).catch(() => { });
|
|
361
|
+
throw error;
|
|
441
362
|
}
|
|
442
|
-
await policy.sleep(delay);
|
|
443
|
-
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
444
363
|
}
|
|
445
364
|
}
|
|
446
365
|
catch (error) {
|
|
447
|
-
|
|
366
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
367
|
+
setSagaError(roomId, reason, 'Member invite or launch failed. Retry with `task recover`.', 'member_failed');
|
|
448
368
|
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');
|
|
369
|
+
blockTask(taskId, reason);
|
|
486
370
|
throw error;
|
|
487
371
|
}
|
|
372
|
+
advanceSaga(roomId, 'wait_seats', 5);
|
|
488
373
|
const deadline = policy.now() + policy.timeoutMs;
|
|
489
374
|
let delay = policy.initialDelayMs;
|
|
490
375
|
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)
|
|
376
|
+
const remote = await cowork.recoverRoom(roomId);
|
|
377
|
+
const reconciled = reconcileMemberSeats(roomId, members, remote.seats);
|
|
378
|
+
if (reconciled.complete)
|
|
532
379
|
break;
|
|
533
380
|
if (policy.now() >= deadline) {
|
|
534
|
-
advanceSaga(roomId, '
|
|
381
|
+
advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
|
|
535
382
|
return getRoomRecord(roomId);
|
|
536
383
|
}
|
|
537
|
-
advanceSaga(roomId, '
|
|
384
|
+
advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
|
|
538
385
|
await policy.sleep(delay);
|
|
539
386
|
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
540
387
|
}
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
388
|
+
if (taskId) {
|
|
389
|
+
const taskMembers = getRoomRecord(roomId).member_seats.map(seat => {
|
|
390
|
+
if (!seat.identity_cid)
|
|
391
|
+
throw new Error(`active room member ${seat.role_name} has no authenticated identity CID`);
|
|
392
|
+
return {
|
|
393
|
+
name: seat.role_name,
|
|
394
|
+
identity_cid: seat.identity_cid,
|
|
395
|
+
slot: seat.slot,
|
|
396
|
+
cowork_role: seat.cowork_role,
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
updateTaskMembers(taskId, taskMembers);
|
|
400
|
+
if (getTask(taskId).blocked)
|
|
401
|
+
unblockTask(taskId);
|
|
554
402
|
}
|
|
555
|
-
advanceSaga(roomId, 'activate',
|
|
403
|
+
advanceSaga(roomId, 'activate', 6);
|
|
556
404
|
const record = activateRoom(roomId);
|
|
557
405
|
if (taskId)
|
|
558
406
|
activateTask(taskId);
|