@ours.network/fleet 1.0.2 → 1.0.4
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 +4 -4
- package/dist/application/role-command-service.d.ts +29 -1
- package/dist/application/role-command-service.js +41 -2
- package/dist/application/role-creation-service.d.ts +32 -1
- package/dist/application/role-creation-service.js +56 -10
- package/dist/application/role-removal-service.d.ts +19 -0
- package/dist/application/role-removal-service.js +13 -3
- package/dist/application/session-mutations.d.ts +7 -0
- package/dist/application/session-mutations.js +8 -0
- package/dist/application/task-room-service.d.ts +227 -0
- package/dist/application/task-room-service.js +529 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +39 -15
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +7 -6
- package/dist/harness/codex-app-server-proxy.d.ts +1 -1
- package/dist/harness/codex-app-server-proxy.js +23 -9
- package/dist/harness/types.d.ts +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/owner-channel/channel.d.ts +5 -0
- package/dist/owner-channel/channel.js +143 -18
- package/dist/owner-channel/commands.d.ts +26 -1
- package/dist/owner-channel/commands.js +66 -128
- package/dist/rooms-tasks/cli.js +181 -462
- package/dist/rooms-tasks/provision.js +71 -8
- package/dist/rooms-tasks/types.d.ts +2 -0
- package/dist/runner.js +19 -44
- package/dist/session/acp.d.ts +8 -3
- package/dist/session/acp.js +24 -8
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +22 -21
- package/dist/watchdog/query.d.ts +2 -0
- package/dist/watchdog/query.js +7 -3
- package/dist/web/server.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { ConfigError, loadConfig } from '../config.js';
|
|
3
|
+
import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
|
|
4
|
+
import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
|
|
5
|
+
import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
|
|
6
|
+
import { activateTask, blockTask as persistBlockTask, createTask as persistTask, deleteTask as persistDeleteTask, getTask as readTask, listTasks as readTasks, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, } from '../rooms-tasks/task-state.js';
|
|
7
|
+
import { hashTemplate, listTemplates, resolveTemplate, snapshotTemplate } from '../rooms-tasks/templates.js';
|
|
8
|
+
import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
|
|
9
|
+
import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from '../rooms-tasks/terminal.js';
|
|
10
|
+
import { TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
|
|
11
|
+
export class TaskRoomApplicationError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
fields;
|
|
14
|
+
constructor(code, message, fields = {}) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.fields = fields;
|
|
18
|
+
this.name = 'TaskRoomApplicationError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Exact extraction of the previously CLI-owned task create/start behavior. */
|
|
22
|
+
export class TaskRoomApplicationService {
|
|
23
|
+
configurationPath;
|
|
24
|
+
deps;
|
|
25
|
+
recovery;
|
|
26
|
+
constructor(configurationPath, deps = {}) {
|
|
27
|
+
this.configurationPath = configurationPath;
|
|
28
|
+
this.deps = deps;
|
|
29
|
+
}
|
|
30
|
+
async createTask(request) {
|
|
31
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
32
|
+
const template = this.createTemplate(cfg, request.template, request.noRoom);
|
|
33
|
+
const ref = template && {
|
|
34
|
+
name: template.name, version: template.version, content_hash: template.content_hash,
|
|
35
|
+
};
|
|
36
|
+
const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
|
|
37
|
+
let task = persistTask({
|
|
38
|
+
title: request.title, brief, brief_file: request.briefFile, template: ref,
|
|
39
|
+
origin: request.origin, idempotency_key: request.idempotencyKey,
|
|
40
|
+
start: !request.backlog,
|
|
41
|
+
no_room: request.noRoom,
|
|
42
|
+
});
|
|
43
|
+
if (!request.backlog && !request.noRoom && ref && !task.room_id) {
|
|
44
|
+
try {
|
|
45
|
+
await this.provisionRoom(cfg, task, template, room => {
|
|
46
|
+
task = updateTaskRoom(task.task_id, room.room_id, room.room_identity_cid);
|
|
47
|
+
});
|
|
48
|
+
task = readTask(task.task_id);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error instanceof CoworkUnavailableError)
|
|
52
|
+
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return task;
|
|
57
|
+
}
|
|
58
|
+
async createRoom(request) {
|
|
59
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
60
|
+
const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
|
|
61
|
+
let template;
|
|
62
|
+
if (request.template) {
|
|
63
|
+
const definition = resolveTemplate(request.template, cfg.roomTemplates ?? {});
|
|
64
|
+
if (!definition)
|
|
65
|
+
throw new TaskRoomApplicationError('template_not_found', `template not found: ${request.template}`, { template: request.template });
|
|
66
|
+
template = snapshotTemplate(definition);
|
|
67
|
+
}
|
|
68
|
+
return this.provisionRoom(cfg, {
|
|
69
|
+
title: request.name, brief, goal: request.goal,
|
|
70
|
+
}, template, () => { });
|
|
71
|
+
}
|
|
72
|
+
async startTask(input) {
|
|
73
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
74
|
+
let task = transitionTask(input.taskId);
|
|
75
|
+
if (task.template && !task.room_id) {
|
|
76
|
+
const template = this.existingTemplate(cfg, task);
|
|
77
|
+
await this.provisionRoom(cfg, task, template, room => {
|
|
78
|
+
task = updateTaskRoom(task.task_id, room.room_id, room.room_identity_cid);
|
|
79
|
+
});
|
|
80
|
+
task = readTask(task.task_id);
|
|
81
|
+
}
|
|
82
|
+
return task;
|
|
83
|
+
}
|
|
84
|
+
listTasks(filter) {
|
|
85
|
+
return readTasks(filter);
|
|
86
|
+
}
|
|
87
|
+
getTask(taskId) {
|
|
88
|
+
const task = readTask(taskId);
|
|
89
|
+
return { task, orchestration: task.room_id ? getRoomRecord(task.room_id) : undefined };
|
|
90
|
+
}
|
|
91
|
+
blockTask(input) {
|
|
92
|
+
return persistBlockTask(input.taskId, input.reason);
|
|
93
|
+
}
|
|
94
|
+
unblockTask(input) {
|
|
95
|
+
return persistUnblockTask(input.taskId);
|
|
96
|
+
}
|
|
97
|
+
reviewTask(input) {
|
|
98
|
+
return persistReviewTask(input.taskId);
|
|
99
|
+
}
|
|
100
|
+
deleteTask(input) {
|
|
101
|
+
return persistDeleteTask(input.taskId);
|
|
102
|
+
}
|
|
103
|
+
async completeTask(input) {
|
|
104
|
+
const task = readTask(input.taskId);
|
|
105
|
+
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
106
|
+
throw new TaskStateError(`task ${input.taskId} is already in terminal state '${task.state}'`);
|
|
107
|
+
if (task.state !== 'review')
|
|
108
|
+
throw new TaskStateError(`cannot transition from '${task.state}' to 'done'`);
|
|
109
|
+
let roomId;
|
|
110
|
+
if (task.room_id) {
|
|
111
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
112
|
+
const shouldClose = cfg.tasks?.close_room_on_done
|
|
113
|
+
?? cfg.rooms?.defaults?.close_when_task_done ?? false;
|
|
114
|
+
if (shouldClose)
|
|
115
|
+
roomId = task.room_id;
|
|
116
|
+
}
|
|
117
|
+
return this.acceptTerminal(input.taskId, 'done', roomId, input.outcome);
|
|
118
|
+
}
|
|
119
|
+
async cancelTask(input) {
|
|
120
|
+
const task = readTask(input.taskId);
|
|
121
|
+
if (!TASK_CANCELLABLE_STATES.includes(task.state))
|
|
122
|
+
throw new TaskStateError(`cannot cancel a '${task.state}' task`);
|
|
123
|
+
if (task.room_id)
|
|
124
|
+
(this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
125
|
+
return this.acceptTerminal(input.taskId, 'cancelled', task.room_id);
|
|
126
|
+
}
|
|
127
|
+
async settleTask(input) {
|
|
128
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
129
|
+
if (!cfg.rooms)
|
|
130
|
+
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
131
|
+
const cowork = this.deps.cowork ? this.deps.cowork(cfg) : createCoworkAdapter({
|
|
132
|
+
configPath: cfg.rooms.cowork?.config,
|
|
133
|
+
});
|
|
134
|
+
return settleTaskTerminalIntent({ taskId: input.taskId, cowork });
|
|
135
|
+
}
|
|
136
|
+
recordSettlementError(input) {
|
|
137
|
+
return recordTaskTerminalIntentError(input.taskId, input.error, input.recoveryHint);
|
|
138
|
+
}
|
|
139
|
+
async beginTaskRecovery(input) {
|
|
140
|
+
const config = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
141
|
+
this.recovery = { taskId: input.taskId, config };
|
|
142
|
+
const task = readTask(input.taskId);
|
|
143
|
+
if (task.terminal_intent?.status === 'pending')
|
|
144
|
+
return { kind: 'terminal_worker_required', taskId: input.taskId };
|
|
145
|
+
return { kind: 'final', result: await this.continueTaskRecovery({
|
|
146
|
+
actor: input.actor, taskId: input.taskId, terminalTimedOut: false,
|
|
147
|
+
}) };
|
|
148
|
+
}
|
|
149
|
+
async continueTaskRecovery(input) {
|
|
150
|
+
if (!this.recovery || this.recovery.taskId !== input.taskId)
|
|
151
|
+
throw new Error('task recovery continuation requires a matching begin');
|
|
152
|
+
const recovery = this.recovery;
|
|
153
|
+
this.recovery = undefined;
|
|
154
|
+
const cfg = recovery.config;
|
|
155
|
+
let task = readTask(input.taskId);
|
|
156
|
+
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
157
|
+
const issues = input.terminalTimedOut ? [{ code: 'terminal_pending' }] : [];
|
|
158
|
+
if (task.state !== 'provisioning')
|
|
159
|
+
return {
|
|
160
|
+
kind: TASK_TERMINAL_STATES.includes(task.state) ? 'terminal' : 'no_op', task, room, issues,
|
|
161
|
+
};
|
|
162
|
+
if (!room)
|
|
163
|
+
return { kind: 'provisioning_non_resumable', task, room, issues, reason: 'missing_room' };
|
|
164
|
+
if (room.provisioning_detail === 'waiting_cowork')
|
|
165
|
+
issues.push({ code: 'waiting_cowork' });
|
|
166
|
+
if (room.provisioning_detail === 'waiting_owner_invite')
|
|
167
|
+
issues.push({ code: 'waiting_owner_invite' });
|
|
168
|
+
if (room.provisioning_detail === 'owner_cid_mismatch')
|
|
169
|
+
issues.push({ code: 'owner_cid_mismatch' });
|
|
170
|
+
if (room.provisioning_detail === 'member_failed')
|
|
171
|
+
issues.push({ code: 'member_failed', stepIndex: room.saga.step_index });
|
|
172
|
+
if (room.provisioning_detail === 'waiting_seats')
|
|
173
|
+
issues.push({ code: 'waiting_seats' });
|
|
174
|
+
const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
|
|
175
|
+
if (!resumable.includes(room.saga.phase))
|
|
176
|
+
return {
|
|
177
|
+
kind: 'provisioning_non_resumable', task, room, issues, reason: 'non_resumable_phase',
|
|
178
|
+
};
|
|
179
|
+
const template = room.template_snapshot;
|
|
180
|
+
if (!template)
|
|
181
|
+
throw new Error(`room ${room.room_id} has no durable template snapshot`);
|
|
182
|
+
if (!task.template || task.template.name !== template.name
|
|
183
|
+
|| task.template.version !== template.version || task.template.content_hash !== template.content_hash)
|
|
184
|
+
throw new Error(`task ${task.task_id} template reference does not match room ${room.room_id}'s durable snapshot`);
|
|
185
|
+
try {
|
|
186
|
+
if (!cfg.rooms)
|
|
187
|
+
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
188
|
+
const cowork = this.deps.cowork ? this.deps.cowork(cfg) : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
189
|
+
await (this.deps.provisionMembers ?? provisionMembers)({
|
|
190
|
+
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template,
|
|
191
|
+
binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief, goal: task.title,
|
|
192
|
+
});
|
|
193
|
+
task = readTask(task.task_id);
|
|
194
|
+
room = getRoomRecord(room.room_id);
|
|
195
|
+
return { kind: 'provisioning_resumed', task, room, issues: [{ code: 'provisioning_resumed' }] };
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
issues.push({ code: 'resume_failed', error: error instanceof Error ? error.message : String(error) });
|
|
199
|
+
return { kind: 'provisioning_resume_failed', task, room, issues };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async acceptTerminal(taskId, kind, roomId, outcome) {
|
|
203
|
+
const task = await acceptTaskTerminalIntent({ taskId, kind, roomId, outcome });
|
|
204
|
+
return { task, settlementRequired: task.terminal_intent?.status === 'pending' && !!roomId };
|
|
205
|
+
}
|
|
206
|
+
listTemplates() {
|
|
207
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
208
|
+
return listTemplates(cfg.roomTemplates ?? {});
|
|
209
|
+
}
|
|
210
|
+
getTemplate(name) {
|
|
211
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
212
|
+
const template = resolveTemplate(name, cfg.roomTemplates ?? {});
|
|
213
|
+
if (!template)
|
|
214
|
+
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: name });
|
|
215
|
+
return { ...template, content_hash: hashTemplate(template) };
|
|
216
|
+
}
|
|
217
|
+
validateTemplates() {
|
|
218
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
219
|
+
return listTemplates(cfg.roomTemplates ?? {}).flatMap(template => {
|
|
220
|
+
const issues = template.members.flatMap(member => cfg.roles.some(role => role.name === member.role_ref)
|
|
221
|
+
? [] : [`member ${member.slot}: role_ref '${member.role_ref}' not found in fleet roles`]);
|
|
222
|
+
return issues.length ? [{ template: `${template.name}@${template.version}`, issues }] : [];
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
async listRooms(filter) {
|
|
226
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
227
|
+
if (!cfg.rooms)
|
|
228
|
+
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
229
|
+
const cowork = this.deps.cowork ? this.deps.cowork(cfg) : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
230
|
+
await deleteLegacyClosedRooms({ cowork });
|
|
231
|
+
const local = new Map(listRoomRecords().map(room => [room.room_id, room]));
|
|
232
|
+
return (await cowork.listRooms()).filter(room => room.state === 'active' || room.state === 'provisioning')
|
|
233
|
+
.filter(room => {
|
|
234
|
+
const tracked = local.get(room.room_id);
|
|
235
|
+
return !tracked || tracked.state === 'active' || tracked.state === 'provisioning';
|
|
236
|
+
})
|
|
237
|
+
.filter(room => !filter?.state || room.state === filter.state)
|
|
238
|
+
.map(room => ({ ...room, orchestration: local.get(room.room_id) ?? null }));
|
|
239
|
+
}
|
|
240
|
+
async getRoomDetail(id) {
|
|
241
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
242
|
+
const orchestration = getRoomRecord(id);
|
|
243
|
+
if (orchestration?.state === 'closing' || orchestration?.state === 'closed')
|
|
244
|
+
throw new TaskRoomApplicationError('room_not_found', 'room not found', { room: id });
|
|
245
|
+
if (!cfg.rooms)
|
|
246
|
+
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
247
|
+
const adapter = this.deps.cowork ? this.deps.cowork(cfg) : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
248
|
+
const room = await adapter.getRoom(id);
|
|
249
|
+
if (!room || room.state === 'closing' || room.state === 'closed')
|
|
250
|
+
throw new TaskRoomApplicationError('room_not_found', 'room not found', { room: id });
|
|
251
|
+
return { room, orchestration };
|
|
252
|
+
}
|
|
253
|
+
async getRoomMembers(id) {
|
|
254
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
255
|
+
const orchestration = getRoomRecord(id);
|
|
256
|
+
if (orchestration?.state === 'closing' || orchestration?.state === 'closed')
|
|
257
|
+
throw new TaskRoomApplicationError('room_not_found', 'room not found', { room: id });
|
|
258
|
+
if (!cfg.rooms)
|
|
259
|
+
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
260
|
+
const adapter = this.deps.cowork ? this.deps.cowork(cfg) : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
261
|
+
const room = await adapter.getRoom(id);
|
|
262
|
+
if (!room || room.state === 'closing' || room.state === 'closed')
|
|
263
|
+
throw new TaskRoomApplicationError('room_not_found', 'room not found', { room: id });
|
|
264
|
+
return { room, orchestration, members: await adapter.getSeats(id) };
|
|
265
|
+
}
|
|
266
|
+
async requestRoomDeletion(input) {
|
|
267
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
268
|
+
if (!cfg.rooms)
|
|
269
|
+
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
270
|
+
if (this.deps.cowork)
|
|
271
|
+
this.deps.cowork(cfg);
|
|
272
|
+
else
|
|
273
|
+
createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
274
|
+
const room = getRoomRecord(input.roomId);
|
|
275
|
+
if (!room) {
|
|
276
|
+
throw new TaskRoomApplicationError('room_record_not_found', 'room record not found', { room: input.roomId });
|
|
277
|
+
}
|
|
278
|
+
return { room: await acceptManagedRoomClose(input.roomId), settlementRequired: true };
|
|
279
|
+
}
|
|
280
|
+
async settleRoomDeletion(input) {
|
|
281
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
282
|
+
if (!cfg.rooms)
|
|
283
|
+
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
284
|
+
const cowork = this.deps.cowork ? this.deps.cowork(cfg)
|
|
285
|
+
: createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
286
|
+
return deleteManagedRoom({ roomId: input.roomId, cowork });
|
|
287
|
+
}
|
|
288
|
+
recordRoomSettlementError(input) {
|
|
289
|
+
return recordManagedRoomCloseError(input.roomId, input.error, input.recoveryHint);
|
|
290
|
+
}
|
|
291
|
+
async recoverRoom(input) {
|
|
292
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
293
|
+
if (!cfg.rooms)
|
|
294
|
+
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
295
|
+
const adapter = this.deps.cowork ? this.deps.cowork(cfg)
|
|
296
|
+
: createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
297
|
+
let orchestration = getRoomRecord(input.roomId);
|
|
298
|
+
if (orchestration?.state === 'closing' || orchestration?.state === 'closed')
|
|
299
|
+
return { kind: 'deletion_worker_required', roomId: input.roomId };
|
|
300
|
+
const room = await adapter.recoverRoom(input.roomId);
|
|
301
|
+
orchestration = getRoomRecord(input.roomId);
|
|
302
|
+
if (orchestration && !orchestration.owner_seat_cid
|
|
303
|
+
&& (orchestration.provisioning_detail === 'waiting_owner_invite'
|
|
304
|
+
|| orchestration.provisioning_detail === 'owner_cid_mismatch')) {
|
|
305
|
+
const expected = cfg.rooms.owner.expected_cid.toLowerCase();
|
|
306
|
+
const existing = (await adapter.getSeats(input.roomId))
|
|
307
|
+
.find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
|
|
308
|
+
if (!existing && !cfg.ownerInvite)
|
|
309
|
+
throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before recovery');
|
|
310
|
+
let acceptedCid = existing?.identity_cid;
|
|
311
|
+
if (!acceptedCid)
|
|
312
|
+
acceptedCid = (await adapter.acceptInvite(input.roomId, cfg.ownerInvite, {
|
|
313
|
+
role: cfg.rooms.owner.role, expected_cid: cfg.rooms.owner.expected_cid,
|
|
314
|
+
})).seat_cid;
|
|
315
|
+
setOwnerSeat(input.roomId, acceptedCid, cfg.ownerInviteFingerprint ?? '');
|
|
316
|
+
orchestration = advanceSaga(input.roomId, 'create_members', 3);
|
|
317
|
+
}
|
|
318
|
+
const issues = [];
|
|
319
|
+
if (orchestration?.saga.error)
|
|
320
|
+
issues.push('A provisioning failure is recorded; inspect role logs for diagnostics.');
|
|
321
|
+
if (orchestration?.saga.recovery_hint)
|
|
322
|
+
issues.push('Recovery guidance is recorded; inspect role logs for diagnostics.');
|
|
323
|
+
if (orchestration?.provisioning_detail === 'waiting_cowork')
|
|
324
|
+
issues.push('Check ours-cowork service status');
|
|
325
|
+
if (orchestration?.provisioning_detail === 'waiting_owner_invite')
|
|
326
|
+
issues.push('Rotate rooms.owner.public_invite in config, then re-run recover');
|
|
327
|
+
if (orchestration?.provisioning_detail === 'waiting_seats')
|
|
328
|
+
issues.push('Inspect temporary member logs for invite acceptance, then re-run recover');
|
|
329
|
+
if (orchestration?.state === 'provisioning'
|
|
330
|
+
&& ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(orchestration.saga.phase)
|
|
331
|
+
&& orchestration.template_snapshot) {
|
|
332
|
+
try {
|
|
333
|
+
let template = orchestration.template_snapshot;
|
|
334
|
+
let brief;
|
|
335
|
+
let goal = orchestration.room_name;
|
|
336
|
+
if (orchestration.task_id) {
|
|
337
|
+
const task = readTask(orchestration.task_id);
|
|
338
|
+
if (!task.template || task.template.name !== template.name || task.template.version !== template.version
|
|
339
|
+
|| task.template.content_hash !== template.content_hash)
|
|
340
|
+
throw new Error(`task ${task.task_id} template reference does not match room ${orchestration.room_id}'s durable snapshot`);
|
|
341
|
+
brief = task.brief;
|
|
342
|
+
goal = task.title;
|
|
343
|
+
}
|
|
344
|
+
await (this.deps.provisionMembers ?? provisionMembers)({ cfg, cowork: adapter,
|
|
345
|
+
roomId: orchestration.room_id, taskId: orchestration.task_id, template,
|
|
346
|
+
binPath: (this.deps.binPath ?? getBinPath)(), brief, goal });
|
|
347
|
+
orchestration = getRoomRecord(input.roomId);
|
|
348
|
+
return { kind: 'provisioning_resumed', room, orchestration,
|
|
349
|
+
issues: ['Provisioning resumed successfully'] };
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
issues.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
353
|
+
return { kind: 'provisioning_resume_failed', room, orchestration, issues };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return { kind: 'recovered', room, orchestration, issues };
|
|
357
|
+
}
|
|
358
|
+
async finishTask(input) {
|
|
359
|
+
let task = readTask(input.taskId);
|
|
360
|
+
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
361
|
+
throw new TaskRoomApplicationError('task_terminal_already', 'task already terminal', { task: input.taskId, state: task.state });
|
|
362
|
+
if (task.state === 'active')
|
|
363
|
+
task = persistReviewTask(task.task_id);
|
|
364
|
+
if (task.room_id)
|
|
365
|
+
(this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
366
|
+
return this.acceptTerminal(task.task_id, 'done', task.room_id, input.outcome);
|
|
367
|
+
}
|
|
368
|
+
async ensureTaskWork(input) {
|
|
369
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
370
|
+
let task = readTask(input.taskId);
|
|
371
|
+
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
372
|
+
throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: input.taskId, state: task.state });
|
|
373
|
+
if (task.state === 'active' && task.room_id) {
|
|
374
|
+
if (input.template) {
|
|
375
|
+
const definition = resolveTemplate(input.template, cfg.roomTemplates ?? {});
|
|
376
|
+
if (!definition)
|
|
377
|
+
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: input.template });
|
|
378
|
+
const requested = snapshotTemplate(definition);
|
|
379
|
+
const pinned = getRoomRecord(task.room_id)?.template_snapshot ?? task.template;
|
|
380
|
+
if (pinned && (pinned.name !== requested.name || pinned.content_hash !== requested.content_hash))
|
|
381
|
+
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
382
|
+
requested: `${requested.name}@${requested.version}`, room: task.room_id,
|
|
383
|
+
provisioned: `${pinned.name}@${pinned.version}`
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return { task, status: 'already_active' };
|
|
387
|
+
}
|
|
388
|
+
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
389
|
+
const durable = room?.template_snapshot;
|
|
390
|
+
if (durable && (!task.template || task.template.name !== durable.name
|
|
391
|
+
|| task.template.version !== durable.version || task.template.content_hash !== durable.content_hash))
|
|
392
|
+
throw new Error(`task ${task.task_id} template reference does not match room ${room.room_id}'s durable snapshot`);
|
|
393
|
+
const templateName = input.template ?? durable?.name ?? task.template?.name
|
|
394
|
+
?? cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'single';
|
|
395
|
+
const definition = durable && !input.template ? undefined
|
|
396
|
+
: resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
397
|
+
if (input.template && !definition)
|
|
398
|
+
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
399
|
+
if (!durable && !definition)
|
|
400
|
+
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
401
|
+
const snapshot = durable ?? snapshotTemplate(definition);
|
|
402
|
+
if (durable && input.template && definition) {
|
|
403
|
+
const requested = snapshotTemplate(definition);
|
|
404
|
+
if (requested.name !== durable.name || requested.content_hash !== durable.content_hash)
|
|
405
|
+
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
406
|
+
requested: `${requested.name}@${requested.version}`, room: room.room_id,
|
|
407
|
+
provisioned: `${durable.name}@${durable.version}`
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash)
|
|
411
|
+
throw new TaskRoomApplicationError('task_template_drift', 'task template drift', {
|
|
412
|
+
template: `${task.template.name}@${task.template.version}`
|
|
413
|
+
});
|
|
414
|
+
if (task.room_id && room?.template_snapshot
|
|
415
|
+
&& (room.template_snapshot.name !== snapshot.name || room.template_snapshot.content_hash !== snapshot.content_hash))
|
|
416
|
+
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
417
|
+
requested: `${snapshot.name}@${snapshot.version}`, room: task.room_id,
|
|
418
|
+
provisioned: `${room.template_snapshot.name}@${room.template_snapshot.version}`
|
|
419
|
+
});
|
|
420
|
+
if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
|
|
421
|
+
task = updateTaskTemplate(task.task_id, { name: snapshot.name, version: snapshot.version, content_hash: snapshot.content_hash });
|
|
422
|
+
if (task.state === 'backlog')
|
|
423
|
+
task = transitionTask(task.task_id);
|
|
424
|
+
if (!task.room_id) {
|
|
425
|
+
try {
|
|
426
|
+
await this.provisionRoom(cfg, task, snapshot, created => {
|
|
427
|
+
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
428
|
+
});
|
|
429
|
+
task = readTask(task.task_id);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
if (error instanceof CoworkUnavailableError)
|
|
433
|
+
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else if (task.state === 'provisioning') {
|
|
438
|
+
if (!room || !['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(room.saga.phase))
|
|
439
|
+
throw new TaskRoomApplicationError('task_non_resumable', 'task non-resumable', { task: task.task_id, room: task.room_id });
|
|
440
|
+
try {
|
|
441
|
+
if (!cfg.rooms)
|
|
442
|
+
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
443
|
+
const cowork = this.deps.cowork ? this.deps.cowork(cfg) : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
444
|
+
await (this.deps.provisionMembers ?? provisionMembers)({ cfg, cowork, roomId: room.room_id,
|
|
445
|
+
taskId: task.task_id, template: snapshot, binPath: (this.deps.binPath ?? getBinPath)(),
|
|
446
|
+
brief: task.brief, goal: task.title });
|
|
447
|
+
task = readTask(task.task_id);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (error instanceof CoworkUnavailableError)
|
|
451
|
+
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return { task, status: 'ready' };
|
|
456
|
+
}
|
|
457
|
+
createTemplate(cfg, requested, noRoom) {
|
|
458
|
+
if (noRoom)
|
|
459
|
+
return undefined;
|
|
460
|
+
const name = requested
|
|
461
|
+
?? cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'team';
|
|
462
|
+
const definition = resolveTemplate(name, cfg.roomTemplates ?? {});
|
|
463
|
+
if (!definition) {
|
|
464
|
+
if (requested)
|
|
465
|
+
throw new TaskRoomApplicationError('template_not_found', `template not found: ${requested}`);
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
return snapshotTemplate(definition);
|
|
469
|
+
}
|
|
470
|
+
existingTemplate(cfg, task) {
|
|
471
|
+
const ref = task.template;
|
|
472
|
+
const definition = resolveTemplate(ref.name, cfg.roomTemplates ?? {});
|
|
473
|
+
const snapshot = definition ? snapshotTemplate(definition) : undefined;
|
|
474
|
+
if (!snapshot || snapshot.content_hash !== ref.content_hash)
|
|
475
|
+
throw new TaskRoomApplicationError('task_template_drift', `task template snapshot no longer matches ${ref.name}@${ref.version}`);
|
|
476
|
+
return snapshot;
|
|
477
|
+
}
|
|
478
|
+
async provisionRoom(cfg, task, template, onCreated) {
|
|
479
|
+
const rooms = cfg.rooms;
|
|
480
|
+
if (!rooms)
|
|
481
|
+
throw new ConfigError('rooms: configuration is required');
|
|
482
|
+
const attachOwner = rooms.defaults?.attach_owner !== false;
|
|
483
|
+
if (attachOwner && !cfg.ownerInvite)
|
|
484
|
+
throw new ConfigError('rooms.owner: public_invite or public_invite_file is required when attach_owner is enabled');
|
|
485
|
+
const cowork = this.deps.cowork ? this.deps.cowork(cfg) : (() => {
|
|
486
|
+
if (!cfg.rooms)
|
|
487
|
+
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
488
|
+
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
489
|
+
})();
|
|
490
|
+
const created = await cowork.createRoom({
|
|
491
|
+
room_name: task.title, goal: task.goal?.trim() || task.title,
|
|
492
|
+
briefing: task.brief?.trim() || template?.contract?.trim() || task.goal?.trim() || task.title,
|
|
493
|
+
quiet_membership: template?.room?.quiet_membership,
|
|
494
|
+
anonymous: template?.room?.anonymous,
|
|
495
|
+
});
|
|
496
|
+
let room = createRoomRecord({
|
|
497
|
+
room_id: created.room_id, room_name: task.title, room_identity_cid: created.identity_cid,
|
|
498
|
+
task_id: task.task_id, template_snapshot: template,
|
|
499
|
+
});
|
|
500
|
+
onCreated(room);
|
|
501
|
+
room = advanceSaga(room.room_id, 'create_room', 1);
|
|
502
|
+
if (attachOwner) {
|
|
503
|
+
try {
|
|
504
|
+
room = advanceSaga(room.room_id, 'attach_owner', 2);
|
|
505
|
+
const accepted = await cowork.acceptInvite(room.room_id, cfg.ownerInvite, {
|
|
506
|
+
role: rooms.owner.role, expected_cid: rooms.owner.expected_cid,
|
|
507
|
+
});
|
|
508
|
+
room = setOwnerSeat(room.room_id, accepted.seat_cid, cfg.ownerInviteFingerprint ?? '');
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
|
|
512
|
+
setSagaError(room.room_id, error instanceof Error ? error.message : String(error), mismatch ? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
|
|
513
|
+
: 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
room = advanceSaga(room.room_id, 'create_members', 3);
|
|
518
|
+
if (template?.members.length)
|
|
519
|
+
return (this.deps.provisionMembers ?? provisionMembers)({
|
|
520
|
+
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template,
|
|
521
|
+
binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
|
|
522
|
+
goal: task.task_id ? task.title : task.goal,
|
|
523
|
+
});
|
|
524
|
+
room = activateRoom(room.room_id);
|
|
525
|
+
if (task.task_id)
|
|
526
|
+
activateTask(task.task_id);
|
|
527
|
+
return room;
|
|
528
|
+
}
|
|
529
|
+
}
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.0.4",
|
|
3
|
+
"buildId": "0891be00930f",
|
|
4
|
+
"commit": "4f7c7292736219d27e8949442478996191cc2b54",
|
|
5
5
|
"dirty": false,
|
|
6
|
-
"builtAt": "2026-08-
|
|
6
|
+
"builtAt": "2026-08-26T06:30:09.239Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|