@ours.network/fleet 0.19.0-nightly.8 → 1.0.0
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 +96 -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 +145 -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 +733 -221
- package/dist/rooms-tasks/close.d.ts +35 -0
- package/dist/rooms-tasks/close.js +182 -0
- package/dist/rooms-tasks/config.js +8 -5
- package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
- package/dist/rooms-tasks/cowork-adapter.js +116 -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 +363 -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 +109 -4
- package/dist/rooms-tasks/types.js +1 -1
- 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,150 @@ async function removeMemberIdentity(name) {
|
|
|
64
70
|
}
|
|
65
71
|
catch { /* best-effort cleanup */ }
|
|
66
72
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
for (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
} });
|
|
92
217
|
}
|
|
93
218
|
export async function provisionMembers(input) {
|
|
94
219
|
const { cfg, cowork, roomId, taskId, template, binPath } = input;
|
|
@@ -96,14 +221,11 @@ export async function provisionMembers(input) {
|
|
|
96
221
|
const plan = expandMembers(template, prefix);
|
|
97
222
|
const createdIdentities = [];
|
|
98
223
|
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
224
|
const existing = getRoomRecord(roomId);
|
|
225
|
+
if (!existing?.room_identity_cid)
|
|
226
|
+
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
103
227
|
const persistedSeats = existing?.member_seats ?? [];
|
|
104
|
-
const resuming =
|
|
105
|
-
&& ['wait_seats', 'launch_work', 'activate'].includes(existing.saga.phase)
|
|
106
|
-
&& plan.length > 0
|
|
228
|
+
const resuming = plan.length > 0
|
|
107
229
|
&& plan.every(p => persistedSeats.some(s => s.role_name === p.name));
|
|
108
230
|
let seats;
|
|
109
231
|
if (resuming) {
|
|
@@ -111,10 +233,19 @@ export async function provisionMembers(input) {
|
|
|
111
233
|
const seat = persistedSeats.find(s => s.role_name === planned.name);
|
|
112
234
|
members.push({ ...planned, cid: seat.identity_cid });
|
|
113
235
|
}
|
|
114
|
-
seats = persistedSeats
|
|
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);
|
|
115
246
|
}
|
|
116
247
|
else {
|
|
117
|
-
//
|
|
248
|
+
// create_members saga phase
|
|
118
249
|
advanceSaga(roomId, 'create_members', 3);
|
|
119
250
|
try {
|
|
120
251
|
for (const planned of plan) {
|
|
@@ -138,6 +269,10 @@ export async function provisionMembers(input) {
|
|
|
138
269
|
slot: m.slot,
|
|
139
270
|
cowork_role: m.coworkRole,
|
|
140
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
|
+
},
|
|
141
276
|
}));
|
|
142
277
|
updateMemberSeats(roomId, seats);
|
|
143
278
|
if (taskId) {
|
|
@@ -149,83 +284,220 @@ export async function provisionMembers(input) {
|
|
|
149
284
|
}));
|
|
150
285
|
updateTaskMembers(taskId, taskMembers);
|
|
151
286
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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`);
|
|
159
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
|
+
});
|
|
160
324
|
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
|
-
}
|
|
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}`);
|
|
173
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
|
+
});
|
|
174
342
|
}
|
|
175
343
|
catch (error) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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');
|
|
179
351
|
if (taskId)
|
|
180
|
-
|
|
352
|
+
blockTask(taskId, error instanceof Error ? error.message : String(error));
|
|
181
353
|
throw error;
|
|
182
354
|
}
|
|
183
355
|
}
|
|
184
|
-
|
|
185
|
-
|
|
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);
|
|
186
393
|
const coworkSeats = await cowork.getSeats(roomId);
|
|
187
394
|
const memberCids = new Set(members.map(m => m.cid));
|
|
188
|
-
const allActive = [...memberCids].every(cid => coworkSeats.some(s => s.identity_cid === 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'));
|
|
189
398
|
if (!allActive) {
|
|
190
|
-
advanceSaga(roomId, 'wait_seats',
|
|
399
|
+
advanceSaga(roomId, 'wait_seats', 6, 'waiting_seats');
|
|
191
400
|
// Seats are pending — return record for caller to retry later
|
|
192
401
|
return getRoomRecord(roomId);
|
|
193
402
|
}
|
|
194
403
|
const activeSeats = seats.map(s => ({ ...s, seat_state: 'active' }));
|
|
195
404
|
updateMemberSeats(roomId, activeSeats);
|
|
196
|
-
|
|
197
|
-
advanceSaga(roomId, 'launch_work', 6);
|
|
405
|
+
advanceSaga(roomId, 'launch_work', 7);
|
|
198
406
|
try {
|
|
199
407
|
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);
|
|
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
|
+
});
|
|
221
420
|
}
|
|
222
421
|
}
|
|
223
422
|
catch (error) {
|
|
224
423
|
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Member launch failed. Some agents may be running. Retry with `task recover`.', 'member_failed');
|
|
225
424
|
throw error;
|
|
226
425
|
}
|
|
227
|
-
|
|
228
|
-
|
|
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);
|
|
229
501
|
const record = activateRoom(roomId);
|
|
230
502
|
if (taskId)
|
|
231
503
|
activateTask(taskId);
|
|
@@ -235,14 +507,8 @@ export async function cleanupMembers(input) {
|
|
|
235
507
|
const room = getRoomRecord(input.roomId);
|
|
236
508
|
if (!room)
|
|
237
509
|
return;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
if (input.closeCoworkRoom && input.cowork) {
|
|
242
|
-
try {
|
|
243
|
-
await input.cowork.closeRoom(input.roomId);
|
|
244
|
-
}
|
|
245
|
-
catch { /* best-effort */ }
|
|
510
|
+
if (!input.closeCoworkRoom || !input.cowork) {
|
|
511
|
+
throw new Error('member cleanup requires the shared deterministic room-close saga and Cowork adapter');
|
|
246
512
|
}
|
|
247
|
-
|
|
513
|
+
await closeManagedRoom({ roomId: input.roomId, cowork: input.cowork });
|
|
248
514
|
}
|
|
@@ -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;
|