@ours.network/fleet 1.1.0-nightly.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -46
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +7 -4
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +4 -4
- package/dist/application/role-creation-service.js +9 -2
- package/dist/application/role-repository.d.ts +4 -2
- package/dist/application/role-repository.js +6 -3
- package/dist/application/session-control.d.ts +3 -1
- package/dist/application/session-control.js +4 -2
- package/dist/application/task-room-service.d.ts +92 -39
- package/dist/application/task-room-service.js +579 -173
- package/dist/briefing.js +85 -28
- package/dist/build-info.json +5 -5
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +130 -55
- package/dist/config.d.ts +38 -3
- package/dist/config.js +193 -28
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +193 -55
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +40 -4
- package/dist/fleet-command-audit.d.ts +59 -9
- package/dist/fleet-command-audit.js +426 -63
- package/dist/fleet-proxy.d.ts +5 -2
- package/dist/harness/claude-code-session.js +2 -1
- package/dist/harness/codex-app-server-proxy.d.ts +40 -0
- package/dist/harness/codex-app-server-proxy.js +348 -14
- package/dist/harness/codex-session.d.ts +9 -2
- package/dist/harness/codex-session.js +18 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +104 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +1 -1
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +631 -0
- package/dist/lifecycle-summary.d.ts +134 -0
- package/dist/lifecycle-summary.js +179 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +38 -5
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +21 -5
- package/dist/owner-channel/channel.js +312 -138
- package/dist/owner-channel/commands.d.ts +28 -6
- package/dist/owner-channel/commands.js +109 -27
- package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
- package/dist/owner-channel/lifecycle-outbox.js +87 -0
- package/dist/owner-channel/ours-client.d.ts +19 -0
- package/dist/owner-channel/ours-client.js +3 -0
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/permissions.js +1 -1
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +450 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.js +13 -1
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +434 -279
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +23 -7
- package/dist/rooms-tasks/config.js +16 -25
- package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
- package/dist/rooms-tasks/cowork-adapter.js +13 -0
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/external-worker.d.ts +8 -0
- package/dist/rooms-tasks/external-worker.js +38 -0
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +181 -0
- package/dist/rooms-tasks/member-overrides.d.ts +30 -0
- package/dist/rooms-tasks/member-overrides.js +204 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +9 -2
- package/dist/rooms-tasks/provision.js +146 -56
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-room-name.d.ts +12 -0
- package/dist/rooms-tasks/task-room-name.js +61 -0
- package/dist/rooms-tasks/task-state.d.ts +115 -3
- package/dist/rooms-tasks/task-state.js +361 -11
- package/dist/rooms-tasks/templates.d.ts +4 -1
- package/dist/rooms-tasks/templates.js +34 -3
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +10 -4
- package/dist/rooms-tasks/types.d.ts +92 -7
- package/dist/rooms-tasks/types.js +14 -1
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +92 -33
- package/dist/session/acp.d.ts +4 -1
- package/dist/session/acp.js +29 -13
- package/dist/session/arbiter.d.ts +2 -1
- package/dist/session/arbiter.js +2 -0
- package/dist/session/codex-app-server-transport.d.ts +53 -0
- package/dist/session/codex-app-server-transport.js +184 -0
- package/dist/session/codex-app-server.d.ts +122 -0
- package/dist/session/codex-app-server.js +1031 -0
- package/dist/session/control.d.ts +2 -2
- package/dist/session/control.js +18 -2
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/types.d.ts +24 -6
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +9 -0
- package/dist/spawn.js +77 -9
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +25 -7
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
- package/dist/web-app/index.html +1 -1
- package/package.json +4 -4
- package/presets/brain-catalog.json +17 -0
- package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
- package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
- package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +37 -0
- package/presets/fleet/roles/Critic.yaml +23 -1
- package/presets/fleet/roles/Developer.yaml +27 -5
- package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
- package/presets/fleet/room_templates/pair.yaml +9 -7
- package/presets/fleet/room_templates/single.yaml +5 -4
- package/presets/fleet/room_templates/team.yaml +12 -8
- package/presets/manifest.json +2 -1
- package/presets/fleet/agents/Architect.yaml +0 -3
- package/presets/fleet/agents/Secretary.yaml +0 -3
- package/presets/fleet/agents/Tester.yaml +0 -3
- package/presets/fleet/roles/Agent.yaml +0 -6
- package/presets/fleet/roles/Architect.yaml +0 -6
- package/presets/fleet/roles/Secretary.yaml +0 -6
- package/presets/fleet/roles/Tester.yaml +0 -6
|
@@ -3,12 +3,51 @@ import { ConfigError, loadConfig } from '../config.js';
|
|
|
3
3
|
import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
|
|
4
4
|
import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
|
|
5
5
|
import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
|
|
6
|
-
import { activateTask, blockTask as persistBlockTask, createTask as persistTask,
|
|
6
|
+
import { activateTask, blockTask as persistBlockTask, createTask as persistTask, getDeletingTask, getTask as readTask, listTasks as readTasks, taskDeletionState, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, updateTaskExecutionPlan, } from '../rooms-tasks/task-state.js';
|
|
7
7
|
import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
|
|
8
|
-
import { hashTemplate, listTemplates, resolveTemplate, snapshotTemplate } from '../rooms-tasks/templates.js';
|
|
8
|
+
import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
|
|
9
|
+
import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
|
|
10
|
+
import { hashMemberOverrides, prepareExecutionPlan } from '../rooms-tasks/member-overrides.js';
|
|
11
|
+
import { recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
|
|
9
12
|
import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
|
|
10
|
-
import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from '../rooms-tasks/terminal.js';
|
|
11
|
-
import {
|
|
13
|
+
import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath, } from '../rooms-tasks/terminal.js';
|
|
14
|
+
import { acceptTaskDeletion, recordTaskDeletionError, settleTaskDeletion, } from '../rooms-tasks/deletion.js';
|
|
15
|
+
import { withFileLock } from '../atomic-file.js';
|
|
16
|
+
import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
|
|
17
|
+
import { storedRoomLaunchPolicy, TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
|
|
18
|
+
import { deriveTaskRoomName } from '../rooms-tasks/task-room-name.js';
|
|
19
|
+
const OWNER_ROOM_COMMANDS = ['list-members', 'remove-member'];
|
|
20
|
+
/** Repairable terminal observation derived entirely from durable Task/Room state. */
|
|
21
|
+
export function recordTaskProvisioningOutcome(outcome) {
|
|
22
|
+
const room = outcome.room;
|
|
23
|
+
recordFleetAuditResource('task', outcome.task.task_id);
|
|
24
|
+
if (room)
|
|
25
|
+
recordFleetAuditResource('room', room.room_id);
|
|
26
|
+
if (outcome.kind === 'ready' && room)
|
|
27
|
+
recordFleetAuditPresentation({
|
|
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 })),
|
|
35
|
+
});
|
|
36
|
+
if (outcome.kind === 'failed')
|
|
37
|
+
recordFleetAuditPresentation({
|
|
38
|
+
kind: 'lifecycle_failure', resource: 'Task', id: outcome.task.task_id,
|
|
39
|
+
label: outcome.task.title,
|
|
40
|
+
state: outcome.task.state, category: 'provision_failed',
|
|
41
|
+
eventId: outcome.task.ended_at ?? room?.created_at ?? outcome.task.created_at,
|
|
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
|
+
});
|
|
50
|
+
}
|
|
12
51
|
export class TaskRoomApplicationError extends Error {
|
|
13
52
|
code;
|
|
14
53
|
fields;
|
|
@@ -19,43 +58,95 @@ export class TaskRoomApplicationError extends Error {
|
|
|
19
58
|
this.name = 'TaskRoomApplicationError';
|
|
20
59
|
}
|
|
21
60
|
}
|
|
61
|
+
export function resolveRoomLaunchPolicy(template, override) {
|
|
62
|
+
const anonymous = override ?? template?.room?.anonymous ?? false;
|
|
63
|
+
return { anonymous };
|
|
64
|
+
}
|
|
22
65
|
/** Exact extraction of the previously CLI-owned task create/start behavior. */
|
|
23
66
|
export class TaskRoomApplicationService {
|
|
24
67
|
configurationPath;
|
|
25
68
|
deps;
|
|
26
|
-
recovery;
|
|
27
69
|
constructor(configurationPath, deps = {}) {
|
|
28
70
|
this.configurationPath = configurationPath;
|
|
29
71
|
this.deps = deps;
|
|
30
72
|
}
|
|
31
73
|
async createTask(request) {
|
|
32
74
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
75
|
+
let template = this.createTemplate(cfg, request.template, request.noRoom);
|
|
76
|
+
if (request.noRoom && request.anonymous !== undefined)
|
|
77
|
+
throw new ConfigError('--anonymous/--no-anonymous cannot be combined with --no-room');
|
|
78
|
+
const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
|
|
79
|
+
let launchDefinitions;
|
|
80
|
+
let executionPlan;
|
|
81
|
+
let preparedPlan;
|
|
82
|
+
if (template) {
|
|
83
|
+
const definition = resolveTemplate(template.name, cfg.roomTemplates ?? {});
|
|
84
|
+
preparedPlan = prepareExecutionPlan(definition, cfg, request.members ?? {});
|
|
85
|
+
launchDefinitions = preparedPlan.launchDefinitions;
|
|
86
|
+
}
|
|
37
87
|
const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
88
|
+
const unlock = preparedPlan ? acquireLaunchSnapshotLock() : undefined;
|
|
89
|
+
let sealedHash;
|
|
90
|
+
let task;
|
|
91
|
+
try {
|
|
92
|
+
if (preparedPlan) {
|
|
93
|
+
template = sealTemplateSnapshot(preparedPlan.snapshot, cfg.agentTemplates ?? {}, preparedPlan.launchDefinitions);
|
|
94
|
+
sealedHash = template.launch_snapshot_hash;
|
|
95
|
+
executionPlan = { schema_version: 1, snapshot: template, room_policy: roomPolicy,
|
|
96
|
+
overrides: preparedPlan.overrides,
|
|
97
|
+
overrides_hash: preparedPlan.overridesHash,
|
|
98
|
+
plan_hash: preparedPlan.planHash };
|
|
99
|
+
}
|
|
100
|
+
const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
|
|
101
|
+
task = await withTaskListsLock(() => {
|
|
102
|
+
const list = resolveTaskList(request.list ?? 'default');
|
|
103
|
+
return persistTask({
|
|
104
|
+
title: request.title, brief, brief_file: request.briefFile, template: ref,
|
|
105
|
+
execution_plan: executionPlan,
|
|
106
|
+
origin: request.origin, idempotency_key: request.idempotencyKey,
|
|
107
|
+
start: !request.backlog, no_room: request.noRoom, listId: list.list_id,
|
|
108
|
+
});
|
|
44
109
|
});
|
|
45
|
-
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
unlock?.();
|
|
113
|
+
if (sealedHash)
|
|
114
|
+
releaseLaunchSnapshot(sealedHash);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
unlock?.();
|
|
118
|
+
const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
|
|
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.
|
|
122
|
+
if (request.backlog || request.noRoom)
|
|
123
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
|
|
124
|
+
title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
|
|
125
|
+
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
126
|
+
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
127
|
+
agents: [] });
|
|
46
128
|
if (!request.backlog && !request.noRoom && ref && !task.room_id) {
|
|
47
129
|
try {
|
|
48
|
-
await this.provisionRoom(cfg, task, template,
|
|
49
|
-
task = updateTaskRoom(task.task_id,
|
|
50
|
-
});
|
|
130
|
+
const room = await this.provisionRoom(cfg, task, template, created => {
|
|
131
|
+
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
132
|
+
}, launchDefinitions, roomPolicy);
|
|
51
133
|
task = readTask(task.task_id);
|
|
134
|
+
void room;
|
|
52
135
|
}
|
|
53
136
|
catch (error) {
|
|
54
137
|
if (error instanceof CoworkUnavailableError)
|
|
55
138
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
56
|
-
|
|
139
|
+
// Once the durable Room exists, provisioning errors are resumable
|
|
140
|
+
// saga state. Return that explicit state so the command can launch a
|
|
141
|
+
// continuation and report the durable in-progress outcome.
|
|
142
|
+
const current = readTask(task.task_id);
|
|
143
|
+
if (!current.room_id || !getRoomRecord(current.room_id))
|
|
144
|
+
throw error;
|
|
145
|
+
task = current;
|
|
57
146
|
}
|
|
58
147
|
}
|
|
148
|
+
if (!request.backlog && !request.noRoom && task.room_id)
|
|
149
|
+
recordTaskProvisioningOutcome(this.taskProvisioningOutcome(task.task_id));
|
|
59
150
|
return task;
|
|
60
151
|
}
|
|
61
152
|
async createRoom(request) {
|
|
@@ -66,27 +157,28 @@ export class TaskRoomApplicationService {
|
|
|
66
157
|
const definition = resolveTemplate(request.template, cfg.roomTemplates ?? {});
|
|
67
158
|
if (!definition)
|
|
68
159
|
throw new TaskRoomApplicationError('template_not_found', `template not found: ${request.template}`, { template: request.template });
|
|
69
|
-
|
|
160
|
+
if (request.members && Object.keys(request.members).length) {
|
|
161
|
+
const prepared = prepareExecutionPlan(definition, cfg, request.members);
|
|
162
|
+
template = prepared.snapshot;
|
|
163
|
+
const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
|
|
164
|
+
return this.provisionRoom(cfg, { title: request.name, brief, goal: request.goal }, template, () => { }, prepared.launchDefinitions, roomPolicy);
|
|
165
|
+
}
|
|
166
|
+
template = snapshotTemplate(definition, cfg.agentTemplates);
|
|
70
167
|
}
|
|
168
|
+
const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
|
|
71
169
|
return this.provisionRoom(cfg, {
|
|
72
170
|
title: request.name, brief, goal: request.goal,
|
|
73
|
-
}, template, () => { });
|
|
171
|
+
}, template, () => { }, undefined, roomPolicy);
|
|
74
172
|
}
|
|
75
173
|
async startTask(input) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await this.provisionRoom(cfg, task, template, room => {
|
|
81
|
-
task = updateTaskRoom(task.task_id, room.room_id, room.room_identity_cid);
|
|
82
|
-
});
|
|
83
|
-
task = readTask(task.task_id);
|
|
84
|
-
}
|
|
85
|
-
return task;
|
|
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;
|
|
86
178
|
}
|
|
87
179
|
listTasks(filter) {
|
|
88
180
|
const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
|
|
89
|
-
return readTasks({ state: filter?.state, listId });
|
|
181
|
+
return readTasks({ state: filter?.state, listId, includeDeleting: filter?.includeDeleting });
|
|
90
182
|
}
|
|
91
183
|
listTaskLists() { return readTaskLists(); }
|
|
92
184
|
groupedTasks(filter) {
|
|
@@ -130,6 +222,55 @@ export class TaskRoomApplicationService {
|
|
|
130
222
|
const task = readTask(taskId);
|
|
131
223
|
return { task, orchestration: task.room_id ? getRoomRecord(task.room_id) : undefined };
|
|
132
224
|
}
|
|
225
|
+
taskProvisioningOutcome(taskId) {
|
|
226
|
+
const task = readTask(taskId);
|
|
227
|
+
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
228
|
+
const expected = room?.template_snapshot?.members.reduce((sum, member) => sum + member.count, 0)
|
|
229
|
+
?? task.execution_plan?.snapshot.members.reduce((sum, member) => sum + member.count, 0) ?? 0;
|
|
230
|
+
const active = room?.member_seats.filter(seat => seat.seat_state === 'active').length ?? 0;
|
|
231
|
+
const launched = room?.member_seats.filter(seat => seat.launch?.state === 'launched').length ?? 0;
|
|
232
|
+
const failed = task.state === 'failed' || room?.saga.phase === 'failed';
|
|
233
|
+
const ready = task.state === 'active' && room?.state === 'active'
|
|
234
|
+
&& active === expected && launched === expected;
|
|
235
|
+
const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
|
|
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
|
+
return {
|
|
248
|
+
kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
|
|
249
|
+
launch: {
|
|
250
|
+
...(room?.template_snapshot
|
|
251
|
+
? { template: `${room.template_snapshot.name}@${room.template_snapshot.version}` }
|
|
252
|
+
: task.template ? { template: `${task.template.name}@${task.template.version}` } : {}),
|
|
253
|
+
anonymous: storedRoomLaunchPolicy(room?.room_policy ?? task.execution_plan?.room_policy).anonymous,
|
|
254
|
+
owner_attached: Boolean(room?.owner_seat_cid),
|
|
255
|
+
},
|
|
256
|
+
members: { expected, active, launched },
|
|
257
|
+
...(blocker ? { blocker } : {}),
|
|
258
|
+
...(nextAction ? { next_action: nextAction } : {}),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async awaitTaskProvisioning(input) {
|
|
262
|
+
const now = this.deps.now ?? Date.now;
|
|
263
|
+
const sleep = this.deps.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
264
|
+
const deadline = now() + (input.waitMs ?? 60_000);
|
|
265
|
+
for (;;) {
|
|
266
|
+
const outcome = this.taskProvisioningOutcome(input.taskId);
|
|
267
|
+
if (outcome.kind !== 'in_progress' || now() >= deadline) {
|
|
268
|
+
recordTaskProvisioningOutcome(outcome);
|
|
269
|
+
return outcome;
|
|
270
|
+
}
|
|
271
|
+
await sleep(100);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
133
274
|
blockTask(input) {
|
|
134
275
|
return persistBlockTask(input.taskId, input.reason);
|
|
135
276
|
}
|
|
@@ -139,8 +280,93 @@ export class TaskRoomApplicationService {
|
|
|
139
280
|
reviewTask(input) {
|
|
140
281
|
return persistReviewTask(input.taskId);
|
|
141
282
|
}
|
|
142
|
-
|
|
143
|
-
|
|
283
|
+
deletionActor(actor) {
|
|
284
|
+
if (actor.kind === 'local_control')
|
|
285
|
+
return { kind: 'local_control', surface: actor.surface };
|
|
286
|
+
if (actor.kind === 'authenticated_owner')
|
|
287
|
+
return { kind: 'authenticated_owner', surface: 'messenger', cid: actor.cid };
|
|
288
|
+
throw new Error('internal workers cannot originate a task deletion acceptance');
|
|
289
|
+
}
|
|
290
|
+
/** Accept a permanent deletion in any lifecycle state; audit at acceptance. */
|
|
291
|
+
async requestTaskDeletion(input) {
|
|
292
|
+
const result = await acceptTaskDeletion(input.taskId, this.deletionActor(input.actor));
|
|
293
|
+
if (result.status === 'accepted') {
|
|
294
|
+
const task = result.task;
|
|
295
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'delete', id: task.task_id,
|
|
296
|
+
title: task.title, previousState: task.state, newState: 'deleting',
|
|
297
|
+
revision: task.deletion?.accepted_at ?? task.created_at, list: task.list_name ?? 'default',
|
|
298
|
+
roomId: task.room_id,
|
|
299
|
+
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
300
|
+
agents: [] });
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
/** Worker entry: converge an accepted deletion; Cowork is resolved lazily. */
|
|
305
|
+
async settleTaskDeletion(input) {
|
|
306
|
+
const result = await settleTaskDeletion({
|
|
307
|
+
taskId: input.taskId,
|
|
308
|
+
cowork: () => {
|
|
309
|
+
// Config resolves only when room work exists: a no-room deletion must
|
|
310
|
+
// settle even with missing or invalid rooms configuration.
|
|
311
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
312
|
+
if (!cfg.rooms)
|
|
313
|
+
throw new ConfigError('rooms: configuration is required before deleting task rooms');
|
|
314
|
+
return this.deps.cowork ? this.deps.cowork(cfg)
|
|
315
|
+
: createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
if (result.deleted && result.previous_state) {
|
|
319
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'delete', id: result.task_id,
|
|
320
|
+
title: result.title, previousState: result.previous_state, newState: 'deleted',
|
|
321
|
+
revision: new Date().toISOString(), list: 'default', roomId: undefined,
|
|
322
|
+
template: undefined, agents: [] });
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Launch the external deletion worker outside the caller's lifecycle and
|
|
328
|
+
* wait boundedly for physical absence. Timeouts and launch failures report
|
|
329
|
+
* a pending, recoverable state — never success. A concurrent worker that
|
|
330
|
+
* already removed the record reads as settled.
|
|
331
|
+
*/
|
|
332
|
+
async launchTaskDeletionWorker(input) {
|
|
333
|
+
const launch = this.deps.launchDeletionWorker
|
|
334
|
+
?? ((taskId) => launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, this.configurationPath));
|
|
335
|
+
const errorAtBefore = (() => {
|
|
336
|
+
try {
|
|
337
|
+
return getDeletingTask(input.taskId).deletion?.error_at;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
})();
|
|
343
|
+
try {
|
|
344
|
+
await launch(input.taskId);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
348
|
+
await recordTaskDeletionError(input.taskId, message, `External delete worker failed to start. Repeat the delete request for ${input.taskId}.`).catch(() => { });
|
|
349
|
+
return { deleted: false, pending: true, error: message };
|
|
350
|
+
}
|
|
351
|
+
const now = this.deps.now ?? Date.now;
|
|
352
|
+
const sleep = this.deps.sleep
|
|
353
|
+
?? ((ms) => new Promise(resolve => { setTimeout(resolve, ms); }));
|
|
354
|
+
const deadline = now() + (input.waitMs ?? 10_000);
|
|
355
|
+
while (now() < deadline) {
|
|
356
|
+
if (taskDeletionState(input.taskId) === 'absent')
|
|
357
|
+
return { deleted: true, pending: false };
|
|
358
|
+
try {
|
|
359
|
+
const current = getDeletingTask(input.taskId).deletion;
|
|
360
|
+
if (current?.error_at !== errorAtBefore && current?.error)
|
|
361
|
+
return { deleted: false, pending: true, error: current.error };
|
|
362
|
+
}
|
|
363
|
+
catch { /* re-checked as absence on the next iteration */ }
|
|
364
|
+
await sleep(100);
|
|
365
|
+
}
|
|
366
|
+
return { deleted: false, pending: true };
|
|
367
|
+
}
|
|
368
|
+
recordDeletionError(input) {
|
|
369
|
+
return recordTaskDeletionError(input.taskId, input.error, input.recoveryHint);
|
|
144
370
|
}
|
|
145
371
|
async completeTask(input) {
|
|
146
372
|
const task = readTask(input.taskId);
|
|
@@ -178,33 +404,63 @@ export class TaskRoomApplicationService {
|
|
|
178
404
|
recordSettlementError(input) {
|
|
179
405
|
return recordTaskTerminalIntentError(input.taskId, input.error, input.recoveryHint);
|
|
180
406
|
}
|
|
181
|
-
async
|
|
182
|
-
const
|
|
183
|
-
this.recovery = { taskId: input.taskId, config };
|
|
184
|
-
const task = readTask(input.taskId);
|
|
185
|
-
if (task.terminal_intent?.status === 'pending')
|
|
186
|
-
return { kind: 'terminal_worker_required', taskId: input.taskId };
|
|
187
|
-
return { kind: 'final', result: await this.continueTaskRecovery({
|
|
188
|
-
actor: input.actor, taskId: input.taskId, terminalTimedOut: false,
|
|
189
|
-
}) };
|
|
190
|
-
}
|
|
191
|
-
async continueTaskRecovery(input) {
|
|
192
|
-
if (!this.recovery || this.recovery.taskId !== input.taskId)
|
|
193
|
-
throw new Error('task recovery continuation requires a matching begin');
|
|
194
|
-
const recovery = this.recovery;
|
|
195
|
-
this.recovery = undefined;
|
|
196
|
-
const cfg = recovery.config;
|
|
407
|
+
async continueTaskProvisioning(input) {
|
|
408
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
197
409
|
let task = readTask(input.taskId);
|
|
198
410
|
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
199
|
-
const issues =
|
|
411
|
+
const issues = [];
|
|
200
412
|
if (task.state !== 'provisioning')
|
|
201
413
|
return {
|
|
202
|
-
kind:
|
|
414
|
+
kind: 'no_op', task, room, issues,
|
|
203
415
|
};
|
|
416
|
+
if (!room) {
|
|
417
|
+
if (!task.template)
|
|
418
|
+
return {
|
|
419
|
+
kind: 'provisioning_non_resumable', task, room, issues, reason: 'missing_room',
|
|
420
|
+
};
|
|
421
|
+
try {
|
|
422
|
+
const template = task.execution_plan?.snapshot ?? this.existingTemplate(cfg, task);
|
|
423
|
+
const roomPolicy = task.execution_plan
|
|
424
|
+
? storedRoomLaunchPolicy(task.execution_plan.room_policy)
|
|
425
|
+
: resolveRoomLaunchPolicy(template, undefined);
|
|
426
|
+
await this.provisionRoom(cfg, task, template, created => {
|
|
427
|
+
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
428
|
+
}, undefined, roomPolicy);
|
|
429
|
+
task = readTask(task.task_id);
|
|
430
|
+
room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
431
|
+
return { kind: 'provisioning_resumed', task, room, issues: [{ code: 'provisioning_resumed' }] };
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
issues.push({ code: 'resume_failed', error: error instanceof Error ? error.message : String(error) });
|
|
435
|
+
return { kind: 'provisioning_resume_failed', task, room, issues };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (room.saga.phase === 'attach_owner'
|
|
439
|
+
|| room.provisioning_detail === 'waiting_owner_authorization'
|
|
440
|
+
|| room.provisioning_detail === 'waiting_owner_invite'
|
|
441
|
+
|| room.provisioning_detail === 'owner_cid_mismatch') {
|
|
442
|
+
try {
|
|
443
|
+
await this.reconcileProvisioningOwner(cfg, room.room_id);
|
|
444
|
+
task = readTask(task.task_id);
|
|
445
|
+
room = getRoomRecord(room.room_id);
|
|
446
|
+
if (task.state === 'active' && room?.state === 'active')
|
|
447
|
+
return {
|
|
448
|
+
kind: 'provisioning_resumed', task, room, issues: [{ code: 'provisioning_resumed' }],
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
issues.push({ code: 'resume_failed', error: error instanceof Error ? error.message : String(error) });
|
|
453
|
+
return { kind: 'provisioning_resume_failed', task, room, issues };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
204
456
|
if (!room)
|
|
205
|
-
return {
|
|
457
|
+
return {
|
|
458
|
+
kind: 'provisioning_non_resumable', task, room, issues, reason: 'missing_room',
|
|
459
|
+
};
|
|
206
460
|
if (room.provisioning_detail === 'waiting_cowork')
|
|
207
461
|
issues.push({ code: 'waiting_cowork' });
|
|
462
|
+
if (room.provisioning_detail === 'waiting_owner_authorization')
|
|
463
|
+
issues.push({ code: 'waiting_owner_authorization' });
|
|
208
464
|
if (room.provisioning_detail === 'waiting_owner_invite')
|
|
209
465
|
issues.push({ code: 'waiting_owner_invite' });
|
|
210
466
|
if (room.provisioning_detail === 'owner_cid_mismatch')
|
|
@@ -241,6 +497,50 @@ export class TaskRoomApplicationService {
|
|
|
241
497
|
return { kind: 'provisioning_resume_failed', task, room, issues };
|
|
242
498
|
}
|
|
243
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
|
+
}
|
|
244
544
|
async acceptTerminal(taskId, kind, roomId, outcome) {
|
|
245
545
|
const task = await acceptTaskTerminalIntent({ taskId, kind, roomId, outcome });
|
|
246
546
|
return { task, settlementRequired: task.terminal_intent?.status === 'pending' && !!roomId };
|
|
@@ -260,11 +560,9 @@ export class TaskRoomApplicationService {
|
|
|
260
560
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
261
561
|
return listTemplates(cfg.roomTemplates ?? {}).flatMap(template => {
|
|
262
562
|
const issues = template.members.flatMap(member => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return cfg.roles.some(role => role.name === ref)
|
|
267
|
-
? [] : [`member ${member.slot}: Agent ref '${ref}' not found`];
|
|
563
|
+
const ref = member.agent_template;
|
|
564
|
+
return cfg.agentTemplates?.[ref]
|
|
565
|
+
? [] : [`member ${member.slot}: Agent Template '${ref}' not found`];
|
|
268
566
|
});
|
|
269
567
|
return issues.length ? [{ template: `${template.name}@${template.version}`, issues }] : [];
|
|
270
568
|
});
|
|
@@ -335,73 +633,6 @@ export class TaskRoomApplicationService {
|
|
|
335
633
|
recordRoomSettlementError(input) {
|
|
336
634
|
return recordManagedRoomCloseError(input.roomId, input.error, input.recoveryHint);
|
|
337
635
|
}
|
|
338
|
-
async recoverRoom(input) {
|
|
339
|
-
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
340
|
-
if (!cfg.rooms)
|
|
341
|
-
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
342
|
-
const adapter = this.deps.cowork ? this.deps.cowork(cfg)
|
|
343
|
-
: createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
344
|
-
let orchestration = getRoomRecord(input.roomId);
|
|
345
|
-
if (orchestration?.state === 'closing' || orchestration?.state === 'closed')
|
|
346
|
-
return { kind: 'deletion_worker_required', roomId: input.roomId };
|
|
347
|
-
const room = await adapter.recoverRoom(input.roomId);
|
|
348
|
-
orchestration = getRoomRecord(input.roomId);
|
|
349
|
-
if (orchestration && !orchestration.owner_seat_cid
|
|
350
|
-
&& (orchestration.provisioning_detail === 'waiting_owner_invite'
|
|
351
|
-
|| orchestration.provisioning_detail === 'owner_cid_mismatch')) {
|
|
352
|
-
const expected = cfg.rooms.owner.expected_cid.toLowerCase();
|
|
353
|
-
const existing = (await adapter.getSeats(input.roomId))
|
|
354
|
-
.find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
|
|
355
|
-
if (!existing && !cfg.ownerInvite)
|
|
356
|
-
throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before recovery');
|
|
357
|
-
let acceptedCid = existing?.identity_cid;
|
|
358
|
-
if (!acceptedCid)
|
|
359
|
-
acceptedCid = (await adapter.acceptInvite(input.roomId, cfg.ownerInvite, {
|
|
360
|
-
role: cfg.rooms.owner.role, expected_cid: cfg.rooms.owner.expected_cid,
|
|
361
|
-
})).seat_cid;
|
|
362
|
-
setOwnerSeat(input.roomId, acceptedCid, cfg.ownerInviteFingerprint ?? '');
|
|
363
|
-
orchestration = advanceSaga(input.roomId, 'create_members', 3);
|
|
364
|
-
}
|
|
365
|
-
const issues = [];
|
|
366
|
-
if (orchestration?.saga.error)
|
|
367
|
-
issues.push('A provisioning failure is recorded; inspect role logs for diagnostics.');
|
|
368
|
-
if (orchestration?.saga.recovery_hint)
|
|
369
|
-
issues.push('Recovery guidance is recorded; inspect role logs for diagnostics.');
|
|
370
|
-
if (orchestration?.provisioning_detail === 'waiting_cowork')
|
|
371
|
-
issues.push('Check ours-cowork service status');
|
|
372
|
-
if (orchestration?.provisioning_detail === 'waiting_owner_invite')
|
|
373
|
-
issues.push('Rotate rooms.owner.public_invite in config, then re-run recover');
|
|
374
|
-
if (orchestration?.provisioning_detail === 'waiting_seats')
|
|
375
|
-
issues.push('Inspect temporary member logs for invite acceptance, then re-run recover');
|
|
376
|
-
if (orchestration?.state === 'provisioning'
|
|
377
|
-
&& ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(orchestration.saga.phase)
|
|
378
|
-
&& orchestration.template_snapshot) {
|
|
379
|
-
try {
|
|
380
|
-
let template = orchestration.template_snapshot;
|
|
381
|
-
let brief;
|
|
382
|
-
let goal = orchestration.room_name;
|
|
383
|
-
if (orchestration.task_id) {
|
|
384
|
-
const task = readTask(orchestration.task_id);
|
|
385
|
-
if (!task.template || task.template.name !== template.name || task.template.version !== template.version
|
|
386
|
-
|| task.template.content_hash !== template.content_hash)
|
|
387
|
-
throw new Error(`task ${task.task_id} template reference does not match room ${orchestration.room_id}'s durable snapshot`);
|
|
388
|
-
brief = task.brief;
|
|
389
|
-
goal = task.title;
|
|
390
|
-
}
|
|
391
|
-
await (this.deps.provisionMembers ?? provisionMembers)({ cfg, cowork: adapter,
|
|
392
|
-
roomId: orchestration.room_id, taskId: orchestration.task_id, template,
|
|
393
|
-
binPath: (this.deps.binPath ?? getBinPath)(), brief, goal });
|
|
394
|
-
orchestration = getRoomRecord(input.roomId);
|
|
395
|
-
return { kind: 'provisioning_resumed', room, orchestration,
|
|
396
|
-
issues: ['Provisioning resumed successfully'] };
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
issues.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
400
|
-
return { kind: 'provisioning_resume_failed', room, orchestration, issues };
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return { kind: 'recovered', room, orchestration, issues };
|
|
404
|
-
}
|
|
405
636
|
async finishTask(input) {
|
|
406
637
|
let task = readTask(input.taskId);
|
|
407
638
|
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
@@ -415,73 +646,161 @@ export class TaskRoomApplicationService {
|
|
|
415
646
|
async ensureTaskWork(input) {
|
|
416
647
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
417
648
|
let task = readTask(input.taskId);
|
|
649
|
+
const recordedRoom = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
650
|
+
const recordedPolicy = storedRoomLaunchPolicy(recordedRoom?.room_policy ?? task.execution_plan?.room_policy);
|
|
651
|
+
if (task.room_id && input.anonymous !== undefined && input.anonymous !== recordedPolicy.anonymous)
|
|
652
|
+
throw new TaskRoomApplicationError('template_mismatch', 'anonymous override does not match the existing Room launch policy', { room: task.room_id });
|
|
418
653
|
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
419
654
|
throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: input.taskId, state: task.state });
|
|
655
|
+
const durableTemplate = task.room_id
|
|
656
|
+
? getRoomRecord(task.room_id)?.template_snapshot ?? task.execution_plan?.snapshot
|
|
657
|
+
: task.execution_plan?.snapshot;
|
|
658
|
+
const templateRequestMatches = !input.template || (durableTemplate !== undefined
|
|
659
|
+
&& (input.template === durableTemplate.name
|
|
660
|
+
|| input.template === `${durableTemplate.name}@${durableTemplate.version}`));
|
|
661
|
+
const memberRequestMatches = input.members === undefined || (task.execution_plan !== undefined
|
|
662
|
+
&& task.execution_plan.overrides_hash === hashMemberOverrides(input.members));
|
|
663
|
+
const durableRequestMatches = durableTemplate !== undefined
|
|
664
|
+
&& templateRequestMatches && memberRequestMatches;
|
|
420
665
|
if (task.state === 'active' && task.room_id) {
|
|
421
|
-
if (input.template) {
|
|
422
|
-
const
|
|
666
|
+
if (input.template || input.members) {
|
|
667
|
+
const pinned = durableTemplate;
|
|
668
|
+
if (!pinned)
|
|
669
|
+
throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
|
|
670
|
+
if (durableRequestMatches)
|
|
671
|
+
return { task, status: 'already_active' };
|
|
672
|
+
const templateName = input.template ?? pinned.name;
|
|
673
|
+
const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
423
674
|
if (!definition)
|
|
424
|
-
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template:
|
|
425
|
-
const requested =
|
|
426
|
-
const
|
|
427
|
-
if (pinned
|
|
675
|
+
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
676
|
+
const requested = prepareExecutionPlan(definition, cfg, input.members ?? {});
|
|
677
|
+
const pinnedPlanHash = task.execution_plan?.plan_hash ?? pinned.content_hash;
|
|
678
|
+
if (pinned.name !== requested.snapshot.name || pinnedPlanHash !== requested.planHash)
|
|
428
679
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
429
|
-
requested: `${requested.name}@${requested.version}`, room: task.room_id,
|
|
680
|
+
requested: `${requested.snapshot.name}@${requested.snapshot.version}`, room: task.room_id,
|
|
430
681
|
provisioned: `${pinned.name}@${pinned.version}`
|
|
431
682
|
});
|
|
432
683
|
}
|
|
433
684
|
return { task, status: 'already_active' };
|
|
434
685
|
}
|
|
435
|
-
|
|
436
|
-
const durable = room?.template_snapshot;
|
|
686
|
+
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
687
|
+
const durable = room?.template_snapshot ?? task.execution_plan?.snapshot;
|
|
437
688
|
if (durable && (!task.template || task.template.name !== durable.name
|
|
438
689
|
|| task.template.version !== durable.version || task.template.content_hash !== durable.content_hash))
|
|
439
|
-
throw new Error(`task ${task.task_id} template reference does not match
|
|
690
|
+
throw new Error(`task ${task.task_id} template reference does not match its durable execution snapshot`);
|
|
440
691
|
const templateName = input.template ?? durable?.name ?? task.template?.name
|
|
441
692
|
?? cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'single';
|
|
442
|
-
const
|
|
693
|
+
const storedOverridesMatch = !!(durable && memberRequestMatches);
|
|
694
|
+
const definition = durable && durableRequestMatches ? undefined
|
|
443
695
|
: resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
444
|
-
if (input.template && !definition)
|
|
696
|
+
if (input.template && !definition && !durableRequestMatches)
|
|
445
697
|
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
446
698
|
if (!durable && !definition)
|
|
447
699
|
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
448
|
-
|
|
700
|
+
let launchDefinitions;
|
|
701
|
+
let preparedPlan = !durable && definition
|
|
702
|
+
? prepareExecutionPlan(definition, cfg, input.members ?? {}) : undefined;
|
|
703
|
+
let snapshot = durable ?? preparedPlan.snapshot;
|
|
704
|
+
const roomPolicy = input.anonymous !== undefined
|
|
705
|
+
? resolveRoomLaunchPolicy(snapshot, input.anonymous)
|
|
706
|
+
: task.execution_plan
|
|
707
|
+
? storedRoomLaunchPolicy(task.execution_plan.room_policy)
|
|
708
|
+
: resolveRoomLaunchPolicy(snapshot, undefined);
|
|
709
|
+
if (preparedPlan)
|
|
710
|
+
launchDefinitions = preparedPlan.launchDefinitions;
|
|
711
|
+
if (input.members && Object.keys(input.members).length && !storedOverridesMatch) {
|
|
712
|
+
if (!definition)
|
|
713
|
+
throw new TaskRoomApplicationError('template_mismatch', 'member overrides cannot change an existing Room execution plan');
|
|
714
|
+
const prepared = preparedPlan ?? prepareExecutionPlan(definition, cfg, input.members);
|
|
715
|
+
preparedPlan = prepared;
|
|
716
|
+
if (durable && prepared.planHash !== durable.content_hash)
|
|
717
|
+
throw new TaskRoomApplicationError('template_mismatch', 'member overrides do not match existing execution plan');
|
|
718
|
+
snapshot = prepared.snapshot;
|
|
719
|
+
launchDefinitions = prepared.launchDefinitions;
|
|
720
|
+
}
|
|
449
721
|
if (durable && input.template && definition) {
|
|
450
|
-
const requested = snapshotTemplate(definition);
|
|
722
|
+
const requested = snapshotTemplate(definition, cfg.agentTemplates);
|
|
451
723
|
if (requested.name !== durable.name || requested.content_hash !== durable.content_hash)
|
|
452
724
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
453
725
|
requested: `${requested.name}@${requested.version}`, room: room.room_id,
|
|
454
726
|
provisioned: `${durable.name}@${durable.version}`
|
|
455
727
|
});
|
|
456
728
|
}
|
|
457
|
-
if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash)
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
729
|
+
if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash) {
|
|
730
|
+
// Pre-execution-plan backlog records used the template-only hash. Accept
|
|
731
|
+
// that exact legacy pin once, then upgrade it to the complete sealed plan.
|
|
732
|
+
const legacyHash = definition
|
|
733
|
+
? snapshotTemplate(definition, cfg.agentTemplates).content_hash : undefined;
|
|
734
|
+
if (task.template.content_hash !== legacyHash)
|
|
735
|
+
throw new TaskRoomApplicationError('task_template_drift', 'task template drift', {
|
|
736
|
+
template: `${task.template.name}@${task.template.version}`
|
|
737
|
+
});
|
|
738
|
+
}
|
|
461
739
|
if (task.room_id && room?.template_snapshot
|
|
462
740
|
&& (room.template_snapshot.name !== snapshot.name || room.template_snapshot.content_hash !== snapshot.content_hash))
|
|
463
741
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
464
742
|
requested: `${snapshot.name}@${snapshot.version}`, room: task.room_id,
|
|
465
743
|
provisioned: `${room.template_snapshot.name}@${room.template_snapshot.version}`
|
|
466
744
|
});
|
|
467
|
-
if (
|
|
745
|
+
if (preparedPlan && !task.room_id) {
|
|
746
|
+
const unlock = acquireLaunchSnapshotLock();
|
|
747
|
+
let sealed;
|
|
748
|
+
try {
|
|
749
|
+
sealed = sealTemplateSnapshot(snapshot, cfg.agentTemplates ?? {}, launchDefinitions);
|
|
750
|
+
task = updateTaskExecutionPlan(task.task_id, { schema_version: 1, snapshot: sealed,
|
|
751
|
+
room_policy: roomPolicy,
|
|
752
|
+
overrides: preparedPlan.overrides, overrides_hash: preparedPlan.overridesHash,
|
|
753
|
+
plan_hash: preparedPlan.planHash });
|
|
754
|
+
snapshot = sealed;
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
unlock();
|
|
758
|
+
if (sealed?.launch_snapshot_hash)
|
|
759
|
+
releaseLaunchSnapshot(sealed.launch_snapshot_hash);
|
|
760
|
+
throw error;
|
|
761
|
+
}
|
|
762
|
+
unlock();
|
|
763
|
+
}
|
|
764
|
+
else if (!task.room_id && task.execution_plan && input.anonymous !== undefined) {
|
|
765
|
+
task = updateTaskExecutionPlan(task.task_id, { ...task.execution_plan, room_policy: roomPolicy });
|
|
766
|
+
}
|
|
767
|
+
else if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
|
|
468
768
|
task = updateTaskTemplate(task.task_id, { name: snapshot.name, version: snapshot.version, content_hash: snapshot.content_hash });
|
|
469
|
-
if (task.state === 'backlog')
|
|
769
|
+
if (task.state === 'backlog') {
|
|
470
770
|
task = transitionTask(task.task_id);
|
|
771
|
+
}
|
|
471
772
|
if (!task.room_id) {
|
|
472
773
|
try {
|
|
473
774
|
await this.provisionRoom(cfg, task, snapshot, created => {
|
|
474
775
|
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
475
|
-
});
|
|
776
|
+
}, launchDefinitions, roomPolicy);
|
|
476
777
|
task = readTask(task.task_id);
|
|
477
778
|
}
|
|
478
779
|
catch (error) {
|
|
479
780
|
if (error instanceof CoworkUnavailableError)
|
|
480
781
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
481
|
-
|
|
782
|
+
const current = readTask(task.task_id);
|
|
783
|
+
if (!current.room_id || !getRoomRecord(current.room_id))
|
|
784
|
+
throw error;
|
|
785
|
+
task = current;
|
|
482
786
|
}
|
|
483
787
|
}
|
|
484
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
|
+
}
|
|
485
804
|
if (!room || !['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(room.saga.phase))
|
|
486
805
|
throw new TaskRoomApplicationError('task_non_resumable', 'task non-resumable', { task: task.task_id, room: task.room_id });
|
|
487
806
|
try {
|
|
@@ -496,10 +815,11 @@ export class TaskRoomApplicationService {
|
|
|
496
815
|
catch (error) {
|
|
497
816
|
if (error instanceof CoworkUnavailableError)
|
|
498
817
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
499
|
-
|
|
818
|
+
task = readTask(task.task_id);
|
|
500
819
|
}
|
|
501
820
|
}
|
|
502
|
-
|
|
821
|
+
const resultingRoom = getRoomRecord(task.room_id);
|
|
822
|
+
return { task, status: task.state === 'active' && resultingRoom?.state === 'active' ? 'ready' : 'in_progress' };
|
|
503
823
|
}
|
|
504
824
|
createTemplate(cfg, requested, noRoom) {
|
|
505
825
|
if (noRoom)
|
|
@@ -512,17 +832,17 @@ export class TaskRoomApplicationService {
|
|
|
512
832
|
throw new TaskRoomApplicationError('template_not_found', `template not found: ${requested}`);
|
|
513
833
|
return undefined;
|
|
514
834
|
}
|
|
515
|
-
return snapshotTemplate(definition);
|
|
835
|
+
return snapshotTemplate(definition, cfg.agentTemplates);
|
|
516
836
|
}
|
|
517
837
|
existingTemplate(cfg, task) {
|
|
518
838
|
const ref = task.template;
|
|
519
839
|
const definition = resolveTemplate(ref.name, cfg.roomTemplates ?? {});
|
|
520
|
-
const snapshot = definition ? snapshotTemplate(definition) : undefined;
|
|
840
|
+
const snapshot = definition ? snapshotTemplate(definition, cfg.agentTemplates) : undefined;
|
|
521
841
|
if (!snapshot || snapshot.content_hash !== ref.content_hash)
|
|
522
842
|
throw new TaskRoomApplicationError('task_template_drift', `task template snapshot no longer matches ${ref.name}@${ref.version}`);
|
|
523
843
|
return snapshot;
|
|
524
844
|
}
|
|
525
|
-
async provisionRoom(cfg, task, template, onCreated) {
|
|
845
|
+
async provisionRoom(cfg, task, template, onCreated, launchDefinitions, policy = resolveRoomLaunchPolicy(template, undefined)) {
|
|
526
846
|
const rooms = cfg.rooms;
|
|
527
847
|
if (!rooms)
|
|
528
848
|
throw new ConfigError('rooms: configuration is required');
|
|
@@ -534,21 +854,61 @@ export class TaskRoomApplicationService {
|
|
|
534
854
|
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
535
855
|
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
536
856
|
})();
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
857
|
+
const roomName = task.task_id ? deriveTaskRoomName(task.title, task.task_id) : task.title;
|
|
858
|
+
const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
|
|
859
|
+
let launchTemplate;
|
|
860
|
+
let room;
|
|
861
|
+
try {
|
|
862
|
+
launchTemplate = template ? (template.launch_snapshot_hash ? template
|
|
863
|
+
: sealTemplateSnapshot(template, cfg.agentTemplates ?? {}, launchDefinitions)) : undefined;
|
|
864
|
+
// Room publication window: for a task-bound room, hold the common
|
|
865
|
+
// task-operation lock across remote creation, local record creation, and
|
|
866
|
+
// the task link, so an accepted deletion linearizes strictly before
|
|
867
|
+
// (bounded task_deleting abort) or strictly after (record visible to the
|
|
868
|
+
// deletion scan). Lock order stays launch-snapshot → task-operation.
|
|
869
|
+
const publishRoom = async () => {
|
|
870
|
+
if (task.task_id) {
|
|
871
|
+
const fresh = readTask(task.task_id);
|
|
872
|
+
if (fresh.deletion?.status === 'pending')
|
|
873
|
+
throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
|
|
874
|
+
}
|
|
875
|
+
const created = await cowork.createRoom({
|
|
876
|
+
room_name: roomName, goal: task.goal?.trim() || task.title,
|
|
877
|
+
briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
|
|
878
|
+
quiet_membership: launchTemplate?.room?.quiet_membership,
|
|
879
|
+
anonymous: policy.anonymous,
|
|
880
|
+
});
|
|
881
|
+
const record = createRoomRecord({
|
|
882
|
+
room_id: created.room_id, room_name: roomName, room_identity_cid: created.identity_cid,
|
|
883
|
+
task_id: task.task_id, template_snapshot: launchTemplate, room_policy: policy,
|
|
884
|
+
});
|
|
885
|
+
onCreated(record);
|
|
886
|
+
return record;
|
|
887
|
+
};
|
|
888
|
+
room = task.task_id
|
|
889
|
+
? await withFileLock(taskOperationLockPath(task.task_id), publishRoom, {}, TASK_OPERATION_LOCK_STALE_MS)
|
|
890
|
+
: await publishRoom();
|
|
891
|
+
}
|
|
892
|
+
catch (error) {
|
|
893
|
+
unlockSnapshot?.();
|
|
894
|
+
if (launchTemplate?.launch_snapshot_hash)
|
|
895
|
+
releaseLaunchSnapshot(launchTemplate.launch_snapshot_hash);
|
|
896
|
+
throw error;
|
|
897
|
+
}
|
|
898
|
+
unlockSnapshot?.();
|
|
548
899
|
room = advanceSaga(room.room_id, 'create_room', 1);
|
|
549
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
|
+
}
|
|
550
911
|
try {
|
|
551
|
-
room = advanceSaga(room.room_id, 'attach_owner', 2);
|
|
552
912
|
const accepted = await cowork.acceptInvite(room.room_id, cfg.ownerInvite, {
|
|
553
913
|
role: rooms.owner.role, expected_cid: rooms.owner.expected_cid,
|
|
554
914
|
});
|
|
@@ -557,19 +917,65 @@ export class TaskRoomApplicationService {
|
|
|
557
917
|
catch (error) {
|
|
558
918
|
const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
|
|
559
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.'
|
|
560
|
-
: '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 });
|
|
561
925
|
throw error;
|
|
562
926
|
}
|
|
563
927
|
}
|
|
564
928
|
room = advanceSaga(room.room_id, 'create_members', 3);
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
929
|
+
if (launchTemplate && (launchTemplate.members.length > 0 || attachOwner))
|
|
930
|
+
try {
|
|
931
|
+
room = await (this.deps.provisionMembers ?? provisionMembers)({
|
|
932
|
+
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
|
|
933
|
+
binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
|
|
934
|
+
goal: task.task_id ? task.title : task.goal,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
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 });
|
|
942
|
+
throw error;
|
|
943
|
+
}
|
|
944
|
+
else if (attachOwner) {
|
|
945
|
+
const now = this.deps.now ?? Date.now;
|
|
946
|
+
const sleep = this.deps.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
947
|
+
const deadline = now() + 60_000;
|
|
948
|
+
for (;;) {
|
|
949
|
+
const seats = await cowork.getSeats(room.room_id);
|
|
950
|
+
if (room.owner_seat_cid && seats.some(seat => seat.identity_cid.toLowerCase() === room.owner_seat_cid.toLowerCase()
|
|
951
|
+
&& seat.seat_state === 'active')) {
|
|
952
|
+
room = activateRoom(room.room_id);
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
if (now() >= deadline) {
|
|
956
|
+
room = advanceSaga(room.room_id, 'wait_seats', 5, 'waiting_seats');
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
await sleep(250);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
else
|
|
963
|
+
room = activateRoom(room.room_id);
|
|
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,
|
|
969
|
+
template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
970
|
+
anonymous: policy.anonymous, memberCount: room.member_seats.length, ownerAttached: attachOwner,
|
|
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,
|
|
570
977
|
});
|
|
571
|
-
|
|
572
|
-
if (task.task_id)
|
|
978
|
+
if (task.task_id && !launchTemplate?.members.length)
|
|
573
979
|
activateTask(task.task_id);
|
|
574
980
|
return room;
|
|
575
981
|
}
|