@ours.network/fleet 0.19.0-nightly.1 → 0.19.0-nightly.11
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 +30 -0
- package/dist/briefing.js +92 -6
- package/dist/build-info.json +4 -4
- package/dist/cli.js +5 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +44 -3
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +28 -0
- package/dist/doctor.js +125 -1
- package/dist/harness/claude-code.js +2 -0
- package/dist/harness/codex.js +2 -0
- package/dist/harness/types.d.ts +2 -0
- package/dist/owner-channel/channel.d.ts +7 -0
- package/dist/owner-channel/channel.js +47 -0
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +393 -0
- package/dist/rooms-tasks/cli.d.ts +4 -0
- package/dist/rooms-tasks/cli.js +1172 -0
- package/dist/rooms-tasks/close.d.ts +21 -0
- package/dist/rooms-tasks/close.js +159 -0
- package/dist/rooms-tasks/config.d.ts +15 -0
- package/dist/rooms-tasks/config.js +174 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +98 -0
- package/dist/rooms-tasks/cowork-adapter.js +385 -0
- package/dist/rooms-tasks/external-worker.d.ts +2 -0
- package/dist/rooms-tasks/external-worker.js +40 -0
- package/dist/rooms-tasks/index.d.ts +10 -0
- package/dist/rooms-tasks/index.js +10 -0
- package/dist/rooms-tasks/member-startup.d.ts +51 -0
- package/dist/rooms-tasks/member-startup.js +151 -0
- package/dist/rooms-tasks/provision.d.ts +29 -0
- package/dist/rooms-tasks/provision.js +514 -0
- package/dist/rooms-tasks/room-state.d.ts +34 -0
- package/dist/rooms-tasks/room-state.js +263 -0
- package/dist/rooms-tasks/task-state.d.ts +43 -0
- package/dist/rooms-tasks/task-state.js +277 -0
- package/dist/rooms-tasks/templates.d.ts +6 -0
- package/dist/rooms-tasks/templates.js +94 -0
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +53 -0
- package/dist/rooms-tasks/types.d.ts +260 -0
- package/dist/rooms-tasks/types.js +20 -0
- package/dist/spawn.d.ts +3 -1
- package/dist/spawn.js +1 -0
- package/dist/temp-lifecycle.d.ts +14 -2
- package/dist/temp-lifecycle.js +90 -0
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CoworkAdapter } from './cowork-adapter.js';
|
|
2
|
+
import type { RoomOrchestrationRecord, TemplateSnapshot } from './types.js';
|
|
3
|
+
import { type FleetConfig } from '../config.js';
|
|
4
|
+
export declare function getBinPath(): string;
|
|
5
|
+
export interface ProvisionMembersInput {
|
|
6
|
+
cfg: FleetConfig;
|
|
7
|
+
cowork: CoworkAdapter;
|
|
8
|
+
roomId: string;
|
|
9
|
+
taskId?: string;
|
|
10
|
+
template: TemplateSnapshot;
|
|
11
|
+
binPath: string;
|
|
12
|
+
brief?: string;
|
|
13
|
+
goal?: string;
|
|
14
|
+
startupWait?: Partial<StartupWaitPolicy>;
|
|
15
|
+
}
|
|
16
|
+
export interface StartupWaitPolicy {
|
|
17
|
+
timeoutMs: number;
|
|
18
|
+
initialDelayMs: number;
|
|
19
|
+
maxDelayMs: number;
|
|
20
|
+
now(): number;
|
|
21
|
+
sleep(ms: number): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare function provisionMembers(input: ProvisionMembersInput): Promise<RoomOrchestrationRecord>;
|
|
24
|
+
export declare function cleanupMembers(input: {
|
|
25
|
+
roomId: string;
|
|
26
|
+
taskId?: string;
|
|
27
|
+
closeCoworkRoom?: boolean;
|
|
28
|
+
cowork?: CoworkAdapter;
|
|
29
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { attachOursClient, } from '@ours.network/sdk/client';
|
|
5
|
+
import { advanceSaga, setSagaError, updateMemberSeats, updateMemberStartup, updateRoomRoleBriefing, updateRoomHistoryCursor, activateRoom, getRoomRecord, } from './room-state.js';
|
|
6
|
+
import { activateTask, updateTaskMembers, failTask, blockTask, unblockTask, getTask, } from './task-state.js';
|
|
7
|
+
import { spawnTemp } from '../spawn.js';
|
|
8
|
+
import { findRole } from '../config.js';
|
|
9
|
+
import { closeManagedRoom } from './close.js';
|
|
10
|
+
import { buildRoomMemberCharter, reconcileBriefingHistory, sha256Text, } from './member-startup.js';
|
|
11
|
+
import { agentDir } from '../paths.js';
|
|
12
|
+
import { readProvenance } from '../creation.js';
|
|
13
|
+
import { readTempSupervisor, secureStoppedTempArchive, tempArchiveForCreationAction, tempSupervisorLiveness, } from '../temp-lifecycle.js';
|
|
14
|
+
export function getBinPath() {
|
|
15
|
+
try {
|
|
16
|
+
return realpathSync(process.argv[1]);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return process.argv[1];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function shortId(id) { return id.slice(0, 8); }
|
|
23
|
+
function expandMembers(template, prefix) {
|
|
24
|
+
const result = [];
|
|
25
|
+
for (const slot of template.members) {
|
|
26
|
+
for (let i = 1; i <= slot.count; i++) {
|
|
27
|
+
result.push({
|
|
28
|
+
name: `${prefix}-${slot.slot}-${i}`,
|
|
29
|
+
slot: slot.slot,
|
|
30
|
+
coworkRole: slot.role,
|
|
31
|
+
roleRef: slot.role_ref,
|
|
32
|
+
overrides: slot.overrides,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
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
|
+
function settingsFor(member, cfg) {
|
|
74
|
+
let refRole;
|
|
75
|
+
try {
|
|
76
|
+
refRole = findRole(cfg, member.roleRef);
|
|
77
|
+
}
|
|
78
|
+
catch { /* no ref role */ }
|
|
79
|
+
return {
|
|
80
|
+
model: member.overrides?.model ?? refRole?.model,
|
|
81
|
+
harness: member.overrides?.harness ?? refRole?.harness,
|
|
82
|
+
cwd: member.overrides?.cwd ?? refRole?.cwd,
|
|
83
|
+
persona: member.overrides?.persona ?? refRole?.persona,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function roomHistory(cowork, roomId) {
|
|
87
|
+
const records = [];
|
|
88
|
+
let after = getRoomRecord(roomId)?.history_cursor ?? 0;
|
|
89
|
+
for (let page = 0; page < 100; page++) {
|
|
90
|
+
const next = await cowork.getHistory(roomId, { after, limit: 200 });
|
|
91
|
+
records.push(...next.records);
|
|
92
|
+
if (next.raw_count === 0)
|
|
93
|
+
return { records, cursor: after, exhausted: true };
|
|
94
|
+
if (next.next_after <= after)
|
|
95
|
+
throw new Error('Cowork room history cursor did not advance');
|
|
96
|
+
after = next.next_after;
|
|
97
|
+
if (next.raw_count < 200)
|
|
98
|
+
return { records, cursor: after, exhausted: true };
|
|
99
|
+
}
|
|
100
|
+
return { records, cursor: after, exhausted: false };
|
|
101
|
+
}
|
|
102
|
+
async function assertCurrentRoleBriefings(cowork, roomId, definitions) {
|
|
103
|
+
const room = await cowork.getRoom(roomId);
|
|
104
|
+
if (!room)
|
|
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(' ');
|
|
121
|
+
}
|
|
122
|
+
function launchMatches(dir, member, actionId, missionSha, gate) {
|
|
123
|
+
const provenance = readProvenance(dir);
|
|
124
|
+
if (provenance?.creationActionId !== actionId || provenance.role !== member.name)
|
|
125
|
+
return false;
|
|
126
|
+
try {
|
|
127
|
+
const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
|
|
128
|
+
return role.identity === member.name
|
|
129
|
+
&& typeof role.mission === 'string'
|
|
130
|
+
&& sha256Text(role.mission) === missionSha
|
|
131
|
+
&& JSON.stringify(role.roomStartupGate) === JSON.stringify(gate);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function ensureLaunch(input) {
|
|
138
|
+
const { provision, member, settings, gate } = input;
|
|
139
|
+
let seat = getRoomRecord(provision.roomId).member_seats
|
|
140
|
+
.find(candidate => candidate.role_name === member.name);
|
|
141
|
+
const mission = startupMission(gate);
|
|
142
|
+
const missionSha = sha256Text(mission);
|
|
143
|
+
const dir = agentDir(member.name, true);
|
|
144
|
+
if ((seat.launch?.state === 'intent' || seat.launch?.state === 'launched') && existsSync(dir)) {
|
|
145
|
+
if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id, seat.launch.mission_sha256 ?? '', gate)) {
|
|
146
|
+
throw new Error(`existing launch for ${member.name} does not match its durable intent`);
|
|
147
|
+
}
|
|
148
|
+
const supervisor = readTempSupervisor(dir);
|
|
149
|
+
if (!supervisor || supervisor.role !== member.name)
|
|
150
|
+
throw new Error(`existing launch for ${member.name} has mismatched supervisor metadata`);
|
|
151
|
+
const live = await tempSupervisorLiveness(dir);
|
|
152
|
+
if (live === 'unknown')
|
|
153
|
+
throw new Error(`existing launch for ${member.name} has unknown liveness`);
|
|
154
|
+
if (live === 'running') {
|
|
155
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
156
|
+
...seat.launch, state: 'launched', launch_id: supervisor.launchId,
|
|
157
|
+
updated_at: new Date().toISOString(),
|
|
158
|
+
} });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
await secureStoppedTempArchive(member.name, supervisor.launchId);
|
|
162
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
163
|
+
...seat.launch, state: 'stopped', launch_id: supervisor.launchId,
|
|
164
|
+
updated_at: new Date().toISOString(),
|
|
165
|
+
} });
|
|
166
|
+
seat = getRoomRecord(provision.roomId).member_seats
|
|
167
|
+
.find(candidate => candidate.role_name === member.name);
|
|
168
|
+
}
|
|
169
|
+
else if (seat.launch?.state === 'launched' && !existsSync(dir)) {
|
|
170
|
+
if (!seat.launch.launch_id || !seat.launch.action_id)
|
|
171
|
+
throw new Error(`missing durable launch identity for disappeared ${member.name}`);
|
|
172
|
+
const archive = await secureStoppedTempArchive(member.name, seat.launch.launch_id);
|
|
173
|
+
if (!launchMatches(archive, member, seat.launch.action_id, missionSha, gate))
|
|
174
|
+
throw new Error(`archive for disappeared ${member.name} does not match its durable intent`);
|
|
175
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
176
|
+
...seat.launch, state: 'stopped', updated_at: new Date().toISOString(),
|
|
177
|
+
} });
|
|
178
|
+
seat = getRoomRecord(provision.roomId).member_seats
|
|
179
|
+
.find(candidate => candidate.role_name === member.name);
|
|
180
|
+
}
|
|
181
|
+
else if (seat.launch?.state === 'intent' && !existsSync(dir)) {
|
|
182
|
+
if (!seat.launch.action_id)
|
|
183
|
+
throw new Error(`missing action ID for disappeared launch intent ${member.name}`);
|
|
184
|
+
const archive = tempArchiveForCreationAction(member.name, seat.launch.action_id);
|
|
185
|
+
if (!archive || !launchMatches(archive.path, member, seat.launch.action_id, missionSha, gate)) {
|
|
186
|
+
throw new Error(`launch intent for ${member.name} has no exact live or terminated archive evidence`);
|
|
187
|
+
}
|
|
188
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
189
|
+
...seat.launch, state: 'stopped', launch_id: archive.launchId,
|
|
190
|
+
updated_at: new Date().toISOString(),
|
|
191
|
+
} });
|
|
192
|
+
seat = getRoomRecord(provision.roomId).member_seats
|
|
193
|
+
.find(candidate => candidate.role_name === member.name);
|
|
194
|
+
}
|
|
195
|
+
const actionId = seat.launch?.state === 'intent' && seat.launch.action_id
|
|
196
|
+
? seat.launch.action_id : randomUUID();
|
|
197
|
+
const attempt = seat.launch?.state === 'intent'
|
|
198
|
+
? seat.launch.attempt : (seat.launch?.attempt ?? 0) + 1;
|
|
199
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
200
|
+
state: 'intent', attempt, action_id: actionId, mission_sha256: missionSha,
|
|
201
|
+
updated_at: new Date().toISOString(),
|
|
202
|
+
} });
|
|
203
|
+
const launchedDir = await spawnTemp({
|
|
204
|
+
name: member.name, temp: true, identity: member.name, mission,
|
|
205
|
+
model: settings.model, harness: settings.harness, cwd: settings.cwd,
|
|
206
|
+
surface: 'agent', creationActionId: actionId, roomStartupGate: gate,
|
|
207
|
+
}, provision.binPath);
|
|
208
|
+
const supervisor = readTempSupervisor(launchedDir);
|
|
209
|
+
if (!supervisor || supervisor.role !== member.name
|
|
210
|
+
|| !launchMatches(launchedDir, member, actionId, missionSha, gate)) {
|
|
211
|
+
throw new Error(`new launch for ${member.name} did not persist matching provenance`);
|
|
212
|
+
}
|
|
213
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
214
|
+
state: 'launched', attempt, action_id: actionId, mission_sha256: missionSha,
|
|
215
|
+
launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
|
|
216
|
+
} });
|
|
217
|
+
}
|
|
218
|
+
export async function provisionMembers(input) {
|
|
219
|
+
const { cfg, cowork, roomId, taskId, template, binPath } = input;
|
|
220
|
+
const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
|
|
221
|
+
const plan = expandMembers(template, prefix);
|
|
222
|
+
const createdIdentities = [];
|
|
223
|
+
const members = [];
|
|
224
|
+
const existing = getRoomRecord(roomId);
|
|
225
|
+
if (!existing?.room_identity_cid)
|
|
226
|
+
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
227
|
+
const persistedSeats = existing?.member_seats ?? [];
|
|
228
|
+
const resuming = plan.length > 0
|
|
229
|
+
&& plan.every(p => persistedSeats.some(s => s.role_name === p.name));
|
|
230
|
+
let seats;
|
|
231
|
+
if (resuming) {
|
|
232
|
+
for (const planned of plan) {
|
|
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
|
+
// Phase 4: create_members
|
|
249
|
+
advanceSaga(roomId, 'create_members', 3);
|
|
250
|
+
try {
|
|
251
|
+
for (const planned of plan) {
|
|
252
|
+
const bio = `Fleet task member: ${planned.coworkRole} for ${taskId ?? roomId}`;
|
|
253
|
+
const { cid } = await createMemberIdentity(planned.name, bio);
|
|
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,
|
|
271
|
+
seat_state: 'pending',
|
|
272
|
+
launch: { state: 'pending', attempt: 0, updated_at: new Date().toISOString() },
|
|
273
|
+
briefing: {
|
|
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
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg)]));
|
|
289
|
+
const roster = members.map(member => ({
|
|
290
|
+
role_name: member.name, cowork_role: member.coworkRole, identity_cid: member.cid,
|
|
291
|
+
}));
|
|
292
|
+
const definitions = new Map();
|
|
293
|
+
for (const member of members) {
|
|
294
|
+
const durable = getRoomRecord(roomId).role_briefings?.[member.coworkRole];
|
|
295
|
+
const text = durable?.text ?? buildRoomMemberCharter({
|
|
296
|
+
taskId, roomId, roomIdentityCid: existing.room_identity_cid,
|
|
297
|
+
ownerSeatCid: existing.owner_seat_cid ?? null,
|
|
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);
|
|
357
|
+
try {
|
|
358
|
+
const observedSeats = await cowork.getSeats(roomId);
|
|
359
|
+
for (const member of members) {
|
|
360
|
+
const observed = observedSeats.find(seat => seat.identity_cid === member.cid
|
|
361
|
+
&& seat.seat_state !== 'removed');
|
|
362
|
+
if (observed && observed.role !== member.coworkRole)
|
|
363
|
+
throw new Error(`Cowork seat ${member.cid} has role ${observed.role}; expected ${member.coworkRole}`);
|
|
364
|
+
}
|
|
365
|
+
const admitted = new Set(observedSeats
|
|
366
|
+
.filter(seat => seat.seat_state !== 'removed').map(seat => seat.identity_cid));
|
|
367
|
+
const roleGroups = new Map();
|
|
368
|
+
for (const member of members.filter(candidate => !admitted.has(candidate.cid))) {
|
|
369
|
+
roleGroups.set(member.coworkRole, [
|
|
370
|
+
...(roleGroups.get(member.coworkRole) ?? []), member,
|
|
371
|
+
]);
|
|
372
|
+
}
|
|
373
|
+
for (const [coworkRole, group] of roleGroups) {
|
|
374
|
+
const { invite } = await cowork.issueInvite(roomId, {
|
|
375
|
+
role: coworkRole, min_accepts: group.length,
|
|
376
|
+
});
|
|
377
|
+
for (const member of group) {
|
|
378
|
+
await cowork.acceptInvite(roomId, invite, {
|
|
379
|
+
role: coworkRole, expected_cid: member.cid,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Role-group admission failed. Retry with `task recover`.', 'member_failed');
|
|
386
|
+
if (taskId)
|
|
387
|
+
blockTask(taskId, error instanceof Error ? error.message : String(error));
|
|
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');
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
const policy = {
|
|
427
|
+
timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
|
|
428
|
+
initialDelayMs: input.startupWait?.initialDelayMs ?? 250,
|
|
429
|
+
maxDelayMs: input.startupWait?.maxDelayMs ?? 2_000,
|
|
430
|
+
now: input.startupWait?.now ?? Date.now,
|
|
431
|
+
sleep: input.startupWait?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
432
|
+
};
|
|
433
|
+
const deadline = policy.now() + policy.timeoutMs;
|
|
434
|
+
let delay = policy.initialDelayMs;
|
|
435
|
+
for (;;) {
|
|
436
|
+
const history = await roomHistory(cowork, roomId);
|
|
437
|
+
let allAcknowledged = true;
|
|
438
|
+
let relayFailed = false;
|
|
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)
|
|
477
|
+
break;
|
|
478
|
+
if (policy.now() >= deadline) {
|
|
479
|
+
advanceSaga(roomId, 'wait_briefing_acks', 8, 'waiting_briefing_acks');
|
|
480
|
+
return getRoomRecord(roomId);
|
|
481
|
+
}
|
|
482
|
+
advanceSaga(roomId, 'wait_briefing_acks', 8, 'waiting_briefing_acks');
|
|
483
|
+
await policy.sleep(delay);
|
|
484
|
+
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
485
|
+
}
|
|
486
|
+
const finalDefinitions = new Map([...definitions].map(([role, definition]) => {
|
|
487
|
+
const persisted = getRoomRecord(roomId).role_briefings[role];
|
|
488
|
+
return [role, { ...definition, version: persisted.version }];
|
|
489
|
+
}));
|
|
490
|
+
try {
|
|
491
|
+
await assertCurrentRoleBriefings(cowork, roomId, finalDefinitions);
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
495
|
+
setSagaError(roomId, reason, 'Cowork current role briefing drifted before activation; inspect room state.', 'uncertain');
|
|
496
|
+
if (taskId)
|
|
497
|
+
blockTask(taskId, reason);
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
advanceSaga(roomId, 'activate', 9);
|
|
501
|
+
const record = activateRoom(roomId);
|
|
502
|
+
if (taskId)
|
|
503
|
+
activateTask(taskId);
|
|
504
|
+
return record;
|
|
505
|
+
}
|
|
506
|
+
export async function cleanupMembers(input) {
|
|
507
|
+
const room = getRoomRecord(input.roomId);
|
|
508
|
+
if (!room)
|
|
509
|
+
return;
|
|
510
|
+
if (!input.closeCoworkRoom || !input.cowork) {
|
|
511
|
+
throw new Error('member cleanup requires the shared deterministic room-close saga and Cowork adapter');
|
|
512
|
+
}
|
|
513
|
+
await closeManagedRoom({ roomId: input.roomId, cowork: input.cowork });
|
|
514
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RoomOrchestrationRecord, RoomOrchestrationState, SagaPhase, RoomMemberSeat, ProvisioningDetail, MemberRetirementPhase, RoomClosePhase, RoomRoleBriefingDefinition, RoomMemberLaunchState, RoomMemberBriefingState } from './types.js';
|
|
2
|
+
export declare const roomsDir: () => string;
|
|
3
|
+
export declare class RoomStateError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export interface CreateRoomInput {
|
|
6
|
+
room_id: string;
|
|
7
|
+
room_name: string;
|
|
8
|
+
goal?: string;
|
|
9
|
+
room_identity_cid?: string;
|
|
10
|
+
task_id?: string;
|
|
11
|
+
template_snapshot?: import('./types.js').TemplateSnapshot;
|
|
12
|
+
}
|
|
13
|
+
export declare function createRoomRecord(input: CreateRoomInput): RoomOrchestrationRecord;
|
|
14
|
+
export declare function getRoomRecord(id: string): RoomOrchestrationRecord | undefined;
|
|
15
|
+
export declare function listRoomRecords(filter?: {
|
|
16
|
+
state?: RoomOrchestrationState | RoomOrchestrationState[];
|
|
17
|
+
}): RoomOrchestrationRecord[];
|
|
18
|
+
export declare function advanceSaga(id: string, phase: SagaPhase, stepIndex?: number, detail?: ProvisioningDetail): RoomOrchestrationRecord;
|
|
19
|
+
export declare function setSagaError(id: string, error: string, hint?: string, detail?: ProvisioningDetail): RoomOrchestrationRecord;
|
|
20
|
+
export declare function setRoomIdentity(id: string, roomIdentityCid: string): RoomOrchestrationRecord;
|
|
21
|
+
export declare function setOwnerSeat(id: string, ownerSeatCid: string, inviteFingerprint: string): RoomOrchestrationRecord;
|
|
22
|
+
export declare function updateMemberSeats(id: string, seats: RoomMemberSeat[]): RoomOrchestrationRecord;
|
|
23
|
+
export declare function updateRoomRoleBriefing(id: string, role: string, definition: RoomRoleBriefingDefinition): RoomOrchestrationRecord;
|
|
24
|
+
export declare function updateRoomHistoryCursor(id: string, cursor: number): RoomOrchestrationRecord;
|
|
25
|
+
export declare function updateMemberStartup(id: string, roleName: string, update: {
|
|
26
|
+
launch?: RoomMemberLaunchState;
|
|
27
|
+
briefing?: RoomMemberBriefingState;
|
|
28
|
+
}): RoomOrchestrationRecord;
|
|
29
|
+
export declare function activateRoom(id: string): RoomOrchestrationRecord;
|
|
30
|
+
export declare function closeRoom(id: string): RoomOrchestrationRecord;
|
|
31
|
+
export declare function beginRoomClose(id: string): RoomOrchestrationRecord;
|
|
32
|
+
export declare function advanceMemberRetirement(id: string, roleName: string, phase: MemberRetirementPhase, launchId: string, archivePath?: string): RoomOrchestrationRecord;
|
|
33
|
+
export declare function advanceRoomClose(id: string, phase: RoomClosePhase): RoomOrchestrationRecord;
|
|
34
|
+
export declare function setRoomCloseError(id: string, error: string, recoveryHint: string): RoomOrchestrationRecord;
|