@ours.network/fleet 1.1.0-nightly.27 → 1.1.0-nightly.29
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 +10 -12
- package/dist/application/task-room-service.d.ts +12 -40
- package/dist/application/task-room-service.js +140 -170
- package/dist/build-info.json +4 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +12 -14
- package/dist/fleet-command-audit.d.ts +5 -0
- package/dist/fleet-command-audit.js +220 -81
- package/dist/owner-channel/channel.d.ts +0 -3
- package/dist/owner-channel/channel.js +5 -96
- package/dist/owner-channel/commands.d.ts +0 -5
- package/dist/owner-channel/commands.js +8 -27
- package/dist/rooms-tasks/cli.js +33 -246
- package/dist/rooms-tasks/close.js +2 -2
- package/dist/rooms-tasks/cowork-adapter.d.ts +4 -0
- package/dist/rooms-tasks/cowork-adapter.js +7 -0
- package/dist/rooms-tasks/deletion.js +1 -1
- package/dist/rooms-tasks/provision.js +1 -1
- package/dist/rooms-tasks/terminal.js +3 -1
- package/dist/rooms-tasks/types.d.ts +1 -1
- package/dist/web/server.js +1 -1
- package/package.json +1 -1
- package/presets/fleet/roles/Coordinator.yaml +2 -2
package/README.md
CHANGED
|
@@ -609,20 +609,18 @@ live Fleet launch, and the configured Owner seat is active when owner attachment
|
|
|
609
609
|
enabled. There is no briefing hash, startup ACK, or separate role-briefing readiness
|
|
610
610
|
gate.
|
|
611
611
|
|
|
612
|
-
Normal provisioning emits exactly
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
612
|
+
Normal Task provisioning emits exactly one authenticated Owner lifecycle notice after
|
|
613
|
+
the ready predicate above is true; standalone Room provisioning likewise emits one
|
|
614
|
+
ready notice. Intermediate task, saga, member-spawn, timeout, and recoverable-failure
|
|
615
|
+
transitions stay in local state and logs. A terminal failed Task emits one actionable
|
|
616
|
+
failure notice; the command result carries the exact blocker and canonical recovery
|
|
617
|
+
action.
|
|
618
618
|
|
|
619
619
|
`task start` and create-and-start wait for readiness. If their bounded wait expires,
|
|
620
|
-
they return an explicit `in_progress` result
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
continuation is serialized per task and remains alive until convergence or an
|
|
625
|
-
Owner-action blocker; invoking `task await` safely re-arms it after a restart.
|
|
620
|
+
they return an explicit `in_progress` result and start a safe continuation. The detached
|
|
621
|
+
continuation is serialized per Task and remains alive until convergence or an
|
|
622
|
+
Owner-action blocker. Re-running `task start <id>` safely resumes the same durable
|
|
623
|
+
provisioning operation after the blocker is corrected or a process restarts.
|
|
626
624
|
|
|
627
625
|
Set `room.anonymous: true` on a room template, or pass `--anonymous` to
|
|
628
626
|
`task create`, `task start`, `task work`, or `room create`, to create an
|
|
@@ -21,8 +21,8 @@ export interface TaskSettlementPlan {
|
|
|
21
21
|
task: TaskRecord;
|
|
22
22
|
settlementRequired: boolean;
|
|
23
23
|
}
|
|
24
|
-
export type
|
|
25
|
-
code: '
|
|
24
|
+
export type TaskProvisioningContinuationIssue = {
|
|
25
|
+
code: 'waiting_cowork' | 'waiting_owner_authorization' | 'waiting_owner_invite' | 'owner_cid_mismatch' | 'waiting_seats' | 'provisioning_resumed';
|
|
26
26
|
} | {
|
|
27
27
|
code: 'member_failed';
|
|
28
28
|
stepIndex: number;
|
|
@@ -30,21 +30,17 @@ export type TaskRecoveryIssue = {
|
|
|
30
30
|
code: 'resume_failed';
|
|
31
31
|
error: string;
|
|
32
32
|
};
|
|
33
|
-
export interface
|
|
34
|
-
kind: 'provisioning_resumed' | 'provisioning_resume_failed' | 'provisioning_non_resumable' | '
|
|
33
|
+
export interface TaskProvisioningContinuationResult {
|
|
34
|
+
kind: 'provisioning_resumed' | 'provisioning_resume_failed' | 'provisioning_non_resumable' | 'no_op';
|
|
35
35
|
task: TaskRecord;
|
|
36
36
|
room: RoomOrchestrationRecord | undefined;
|
|
37
|
-
issues:
|
|
37
|
+
issues: TaskProvisioningContinuationIssue[];
|
|
38
38
|
reason?: 'missing_room' | 'missing_durable_template' | 'non_resumable_phase';
|
|
39
39
|
}
|
|
40
40
|
export interface TaskProvisioningOutcome {
|
|
41
41
|
kind: 'ready' | 'in_progress' | 'failed';
|
|
42
42
|
task: TaskRecord;
|
|
43
43
|
room?: RoomOrchestrationRecord;
|
|
44
|
-
handle: {
|
|
45
|
-
command: string;
|
|
46
|
-
task_id: string;
|
|
47
|
-
};
|
|
48
44
|
launch: {
|
|
49
45
|
template?: string;
|
|
50
46
|
anonymous: boolean;
|
|
@@ -60,16 +56,6 @@ export interface TaskProvisioningOutcome {
|
|
|
60
56
|
}
|
|
61
57
|
/** Repairable terminal observation derived entirely from durable Task/Room state. */
|
|
62
58
|
export declare function recordTaskProvisioningOutcome(outcome: TaskProvisioningOutcome): void;
|
|
63
|
-
export type TaskRecoveryBegin = {
|
|
64
|
-
kind: 'terminal_worker_required';
|
|
65
|
-
taskId: string;
|
|
66
|
-
} | {
|
|
67
|
-
kind: 'deletion_worker_required';
|
|
68
|
-
taskId: string;
|
|
69
|
-
} | {
|
|
70
|
-
kind: 'final';
|
|
71
|
-
result: TaskRecoveryResult;
|
|
72
|
-
};
|
|
73
59
|
export declare class TaskRoomApplicationError extends Error {
|
|
74
60
|
readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found';
|
|
75
61
|
readonly fields: Readonly<Record<string, string>>;
|
|
@@ -114,7 +100,6 @@ export interface TaskRoomServiceDeps {
|
|
|
114
100
|
export declare class TaskRoomApplicationService {
|
|
115
101
|
private readonly configurationPath?;
|
|
116
102
|
private readonly deps;
|
|
117
|
-
private recovery?;
|
|
118
103
|
constructor(configurationPath?: string | undefined, deps?: TaskRoomServiceDeps);
|
|
119
104
|
createTask(request: CreateTaskRequest): Promise<TaskRecord>;
|
|
120
105
|
createRoom(request: CreateRoomRequest): Promise<RoomOrchestrationRecord>;
|
|
@@ -241,15 +226,14 @@ export declare class TaskRoomApplicationService {
|
|
|
241
226
|
error: string;
|
|
242
227
|
recoveryHint: string;
|
|
243
228
|
}): Promise<TaskRecord>;
|
|
244
|
-
|
|
245
|
-
actor:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
actor: TaskRoomActor;
|
|
229
|
+
continueTaskProvisioning(input: {
|
|
230
|
+
actor: {
|
|
231
|
+
kind: 'internal_worker';
|
|
232
|
+
surface: 'cli';
|
|
233
|
+
};
|
|
250
234
|
taskId: string;
|
|
251
|
-
|
|
252
|
-
|
|
235
|
+
}): Promise<TaskProvisioningContinuationResult>;
|
|
236
|
+
private reconcileProvisioningOwner;
|
|
253
237
|
private acceptTerminal;
|
|
254
238
|
listTemplates(): import("../rooms-tasks/types.js").TemplateDefinition[];
|
|
255
239
|
getTemplate(name: string): {
|
|
@@ -310,18 +294,6 @@ export declare class TaskRoomApplicationService {
|
|
|
310
294
|
error: string;
|
|
311
295
|
recoveryHint: string;
|
|
312
296
|
}): Promise<RoomOrchestrationRecord>;
|
|
313
|
-
recoverRoom(input: {
|
|
314
|
-
actor: TaskRoomActor;
|
|
315
|
-
roomId: string;
|
|
316
|
-
}): Promise<{
|
|
317
|
-
kind: 'deletion_worker_required';
|
|
318
|
-
roomId: string;
|
|
319
|
-
} | {
|
|
320
|
-
kind: 'recovered' | 'provisioning_resumed' | 'provisioning_resume_failed';
|
|
321
|
-
room: Awaited<ReturnType<CoworkAdapter['recoverRoom']>>;
|
|
322
|
-
orchestration: RoomOrchestrationRecord | undefined;
|
|
323
|
-
issues: string[];
|
|
324
|
-
}>;
|
|
325
297
|
finishTask(input: {
|
|
326
298
|
actor: TaskRoomActor;
|
|
327
299
|
taskId: string;
|
|
@@ -8,7 +8,7 @@ import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked,
|
|
|
8
8
|
import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
|
|
9
9
|
import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
|
|
10
10
|
import { hashMemberOverrides, prepareExecutionPlan } from '../rooms-tasks/member-overrides.js';
|
|
11
|
-
import {
|
|
11
|
+
import { recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
|
|
12
12
|
import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
|
|
13
13
|
import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath, } from '../rooms-tasks/terminal.js';
|
|
14
14
|
import { acceptTaskDeletion, recordTaskDeletionError, settleTaskDeletion, } from '../rooms-tasks/deletion.js';
|
|
@@ -16,43 +16,37 @@ import { withFileLock } from '../atomic-file.js';
|
|
|
16
16
|
import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
|
|
17
17
|
import { storedRoomLaunchPolicy, TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
|
|
18
18
|
import { deriveTaskRoomName } from '../rooms-tasks/task-room-name.js';
|
|
19
|
-
|
|
20
|
-
recordFleetAuditPresentation({ kind: 'room', operation: 'create',
|
|
21
|
-
eventId: `room-created:${room.created_at}`, id: room.room_id,
|
|
22
|
-
name: room.room_name, previousState: 'none', newState: 'provisioning',
|
|
23
|
-
revision: room.created_at, taskId: room.task_id,
|
|
24
|
-
template: room.template_snapshot
|
|
25
|
-
? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
26
|
-
anonymous: storedRoomLaunchPolicy(room.room_policy).anonymous,
|
|
27
|
-
memberCount: room.template_snapshot?.members.reduce((sum, member) => sum + member.count, 0) ?? 0,
|
|
28
|
-
participants: [] });
|
|
29
|
-
}
|
|
19
|
+
const OWNER_ROOM_COMMANDS = ['list-members', 'remove-member'];
|
|
30
20
|
/** Repairable terminal observation derived entirely from durable Task/Room state. */
|
|
31
21
|
export function recordTaskProvisioningOutcome(outcome) {
|
|
32
22
|
const room = outcome.room;
|
|
33
23
|
recordFleetAuditResource('task', outcome.task.task_id);
|
|
34
|
-
if (room)
|
|
24
|
+
if (room)
|
|
35
25
|
recordFleetAuditResource('room', room.room_id);
|
|
36
|
-
// Always restate created before a later terminal transition. The stable
|
|
37
|
-
// digest makes this a no-op when the original checkpoint was delivered,
|
|
38
|
-
// and repairs a crash after Room persistence but before that checkpoint.
|
|
39
|
-
recordCanonicalRoomCreated(room);
|
|
40
|
-
}
|
|
41
26
|
if (outcome.kind === 'ready' && room)
|
|
42
27
|
recordFleetAuditPresentation({
|
|
43
|
-
kind: '
|
|
44
|
-
id:
|
|
45
|
-
revision: room.activated_at ?? room.created_at,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
28
|
+
kind: 'task', operation: 'work', eventId: `task-ready:${room.activated_at ?? room.created_at}`,
|
|
29
|
+
id: outcome.task.task_id, title: outcome.task.title,
|
|
30
|
+
previousState: 'provisioning', newState: 'active', revision: room.activated_at ?? room.created_at,
|
|
31
|
+
list: outcome.task.list_name ?? 'default', roomId: room.room_id, roomName: room.room_name,
|
|
32
|
+
template: outcome.launch.template,
|
|
33
|
+
agents: room.member_seats.map(member => ({ name: member.role_name, role: member.cowork_role,
|
|
34
|
+
configuration: member.launch?.presentation })),
|
|
49
35
|
});
|
|
50
36
|
if (outcome.kind === 'failed')
|
|
51
37
|
recordFleetAuditPresentation({
|
|
52
38
|
kind: 'lifecycle_failure', resource: 'Task', id: outcome.task.task_id,
|
|
39
|
+
label: outcome.task.title,
|
|
53
40
|
state: outcome.task.state, category: 'provision_failed',
|
|
54
41
|
eventId: outcome.task.ended_at ?? room?.created_at ?? outcome.task.created_at,
|
|
55
42
|
});
|
|
43
|
+
if (outcome.kind === 'in_progress')
|
|
44
|
+
recordFleetAuditPresentation({
|
|
45
|
+
kind: 'lifecycle_failure', resource: 'Task', id: outcome.task.task_id,
|
|
46
|
+
label: outcome.task.title,
|
|
47
|
+
state: outcome.task.state, category: 'provision_pending',
|
|
48
|
+
eventId: room?.created_at ?? outcome.task.created_at,
|
|
49
|
+
});
|
|
56
50
|
}
|
|
57
51
|
export class TaskRoomApplicationError extends Error {
|
|
58
52
|
code;
|
|
@@ -72,7 +66,6 @@ export function resolveRoomLaunchPolicy(template, override) {
|
|
|
72
66
|
export class TaskRoomApplicationService {
|
|
73
67
|
configurationPath;
|
|
74
68
|
deps;
|
|
75
|
-
recovery;
|
|
76
69
|
constructor(configurationPath, deps = {}) {
|
|
77
70
|
this.configurationPath = configurationPath;
|
|
78
71
|
this.deps = deps;
|
|
@@ -123,9 +116,9 @@ export class TaskRoomApplicationService {
|
|
|
123
116
|
}
|
|
124
117
|
unlock?.();
|
|
125
118
|
const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
|
|
126
|
-
// A create-and-start launch is
|
|
127
|
-
//
|
|
128
|
-
// their
|
|
119
|
+
// A successful create-and-start launch is recorded once by
|
|
120
|
+
// recordTaskProvisioningOutcome after the linked Room and seats are ready.
|
|
121
|
+
// Backlog and no-room tasks retain their direct Task lifecycle notice.
|
|
129
122
|
if (request.backlog || request.noRoom)
|
|
130
123
|
recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
|
|
131
124
|
title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
|
|
@@ -145,13 +138,15 @@ export class TaskRoomApplicationService {
|
|
|
145
138
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
146
139
|
// Once the durable Room exists, provisioning errors are resumable
|
|
147
140
|
// saga state. Return that explicit state so the command can launch a
|
|
148
|
-
// continuation and
|
|
141
|
+
// continuation and report the durable in-progress outcome.
|
|
149
142
|
const current = readTask(task.task_id);
|
|
150
143
|
if (!current.room_id || !getRoomRecord(current.room_id))
|
|
151
144
|
throw error;
|
|
152
145
|
task = current;
|
|
153
146
|
}
|
|
154
147
|
}
|
|
148
|
+
if (!request.backlog && !request.noRoom && task.room_id)
|
|
149
|
+
recordTaskProvisioningOutcome(this.taskProvisioningOutcome(task.task_id));
|
|
155
150
|
return task;
|
|
156
151
|
}
|
|
157
152
|
async createRoom(request) {
|
|
@@ -176,7 +171,10 @@ export class TaskRoomApplicationService {
|
|
|
176
171
|
}, template, () => { }, undefined, roomPolicy);
|
|
177
172
|
}
|
|
178
173
|
async startTask(input) {
|
|
179
|
-
|
|
174
|
+
const result = await this.ensureTaskWork(input);
|
|
175
|
+
if (result.status !== 'already_active')
|
|
176
|
+
recordTaskProvisioningOutcome(this.taskProvisioningOutcome(result.task.task_id));
|
|
177
|
+
return result.task;
|
|
180
178
|
}
|
|
181
179
|
listTasks(filter) {
|
|
182
180
|
const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
|
|
@@ -235,18 +233,19 @@ export class TaskRoomApplicationService {
|
|
|
235
233
|
const ready = task.state === 'active' && room?.state === 'active'
|
|
236
234
|
&& active === expected && launched === expected;
|
|
237
235
|
const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
|
|
238
|
-
const nextAction = room?.provisioning_detail === '
|
|
239
|
-
?
|
|
240
|
-
: room?.provisioning_detail === '
|
|
241
|
-
?
|
|
242
|
-
: room?.provisioning_detail === '
|
|
243
|
-
?
|
|
244
|
-
:
|
|
245
|
-
? `
|
|
246
|
-
:
|
|
236
|
+
const nextAction = room?.provisioning_detail === 'waiting_owner_authorization'
|
|
237
|
+
? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
|
|
238
|
+
: room?.provisioning_detail === 'waiting_owner_invite'
|
|
239
|
+
? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
|
|
240
|
+
: room?.provisioning_detail === 'owner_cid_mismatch'
|
|
241
|
+
? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
|
|
242
|
+
: room?.provisioning_detail === 'waiting_cowork'
|
|
243
|
+
? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
|
|
244
|
+
: failed
|
|
245
|
+
? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
|
|
246
|
+
: undefined;
|
|
247
247
|
return {
|
|
248
248
|
kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
|
|
249
|
-
handle: { command: `ours-fleet task await ${task.task_id}`, task_id: task.task_id },
|
|
250
249
|
launch: {
|
|
251
250
|
...(room?.template_snapshot
|
|
252
251
|
? { template: `${room.template_snapshot.name}@${room.template_snapshot.version}` }
|
|
@@ -405,40 +404,14 @@ export class TaskRoomApplicationService {
|
|
|
405
404
|
recordSettlementError(input) {
|
|
406
405
|
return recordTaskTerminalIntentError(input.taskId, input.error, input.recoveryHint);
|
|
407
406
|
}
|
|
408
|
-
async
|
|
409
|
-
|
|
410
|
-
// task-operation lock and must not require configuration: recovery of a
|
|
411
|
-
// deletion-pending task continues the deletion and never resurrects.
|
|
412
|
-
const routed = await withFileLock(taskOperationLockPath(input.taskId), () => {
|
|
413
|
-
const task = readTask(input.taskId);
|
|
414
|
-
if (task.deletion?.status === 'pending')
|
|
415
|
-
return 'deletion';
|
|
416
|
-
if (task.terminal_intent?.status === 'pending')
|
|
417
|
-
return 'terminal';
|
|
418
|
-
return 'none';
|
|
419
|
-
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
420
|
-
if (routed === 'deletion')
|
|
421
|
-
return { kind: 'deletion_worker_required', taskId: input.taskId };
|
|
422
|
-
const config = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
423
|
-
this.recovery = { taskId: input.taskId, config };
|
|
424
|
-
if (routed === 'terminal')
|
|
425
|
-
return { kind: 'terminal_worker_required', taskId: input.taskId };
|
|
426
|
-
return { kind: 'final', result: await this.continueTaskRecovery({
|
|
427
|
-
actor: input.actor, taskId: input.taskId, terminalTimedOut: false,
|
|
428
|
-
}) };
|
|
429
|
-
}
|
|
430
|
-
async continueTaskRecovery(input) {
|
|
431
|
-
if (!this.recovery || this.recovery.taskId !== input.taskId)
|
|
432
|
-
throw new Error('task recovery continuation requires a matching begin');
|
|
433
|
-
const recovery = this.recovery;
|
|
434
|
-
this.recovery = undefined;
|
|
435
|
-
const cfg = recovery.config;
|
|
407
|
+
async continueTaskProvisioning(input) {
|
|
408
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
436
409
|
let task = readTask(input.taskId);
|
|
437
410
|
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
438
|
-
const issues =
|
|
411
|
+
const issues = [];
|
|
439
412
|
if (task.state !== 'provisioning')
|
|
440
413
|
return {
|
|
441
|
-
kind:
|
|
414
|
+
kind: 'no_op', task, room, issues,
|
|
442
415
|
};
|
|
443
416
|
if (!room) {
|
|
444
417
|
if (!task.template)
|
|
@@ -462,10 +435,12 @@ export class TaskRoomApplicationService {
|
|
|
462
435
|
return { kind: 'provisioning_resume_failed', task, room, issues };
|
|
463
436
|
}
|
|
464
437
|
}
|
|
465
|
-
if (room.
|
|
438
|
+
if (room.saga.phase === 'attach_owner'
|
|
439
|
+
|| room.provisioning_detail === 'waiting_owner_authorization'
|
|
440
|
+
|| room.provisioning_detail === 'waiting_owner_invite'
|
|
466
441
|
|| room.provisioning_detail === 'owner_cid_mismatch') {
|
|
467
442
|
try {
|
|
468
|
-
await this.
|
|
443
|
+
await this.reconcileProvisioningOwner(cfg, room.room_id);
|
|
469
444
|
task = readTask(task.task_id);
|
|
470
445
|
room = getRoomRecord(room.room_id);
|
|
471
446
|
if (task.state === 'active' && room?.state === 'active')
|
|
@@ -484,6 +459,8 @@ export class TaskRoomApplicationService {
|
|
|
484
459
|
};
|
|
485
460
|
if (room.provisioning_detail === 'waiting_cowork')
|
|
486
461
|
issues.push({ code: 'waiting_cowork' });
|
|
462
|
+
if (room.provisioning_detail === 'waiting_owner_authorization')
|
|
463
|
+
issues.push({ code: 'waiting_owner_authorization' });
|
|
487
464
|
if (room.provisioning_detail === 'waiting_owner_invite')
|
|
488
465
|
issues.push({ code: 'waiting_owner_invite' });
|
|
489
466
|
if (room.provisioning_detail === 'owner_cid_mismatch')
|
|
@@ -520,6 +497,50 @@ export class TaskRoomApplicationService {
|
|
|
520
497
|
return { kind: 'provisioning_resume_failed', task, room, issues };
|
|
521
498
|
}
|
|
522
499
|
}
|
|
500
|
+
async reconcileProvisioningOwner(cfg, roomId) {
|
|
501
|
+
if (!cfg.rooms)
|
|
502
|
+
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
503
|
+
const cowork = this.deps.cowork ? this.deps.cowork(cfg)
|
|
504
|
+
: createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
505
|
+
const remote = await cowork.recoverRoom(roomId);
|
|
506
|
+
const room = getRoomRecord(roomId);
|
|
507
|
+
if (!room)
|
|
508
|
+
throw new Error(`room ${roomId} has no durable orchestration record`);
|
|
509
|
+
if (room.state === 'closing' || room.state === 'closed')
|
|
510
|
+
throw new Error(`room ${roomId} is already ${room.state}`);
|
|
511
|
+
const policy = storedRoomLaunchPolicy(room.room_policy);
|
|
512
|
+
if ((remote.anonymous ?? false) !== policy.anonymous) {
|
|
513
|
+
const error = `Cowork anonymity (${String(remote.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(policy.anonymous)})`;
|
|
514
|
+
setSagaError(roomId, error, 'Do not respawn members. Repair or upgrade Cowork, then retry the originating task.', 'waiting_cowork');
|
|
515
|
+
throw new Error(error);
|
|
516
|
+
}
|
|
517
|
+
if (room.owner_seat_cid) {
|
|
518
|
+
const ownerCid = room.owner_seat_cid.toLowerCase();
|
|
519
|
+
const ownerSeat = remote.seats.find(seat => seat.identity_cid.toLowerCase() === ownerCid && seat.seat_state !== 'removed');
|
|
520
|
+
if (ownerSeat)
|
|
521
|
+
await cowork.setRoleCommands(roomId, {
|
|
522
|
+
role: ownerSeat.role, commands: [...OWNER_ROOM_COMMANDS],
|
|
523
|
+
});
|
|
524
|
+
if (room.saga.phase === 'attach_owner')
|
|
525
|
+
advanceSaga(roomId, 'create_members', 3);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const expected = cfg.rooms.owner.expected_cid?.toLowerCase();
|
|
529
|
+
if (!expected)
|
|
530
|
+
throw new ConfigError('rooms.owner.expected_cid is required before continuing Owner-seat provisioning');
|
|
531
|
+
const existing = (await cowork.getSeats(roomId))
|
|
532
|
+
.find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
|
|
533
|
+
if (!existing && !cfg.ownerInvite)
|
|
534
|
+
throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before continuing task provisioning');
|
|
535
|
+
await cowork.setRoleCommands(roomId, {
|
|
536
|
+
role: existing?.role ?? cfg.rooms.owner.role, commands: [...OWNER_ROOM_COMMANDS],
|
|
537
|
+
});
|
|
538
|
+
const acceptedCid = existing?.identity_cid ?? (await cowork.acceptInvite(roomId, cfg.ownerInvite, {
|
|
539
|
+
role: cfg.rooms.owner.role, expected_cid: cfg.rooms.owner.expected_cid,
|
|
540
|
+
})).seat_cid;
|
|
541
|
+
setOwnerSeat(roomId, acceptedCid, cfg.ownerInviteFingerprint ?? '');
|
|
542
|
+
advanceSaga(roomId, 'create_members', 3);
|
|
543
|
+
}
|
|
523
544
|
async acceptTerminal(taskId, kind, roomId, outcome) {
|
|
524
545
|
const task = await acceptTaskTerminalIntent({ taskId, kind, roomId, outcome });
|
|
525
546
|
return { task, settlementRequired: task.terminal_intent?.status === 'pending' && !!roomId };
|
|
@@ -612,91 +633,6 @@ export class TaskRoomApplicationService {
|
|
|
612
633
|
recordRoomSettlementError(input) {
|
|
613
634
|
return recordManagedRoomCloseError(input.roomId, input.error, input.recoveryHint);
|
|
614
635
|
}
|
|
615
|
-
async recoverRoom(input) {
|
|
616
|
-
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
617
|
-
if (!cfg.rooms)
|
|
618
|
-
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
619
|
-
const adapter = this.deps.cowork ? this.deps.cowork(cfg)
|
|
620
|
-
: createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
621
|
-
let orchestration = getRoomRecord(input.roomId);
|
|
622
|
-
if (orchestration?.state === 'closing' || orchestration?.state === 'closed')
|
|
623
|
-
return { kind: 'deletion_worker_required', roomId: input.roomId };
|
|
624
|
-
const room = await adapter.recoverRoom(input.roomId);
|
|
625
|
-
orchestration = getRoomRecord(input.roomId);
|
|
626
|
-
if (orchestration) {
|
|
627
|
-
const policy = storedRoomLaunchPolicy(orchestration.room_policy);
|
|
628
|
-
if ((room.anonymous ?? false) !== policy.anonymous) {
|
|
629
|
-
const error = `Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(policy.anonymous)})`;
|
|
630
|
-
setSagaError(input.roomId, error, 'Do not respawn members. Repair or upgrade Cowork, then recover the Room without changing its policy.', 'waiting_cowork');
|
|
631
|
-
throw new Error(error);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
if (orchestration && !orchestration.owner_seat_cid
|
|
635
|
-
&& (orchestration.provisioning_detail === 'waiting_owner_invite'
|
|
636
|
-
|| orchestration.provisioning_detail === 'owner_cid_mismatch')) {
|
|
637
|
-
const expected = cfg.rooms.owner.expected_cid.toLowerCase();
|
|
638
|
-
const existing = (await adapter.getSeats(input.roomId))
|
|
639
|
-
.find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
|
|
640
|
-
if (!existing && !cfg.ownerInvite)
|
|
641
|
-
throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before recovery');
|
|
642
|
-
let acceptedCid = existing?.identity_cid;
|
|
643
|
-
if (!acceptedCid)
|
|
644
|
-
acceptedCid = (await adapter.acceptInvite(input.roomId, cfg.ownerInvite, {
|
|
645
|
-
role: cfg.rooms.owner.role, expected_cid: cfg.rooms.owner.expected_cid,
|
|
646
|
-
})).seat_cid;
|
|
647
|
-
setOwnerSeat(input.roomId, acceptedCid, cfg.ownerInviteFingerprint ?? '');
|
|
648
|
-
orchestration = advanceSaga(input.roomId, 'create_members', 3);
|
|
649
|
-
}
|
|
650
|
-
const issues = [];
|
|
651
|
-
if (orchestration?.saga.error)
|
|
652
|
-
issues.push('A provisioning failure is recorded; inspect role logs for diagnostics.');
|
|
653
|
-
if (orchestration?.saga.recovery_hint)
|
|
654
|
-
issues.push('Recovery guidance is recorded; inspect role logs for diagnostics.');
|
|
655
|
-
if (orchestration?.provisioning_detail === 'waiting_cowork')
|
|
656
|
-
issues.push('Check ours-cowork service status');
|
|
657
|
-
if (orchestration?.provisioning_detail === 'waiting_owner_invite')
|
|
658
|
-
issues.push('Rotate rooms.owner.public_invite in config, then re-run recover');
|
|
659
|
-
if (orchestration?.provisioning_detail === 'waiting_seats')
|
|
660
|
-
issues.push('Inspect temporary member logs for invite acceptance, then re-run recover');
|
|
661
|
-
if (orchestration?.state === 'provisioning'
|
|
662
|
-
&& ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(orchestration.saga.phase)
|
|
663
|
-
&& orchestration.template_snapshot) {
|
|
664
|
-
try {
|
|
665
|
-
let template = orchestration.template_snapshot;
|
|
666
|
-
let brief;
|
|
667
|
-
let goal = orchestration.room_name;
|
|
668
|
-
if (orchestration.task_id) {
|
|
669
|
-
const task = readTask(orchestration.task_id);
|
|
670
|
-
if (!task.template || task.template.name !== template.name || task.template.version !== template.version
|
|
671
|
-
|| task.template.content_hash !== template.content_hash)
|
|
672
|
-
throw new Error(`task ${task.task_id} template reference does not match room ${orchestration.room_id}'s durable snapshot`);
|
|
673
|
-
brief = task.brief;
|
|
674
|
-
goal = task.title;
|
|
675
|
-
}
|
|
676
|
-
await (this.deps.provisionMembers ?? provisionMembers)({ cfg, cowork: adapter,
|
|
677
|
-
roomId: orchestration.room_id, taskId: orchestration.task_id, template,
|
|
678
|
-
binPath: (this.deps.binPath ?? getBinPath)(), brief, goal });
|
|
679
|
-
orchestration = getRoomRecord(input.roomId);
|
|
680
|
-
return { kind: 'provisioning_resumed', room, orchestration,
|
|
681
|
-
issues: ['Provisioning resumed successfully'] };
|
|
682
|
-
}
|
|
683
|
-
catch (error) {
|
|
684
|
-
issues.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
685
|
-
return { kind: 'provisioning_resume_failed', room, orchestration, issues };
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
if (orchestration?.state === 'provisioning' && !orchestration.template_snapshot) {
|
|
689
|
-
const seats = await adapter.getSeats(input.roomId);
|
|
690
|
-
const ownerReady = !orchestration.owner_seat_cid || seats.some(seat => seat.identity_cid.toLowerCase() === orchestration.owner_seat_cid.toLowerCase()
|
|
691
|
-
&& seat.seat_state === 'active');
|
|
692
|
-
if (ownerReady) {
|
|
693
|
-
orchestration = activateRoom(input.roomId);
|
|
694
|
-
if (orchestration.task_id)
|
|
695
|
-
activateTask(orchestration.task_id);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
return { kind: 'recovered', room, orchestration, issues };
|
|
699
|
-
}
|
|
700
636
|
async finishTask(input) {
|
|
701
637
|
let task = readTask(input.taskId);
|
|
702
638
|
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
@@ -747,7 +683,7 @@ export class TaskRoomApplicationService {
|
|
|
747
683
|
}
|
|
748
684
|
return { task, status: 'already_active' };
|
|
749
685
|
}
|
|
750
|
-
|
|
686
|
+
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
751
687
|
const durable = room?.template_snapshot ?? task.execution_plan?.snapshot;
|
|
752
688
|
if (durable && (!task.template || task.template.name !== durable.name
|
|
753
689
|
|| task.template.version !== durable.version || task.template.content_hash !== durable.content_hash))
|
|
@@ -850,6 +786,21 @@ export class TaskRoomApplicationService {
|
|
|
850
786
|
}
|
|
851
787
|
}
|
|
852
788
|
else if (task.state === 'provisioning') {
|
|
789
|
+
if (room && (room.saga.phase === 'attach_owner'
|
|
790
|
+
|| room.provisioning_detail === 'waiting_owner_authorization'
|
|
791
|
+
|| room.provisioning_detail === 'waiting_owner_invite'
|
|
792
|
+
|| room.provisioning_detail === 'owner_cid_mismatch')) {
|
|
793
|
+
try {
|
|
794
|
+
await this.reconcileProvisioningOwner(cfg, room.room_id);
|
|
795
|
+
room = getRoomRecord(room.room_id);
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
if (error instanceof CoworkUnavailableError)
|
|
799
|
+
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
800
|
+
task = readTask(task.task_id);
|
|
801
|
+
return { task, status: 'in_progress' };
|
|
802
|
+
}
|
|
803
|
+
}
|
|
853
804
|
if (!room || !['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(room.saga.phase))
|
|
854
805
|
throw new TaskRoomApplicationError('task_non_resumable', 'task non-resumable', { task: task.task_id, room: task.room_id });
|
|
855
806
|
try {
|
|
@@ -945,14 +896,19 @@ export class TaskRoomApplicationService {
|
|
|
945
896
|
throw error;
|
|
946
897
|
}
|
|
947
898
|
unlockSnapshot?.();
|
|
948
|
-
recordCanonicalRoomCreated(room);
|
|
949
|
-
// The created notice is observable while member admission is still in
|
|
950
|
-
// progress; it is not delayed behind task-start command completion.
|
|
951
|
-
await checkpointFleetAuditPresentations();
|
|
952
899
|
room = advanceSaga(room.room_id, 'create_room', 1);
|
|
953
900
|
if (attachOwner) {
|
|
901
|
+
room = advanceSaga(room.room_id, 'attach_owner', 2);
|
|
902
|
+
try {
|
|
903
|
+
await cowork.setRoleCommands(room.room_id, {
|
|
904
|
+
role: rooms.owner.role, commands: [...OWNER_ROOM_COMMANDS],
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
setSagaError(room.room_id, error instanceof Error ? error.message : String(error), 'Restore Cowork availability, then retry the originating task or room create command.', 'waiting_owner_authorization');
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
954
911
|
try {
|
|
955
|
-
room = advanceSaga(room.room_id, 'attach_owner', 2);
|
|
956
912
|
const accepted = await cowork.acceptInvite(room.room_id, cfg.ownerInvite, {
|
|
957
913
|
role: rooms.owner.role, expected_cid: rooms.owner.expected_cid,
|
|
958
914
|
});
|
|
@@ -961,7 +917,11 @@ export class TaskRoomApplicationService {
|
|
|
961
917
|
catch (error) {
|
|
962
918
|
const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
|
|
963
919
|
setSagaError(room.room_id, error instanceof Error ? error.message : String(error), mismatch ? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
|
|
964
|
-
: 'Rotate rooms.owner.public_invite or public_invite_file, then
|
|
920
|
+
: 'Rotate rooms.owner.public_invite or public_invite_file, then retry the originating task or room create command.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
|
|
921
|
+
if (!task.task_id)
|
|
922
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
|
|
923
|
+
id: room.room_id, label: room.room_name, state: room.state, category: 'provision_failed',
|
|
924
|
+
eventId: room.created_at });
|
|
965
925
|
throw error;
|
|
966
926
|
}
|
|
967
927
|
}
|
|
@@ -975,6 +935,10 @@ export class TaskRoomApplicationService {
|
|
|
975
935
|
});
|
|
976
936
|
}
|
|
977
937
|
catch (error) {
|
|
938
|
+
if (!task.task_id)
|
|
939
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
|
|
940
|
+
id: room.room_id, label: room.room_name, state: room.state, category: 'provision_failed',
|
|
941
|
+
eventId: room.created_at });
|
|
978
942
|
throw error;
|
|
979
943
|
}
|
|
980
944
|
else if (attachOwner) {
|
|
@@ -997,14 +961,20 @@ export class TaskRoomApplicationService {
|
|
|
997
961
|
}
|
|
998
962
|
else
|
|
999
963
|
room = activateRoom(room.room_id);
|
|
1000
|
-
if (room.state === 'active')
|
|
1001
|
-
recordFleetAuditPresentation({
|
|
1002
|
-
eventId: `room-ready:${room.activated_at ?? room.created_at}`,
|
|
1003
|
-
name: room.room_name, previousState: 'provisioning', newState: 'active',
|
|
1004
|
-
revision: room.activated_at ?? room.created_at,
|
|
964
|
+
if (!task.task_id && room.state === 'active')
|
|
965
|
+
recordFleetAuditPresentation({
|
|
966
|
+
kind: 'room', operation: 'activate', eventId: `room-ready:${room.activated_at ?? room.created_at}`,
|
|
967
|
+
id: room.room_id, name: room.room_name, previousState: 'provisioning', newState: 'active',
|
|
968
|
+
revision: room.activated_at ?? room.created_at,
|
|
1005
969
|
template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
1006
970
|
anonymous: policy.anonymous, memberCount: room.member_seats.length, ownerAttached: attachOwner,
|
|
1007
|
-
participants: []
|
|
971
|
+
participants: [],
|
|
972
|
+
});
|
|
973
|
+
if (!task.task_id && room.state !== 'active')
|
|
974
|
+
recordFleetAuditPresentation({
|
|
975
|
+
kind: 'lifecycle_failure', resource: 'Room', id: room.room_id, label: room.room_name,
|
|
976
|
+
state: room.state, category: 'provision_pending', eventId: room.created_at,
|
|
977
|
+
});
|
|
1008
978
|
if (task.task_id && !launchTemplate?.members.length)
|
|
1009
979
|
activateTask(task.task_id);
|
|
1010
980
|
return room;
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.1.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.1.0-nightly.29",
|
|
3
|
+
"buildId": "4c492a1ee52e",
|
|
4
|
+
"commit": "b201f3f81183766a67423d34c3acd938ddd2d115",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-09-
|
|
6
|
+
"builtAt": "2026-09-04T16:18:42.272Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|