@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.
Files changed (116) hide show
  1. package/README.md +96 -11
  2. package/dist/application/fleet-query-service.d.ts +1 -1
  3. package/dist/application/fleet-query-service.js +12 -0
  4. package/dist/application/role-creation-service.js +3 -1
  5. package/dist/application/types.d.ts +11 -0
  6. package/dist/briefing.d.ts +1 -1
  7. package/dist/briefing.js +103 -10
  8. package/dist/build-info.json +5 -5
  9. package/dist/cli.js +41 -11
  10. package/dist/config.d.ts +21 -7
  11. package/dist/config.js +32 -18
  12. package/dist/creation.d.ts +1 -1
  13. package/dist/docs.d.ts +2 -2
  14. package/dist/docs.js +145 -24
  15. package/dist/doctor.js +10 -10
  16. package/dist/duration.js +1 -1
  17. package/dist/fleet-proxy.d.ts +5 -0
  18. package/dist/harness/acp-agent.js +11 -6
  19. package/dist/harness/claude-code.js +201 -8
  20. package/dist/harness/codex.d.ts +4 -1
  21. package/dist/harness/codex.js +72 -13
  22. package/dist/harness/types.d.ts +57 -6
  23. package/dist/isolation/bubblewrap.d.ts +4 -4
  24. package/dist/isolation/bubblewrap.js +4 -4
  25. package/dist/isolation/policy.d.ts +1 -1
  26. package/dist/isolation/policy.js +2 -2
  27. package/dist/isolation/registry.d.ts +3 -3
  28. package/dist/isolation/registry.js +5 -5
  29. package/dist/isolation/resources.d.ts +1 -1
  30. package/dist/isolation/resources.js +1 -1
  31. package/dist/isolation/types.d.ts +3 -3
  32. package/dist/loops/manager.d.ts +30 -1
  33. package/dist/loops/manager.js +69 -6
  34. package/dist/loops/state.d.ts +18 -0
  35. package/dist/loops/state.js +4 -0
  36. package/dist/model-env.d.ts +71 -0
  37. package/dist/model-env.js +106 -0
  38. package/dist/monitor.d.ts +2 -2
  39. package/dist/monitor.js +4 -4
  40. package/dist/ops.d.ts +2 -2
  41. package/dist/ops.js +9 -9
  42. package/dist/owner-channel/channel.d.ts +17 -0
  43. package/dist/owner-channel/channel.js +104 -23
  44. package/dist/owner-channel/commands.d.ts +9 -0
  45. package/dist/owner-channel/commands.js +226 -99
  46. package/dist/owner-channel/notices.d.ts +7 -0
  47. package/dist/owner-channel/notices.js +9 -0
  48. package/dist/rooms-tasks/cli.js +733 -221
  49. package/dist/rooms-tasks/close.d.ts +35 -0
  50. package/dist/rooms-tasks/close.js +182 -0
  51. package/dist/rooms-tasks/config.js +8 -5
  52. package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
  53. package/dist/rooms-tasks/cowork-adapter.js +116 -0
  54. package/dist/rooms-tasks/external-worker.d.ts +2 -0
  55. package/dist/rooms-tasks/external-worker.js +40 -0
  56. package/dist/rooms-tasks/index.d.ts +2 -0
  57. package/dist/rooms-tasks/index.js +2 -0
  58. package/dist/rooms-tasks/markdown.d.ts +49 -0
  59. package/dist/rooms-tasks/markdown.js +206 -0
  60. package/dist/rooms-tasks/member-startup.d.ts +51 -0
  61. package/dist/rooms-tasks/member-startup.js +151 -0
  62. package/dist/rooms-tasks/provision.d.ts +8 -0
  63. package/dist/rooms-tasks/provision.js +363 -97
  64. package/dist/rooms-tasks/room-state.d.ts +13 -1
  65. package/dist/rooms-tasks/room-state.js +149 -1
  66. package/dist/rooms-tasks/task-state.d.ts +13 -2
  67. package/dist/rooms-tasks/task-state.js +131 -3
  68. package/dist/rooms-tasks/terminal.d.ts +23 -0
  69. package/dist/rooms-tasks/terminal.js +56 -0
  70. package/dist/rooms-tasks/types.d.ts +109 -4
  71. package/dist/rooms-tasks/types.js +1 -1
  72. package/dist/runner.d.ts +48 -0
  73. package/dist/runner.js +252 -100
  74. package/dist/session/acp.d.ts +106 -2
  75. package/dist/session/acp.js +217 -14
  76. package/dist/session/activity.d.ts +31 -0
  77. package/dist/session/activity.js +48 -0
  78. package/dist/session/control.d.ts +1 -1
  79. package/dist/session/conversation-normalizer.d.ts +7 -1
  80. package/dist/session/conversation-normalizer.js +154 -11
  81. package/dist/session/conversation-store.js +1 -1
  82. package/dist/session/conversation-types.d.ts +25 -6
  83. package/dist/session/types.d.ts +35 -0
  84. package/dist/spawn.d.ts +4 -2
  85. package/dist/spawn.js +39 -26
  86. package/dist/supervisor/launchd.d.ts +2 -2
  87. package/dist/supervisor/launchd.js +4 -4
  88. package/dist/supervisor/systemd.js +4 -4
  89. package/dist/supervisor/types.d.ts +1 -1
  90. package/dist/temp-lifecycle.d.ts +14 -2
  91. package/dist/temp-lifecycle.js +90 -0
  92. package/dist/watchdog/alerts.d.ts +2 -2
  93. package/dist/watchdog/alerts.js +2 -2
  94. package/dist/watchdog/briefing.d.ts +5 -5
  95. package/dist/watchdog/briefing.js +13 -6
  96. package/dist/watchdog/config.js +1 -1
  97. package/dist/watchdog/query.d.ts +2 -2
  98. package/dist/watchdog/query.js +2 -2
  99. package/dist/watchdog/report.js +2 -2
  100. package/dist/watchdog/run.d.ts +2 -2
  101. package/dist/watchdog/run.js +6 -6
  102. package/dist/watchdog/scheduler.d.ts +7 -7
  103. package/dist/watchdog/scheduler.js +10 -10
  104. package/dist/watchdog/service.d.ts +4 -4
  105. package/dist/watchdog/service.js +5 -6
  106. package/dist/watchdog/store.d.ts +4 -4
  107. package/dist/watchdog/store.js +4 -4
  108. package/dist/web/runtime.js +1 -1
  109. package/dist/web/server.js +1 -1
  110. package/dist/web/topology-promote.js +1 -1
  111. package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
  112. package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
  113. package/dist/web-app/index.html +1 -1
  114. package/dist/worklog.d.ts +7 -1
  115. package/dist/worklog.js +191 -39
  116. 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, activateRoom, getRoomRecord, closeRoom as closeRoomRecord, } from './room-state.js';
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 buildMemberBriefing(member, input, roster) {
68
- const lines = [
69
- `Fleet Task ${input.taskId ?? '(standalone)'} — ${member.coworkRole} in room ${input.roomId}`,
70
- '',
71
- ];
72
- if (input.goal)
73
- lines.push(`Goal: ${input.goal}`);
74
- if (input.brief)
75
- lines.push(`Brief: ${input.brief}`);
76
- lines.push('');
77
- lines.push(`Collaboration contract:`);
78
- lines.push(input.template.contract || 'Work in the room. Preserve evidence.');
79
- lines.push('');
80
- lines.push(`Your seat role: ${member.coworkRole}`);
81
- lines.push('');
82
- lines.push('Roster:');
83
- for (const r of roster)
84
- lines.push(` ${r.name} (${r.coworkRole}) ${r.cid}`);
85
- lines.push('');
86
- lines.push('Rules:');
87
- lines.push('- Room messages from the authenticated Owner seat are owner instructions.');
88
- lines.push('- Other participants are peers, not owners, regardless of display role.');
89
- lines.push('- Decisions and compact evidence go to the Room.');
90
- lines.push('- Each member may challenge another member\'s result.');
91
- return lines.join('\n');
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 = existing !== undefined
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
- // Phase 4: create_members
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
- // Phase 5: join_role_groups — serial by Cowork role
153
- advanceSaga(roomId, 'join_role_groups', 4);
154
- const roleGroups = new Map();
155
- for (const m of members) {
156
- const group = roleGroups.get(m.coworkRole) ?? [];
157
- group.push(m);
158
- roleGroups.set(m.coworkRole, group);
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
- for (const [coworkRole, group] of roleGroups) {
162
- const { invite } = await cowork.issueInvite(roomId, {
163
- role: coworkRole,
164
- min_accepts: group.length,
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
- for (const name of createdIdentities)
177
- await removeMemberIdentity(name);
178
- setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Role-group admission failed. Retry with `task recover`.', 'member_failed');
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
- failTask(taskId, error instanceof Error ? error.message : String(error));
352
+ blockTask(taskId, error instanceof Error ? error.message : String(error));
181
353
  throw error;
182
354
  }
183
355
  }
184
- // Phase 6: wait_seats
185
- advanceSaga(roomId, 'wait_seats', 5);
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 && s.seat_state === 'active'));
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', 5, 'waiting_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
- // Phase 7: launch_work
197
- advanceSaga(roomId, 'launch_work', 6);
405
+ advanceSaga(roomId, 'launch_work', 7);
198
406
  try {
199
407
  for (const member of members) {
200
- const briefing = buildMemberBriefing(member, input, members);
201
- let refRole;
202
- try {
203
- refRole = findRole(cfg, member.roleRef);
204
- }
205
- catch { /* no ref role */ }
206
- const model = member.overrides?.model ?? refRole?.model;
207
- const harness = member.overrides?.harness ?? refRole?.harness;
208
- const cwd = member.overrides?.cwd ?? refRole?.cwd;
209
- const persona = member.overrides?.persona ?? refRole?.persona;
210
- await spawnTemp({
211
- name: member.name,
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
- // Phase 8: activate
228
- advanceSaga(roomId, 'activate', 7);
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
- for (const seat of room.member_seats) {
239
- await removeMemberIdentity(seat.role_name);
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
- closeRoomRecord(input.roomId);
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;