@ours.network/fleet 1.1.0-nightly.9 → 1.1.1
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 +285 -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 +8 -1
- 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 +562 -289
- package/dist/briefing.js +79 -22
- 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 +129 -47
- package/dist/config.d.ts +42 -3
- package/dist/config.js +202 -29
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +195 -41
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +40 -4
- package/dist/fleet-command-audit.d.ts +22 -1
- package/dist/fleet-command-audit.js +355 -50
- package/dist/fleet-proxy.d.ts +5 -2
- package/dist/harness/claude-code-session.js +5 -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 +21 -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 +19 -3
- package/dist/owner-channel/channel.js +301 -120
- 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/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 +361 -285
- 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 -83
- 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 +97 -47
- package/dist/session/acp.d.ts +27 -1
- package/dist/session/acp.js +307 -17
- 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 +1 -1
- package/dist/session/control.js +17 -1
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/stall-watchdog.d.ts +60 -0
- package/dist/session/stall-watchdog.js +210 -0
- package/dist/session/types.d.ts +29 -8
- 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
|
@@ -1,16 +1,53 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
2
|
import { ConfigError, loadConfig } from '../config.js';
|
|
4
3
|
import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
|
|
5
4
|
import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
|
|
6
5
|
import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
|
|
7
|
-
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';
|
|
8
7
|
import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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';
|
|
11
12
|
import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
|
|
12
|
-
import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from '../rooms-tasks/terminal.js';
|
|
13
|
-
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
|
+
}
|
|
14
51
|
export class TaskRoomApplicationError extends Error {
|
|
15
52
|
code;
|
|
16
53
|
fields;
|
|
@@ -21,102 +58,95 @@ export class TaskRoomApplicationError extends Error {
|
|
|
21
58
|
this.name = 'TaskRoomApplicationError';
|
|
22
59
|
}
|
|
23
60
|
}
|
|
24
|
-
function
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
return undefined;
|
|
28
|
-
const value = selected;
|
|
29
|
-
if (value.kind === 'ref' && typeof value.id === 'string')
|
|
30
|
-
return `ref:${value.id} (reference)`;
|
|
31
|
-
if (value.kind === 'inline' && typeof value.fingerprint === 'string')
|
|
32
|
-
return `inline:${value.fingerprint} (inline)`;
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
function launchPermissions(definition) {
|
|
36
|
-
const selected = definition?.permissions;
|
|
37
|
-
if (!selected || typeof selected !== 'object' || Array.isArray(selected))
|
|
38
|
-
return undefined;
|
|
39
|
-
const value = selected;
|
|
40
|
-
const fields = ['approval', 'filesystem', 'unattended'].flatMap(key => typeof value[key] === 'string' ? [`${key}=${value[key]}`] : []);
|
|
41
|
-
return fields.length ? fields.join(',') : undefined;
|
|
42
|
-
}
|
|
43
|
-
function roomParticipants(room) {
|
|
44
|
-
return [...room.member_seats].sort((a, b) => a.role_name.localeCompare(b.role_name)).map(seat => ({
|
|
45
|
-
name: seat.role_name, id: seat.identity_cid,
|
|
46
|
-
brain: launchSelection(seat.launch?.agent_definition, 'brain'),
|
|
47
|
-
role: launchSelection(seat.launch?.agent_definition, 'role') ?? seat.cowork_role,
|
|
48
|
-
permissions: launchPermissions(seat.launch?.agent_definition),
|
|
49
|
-
}));
|
|
50
|
-
}
|
|
51
|
-
function taskAgents(task) {
|
|
52
|
-
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
53
|
-
const seats = new Map(room?.member_seats.map(seat => [seat.role_name, seat]) ?? []);
|
|
54
|
-
return [...task.member_roles].sort((a, b) => a.name.localeCompare(b.name)).map(member => {
|
|
55
|
-
const seat = seats.get(member.name);
|
|
56
|
-
return { name: member.name, brain: launchSelection(seat?.launch?.agent_definition, 'brain'),
|
|
57
|
-
role: launchSelection(seat?.launch?.agent_definition, 'role') ?? member.cowork_role,
|
|
58
|
-
permissions: launchPermissions(seat?.launch?.agent_definition) };
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
function roomFailureEventId(room) {
|
|
62
|
-
const evidence = `${room.created_at}\0${room.saga.phase}\0${room.saga.step_index}\0${room.saga.error ?? ''}`;
|
|
63
|
-
return `sha256:${createHash('sha256').update(evidence).digest('hex')}`;
|
|
61
|
+
export function resolveRoomLaunchPolicy(template, override) {
|
|
62
|
+
const anonymous = override ?? template?.room?.anonymous ?? false;
|
|
63
|
+
return { anonymous };
|
|
64
64
|
}
|
|
65
65
|
/** Exact extraction of the previously CLI-owned task create/start behavior. */
|
|
66
66
|
export class TaskRoomApplicationService {
|
|
67
67
|
configurationPath;
|
|
68
68
|
deps;
|
|
69
|
-
recovery;
|
|
70
69
|
constructor(configurationPath, deps = {}) {
|
|
71
70
|
this.configurationPath = configurationPath;
|
|
72
71
|
this.deps = deps;
|
|
73
72
|
}
|
|
74
73
|
async createTask(request) {
|
|
75
74
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
87
|
const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
});
|
|
87
109
|
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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: [] });
|
|
94
128
|
if (!request.backlog && !request.noRoom && ref && !task.room_id) {
|
|
95
129
|
try {
|
|
96
130
|
const room = await this.provisionRoom(cfg, task, template, created => {
|
|
97
131
|
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
98
|
-
});
|
|
132
|
+
}, launchDefinitions, roomPolicy);
|
|
99
133
|
task = readTask(task.task_id);
|
|
100
|
-
|
|
101
|
-
recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
|
|
102
|
-
title: task.title, previousState: 'provisioning', newState: task.state,
|
|
103
|
-
revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
|
|
104
|
-
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
105
|
-
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
106
|
-
agents: taskAgents(task) });
|
|
107
|
-
else
|
|
108
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: task.task_id,
|
|
109
|
-
state: task.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
|
|
134
|
+
void room;
|
|
110
135
|
}
|
|
111
136
|
catch (error) {
|
|
112
137
|
if (error instanceof CoworkUnavailableError)
|
|
113
138
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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;
|
|
118
146
|
}
|
|
119
147
|
}
|
|
148
|
+
if (!request.backlog && !request.noRoom && task.room_id)
|
|
149
|
+
recordTaskProvisioningOutcome(this.taskProvisioningOutcome(task.task_id));
|
|
120
150
|
return task;
|
|
121
151
|
}
|
|
122
152
|
async createRoom(request) {
|
|
@@ -127,51 +157,28 @@ export class TaskRoomApplicationService {
|
|
|
127
157
|
const definition = resolveTemplate(request.template, cfg.roomTemplates ?? {});
|
|
128
158
|
if (!definition)
|
|
129
159
|
throw new TaskRoomApplicationError('template_not_found', `template not found: ${request.template}`, { template: request.template });
|
|
130
|
-
|
|
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);
|
|
131
167
|
}
|
|
168
|
+
const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
|
|
132
169
|
return this.provisionRoom(cfg, {
|
|
133
170
|
title: request.name, brief, goal: request.goal,
|
|
134
|
-
}, template, () => { });
|
|
171
|
+
}, template, () => { }, undefined, roomPolicy);
|
|
135
172
|
}
|
|
136
173
|
async startTask(input) {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
revision: task.started_at ?? task.created_at, list: task.list_name ?? 'default',
|
|
142
|
-
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
143
|
-
agents: [] });
|
|
144
|
-
if (task.template && !task.room_id) {
|
|
145
|
-
const template = this.existingTemplate(cfg, task);
|
|
146
|
-
try {
|
|
147
|
-
const room = await this.provisionRoom(cfg, task, template, created => {
|
|
148
|
-
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
149
|
-
});
|
|
150
|
-
task = readTask(task.task_id);
|
|
151
|
-
if (task.state === 'active' && room.state === 'active')
|
|
152
|
-
recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
|
|
153
|
-
title: task.title, previousState: 'provisioning', newState: task.state,
|
|
154
|
-
revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
|
|
155
|
-
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
156
|
-
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
157
|
-
agents: taskAgents(task) });
|
|
158
|
-
else
|
|
159
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: task.task_id,
|
|
160
|
-
state: task.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
const failed = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
164
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: task.task_id,
|
|
165
|
-
state: readTask(task.task_id).state, category: 'provision_failed',
|
|
166
|
-
eventId: failed ? roomFailureEventId(failed) : task.created_at });
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
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;
|
|
171
178
|
}
|
|
172
179
|
listTasks(filter) {
|
|
173
180
|
const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
|
|
174
|
-
return readTasks({ state: filter?.state, listId });
|
|
181
|
+
return readTasks({ state: filter?.state, listId, includeDeleting: filter?.includeDeleting });
|
|
175
182
|
}
|
|
176
183
|
listTaskLists() { return readTaskLists(); }
|
|
177
184
|
groupedTasks(filter) {
|
|
@@ -215,6 +222,55 @@ export class TaskRoomApplicationService {
|
|
|
215
222
|
const task = readTask(taskId);
|
|
216
223
|
return { task, orchestration: task.room_id ? getRoomRecord(task.room_id) : undefined };
|
|
217
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
|
+
}
|
|
218
274
|
blockTask(input) {
|
|
219
275
|
return persistBlockTask(input.taskId, input.reason);
|
|
220
276
|
}
|
|
@@ -224,8 +280,93 @@ export class TaskRoomApplicationService {
|
|
|
224
280
|
reviewTask(input) {
|
|
225
281
|
return persistReviewTask(input.taskId);
|
|
226
282
|
}
|
|
227
|
-
|
|
228
|
-
|
|
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);
|
|
229
370
|
}
|
|
230
371
|
async completeTask(input) {
|
|
231
372
|
const task = readTask(input.taskId);
|
|
@@ -263,33 +404,63 @@ export class TaskRoomApplicationService {
|
|
|
263
404
|
recordSettlementError(input) {
|
|
264
405
|
return recordTaskTerminalIntentError(input.taskId, input.error, input.recoveryHint);
|
|
265
406
|
}
|
|
266
|
-
async
|
|
267
|
-
const
|
|
268
|
-
this.recovery = { taskId: input.taskId, config };
|
|
269
|
-
const task = readTask(input.taskId);
|
|
270
|
-
if (task.terminal_intent?.status === 'pending')
|
|
271
|
-
return { kind: 'terminal_worker_required', taskId: input.taskId };
|
|
272
|
-
return { kind: 'final', result: await this.continueTaskRecovery({
|
|
273
|
-
actor: input.actor, taskId: input.taskId, terminalTimedOut: false,
|
|
274
|
-
}) };
|
|
275
|
-
}
|
|
276
|
-
async continueTaskRecovery(input) {
|
|
277
|
-
if (!this.recovery || this.recovery.taskId !== input.taskId)
|
|
278
|
-
throw new Error('task recovery continuation requires a matching begin');
|
|
279
|
-
const recovery = this.recovery;
|
|
280
|
-
this.recovery = undefined;
|
|
281
|
-
const cfg = recovery.config;
|
|
407
|
+
async continueTaskProvisioning(input) {
|
|
408
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
282
409
|
let task = readTask(input.taskId);
|
|
283
410
|
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
284
|
-
const issues =
|
|
411
|
+
const issues = [];
|
|
285
412
|
if (task.state !== 'provisioning')
|
|
286
413
|
return {
|
|
287
|
-
kind:
|
|
414
|
+
kind: 'no_op', task, room, issues,
|
|
288
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
|
+
}
|
|
289
456
|
if (!room)
|
|
290
|
-
return {
|
|
457
|
+
return {
|
|
458
|
+
kind: 'provisioning_non_resumable', task, room, issues, reason: 'missing_room',
|
|
459
|
+
};
|
|
291
460
|
if (room.provisioning_detail === 'waiting_cowork')
|
|
292
461
|
issues.push({ code: 'waiting_cowork' });
|
|
462
|
+
if (room.provisioning_detail === 'waiting_owner_authorization')
|
|
463
|
+
issues.push({ code: 'waiting_owner_authorization' });
|
|
293
464
|
if (room.provisioning_detail === 'waiting_owner_invite')
|
|
294
465
|
issues.push({ code: 'waiting_owner_invite' });
|
|
295
466
|
if (room.provisioning_detail === 'owner_cid_mismatch')
|
|
@@ -326,6 +497,50 @@ export class TaskRoomApplicationService {
|
|
|
326
497
|
return { kind: 'provisioning_resume_failed', task, room, issues };
|
|
327
498
|
}
|
|
328
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
|
+
}
|
|
329
544
|
async acceptTerminal(taskId, kind, roomId, outcome) {
|
|
330
545
|
const task = await acceptTaskTerminalIntent({ taskId, kind, roomId, outcome });
|
|
331
546
|
return { task, settlementRequired: task.terminal_intent?.status === 'pending' && !!roomId };
|
|
@@ -345,11 +560,9 @@ export class TaskRoomApplicationService {
|
|
|
345
560
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
346
561
|
return listTemplates(cfg.roomTemplates ?? {}).flatMap(template => {
|
|
347
562
|
const issues = template.members.flatMap(member => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return cfg.roles.some(role => role.name === ref)
|
|
352
|
-
? [] : [`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`];
|
|
353
566
|
});
|
|
354
567
|
return issues.length ? [{ template: `${template.name}@${template.version}`, issues }] : [];
|
|
355
568
|
});
|
|
@@ -420,73 +633,6 @@ export class TaskRoomApplicationService {
|
|
|
420
633
|
recordRoomSettlementError(input) {
|
|
421
634
|
return recordManagedRoomCloseError(input.roomId, input.error, input.recoveryHint);
|
|
422
635
|
}
|
|
423
|
-
async recoverRoom(input) {
|
|
424
|
-
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
425
|
-
if (!cfg.rooms)
|
|
426
|
-
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
427
|
-
const adapter = this.deps.cowork ? this.deps.cowork(cfg)
|
|
428
|
-
: createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
429
|
-
let orchestration = getRoomRecord(input.roomId);
|
|
430
|
-
if (orchestration?.state === 'closing' || orchestration?.state === 'closed')
|
|
431
|
-
return { kind: 'deletion_worker_required', roomId: input.roomId };
|
|
432
|
-
const room = await adapter.recoverRoom(input.roomId);
|
|
433
|
-
orchestration = getRoomRecord(input.roomId);
|
|
434
|
-
if (orchestration && !orchestration.owner_seat_cid
|
|
435
|
-
&& (orchestration.provisioning_detail === 'waiting_owner_invite'
|
|
436
|
-
|| orchestration.provisioning_detail === 'owner_cid_mismatch')) {
|
|
437
|
-
const expected = cfg.rooms.owner.expected_cid.toLowerCase();
|
|
438
|
-
const existing = (await adapter.getSeats(input.roomId))
|
|
439
|
-
.find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
|
|
440
|
-
if (!existing && !cfg.ownerInvite)
|
|
441
|
-
throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before recovery');
|
|
442
|
-
let acceptedCid = existing?.identity_cid;
|
|
443
|
-
if (!acceptedCid)
|
|
444
|
-
acceptedCid = (await adapter.acceptInvite(input.roomId, cfg.ownerInvite, {
|
|
445
|
-
role: cfg.rooms.owner.role, expected_cid: cfg.rooms.owner.expected_cid,
|
|
446
|
-
})).seat_cid;
|
|
447
|
-
setOwnerSeat(input.roomId, acceptedCid, cfg.ownerInviteFingerprint ?? '');
|
|
448
|
-
orchestration = advanceSaga(input.roomId, 'create_members', 3);
|
|
449
|
-
}
|
|
450
|
-
const issues = [];
|
|
451
|
-
if (orchestration?.saga.error)
|
|
452
|
-
issues.push('A provisioning failure is recorded; inspect role logs for diagnostics.');
|
|
453
|
-
if (orchestration?.saga.recovery_hint)
|
|
454
|
-
issues.push('Recovery guidance is recorded; inspect role logs for diagnostics.');
|
|
455
|
-
if (orchestration?.provisioning_detail === 'waiting_cowork')
|
|
456
|
-
issues.push('Check ours-cowork service status');
|
|
457
|
-
if (orchestration?.provisioning_detail === 'waiting_owner_invite')
|
|
458
|
-
issues.push('Rotate rooms.owner.public_invite in config, then re-run recover');
|
|
459
|
-
if (orchestration?.provisioning_detail === 'waiting_seats')
|
|
460
|
-
issues.push('Inspect temporary member logs for invite acceptance, then re-run recover');
|
|
461
|
-
if (orchestration?.state === 'provisioning'
|
|
462
|
-
&& ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(orchestration.saga.phase)
|
|
463
|
-
&& orchestration.template_snapshot) {
|
|
464
|
-
try {
|
|
465
|
-
let template = orchestration.template_snapshot;
|
|
466
|
-
let brief;
|
|
467
|
-
let goal = orchestration.room_name;
|
|
468
|
-
if (orchestration.task_id) {
|
|
469
|
-
const task = readTask(orchestration.task_id);
|
|
470
|
-
if (!task.template || task.template.name !== template.name || task.template.version !== template.version
|
|
471
|
-
|| task.template.content_hash !== template.content_hash)
|
|
472
|
-
throw new Error(`task ${task.task_id} template reference does not match room ${orchestration.room_id}'s durable snapshot`);
|
|
473
|
-
brief = task.brief;
|
|
474
|
-
goal = task.title;
|
|
475
|
-
}
|
|
476
|
-
await (this.deps.provisionMembers ?? provisionMembers)({ cfg, cowork: adapter,
|
|
477
|
-
roomId: orchestration.room_id, taskId: orchestration.task_id, template,
|
|
478
|
-
binPath: (this.deps.binPath ?? getBinPath)(), brief, goal });
|
|
479
|
-
orchestration = getRoomRecord(input.roomId);
|
|
480
|
-
return { kind: 'provisioning_resumed', room, orchestration,
|
|
481
|
-
issues: ['Provisioning resumed successfully'] };
|
|
482
|
-
}
|
|
483
|
-
catch (error) {
|
|
484
|
-
issues.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
485
|
-
return { kind: 'provisioning_resume_failed', room, orchestration, issues };
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return { kind: 'recovered', room, orchestration, issues };
|
|
489
|
-
}
|
|
490
636
|
async finishTask(input) {
|
|
491
637
|
let task = readTask(input.taskId);
|
|
492
638
|
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
@@ -500,81 +646,161 @@ export class TaskRoomApplicationService {
|
|
|
500
646
|
async ensureTaskWork(input) {
|
|
501
647
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
502
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 });
|
|
503
653
|
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
504
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;
|
|
505
665
|
if (task.state === 'active' && task.room_id) {
|
|
506
|
-
if (input.template) {
|
|
507
|
-
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 ?? {});
|
|
508
674
|
if (!definition)
|
|
509
|
-
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template:
|
|
510
|
-
const requested =
|
|
511
|
-
const
|
|
512
|
-
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)
|
|
513
679
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
514
|
-
requested: `${requested.name}@${requested.version}`, room: task.room_id,
|
|
680
|
+
requested: `${requested.snapshot.name}@${requested.snapshot.version}`, room: task.room_id,
|
|
515
681
|
provisioned: `${pinned.name}@${pinned.version}`
|
|
516
682
|
});
|
|
517
683
|
}
|
|
518
684
|
return { task, status: 'already_active' };
|
|
519
685
|
}
|
|
520
|
-
|
|
521
|
-
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;
|
|
522
688
|
if (durable && (!task.template || task.template.name !== durable.name
|
|
523
689
|
|| task.template.version !== durable.version || task.template.content_hash !== durable.content_hash))
|
|
524
|
-
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`);
|
|
525
691
|
const templateName = input.template ?? durable?.name ?? task.template?.name
|
|
526
692
|
?? cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'single';
|
|
527
|
-
const
|
|
693
|
+
const storedOverridesMatch = !!(durable && memberRequestMatches);
|
|
694
|
+
const definition = durable && durableRequestMatches ? undefined
|
|
528
695
|
: resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
529
|
-
if (input.template && !definition)
|
|
696
|
+
if (input.template && !definition && !durableRequestMatches)
|
|
530
697
|
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
531
698
|
if (!durable && !definition)
|
|
532
699
|
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
533
|
-
|
|
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
|
+
}
|
|
534
721
|
if (durable && input.template && definition) {
|
|
535
|
-
const requested = snapshotTemplate(definition);
|
|
722
|
+
const requested = snapshotTemplate(definition, cfg.agentTemplates);
|
|
536
723
|
if (requested.name !== durable.name || requested.content_hash !== durable.content_hash)
|
|
537
724
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
538
725
|
requested: `${requested.name}@${requested.version}`, room: room.room_id,
|
|
539
726
|
provisioned: `${durable.name}@${durable.version}`
|
|
540
727
|
});
|
|
541
728
|
}
|
|
542
|
-
if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash)
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
+
}
|
|
546
739
|
if (task.room_id && room?.template_snapshot
|
|
547
740
|
&& (room.template_snapshot.name !== snapshot.name || room.template_snapshot.content_hash !== snapshot.content_hash))
|
|
548
741
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
549
742
|
requested: `${snapshot.name}@${snapshot.version}`, room: task.room_id,
|
|
550
743
|
provisioned: `${room.template_snapshot.name}@${room.template_snapshot.version}`
|
|
551
744
|
});
|
|
552
|
-
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)
|
|
553
768
|
task = updateTaskTemplate(task.task_id, { name: snapshot.name, version: snapshot.version, content_hash: snapshot.content_hash });
|
|
554
769
|
if (task.state === 'backlog') {
|
|
555
770
|
task = transitionTask(task.task_id);
|
|
556
|
-
recordFleetAuditPresentation({ kind: 'task', operation: 'start', id: task.task_id,
|
|
557
|
-
title: task.title, previousState: 'backlog', newState: task.state,
|
|
558
|
-
revision: task.started_at ?? task.created_at, list: task.list_name ?? 'default',
|
|
559
|
-
template: `${snapshot.name}@${snapshot.version}`, agents: [] });
|
|
560
771
|
}
|
|
561
772
|
if (!task.room_id) {
|
|
562
773
|
try {
|
|
563
774
|
await this.provisionRoom(cfg, task, snapshot, created => {
|
|
564
775
|
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
565
|
-
});
|
|
776
|
+
}, launchDefinitions, roomPolicy);
|
|
566
777
|
task = readTask(task.task_id);
|
|
567
778
|
}
|
|
568
779
|
catch (error) {
|
|
569
780
|
if (error instanceof CoworkUnavailableError)
|
|
570
781
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
782
|
+
const current = readTask(task.task_id);
|
|
783
|
+
if (!current.room_id || !getRoomRecord(current.room_id))
|
|
784
|
+
throw error;
|
|
785
|
+
task = current;
|
|
575
786
|
}
|
|
576
787
|
}
|
|
577
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
|
+
}
|
|
578
804
|
if (!room || !['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(room.saga.phase))
|
|
579
805
|
throw new TaskRoomApplicationError('task_non_resumable', 'task non-resumable', { task: task.task_id, room: task.room_id });
|
|
580
806
|
try {
|
|
@@ -589,24 +815,11 @@ export class TaskRoomApplicationService {
|
|
|
589
815
|
catch (error) {
|
|
590
816
|
if (error instanceof CoworkUnavailableError)
|
|
591
817
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
592
|
-
|
|
593
|
-
id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
|
|
594
|
-
eventId: roomFailureEventId(getRoomRecord(task.room_id)) });
|
|
595
|
-
throw error;
|
|
818
|
+
task = readTask(task.task_id);
|
|
596
819
|
}
|
|
597
820
|
}
|
|
598
821
|
const resultingRoom = getRoomRecord(task.room_id);
|
|
599
|
-
|
|
600
|
-
recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
|
|
601
|
-
title: task.title, previousState: 'provisioning', newState: task.state,
|
|
602
|
-
revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
|
|
603
|
-
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
604
|
-
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
605
|
-
agents: taskAgents(task) });
|
|
606
|
-
else if (resultingRoom)
|
|
607
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
|
|
608
|
-
id: task.task_id, state: task.state, category: 'provision_pending', eventId: roomFailureEventId(resultingRoom) });
|
|
609
|
-
return { task, status: 'ready' };
|
|
822
|
+
return { task, status: task.state === 'active' && resultingRoom?.state === 'active' ? 'ready' : 'in_progress' };
|
|
610
823
|
}
|
|
611
824
|
createTemplate(cfg, requested, noRoom) {
|
|
612
825
|
if (noRoom)
|
|
@@ -619,17 +832,17 @@ export class TaskRoomApplicationService {
|
|
|
619
832
|
throw new TaskRoomApplicationError('template_not_found', `template not found: ${requested}`);
|
|
620
833
|
return undefined;
|
|
621
834
|
}
|
|
622
|
-
return snapshotTemplate(definition);
|
|
835
|
+
return snapshotTemplate(definition, cfg.agentTemplates);
|
|
623
836
|
}
|
|
624
837
|
existingTemplate(cfg, task) {
|
|
625
838
|
const ref = task.template;
|
|
626
839
|
const definition = resolveTemplate(ref.name, cfg.roomTemplates ?? {});
|
|
627
|
-
const snapshot = definition ? snapshotTemplate(definition) : undefined;
|
|
840
|
+
const snapshot = definition ? snapshotTemplate(definition, cfg.agentTemplates) : undefined;
|
|
628
841
|
if (!snapshot || snapshot.content_hash !== ref.content_hash)
|
|
629
842
|
throw new TaskRoomApplicationError('task_template_drift', `task template snapshot no longer matches ${ref.name}@${ref.version}`);
|
|
630
843
|
return snapshot;
|
|
631
844
|
}
|
|
632
|
-
async provisionRoom(cfg, task, template, onCreated) {
|
|
845
|
+
async provisionRoom(cfg, task, template, onCreated, launchDefinitions, policy = resolveRoomLaunchPolicy(template, undefined)) {
|
|
633
846
|
const rooms = cfg.rooms;
|
|
634
847
|
if (!rooms)
|
|
635
848
|
throw new ConfigError('rooms: configuration is required');
|
|
@@ -641,26 +854,61 @@ export class TaskRoomApplicationService {
|
|
|
641
854
|
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
642
855
|
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
643
856
|
})();
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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?.();
|
|
660
899
|
room = advanceSaga(room.room_id, 'create_room', 1);
|
|
661
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
|
+
}
|
|
662
911
|
try {
|
|
663
|
-
room = advanceSaga(room.room_id, 'attach_owner', 2);
|
|
664
912
|
const accepted = await cowork.acceptInvite(room.room_id, cfg.ownerInvite, {
|
|
665
913
|
role: rooms.owner.role, expected_cid: rooms.owner.expected_cid,
|
|
666
914
|
});
|
|
@@ -669,40 +917,65 @@ export class TaskRoomApplicationService {
|
|
|
669
917
|
catch (error) {
|
|
670
918
|
const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
|
|
671
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.'
|
|
672
|
-
: 'Rotate rooms.owner.public_invite or public_invite_file, then
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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 });
|
|
676
925
|
throw error;
|
|
677
926
|
}
|
|
678
927
|
}
|
|
679
928
|
room = advanceSaga(room.room_id, 'create_members', 3);
|
|
680
|
-
if (
|
|
929
|
+
if (launchTemplate && (launchTemplate.members.length > 0 || attachOwner))
|
|
681
930
|
try {
|
|
682
931
|
room = await (this.deps.provisionMembers ?? provisionMembers)({
|
|
683
|
-
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template,
|
|
932
|
+
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
|
|
684
933
|
binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
|
|
685
934
|
goal: task.task_id ? task.title : task.goal,
|
|
686
935
|
});
|
|
687
936
|
}
|
|
688
937
|
catch (error) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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 });
|
|
692
942
|
throw error;
|
|
693
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
|
+
}
|
|
694
962
|
else
|
|
695
963
|
room = activateRoom(room.room_id);
|
|
696
|
-
if (room.state === 'active')
|
|
697
|
-
recordFleetAuditPresentation({
|
|
698
|
-
|
|
699
|
-
|
|
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,
|
|
700
969
|
template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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,
|
|
977
|
+
});
|
|
978
|
+
if (task.task_id && !launchTemplate?.members.length)
|
|
706
979
|
activateTask(task.task_id);
|
|
707
980
|
return room;
|
|
708
981
|
}
|