@ours.network/fleet 0.19.0-nightly.9 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -11
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +12 -0
- package/dist/application/role-creation-service.js +3 -1
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +103 -10
- package/dist/build-info.json +5 -5
- package/dist/cli.js +41 -11
- package/dist/config.d.ts +21 -7
- package/dist/config.js +32 -18
- package/dist/creation.d.ts +1 -1
- package/dist/docs.d.ts +2 -2
- package/dist/docs.js +117 -24
- package/dist/doctor.js +10 -10
- package/dist/duration.js +1 -1
- package/dist/fleet-proxy.d.ts +5 -0
- package/dist/harness/acp-agent.js +11 -6
- package/dist/harness/claude-code.js +201 -8
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +72 -13
- package/dist/harness/types.d.ts +57 -6
- package/dist/isolation/bubblewrap.d.ts +4 -4
- package/dist/isolation/bubblewrap.js +4 -4
- package/dist/isolation/policy.d.ts +1 -1
- package/dist/isolation/policy.js +2 -2
- package/dist/isolation/registry.d.ts +3 -3
- package/dist/isolation/registry.js +5 -5
- package/dist/isolation/resources.d.ts +1 -1
- package/dist/isolation/resources.js +1 -1
- package/dist/isolation/types.d.ts +3 -3
- package/dist/loops/manager.d.ts +30 -1
- package/dist/loops/manager.js +69 -6
- package/dist/loops/state.d.ts +18 -0
- package/dist/loops/state.js +4 -0
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/monitor.d.ts +2 -2
- package/dist/monitor.js +4 -4
- package/dist/ops.d.ts +2 -2
- package/dist/ops.js +9 -9
- package/dist/owner-channel/channel.d.ts +17 -0
- package/dist/owner-channel/channel.js +104 -23
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +226 -99
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/rooms-tasks/cli.js +731 -219
- package/dist/rooms-tasks/close.d.ts +35 -0
- package/dist/rooms-tasks/close.js +190 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +24 -0
- package/dist/rooms-tasks/cowork-adapter.js +120 -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 +2 -0
- package/dist/rooms-tasks/index.js +2 -0
- package/dist/rooms-tasks/markdown.d.ts +49 -0
- package/dist/rooms-tasks/markdown.js +206 -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 +8 -0
- package/dist/rooms-tasks/provision.js +418 -97
- package/dist/rooms-tasks/room-state.d.ts +13 -1
- package/dist/rooms-tasks/room-state.js +149 -1
- package/dist/rooms-tasks/task-state.d.ts +13 -2
- package/dist/rooms-tasks/task-state.js +131 -3
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +56 -0
- package/dist/rooms-tasks/types.d.ts +108 -2
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +252 -100
- package/dist/session/acp.d.ts +106 -2
- package/dist/session/acp.js +217 -14
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/control.d.ts +1 -1
- package/dist/session/conversation-normalizer.d.ts +7 -1
- package/dist/session/conversation-normalizer.js +154 -11
- package/dist/session/conversation-store.js +1 -1
- package/dist/session/conversation-types.d.ts +25 -6
- package/dist/session/types.d.ts +35 -0
- package/dist/spawn.d.ts +4 -2
- package/dist/spawn.js +39 -26
- package/dist/supervisor/launchd.d.ts +2 -2
- package/dist/supervisor/launchd.js +4 -4
- package/dist/supervisor/systemd.js +4 -4
- package/dist/supervisor/types.d.ts +1 -1
- package/dist/temp-lifecycle.d.ts +14 -2
- package/dist/temp-lifecycle.js +90 -0
- package/dist/watchdog/alerts.d.ts +2 -2
- package/dist/watchdog/alerts.js +2 -2
- package/dist/watchdog/briefing.d.ts +5 -5
- package/dist/watchdog/briefing.js +13 -6
- package/dist/watchdog/config.js +1 -1
- package/dist/watchdog/query.d.ts +2 -2
- package/dist/watchdog/query.js +2 -2
- package/dist/watchdog/report.js +2 -2
- package/dist/watchdog/run.d.ts +2 -2
- package/dist/watchdog/run.js +6 -6
- package/dist/watchdog/scheduler.d.ts +7 -7
- package/dist/watchdog/scheduler.js +10 -10
- package/dist/watchdog/service.d.ts +4 -4
- package/dist/watchdog/service.js +5 -6
- package/dist/watchdog/store.d.ts +4 -4
- package/dist/watchdog/store.js +4 -4
- package/dist/web/runtime.js +1 -1
- package/dist/web/server.js +1 -1
- package/dist/web/topology-promote.js +1 -1
- package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
- package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +7 -1
- package/dist/worklog.js +191 -39
- package/package.json +1 -1
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { parse } from 'yaml';
|
|
3
4
|
import { attachOursClient, } from '@ours.network/sdk/client';
|
|
4
|
-
import { advanceSaga, setSagaError, updateMemberSeats,
|
|
5
|
-
import { activateTask, updateTaskMembers, failTask, } from './task-state.js';
|
|
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';
|
|
6
7
|
import { spawnTemp } from '../spawn.js';
|
|
7
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';
|
|
8
14
|
export function getBinPath() {
|
|
9
15
|
try {
|
|
10
16
|
return realpathSync(process.argv[1]);
|
|
@@ -64,31 +70,170 @@ async function removeMemberIdentity(name) {
|
|
|
64
70
|
}
|
|
65
71
|
catch { /* best-effort cleanup */ }
|
|
66
72
|
}
|
|
67
|
-
function
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
function settingsFor(member, cfg) {
|
|
94
|
+
let refRole;
|
|
95
|
+
try {
|
|
96
|
+
refRole = findRole(cfg, member.roleRef);
|
|
97
|
+
}
|
|
98
|
+
catch { /* no ref role */ }
|
|
99
|
+
return {
|
|
100
|
+
model: member.overrides?.model ?? refRole?.model,
|
|
101
|
+
harness: member.overrides?.harness ?? refRole?.harness,
|
|
102
|
+
cwd: member.overrides?.cwd ?? refRole?.cwd,
|
|
103
|
+
persona: member.overrides?.persona ?? refRole?.persona,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function roomHistory(cowork, roomId) {
|
|
107
|
+
const records = [];
|
|
108
|
+
let after = getRoomRecord(roomId)?.history_cursor ?? 0;
|
|
109
|
+
for (let page = 0; page < 100; page++) {
|
|
110
|
+
const next = await cowork.getHistory(roomId, { after, limit: 200 });
|
|
111
|
+
records.push(...next.records);
|
|
112
|
+
if (next.raw_count === 0)
|
|
113
|
+
return { records, cursor: after, exhausted: true };
|
|
114
|
+
if (next.next_after <= after)
|
|
115
|
+
throw new Error('Cowork room history cursor did not advance');
|
|
116
|
+
after = next.next_after;
|
|
117
|
+
if (next.raw_count < 200)
|
|
118
|
+
return { records, cursor: after, exhausted: true };
|
|
119
|
+
}
|
|
120
|
+
return { records, cursor: after, exhausted: false };
|
|
121
|
+
}
|
|
122
|
+
async function assertCurrentRoleBriefings(cowork, roomId, definitions) {
|
|
123
|
+
const room = await cowork.getRoom(roomId);
|
|
124
|
+
if (!room)
|
|
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(' ');
|
|
141
|
+
}
|
|
142
|
+
function launchMatches(dir, member, actionId, missionSha, gate) {
|
|
143
|
+
const provenance = readProvenance(dir);
|
|
144
|
+
if (provenance?.creationActionId !== actionId || provenance.role !== member.name)
|
|
145
|
+
return false;
|
|
146
|
+
try {
|
|
147
|
+
const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
|
|
148
|
+
return role.identity === member.name
|
|
149
|
+
&& typeof role.mission === 'string'
|
|
150
|
+
&& sha256Text(role.mission) === missionSha
|
|
151
|
+
&& JSON.stringify(role.roomStartupGate) === JSON.stringify(gate);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function ensureLaunch(input) {
|
|
158
|
+
const { provision, member, settings, gate } = input;
|
|
159
|
+
let seat = getRoomRecord(provision.roomId).member_seats
|
|
160
|
+
.find(candidate => candidate.role_name === member.name);
|
|
161
|
+
const mission = startupMission(gate);
|
|
162
|
+
const missionSha = sha256Text(mission);
|
|
163
|
+
const dir = agentDir(member.name, true);
|
|
164
|
+
if ((seat.launch?.state === 'intent' || seat.launch?.state === 'launched') && existsSync(dir)) {
|
|
165
|
+
if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id, seat.launch.mission_sha256 ?? '', gate)) {
|
|
166
|
+
throw new Error(`existing launch for ${member.name} does not match its durable intent`);
|
|
167
|
+
}
|
|
168
|
+
const supervisor = readTempSupervisor(dir);
|
|
169
|
+
if (!supervisor || supervisor.role !== member.name)
|
|
170
|
+
throw new Error(`existing launch for ${member.name} has mismatched supervisor metadata`);
|
|
171
|
+
const live = await tempSupervisorLiveness(dir);
|
|
172
|
+
if (live === 'unknown')
|
|
173
|
+
throw new Error(`existing launch for ${member.name} has unknown liveness`);
|
|
174
|
+
if (live === 'running') {
|
|
175
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
176
|
+
...seat.launch, state: 'launched', launch_id: supervisor.launchId,
|
|
177
|
+
updated_at: new Date().toISOString(),
|
|
178
|
+
} });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
await secureStoppedTempArchive(member.name, supervisor.launchId);
|
|
182
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
183
|
+
...seat.launch, state: 'stopped', launch_id: supervisor.launchId,
|
|
184
|
+
updated_at: new Date().toISOString(),
|
|
185
|
+
} });
|
|
186
|
+
seat = getRoomRecord(provision.roomId).member_seats
|
|
187
|
+
.find(candidate => candidate.role_name === member.name);
|
|
188
|
+
}
|
|
189
|
+
else if (seat.launch?.state === 'launched' && !existsSync(dir)) {
|
|
190
|
+
if (!seat.launch.launch_id || !seat.launch.action_id)
|
|
191
|
+
throw new Error(`missing durable launch identity for disappeared ${member.name}`);
|
|
192
|
+
const archive = await secureStoppedTempArchive(member.name, seat.launch.launch_id);
|
|
193
|
+
if (!launchMatches(archive, member, seat.launch.action_id, missionSha, gate))
|
|
194
|
+
throw new Error(`archive for disappeared ${member.name} does not match its durable intent`);
|
|
195
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
196
|
+
...seat.launch, state: 'stopped', updated_at: new Date().toISOString(),
|
|
197
|
+
} });
|
|
198
|
+
seat = getRoomRecord(provision.roomId).member_seats
|
|
199
|
+
.find(candidate => candidate.role_name === member.name);
|
|
200
|
+
}
|
|
201
|
+
else if (seat.launch?.state === 'intent' && !existsSync(dir)) {
|
|
202
|
+
if (!seat.launch.action_id)
|
|
203
|
+
throw new Error(`missing action ID for disappeared launch intent ${member.name}`);
|
|
204
|
+
const archive = tempArchiveForCreationAction(member.name, seat.launch.action_id);
|
|
205
|
+
if (!archive || !launchMatches(archive.path, member, seat.launch.action_id, missionSha, gate)) {
|
|
206
|
+
throw new Error(`launch intent for ${member.name} has no exact live or terminated archive evidence`);
|
|
207
|
+
}
|
|
208
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
209
|
+
...seat.launch, state: 'stopped', launch_id: archive.launchId,
|
|
210
|
+
updated_at: new Date().toISOString(),
|
|
211
|
+
} });
|
|
212
|
+
seat = getRoomRecord(provision.roomId).member_seats
|
|
213
|
+
.find(candidate => candidate.role_name === member.name);
|
|
214
|
+
}
|
|
215
|
+
const actionId = seat.launch?.state === 'intent' && seat.launch.action_id
|
|
216
|
+
? seat.launch.action_id : randomUUID();
|
|
217
|
+
const attempt = seat.launch?.state === 'intent'
|
|
218
|
+
? seat.launch.attempt : (seat.launch?.attempt ?? 0) + 1;
|
|
219
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
220
|
+
state: 'intent', attempt, action_id: actionId, mission_sha256: missionSha,
|
|
221
|
+
updated_at: new Date().toISOString(),
|
|
222
|
+
} });
|
|
223
|
+
const launchedDir = await spawnTemp({
|
|
224
|
+
name: member.name, temp: true, identity: member.name, mission,
|
|
225
|
+
model: settings.model, harness: settings.harness, cwd: settings.cwd,
|
|
226
|
+
surface: 'agent', creationActionId: actionId, roomStartupGate: gate,
|
|
227
|
+
}, provision.binPath);
|
|
228
|
+
const supervisor = readTempSupervisor(launchedDir);
|
|
229
|
+
if (!supervisor || supervisor.role !== member.name
|
|
230
|
+
|| !launchMatches(launchedDir, member, actionId, missionSha, gate)) {
|
|
231
|
+
throw new Error(`new launch for ${member.name} did not persist matching provenance`);
|
|
232
|
+
}
|
|
233
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
234
|
+
state: 'launched', attempt, action_id: actionId, mission_sha256: missionSha,
|
|
235
|
+
launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
|
|
236
|
+
} });
|
|
92
237
|
}
|
|
93
238
|
export async function provisionMembers(input) {
|
|
94
239
|
const { cfg, cowork, roomId, taskId, template, binPath } = input;
|
|
@@ -96,14 +241,12 @@ export async function provisionMembers(input) {
|
|
|
96
241
|
const plan = expandMembers(template, prefix);
|
|
97
242
|
const createdIdentities = [];
|
|
98
243
|
const members = [];
|
|
99
|
-
// A room that already reached wait_seats keeps its member identities, seats
|
|
100
|
-
// and accepted invitations; recreating them on a retry would collide. Resume
|
|
101
|
-
// from the persisted seats and re-check seat activity instead.
|
|
102
244
|
const existing = getRoomRecord(roomId);
|
|
245
|
+
if (!existing?.room_identity_cid)
|
|
246
|
+
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
247
|
+
const roomIdentityCid = existing.room_identity_cid;
|
|
103
248
|
const persistedSeats = existing?.member_seats ?? [];
|
|
104
|
-
const resuming =
|
|
105
|
-
&& ['wait_seats', 'launch_work', 'activate'].includes(existing.saga.phase)
|
|
106
|
-
&& plan.length > 0
|
|
249
|
+
const resuming = plan.length > 0
|
|
107
250
|
&& plan.every(p => persistedSeats.some(s => s.role_name === p.name));
|
|
108
251
|
let seats;
|
|
109
252
|
if (resuming) {
|
|
@@ -111,10 +254,19 @@ export async function provisionMembers(input) {
|
|
|
111
254
|
const seat = persistedSeats.find(s => s.role_name === planned.name);
|
|
112
255
|
members.push({ ...planned, cid: seat.identity_cid });
|
|
113
256
|
}
|
|
114
|
-
seats = persistedSeats
|
|
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);
|
|
115
267
|
}
|
|
116
268
|
else {
|
|
117
|
-
//
|
|
269
|
+
// create_members saga phase
|
|
118
270
|
advanceSaga(roomId, 'create_members', 3);
|
|
119
271
|
try {
|
|
120
272
|
for (const planned of plan) {
|
|
@@ -138,6 +290,10 @@ export async function provisionMembers(input) {
|
|
|
138
290
|
slot: m.slot,
|
|
139
291
|
cowork_role: m.coworkRole,
|
|
140
292
|
seat_state: 'pending',
|
|
293
|
+
launch: { state: 'pending', attempt: 0, updated_at: new Date().toISOString() },
|
|
294
|
+
briefing: {
|
|
295
|
+
role: m.coworkRole, state: 'pending', rejected_ack_count: 0,
|
|
296
|
+
},
|
|
141
297
|
}));
|
|
142
298
|
updateMemberSeats(roomId, seats);
|
|
143
299
|
if (taskId) {
|
|
@@ -149,83 +305,254 @@ export async function provisionMembers(input) {
|
|
|
149
305
|
}));
|
|
150
306
|
updateTaskMembers(taskId, taskMembers);
|
|
151
307
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
308
|
+
}
|
|
309
|
+
const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg)]));
|
|
310
|
+
const roster = members.map(member => ({
|
|
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`);
|
|
159
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
|
+
});
|
|
160
345
|
try {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
});
|
|
166
|
-
// invite is used in-memory only — NEVER persisted
|
|
167
|
-
for (const member of group) {
|
|
168
|
-
await cowork.acceptInvite(roomId, invite, {
|
|
169
|
-
role: coworkRole,
|
|
170
|
-
expected_cid: member.cid,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
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}`);
|
|
173
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
|
+
});
|
|
174
363
|
}
|
|
175
364
|
catch (error) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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');
|
|
179
372
|
if (taskId)
|
|
180
|
-
|
|
373
|
+
blockTask(taskId, error instanceof Error ? error.message : String(error));
|
|
181
374
|
throw error;
|
|
182
375
|
}
|
|
183
376
|
}
|
|
184
|
-
|
|
185
|
-
|
|
377
|
+
const policy = {
|
|
378
|
+
timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
|
|
379
|
+
initialDelayMs: input.startupWait?.initialDelayMs ?? 250,
|
|
380
|
+
maxDelayMs: input.startupWait?.maxDelayMs ?? 2_000,
|
|
381
|
+
now: input.startupWait?.now ?? Date.now,
|
|
382
|
+
sleep: input.startupWait?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
383
|
+
};
|
|
384
|
+
advanceSaga(roomId, 'join_role_groups', 5);
|
|
385
|
+
try {
|
|
386
|
+
const observedSeats = await cowork.getSeats(roomId);
|
|
387
|
+
for (const member of members) {
|
|
388
|
+
const observed = observedSeats.find(seat => seat.identity_cid === member.cid
|
|
389
|
+
&& seat.seat_state !== 'removed');
|
|
390
|
+
if (observed && observed.role !== member.coworkRole)
|
|
391
|
+
throw new Error(`Cowork seat ${member.cid} has role ${observed.role}; expected ${member.coworkRole}`);
|
|
392
|
+
}
|
|
393
|
+
const admitted = new Set(observedSeats
|
|
394
|
+
.filter(seat => seat.seat_state !== 'removed').map(seat => seat.identity_cid));
|
|
395
|
+
const roleGroups = new Map();
|
|
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}`);
|
|
434
|
+
}
|
|
435
|
+
return !seat;
|
|
436
|
+
});
|
|
437
|
+
if (!waiting)
|
|
438
|
+
break;
|
|
439
|
+
if (policy.now() >= deadline) {
|
|
440
|
+
throw new Error('Cowork did not authenticate every required room seat after invite redemption');
|
|
441
|
+
}
|
|
442
|
+
await policy.sleep(delay);
|
|
443
|
+
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Role-group admission failed. Retry with `task recover`.', 'member_failed');
|
|
448
|
+
if (taskId)
|
|
449
|
+
blockTask(taskId, error instanceof Error ? error.message : String(error));
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
if (taskId && getTask(taskId).blocked)
|
|
453
|
+
unblockTask(taskId);
|
|
454
|
+
advanceSaga(roomId, 'wait_seats', 6);
|
|
186
455
|
const coworkSeats = await cowork.getSeats(roomId);
|
|
187
456
|
const memberCids = new Set(members.map(m => m.cid));
|
|
188
|
-
const allActive = [...memberCids].every(cid => coworkSeats.some(s => s.identity_cid === 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'));
|
|
189
460
|
if (!allActive) {
|
|
190
|
-
advanceSaga(roomId, 'wait_seats',
|
|
461
|
+
advanceSaga(roomId, 'wait_seats', 6, 'waiting_seats');
|
|
191
462
|
// Seats are pending — return record for caller to retry later
|
|
192
463
|
return getRoomRecord(roomId);
|
|
193
464
|
}
|
|
194
465
|
const activeSeats = seats.map(s => ({ ...s, seat_state: 'active' }));
|
|
195
466
|
updateMemberSeats(roomId, activeSeats);
|
|
196
|
-
|
|
197
|
-
advanceSaga(roomId, 'launch_work', 6);
|
|
467
|
+
advanceSaga(roomId, 'launch_work', 7);
|
|
198
468
|
try {
|
|
199
469
|
for (const member of members) {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
temp: true,
|
|
213
|
-
identity: member.name,
|
|
214
|
-
mission: briefing,
|
|
215
|
-
model,
|
|
216
|
-
harness,
|
|
217
|
-
cwd,
|
|
218
|
-
persona,
|
|
219
|
-
surface: 'agent',
|
|
220
|
-
}, binPath);
|
|
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
|
+
});
|
|
221
482
|
}
|
|
222
483
|
}
|
|
223
484
|
catch (error) {
|
|
224
485
|
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Member launch failed. Some agents may be running. Retry with `task recover`.', 'member_failed');
|
|
225
486
|
throw error;
|
|
226
487
|
}
|
|
227
|
-
|
|
228
|
-
|
|
488
|
+
const deadline = policy.now() + policy.timeoutMs;
|
|
489
|
+
let delay = policy.initialDelayMs;
|
|
490
|
+
for (;;) {
|
|
491
|
+
const history = await roomHistory(cowork, roomId);
|
|
492
|
+
let allAcknowledged = true;
|
|
493
|
+
let relayFailed = false;
|
|
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)
|
|
532
|
+
break;
|
|
533
|
+
if (policy.now() >= deadline) {
|
|
534
|
+
advanceSaga(roomId, 'wait_briefing_acks', 8, 'waiting_briefing_acks');
|
|
535
|
+
return getRoomRecord(roomId);
|
|
536
|
+
}
|
|
537
|
+
advanceSaga(roomId, 'wait_briefing_acks', 8, 'waiting_briefing_acks');
|
|
538
|
+
await policy.sleep(delay);
|
|
539
|
+
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
540
|
+
}
|
|
541
|
+
const finalDefinitions = new Map([...definitions].map(([role, definition]) => {
|
|
542
|
+
const persisted = getRoomRecord(roomId).role_briefings[role];
|
|
543
|
+
return [role, { ...definition, version: persisted.version }];
|
|
544
|
+
}));
|
|
545
|
+
try {
|
|
546
|
+
await assertCurrentRoleBriefings(cowork, roomId, finalDefinitions);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
550
|
+
setSagaError(roomId, reason, 'Cowork current role briefing drifted before activation; inspect room state.', 'uncertain');
|
|
551
|
+
if (taskId)
|
|
552
|
+
blockTask(taskId, reason);
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
advanceSaga(roomId, 'activate', 9);
|
|
229
556
|
const record = activateRoom(roomId);
|
|
230
557
|
if (taskId)
|
|
231
558
|
activateTask(taskId);
|
|
@@ -235,14 +562,8 @@ export async function cleanupMembers(input) {
|
|
|
235
562
|
const room = getRoomRecord(input.roomId);
|
|
236
563
|
if (!room)
|
|
237
564
|
return;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
if (input.closeCoworkRoom && input.cowork) {
|
|
242
|
-
try {
|
|
243
|
-
await input.cowork.closeRoom(input.roomId);
|
|
244
|
-
}
|
|
245
|
-
catch { /* best-effort */ }
|
|
565
|
+
if (!input.closeCoworkRoom || !input.cowork) {
|
|
566
|
+
throw new Error('member cleanup requires the shared deterministic room-close saga and Cowork adapter');
|
|
246
567
|
}
|
|
247
|
-
|
|
568
|
+
await closeManagedRoom({ roomId: input.roomId, cowork: input.cowork });
|
|
248
569
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RoomOrchestrationRecord, RoomOrchestrationState, SagaPhase, RoomMemberSeat, ProvisioningDetail } from './types.js';
|
|
1
|
+
import type { RoomOrchestrationRecord, RoomOrchestrationState, SagaPhase, RoomMemberSeat, ProvisioningDetail, MemberRetirementPhase, RoomClosePhase, RoomRoleBriefingDefinition, RoomMemberLaunchState, RoomMemberBriefingState } from './types.js';
|
|
2
2
|
export declare const roomsDir: () => string;
|
|
3
3
|
export declare class RoomStateError extends Error {
|
|
4
4
|
}
|
|
@@ -12,6 +12,8 @@ export interface CreateRoomInput {
|
|
|
12
12
|
}
|
|
13
13
|
export declare function createRoomRecord(input: CreateRoomInput): RoomOrchestrationRecord;
|
|
14
14
|
export declare function getRoomRecord(id: string): RoomOrchestrationRecord | undefined;
|
|
15
|
+
/** Remove a terminal orchestration record from the live room inventory. */
|
|
16
|
+
export declare function deleteRoomRecord(id: string): void;
|
|
15
17
|
export declare function listRoomRecords(filter?: {
|
|
16
18
|
state?: RoomOrchestrationState | RoomOrchestrationState[];
|
|
17
19
|
}): RoomOrchestrationRecord[];
|
|
@@ -20,5 +22,15 @@ export declare function setSagaError(id: string, error: string, hint?: string, d
|
|
|
20
22
|
export declare function setRoomIdentity(id: string, roomIdentityCid: string): RoomOrchestrationRecord;
|
|
21
23
|
export declare function setOwnerSeat(id: string, ownerSeatCid: string, inviteFingerprint: string): RoomOrchestrationRecord;
|
|
22
24
|
export declare function updateMemberSeats(id: string, seats: RoomMemberSeat[]): RoomOrchestrationRecord;
|
|
25
|
+
export declare function updateRoomRoleBriefing(id: string, role: string, definition: RoomRoleBriefingDefinition): RoomOrchestrationRecord;
|
|
26
|
+
export declare function updateRoomHistoryCursor(id: string, cursor: number): RoomOrchestrationRecord;
|
|
27
|
+
export declare function updateMemberStartup(id: string, roleName: string, update: {
|
|
28
|
+
launch?: RoomMemberLaunchState;
|
|
29
|
+
briefing?: RoomMemberBriefingState;
|
|
30
|
+
}): RoomOrchestrationRecord;
|
|
23
31
|
export declare function activateRoom(id: string): RoomOrchestrationRecord;
|
|
24
32
|
export declare function closeRoom(id: string): RoomOrchestrationRecord;
|
|
33
|
+
export declare function beginRoomClose(id: string): RoomOrchestrationRecord;
|
|
34
|
+
export declare function advanceMemberRetirement(id: string, roleName: string, phase: MemberRetirementPhase, launchId: string, archivePath?: string): RoomOrchestrationRecord;
|
|
35
|
+
export declare function advanceRoomClose(id: string, phase: RoomClosePhase): RoomOrchestrationRecord;
|
|
36
|
+
export declare function setRoomCloseError(id: string, error: string, recoveryHint: string): RoomOrchestrationRecord;
|