@ours.network/fleet 1.1.0-nightly.28 → 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 +119 -180
- 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 -97
- package/dist/owner-channel/commands.d.ts +0 -5
- package/dist/owner-channel/commands.js +8 -27
- package/dist/rooms-tasks/cli.js +33 -248
- package/dist/rooms-tasks/close.js +2 -2
- 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/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';
|
|
@@ -17,43 +17,36 @@ 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
19
|
const OWNER_ROOM_COMMANDS = ['list-members', 'remove-member'];
|
|
20
|
-
function recordCanonicalRoomCreated(room) {
|
|
21
|
-
recordFleetAuditPresentation({ kind: 'room', operation: 'create',
|
|
22
|
-
eventId: `room-created:${room.created_at}`, id: room.room_id,
|
|
23
|
-
name: room.room_name, previousState: 'none', newState: 'provisioning',
|
|
24
|
-
revision: room.created_at, taskId: room.task_id,
|
|
25
|
-
template: room.template_snapshot
|
|
26
|
-
? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
27
|
-
anonymous: storedRoomLaunchPolicy(room.room_policy).anonymous,
|
|
28
|
-
memberCount: room.template_snapshot?.members.reduce((sum, member) => sum + member.count, 0) ?? 0,
|
|
29
|
-
participants: [] });
|
|
30
|
-
}
|
|
31
20
|
/** Repairable terminal observation derived entirely from durable Task/Room state. */
|
|
32
21
|
export function recordTaskProvisioningOutcome(outcome) {
|
|
33
22
|
const room = outcome.room;
|
|
34
23
|
recordFleetAuditResource('task', outcome.task.task_id);
|
|
35
|
-
if (room)
|
|
24
|
+
if (room)
|
|
36
25
|
recordFleetAuditResource('room', room.room_id);
|
|
37
|
-
// Always restate created before a later terminal transition. The stable
|
|
38
|
-
// digest makes this a no-op when the original checkpoint was delivered,
|
|
39
|
-
// and repairs a crash after Room persistence but before that checkpoint.
|
|
40
|
-
recordCanonicalRoomCreated(room);
|
|
41
|
-
}
|
|
42
26
|
if (outcome.kind === 'ready' && room)
|
|
43
27
|
recordFleetAuditPresentation({
|
|
44
|
-
kind: '
|
|
45
|
-
id:
|
|
46
|
-
revision: room.activated_at ?? room.created_at,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 })),
|
|
50
35
|
});
|
|
51
36
|
if (outcome.kind === 'failed')
|
|
52
37
|
recordFleetAuditPresentation({
|
|
53
38
|
kind: 'lifecycle_failure', resource: 'Task', id: outcome.task.task_id,
|
|
39
|
+
label: outcome.task.title,
|
|
54
40
|
state: outcome.task.state, category: 'provision_failed',
|
|
55
41
|
eventId: outcome.task.ended_at ?? room?.created_at ?? outcome.task.created_at,
|
|
56
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
|
+
});
|
|
57
50
|
}
|
|
58
51
|
export class TaskRoomApplicationError extends Error {
|
|
59
52
|
code;
|
|
@@ -73,7 +66,6 @@ export function resolveRoomLaunchPolicy(template, override) {
|
|
|
73
66
|
export class TaskRoomApplicationService {
|
|
74
67
|
configurationPath;
|
|
75
68
|
deps;
|
|
76
|
-
recovery;
|
|
77
69
|
constructor(configurationPath, deps = {}) {
|
|
78
70
|
this.configurationPath = configurationPath;
|
|
79
71
|
this.deps = deps;
|
|
@@ -124,9 +116,9 @@ export class TaskRoomApplicationService {
|
|
|
124
116
|
}
|
|
125
117
|
unlock?.();
|
|
126
118
|
const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
|
|
127
|
-
// A create-and-start launch is
|
|
128
|
-
//
|
|
129
|
-
// 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.
|
|
130
122
|
if (request.backlog || request.noRoom)
|
|
131
123
|
recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
|
|
132
124
|
title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
|
|
@@ -146,13 +138,15 @@ export class TaskRoomApplicationService {
|
|
|
146
138
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
147
139
|
// Once the durable Room exists, provisioning errors are resumable
|
|
148
140
|
// saga state. Return that explicit state so the command can launch a
|
|
149
|
-
// continuation and
|
|
141
|
+
// continuation and report the durable in-progress outcome.
|
|
150
142
|
const current = readTask(task.task_id);
|
|
151
143
|
if (!current.room_id || !getRoomRecord(current.room_id))
|
|
152
144
|
throw error;
|
|
153
145
|
task = current;
|
|
154
146
|
}
|
|
155
147
|
}
|
|
148
|
+
if (!request.backlog && !request.noRoom && task.room_id)
|
|
149
|
+
recordTaskProvisioningOutcome(this.taskProvisioningOutcome(task.task_id));
|
|
156
150
|
return task;
|
|
157
151
|
}
|
|
158
152
|
async createRoom(request) {
|
|
@@ -177,7 +171,10 @@ export class TaskRoomApplicationService {
|
|
|
177
171
|
}, template, () => { }, undefined, roomPolicy);
|
|
178
172
|
}
|
|
179
173
|
async startTask(input) {
|
|
180
|
-
|
|
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;
|
|
181
178
|
}
|
|
182
179
|
listTasks(filter) {
|
|
183
180
|
const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
|
|
@@ -237,19 +234,18 @@ export class TaskRoomApplicationService {
|
|
|
237
234
|
&& active === expected && launched === expected;
|
|
238
235
|
const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
|
|
239
236
|
const nextAction = room?.provisioning_detail === 'waiting_owner_authorization'
|
|
240
|
-
?
|
|
237
|
+
? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
|
|
241
238
|
: room?.provisioning_detail === 'waiting_owner_invite'
|
|
242
|
-
?
|
|
239
|
+
? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
|
|
243
240
|
: room?.provisioning_detail === 'owner_cid_mismatch'
|
|
244
|
-
?
|
|
241
|
+
? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
|
|
245
242
|
: room?.provisioning_detail === 'waiting_cowork'
|
|
246
|
-
?
|
|
243
|
+
? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
|
|
247
244
|
: failed
|
|
248
|
-
? `Correct the blocker, then run ours-fleet task
|
|
245
|
+
? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
|
|
249
246
|
: undefined;
|
|
250
247
|
return {
|
|
251
248
|
kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
|
|
252
|
-
handle: { command: `ours-fleet task await ${task.task_id}`, task_id: task.task_id },
|
|
253
249
|
launch: {
|
|
254
250
|
...(room?.template_snapshot
|
|
255
251
|
? { template: `${room.template_snapshot.name}@${room.template_snapshot.version}` }
|
|
@@ -408,40 +404,14 @@ export class TaskRoomApplicationService {
|
|
|
408
404
|
recordSettlementError(input) {
|
|
409
405
|
return recordTaskTerminalIntentError(input.taskId, input.error, input.recoveryHint);
|
|
410
406
|
}
|
|
411
|
-
async
|
|
412
|
-
|
|
413
|
-
// task-operation lock and must not require configuration: recovery of a
|
|
414
|
-
// deletion-pending task continues the deletion and never resurrects.
|
|
415
|
-
const routed = await withFileLock(taskOperationLockPath(input.taskId), () => {
|
|
416
|
-
const task = readTask(input.taskId);
|
|
417
|
-
if (task.deletion?.status === 'pending')
|
|
418
|
-
return 'deletion';
|
|
419
|
-
if (task.terminal_intent?.status === 'pending')
|
|
420
|
-
return 'terminal';
|
|
421
|
-
return 'none';
|
|
422
|
-
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
423
|
-
if (routed === 'deletion')
|
|
424
|
-
return { kind: 'deletion_worker_required', taskId: input.taskId };
|
|
425
|
-
const config = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
426
|
-
this.recovery = { taskId: input.taskId, config };
|
|
427
|
-
if (routed === 'terminal')
|
|
428
|
-
return { kind: 'terminal_worker_required', taskId: input.taskId };
|
|
429
|
-
return { kind: 'final', result: await this.continueTaskRecovery({
|
|
430
|
-
actor: input.actor, taskId: input.taskId, terminalTimedOut: false,
|
|
431
|
-
}) };
|
|
432
|
-
}
|
|
433
|
-
async continueTaskRecovery(input) {
|
|
434
|
-
if (!this.recovery || this.recovery.taskId !== input.taskId)
|
|
435
|
-
throw new Error('task recovery continuation requires a matching begin');
|
|
436
|
-
const recovery = this.recovery;
|
|
437
|
-
this.recovery = undefined;
|
|
438
|
-
const cfg = recovery.config;
|
|
407
|
+
async continueTaskProvisioning(input) {
|
|
408
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
439
409
|
let task = readTask(input.taskId);
|
|
440
410
|
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
441
|
-
const issues =
|
|
411
|
+
const issues = [];
|
|
442
412
|
if (task.state !== 'provisioning')
|
|
443
413
|
return {
|
|
444
|
-
kind:
|
|
414
|
+
kind: 'no_op', task, room, issues,
|
|
445
415
|
};
|
|
446
416
|
if (!room) {
|
|
447
417
|
if (!task.template)
|
|
@@ -470,7 +440,7 @@ export class TaskRoomApplicationService {
|
|
|
470
440
|
|| room.provisioning_detail === 'waiting_owner_invite'
|
|
471
441
|
|| room.provisioning_detail === 'owner_cid_mismatch') {
|
|
472
442
|
try {
|
|
473
|
-
await this.
|
|
443
|
+
await this.reconcileProvisioningOwner(cfg, room.room_id);
|
|
474
444
|
task = readTask(task.task_id);
|
|
475
445
|
room = getRoomRecord(room.room_id);
|
|
476
446
|
if (task.state === 'active' && room?.state === 'active')
|
|
@@ -527,6 +497,50 @@ export class TaskRoomApplicationService {
|
|
|
527
497
|
return { kind: 'provisioning_resume_failed', task, room, issues };
|
|
528
498
|
}
|
|
529
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
|
+
}
|
|
530
544
|
async acceptTerminal(taskId, kind, roomId, outcome) {
|
|
531
545
|
const task = await acceptTaskTerminalIntent({ taskId, kind, roomId, outcome });
|
|
532
546
|
return { task, settlementRequired: task.terminal_intent?.status === 'pending' && !!roomId };
|
|
@@ -619,106 +633,6 @@ export class TaskRoomApplicationService {
|
|
|
619
633
|
recordRoomSettlementError(input) {
|
|
620
634
|
return recordManagedRoomCloseError(input.roomId, input.error, input.recoveryHint);
|
|
621
635
|
}
|
|
622
|
-
async recoverRoom(input) {
|
|
623
|
-
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
624
|
-
if (!cfg.rooms)
|
|
625
|
-
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
626
|
-
const adapter = this.deps.cowork ? this.deps.cowork(cfg)
|
|
627
|
-
: createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
628
|
-
let orchestration = getRoomRecord(input.roomId);
|
|
629
|
-
if (orchestration?.state === 'closing' || orchestration?.state === 'closed')
|
|
630
|
-
return { kind: 'deletion_worker_required', roomId: input.roomId };
|
|
631
|
-
const room = await adapter.recoverRoom(input.roomId);
|
|
632
|
-
orchestration = getRoomRecord(input.roomId);
|
|
633
|
-
if (orchestration) {
|
|
634
|
-
const policy = storedRoomLaunchPolicy(orchestration.room_policy);
|
|
635
|
-
if ((room.anonymous ?? false) !== policy.anonymous) {
|
|
636
|
-
const error = `Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(policy.anonymous)})`;
|
|
637
|
-
setSagaError(input.roomId, error, 'Do not respawn members. Repair or upgrade Cowork, then recover the Room without changing its policy.', 'waiting_cowork');
|
|
638
|
-
throw new Error(error);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
if (orchestration?.owner_seat_cid) {
|
|
642
|
-
const ownerCid = orchestration.owner_seat_cid.toLowerCase();
|
|
643
|
-
const ownerSeat = room.seats.find(seat => seat.identity_cid.toLowerCase() === ownerCid && seat.seat_state !== 'removed');
|
|
644
|
-
if (ownerSeat)
|
|
645
|
-
await adapter.setRoleCommands(input.roomId, {
|
|
646
|
-
role: ownerSeat.role, commands: [...OWNER_ROOM_COMMANDS],
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
if (orchestration && !orchestration.owner_seat_cid
|
|
650
|
-
&& (orchestration.saga.phase === 'attach_owner'
|
|
651
|
-
|| orchestration.provisioning_detail === 'waiting_owner_authorization'
|
|
652
|
-
|| orchestration.provisioning_detail === 'waiting_owner_invite'
|
|
653
|
-
|| orchestration.provisioning_detail === 'owner_cid_mismatch')) {
|
|
654
|
-
const expected = cfg.rooms.owner.expected_cid.toLowerCase();
|
|
655
|
-
const existing = (await adapter.getSeats(input.roomId))
|
|
656
|
-
.find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
|
|
657
|
-
if (!existing && !cfg.ownerInvite)
|
|
658
|
-
throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before recovery');
|
|
659
|
-
await adapter.setRoleCommands(input.roomId, {
|
|
660
|
-
role: existing?.role ?? cfg.rooms.owner.role, commands: [...OWNER_ROOM_COMMANDS],
|
|
661
|
-
});
|
|
662
|
-
let acceptedCid = existing?.identity_cid;
|
|
663
|
-
if (!acceptedCid)
|
|
664
|
-
acceptedCid = (await adapter.acceptInvite(input.roomId, cfg.ownerInvite, {
|
|
665
|
-
role: cfg.rooms.owner.role, expected_cid: cfg.rooms.owner.expected_cid,
|
|
666
|
-
})).seat_cid;
|
|
667
|
-
setOwnerSeat(input.roomId, acceptedCid, cfg.ownerInviteFingerprint ?? '');
|
|
668
|
-
orchestration = advanceSaga(input.roomId, 'create_members', 3);
|
|
669
|
-
}
|
|
670
|
-
const issues = [];
|
|
671
|
-
if (orchestration?.saga.error)
|
|
672
|
-
issues.push('A provisioning failure is recorded; inspect role logs for diagnostics.');
|
|
673
|
-
if (orchestration?.saga.recovery_hint)
|
|
674
|
-
issues.push('Recovery guidance is recorded; inspect role logs for diagnostics.');
|
|
675
|
-
if (orchestration?.provisioning_detail === 'waiting_cowork')
|
|
676
|
-
issues.push('Check ours-cowork service status');
|
|
677
|
-
if (orchestration?.provisioning_detail === 'waiting_owner_authorization')
|
|
678
|
-
issues.push('Restore Cowork availability, then re-run recover');
|
|
679
|
-
if (orchestration?.provisioning_detail === 'waiting_owner_invite')
|
|
680
|
-
issues.push('Rotate rooms.owner.public_invite in config, then re-run recover');
|
|
681
|
-
if (orchestration?.provisioning_detail === 'waiting_seats')
|
|
682
|
-
issues.push('Inspect temporary member logs for invite acceptance, then re-run recover');
|
|
683
|
-
if (orchestration?.state === 'provisioning'
|
|
684
|
-
&& ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(orchestration.saga.phase)
|
|
685
|
-
&& orchestration.template_snapshot) {
|
|
686
|
-
try {
|
|
687
|
-
let template = orchestration.template_snapshot;
|
|
688
|
-
let brief;
|
|
689
|
-
let goal = orchestration.room_name;
|
|
690
|
-
if (orchestration.task_id) {
|
|
691
|
-
const task = readTask(orchestration.task_id);
|
|
692
|
-
if (!task.template || task.template.name !== template.name || task.template.version !== template.version
|
|
693
|
-
|| task.template.content_hash !== template.content_hash)
|
|
694
|
-
throw new Error(`task ${task.task_id} template reference does not match room ${orchestration.room_id}'s durable snapshot`);
|
|
695
|
-
brief = task.brief;
|
|
696
|
-
goal = task.title;
|
|
697
|
-
}
|
|
698
|
-
await (this.deps.provisionMembers ?? provisionMembers)({ cfg, cowork: adapter,
|
|
699
|
-
roomId: orchestration.room_id, taskId: orchestration.task_id, template,
|
|
700
|
-
binPath: (this.deps.binPath ?? getBinPath)(), brief, goal });
|
|
701
|
-
orchestration = getRoomRecord(input.roomId);
|
|
702
|
-
return { kind: 'provisioning_resumed', room, orchestration,
|
|
703
|
-
issues: ['Provisioning resumed successfully'] };
|
|
704
|
-
}
|
|
705
|
-
catch (error) {
|
|
706
|
-
issues.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
707
|
-
return { kind: 'provisioning_resume_failed', room, orchestration, issues };
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
if (orchestration?.state === 'provisioning' && !orchestration.template_snapshot) {
|
|
711
|
-
const seats = await adapter.getSeats(input.roomId);
|
|
712
|
-
const ownerReady = !orchestration.owner_seat_cid || seats.some(seat => seat.identity_cid.toLowerCase() === orchestration.owner_seat_cid.toLowerCase()
|
|
713
|
-
&& seat.seat_state === 'active');
|
|
714
|
-
if (ownerReady) {
|
|
715
|
-
orchestration = activateRoom(input.roomId);
|
|
716
|
-
if (orchestration.task_id)
|
|
717
|
-
activateTask(orchestration.task_id);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
return { kind: 'recovered', room, orchestration, issues };
|
|
721
|
-
}
|
|
722
636
|
async finishTask(input) {
|
|
723
637
|
let task = readTask(input.taskId);
|
|
724
638
|
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
@@ -769,7 +683,7 @@ export class TaskRoomApplicationService {
|
|
|
769
683
|
}
|
|
770
684
|
return { task, status: 'already_active' };
|
|
771
685
|
}
|
|
772
|
-
|
|
686
|
+
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
773
687
|
const durable = room?.template_snapshot ?? task.execution_plan?.snapshot;
|
|
774
688
|
if (durable && (!task.template || task.template.name !== durable.name
|
|
775
689
|
|| task.template.version !== durable.version || task.template.content_hash !== durable.content_hash))
|
|
@@ -872,6 +786,21 @@ export class TaskRoomApplicationService {
|
|
|
872
786
|
}
|
|
873
787
|
}
|
|
874
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
|
+
}
|
|
875
804
|
if (!room || !['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(room.saga.phase))
|
|
876
805
|
throw new TaskRoomApplicationError('task_non_resumable', 'task non-resumable', { task: task.task_id, room: task.room_id });
|
|
877
806
|
try {
|
|
@@ -967,10 +896,6 @@ export class TaskRoomApplicationService {
|
|
|
967
896
|
throw error;
|
|
968
897
|
}
|
|
969
898
|
unlockSnapshot?.();
|
|
970
|
-
recordCanonicalRoomCreated(room);
|
|
971
|
-
// The created notice is observable while member admission is still in
|
|
972
|
-
// progress; it is not delayed behind task-start command completion.
|
|
973
|
-
await checkpointFleetAuditPresentations();
|
|
974
899
|
room = advanceSaga(room.room_id, 'create_room', 1);
|
|
975
900
|
if (attachOwner) {
|
|
976
901
|
room = advanceSaga(room.room_id, 'attach_owner', 2);
|
|
@@ -980,7 +905,7 @@ export class TaskRoomApplicationService {
|
|
|
980
905
|
});
|
|
981
906
|
}
|
|
982
907
|
catch (error) {
|
|
983
|
-
setSagaError(room.room_id, error instanceof Error ? error.message : String(error), 'Restore Cowork availability, then
|
|
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');
|
|
984
909
|
throw error;
|
|
985
910
|
}
|
|
986
911
|
try {
|
|
@@ -992,7 +917,11 @@ export class TaskRoomApplicationService {
|
|
|
992
917
|
catch (error) {
|
|
993
918
|
const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
|
|
994
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.'
|
|
995
|
-
: '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 });
|
|
996
925
|
throw error;
|
|
997
926
|
}
|
|
998
927
|
}
|
|
@@ -1006,6 +935,10 @@ export class TaskRoomApplicationService {
|
|
|
1006
935
|
});
|
|
1007
936
|
}
|
|
1008
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 });
|
|
1009
942
|
throw error;
|
|
1010
943
|
}
|
|
1011
944
|
else if (attachOwner) {
|
|
@@ -1028,14 +961,20 @@ export class TaskRoomApplicationService {
|
|
|
1028
961
|
}
|
|
1029
962
|
else
|
|
1030
963
|
room = activateRoom(room.room_id);
|
|
1031
|
-
if (room.state === 'active')
|
|
1032
|
-
recordFleetAuditPresentation({
|
|
1033
|
-
eventId: `room-ready:${room.activated_at ?? room.created_at}`,
|
|
1034
|
-
name: room.room_name, previousState: 'provisioning', newState: 'active',
|
|
1035
|
-
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,
|
|
1036
969
|
template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
1037
970
|
anonymous: policy.anonymous, memberCount: room.member_seats.length, ownerAttached: attachOwner,
|
|
1038
|
-
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
|
+
});
|
|
1039
978
|
if (task.task_id && !launchTemplate?.members.length)
|
|
1040
979
|
activateTask(task.task_id);
|
|
1041
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
|
]
|