@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.20
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 +169 -54
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +20 -5
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +6 -25
- package/dist/application/role-creation-service.js +34 -72
- 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 +58 -6
- package/dist/application/task-room-service.js +426 -78
- package/dist/briefing.js +74 -24
- package/dist/build-info.json +4 -4
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +227 -73
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +67 -2
- package/dist/config.js +260 -30
- package/dist/daemon-recovery.d.ts +93 -0
- package/dist/daemon-recovery.js +328 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +171 -66
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +54 -4
- package/dist/fleet-command-audit.d.ts +165 -0
- package/dist/fleet-command-audit.js +543 -0
- package/dist/fleet-proxy.d.ts +7 -2
- package/dist/fleet-proxy.js +30 -8
- package/dist/harness/agent-session.d.ts +5 -0
- package/dist/harness/claude-code-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +6 -1
- package/dist/harness/claude-code.js +2 -0
- 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 +11 -2
- package/dist/harness/codex-session.js +22 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +110 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +4 -3
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +629 -0
- package/dist/lifecycle-summary.d.ts +118 -0
- package/dist/lifecycle-summary.js +161 -0
- package/dist/loops/config.js +3 -2
- package/dist/model-env.d.ts +2 -2
- package/dist/model-env.js +1 -1
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +58 -0
- package/dist/owner-channel/channel.js +321 -42
- package/dist/owner-channel/commands.d.ts +6 -1
- package/dist/owner-channel/commands.js +34 -19
- 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-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -0
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +414 -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 +4 -0
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +503 -75
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +20 -4
- package/dist/rooms-tasks/config.js +16 -14
- package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
- package/dist/rooms-tasks/cowork-adapter.js +6 -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 +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +176 -0
- package/dist/rooms-tasks/member-overrides.d.ts +27 -0
- package/dist/rooms-tasks/member-overrides.js +170 -0
- package/dist/rooms-tasks/provision.js +179 -49
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- 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 -2
- package/dist/rooms-tasks/templates.js +42 -78
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +7 -3
- package/dist/rooms-tasks/types.d.ts +94 -17
- package/dist/rooms-tasks/types.js +14 -4
- package/dist/runner.d.ts +9 -2
- package/dist/runner.js +107 -40
- package/dist/session/acp.d.ts +9 -1
- package/dist/session/acp.js +35 -9
- 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 +14 -1
- package/dist/session/control.js +38 -0
- 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 +6 -31
- package/dist/spawn.js +57 -234
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/watchdog/config.d.ts +10 -7
- package/dist/watchdog/config.js +29 -19
- package/dist/watchdog/run.js +4 -13
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +27 -8
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web/topology-promote.js +8 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
- package/dist/web-app/index.html +1 -1
- package/package.json +3 -2
- package/presets/brain-catalog.json +16 -0
- package/presets/fleet/agent_templates/Critic.yaml +4 -0
- package/presets/fleet/agent_templates/Developer.yaml +4 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
- package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- 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/roles/Coordinator.yaml +30 -0
- package/presets/fleet/roles/Critic.yaml +19 -0
- package/presets/fleet/roles/Developer.yaml +19 -0
- package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
- package/presets/fleet/room_templates/pair.yaml +10 -0
- package/presets/fleet/room_templates/single.yaml +7 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +6 -0
- package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { ConfigError, loadConfig } from '../config.js';
|
|
3
4
|
import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
|
|
4
5
|
import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
|
|
5
6
|
import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
|
|
6
|
-
import { activateTask, blockTask as persistBlockTask, createTask as persistTask,
|
|
7
|
+
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
8
|
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';
|
|
9
|
+
import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
|
|
10
|
+
import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
|
|
11
|
+
import { hashMemberOverrides, prepareExecutionPlan } from '../rooms-tasks/member-overrides.js';
|
|
12
|
+
import { recordFleetAuditPresentation } from '../fleet-command-audit.js';
|
|
9
13
|
import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
|
|
10
|
-
import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from '../rooms-tasks/terminal.js';
|
|
11
|
-
import {
|
|
14
|
+
import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath, } from '../rooms-tasks/terminal.js';
|
|
15
|
+
import { acceptTaskDeletion, recordTaskDeletionError, settleTaskDeletion, } from '../rooms-tasks/deletion.js';
|
|
16
|
+
import { withFileLock } from '../atomic-file.js';
|
|
17
|
+
import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
|
|
18
|
+
import { storedRoomLaunchPolicy, TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
|
|
12
19
|
export class TaskRoomApplicationError extends Error {
|
|
13
20
|
code;
|
|
14
21
|
fields;
|
|
@@ -19,6 +26,53 @@ export class TaskRoomApplicationError extends Error {
|
|
|
19
26
|
this.name = 'TaskRoomApplicationError';
|
|
20
27
|
}
|
|
21
28
|
}
|
|
29
|
+
export function resolveRoomLaunchPolicy(template, override) {
|
|
30
|
+
const anonymous = override ?? template?.room?.anonymous ?? false;
|
|
31
|
+
return { anonymous };
|
|
32
|
+
}
|
|
33
|
+
function launchSelection(definition, key) {
|
|
34
|
+
const selected = definition?.[key];
|
|
35
|
+
if (!selected || typeof selected !== 'object' || Array.isArray(selected))
|
|
36
|
+
return undefined;
|
|
37
|
+
const value = selected;
|
|
38
|
+
if (value.kind === 'ref' && typeof value.id === 'string')
|
|
39
|
+
return `ref:${value.id} (reference)`;
|
|
40
|
+
if (value.kind === 'inline' && typeof value.fingerprint === 'string')
|
|
41
|
+
return `inline:${value.fingerprint} (inline)`;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function launchPermissions(definition) {
|
|
45
|
+
const selected = definition?.permissions;
|
|
46
|
+
if (!selected || typeof selected !== 'object' || Array.isArray(selected))
|
|
47
|
+
return undefined;
|
|
48
|
+
const value = selected;
|
|
49
|
+
const fields = ['approval', 'filesystem', 'unattended'].flatMap(key => typeof value[key] === 'string' ? [`${key}=${value[key]}`] : []);
|
|
50
|
+
return fields.length ? fields.join(',') : undefined;
|
|
51
|
+
}
|
|
52
|
+
function roomParticipants(room) {
|
|
53
|
+
return [...room.member_seats].sort((a, b) => a.role_name.localeCompare(b.role_name)).map(seat => ({
|
|
54
|
+
name: seat.role_name, id: seat.identity_cid,
|
|
55
|
+
brain: launchSelection(seat.launch?.agent_definition, 'brain'),
|
|
56
|
+
role: launchSelection(seat.launch?.agent_definition, 'role') ?? seat.cowork_role,
|
|
57
|
+
permissions: launchPermissions(seat.launch?.agent_definition),
|
|
58
|
+
...(seat.launch?.presentation ? { configuration: seat.launch.presentation } : {}),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
function taskAgents(task) {
|
|
62
|
+
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
63
|
+
const seats = new Map(room?.member_seats.map(seat => [seat.role_name, seat]) ?? []);
|
|
64
|
+
return [...task.member_roles].sort((a, b) => a.name.localeCompare(b.name)).map(member => {
|
|
65
|
+
const seat = seats.get(member.name);
|
|
66
|
+
return { name: member.name, brain: launchSelection(seat?.launch?.agent_definition, 'brain'),
|
|
67
|
+
role: launchSelection(seat?.launch?.agent_definition, 'role') ?? member.cowork_role,
|
|
68
|
+
permissions: launchPermissions(seat?.launch?.agent_definition),
|
|
69
|
+
...(seat?.launch?.presentation ? { configuration: seat.launch.presentation } : {}) };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function roomFailureEventId(room) {
|
|
73
|
+
const evidence = `${room.created_at}\0${room.saga.phase}\0${room.saga.step_index}\0${room.saga.error ?? ''}`;
|
|
74
|
+
return `sha256:${createHash('sha256').update(evidence).digest('hex')}`;
|
|
75
|
+
}
|
|
22
76
|
/** Exact extraction of the previously CLI-owned task create/start behavior. */
|
|
23
77
|
export class TaskRoomApplicationService {
|
|
24
78
|
configurationPath;
|
|
@@ -30,29 +84,78 @@ export class TaskRoomApplicationService {
|
|
|
30
84
|
}
|
|
31
85
|
async createTask(request) {
|
|
32
86
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
87
|
+
let template = this.createTemplate(cfg, request.template, request.noRoom);
|
|
88
|
+
if (request.noRoom && request.anonymous !== undefined)
|
|
89
|
+
throw new ConfigError('--anonymous/--no-anonymous cannot be combined with --no-room');
|
|
90
|
+
const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
|
|
91
|
+
let launchDefinitions;
|
|
92
|
+
let executionPlan;
|
|
93
|
+
let preparedPlan;
|
|
94
|
+
if (template) {
|
|
95
|
+
const definition = resolveTemplate(template.name, cfg.roomTemplates ?? {});
|
|
96
|
+
preparedPlan = prepareExecutionPlan(definition, cfg, request.members ?? {});
|
|
97
|
+
launchDefinitions = preparedPlan.launchDefinitions;
|
|
98
|
+
}
|
|
37
99
|
const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
100
|
+
const unlock = preparedPlan ? acquireLaunchSnapshotLock() : undefined;
|
|
101
|
+
let sealedHash;
|
|
102
|
+
let task;
|
|
103
|
+
try {
|
|
104
|
+
if (preparedPlan) {
|
|
105
|
+
template = sealTemplateSnapshot(preparedPlan.snapshot, cfg.agentTemplates ?? {}, preparedPlan.launchDefinitions);
|
|
106
|
+
sealedHash = template.launch_snapshot_hash;
|
|
107
|
+
executionPlan = { schema_version: 1, snapshot: template, room_policy: roomPolicy,
|
|
108
|
+
overrides: preparedPlan.overrides,
|
|
109
|
+
overrides_hash: preparedPlan.overridesHash,
|
|
110
|
+
plan_hash: preparedPlan.planHash };
|
|
111
|
+
}
|
|
112
|
+
const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
|
|
113
|
+
task = await withTaskListsLock(() => {
|
|
114
|
+
const list = resolveTaskList(request.list ?? 'default');
|
|
115
|
+
return persistTask({
|
|
116
|
+
title: request.title, brief, brief_file: request.briefFile, template: ref,
|
|
117
|
+
execution_plan: executionPlan,
|
|
118
|
+
origin: request.origin, idempotency_key: request.idempotencyKey,
|
|
119
|
+
start: !request.backlog, no_room: request.noRoom, listId: list.list_id,
|
|
120
|
+
});
|
|
44
121
|
});
|
|
45
|
-
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
unlock?.();
|
|
125
|
+
if (sealedHash)
|
|
126
|
+
releaseLaunchSnapshot(sealedHash);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
unlock?.();
|
|
130
|
+
const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
|
|
131
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
|
|
132
|
+
title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
|
|
133
|
+
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
134
|
+
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
135
|
+
agents: [] });
|
|
46
136
|
if (!request.backlog && !request.noRoom && ref && !task.room_id) {
|
|
47
137
|
try {
|
|
48
|
-
await this.provisionRoom(cfg, task, template,
|
|
49
|
-
task = updateTaskRoom(task.task_id,
|
|
50
|
-
});
|
|
138
|
+
const room = await this.provisionRoom(cfg, task, template, created => {
|
|
139
|
+
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
140
|
+
}, launchDefinitions, roomPolicy);
|
|
51
141
|
task = readTask(task.task_id);
|
|
142
|
+
if (task.state === 'active' && room.state === 'active')
|
|
143
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
|
|
144
|
+
title: task.title, previousState: 'provisioning', newState: task.state,
|
|
145
|
+
revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
|
|
146
|
+
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
147
|
+
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
148
|
+
agents: taskAgents(task) });
|
|
149
|
+
else
|
|
150
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: task.task_id,
|
|
151
|
+
state: task.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
|
|
52
152
|
}
|
|
53
153
|
catch (error) {
|
|
54
154
|
if (error instanceof CoworkUnavailableError)
|
|
55
155
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
156
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
|
|
157
|
+
id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
|
|
158
|
+
eventId: task.room_id ? roomFailureEventId(getRoomRecord(task.room_id)) : task.created_at });
|
|
56
159
|
throw error;
|
|
57
160
|
}
|
|
58
161
|
}
|
|
@@ -66,27 +169,25 @@ export class TaskRoomApplicationService {
|
|
|
66
169
|
const definition = resolveTemplate(request.template, cfg.roomTemplates ?? {});
|
|
67
170
|
if (!definition)
|
|
68
171
|
throw new TaskRoomApplicationError('template_not_found', `template not found: ${request.template}`, { template: request.template });
|
|
69
|
-
|
|
172
|
+
if (request.members && Object.keys(request.members).length) {
|
|
173
|
+
const prepared = prepareExecutionPlan(definition, cfg, request.members);
|
|
174
|
+
template = prepared.snapshot;
|
|
175
|
+
const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
|
|
176
|
+
return this.provisionRoom(cfg, { title: request.name, brief, goal: request.goal }, template, () => { }, prepared.launchDefinitions, roomPolicy);
|
|
177
|
+
}
|
|
178
|
+
template = snapshotTemplate(definition, cfg.agentTemplates);
|
|
70
179
|
}
|
|
180
|
+
const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
|
|
71
181
|
return this.provisionRoom(cfg, {
|
|
72
182
|
title: request.name, brief, goal: request.goal,
|
|
73
|
-
}, template, () => { });
|
|
183
|
+
}, template, () => { }, undefined, roomPolicy);
|
|
74
184
|
}
|
|
75
185
|
async startTask(input) {
|
|
76
|
-
|
|
77
|
-
let task = transitionTask(input.taskId);
|
|
78
|
-
if (task.template && !task.room_id) {
|
|
79
|
-
const template = this.existingTemplate(cfg, task);
|
|
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;
|
|
186
|
+
return (await this.ensureTaskWork(input)).task;
|
|
86
187
|
}
|
|
87
188
|
listTasks(filter) {
|
|
88
189
|
const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
|
|
89
|
-
return readTasks({ state: filter?.state, listId });
|
|
190
|
+
return readTasks({ state: filter?.state, listId, includeDeleting: filter?.includeDeleting });
|
|
90
191
|
}
|
|
91
192
|
listTaskLists() { return readTaskLists(); }
|
|
92
193
|
groupedTasks(filter) {
|
|
@@ -139,8 +240,93 @@ export class TaskRoomApplicationService {
|
|
|
139
240
|
reviewTask(input) {
|
|
140
241
|
return persistReviewTask(input.taskId);
|
|
141
242
|
}
|
|
142
|
-
|
|
143
|
-
|
|
243
|
+
deletionActor(actor) {
|
|
244
|
+
if (actor.kind === 'local_control')
|
|
245
|
+
return { kind: 'local_control', surface: actor.surface };
|
|
246
|
+
if (actor.kind === 'authenticated_owner')
|
|
247
|
+
return { kind: 'authenticated_owner', surface: 'messenger', cid: actor.cid };
|
|
248
|
+
throw new Error('internal workers cannot originate a task deletion acceptance');
|
|
249
|
+
}
|
|
250
|
+
/** Accept a permanent deletion in any lifecycle state; audit at acceptance. */
|
|
251
|
+
async requestTaskDeletion(input) {
|
|
252
|
+
const result = await acceptTaskDeletion(input.taskId, this.deletionActor(input.actor));
|
|
253
|
+
if (result.status === 'accepted') {
|
|
254
|
+
const task = result.task;
|
|
255
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'delete', id: task.task_id,
|
|
256
|
+
title: task.title, previousState: task.state, newState: 'deleting',
|
|
257
|
+
revision: task.deletion?.accepted_at ?? task.created_at, list: task.list_name ?? 'default',
|
|
258
|
+
roomId: task.room_id,
|
|
259
|
+
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
260
|
+
agents: [] });
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
/** Worker entry: converge an accepted deletion; Cowork is resolved lazily. */
|
|
265
|
+
async settleTaskDeletion(input) {
|
|
266
|
+
const result = await settleTaskDeletion({
|
|
267
|
+
taskId: input.taskId,
|
|
268
|
+
cowork: () => {
|
|
269
|
+
// Config resolves only when room work exists: a no-room deletion must
|
|
270
|
+
// settle even with missing or invalid rooms configuration.
|
|
271
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
272
|
+
if (!cfg.rooms)
|
|
273
|
+
throw new ConfigError('rooms: configuration is required before deleting task rooms');
|
|
274
|
+
return this.deps.cowork ? this.deps.cowork(cfg)
|
|
275
|
+
: createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
if (result.deleted && result.previous_state) {
|
|
279
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'delete', id: result.task_id,
|
|
280
|
+
title: result.title, previousState: result.previous_state, newState: 'deleted',
|
|
281
|
+
revision: new Date().toISOString(), list: 'default', roomId: undefined,
|
|
282
|
+
template: undefined, agents: [] });
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Launch the external deletion worker outside the caller's lifecycle and
|
|
288
|
+
* wait boundedly for physical absence. Timeouts and launch failures report
|
|
289
|
+
* a pending, recoverable state — never success. A concurrent worker that
|
|
290
|
+
* already removed the record reads as settled.
|
|
291
|
+
*/
|
|
292
|
+
async launchTaskDeletionWorker(input) {
|
|
293
|
+
const launch = this.deps.launchDeletionWorker
|
|
294
|
+
?? ((taskId) => launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, this.configurationPath));
|
|
295
|
+
const errorAtBefore = (() => {
|
|
296
|
+
try {
|
|
297
|
+
return getDeletingTask(input.taskId).deletion?.error_at;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
})();
|
|
303
|
+
try {
|
|
304
|
+
await launch(input.taskId);
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
+
await recordTaskDeletionError(input.taskId, message, `External delete worker failed to start. Repeat the delete request for ${input.taskId}.`).catch(() => { });
|
|
309
|
+
return { deleted: false, pending: true, error: message };
|
|
310
|
+
}
|
|
311
|
+
const now = this.deps.now ?? Date.now;
|
|
312
|
+
const sleep = this.deps.sleep
|
|
313
|
+
?? ((ms) => new Promise(resolve => { setTimeout(resolve, ms); }));
|
|
314
|
+
const deadline = now() + (input.waitMs ?? 10_000);
|
|
315
|
+
while (now() < deadline) {
|
|
316
|
+
if (taskDeletionState(input.taskId) === 'absent')
|
|
317
|
+
return { deleted: true, pending: false };
|
|
318
|
+
try {
|
|
319
|
+
const current = getDeletingTask(input.taskId).deletion;
|
|
320
|
+
if (current?.error_at !== errorAtBefore && current?.error)
|
|
321
|
+
return { deleted: false, pending: true, error: current.error };
|
|
322
|
+
}
|
|
323
|
+
catch { /* re-checked as absence on the next iteration */ }
|
|
324
|
+
await sleep(100);
|
|
325
|
+
}
|
|
326
|
+
return { deleted: false, pending: true };
|
|
327
|
+
}
|
|
328
|
+
recordDeletionError(input) {
|
|
329
|
+
return recordTaskDeletionError(input.taskId, input.error, input.recoveryHint);
|
|
144
330
|
}
|
|
145
331
|
async completeTask(input) {
|
|
146
332
|
const task = readTask(input.taskId);
|
|
@@ -179,10 +365,22 @@ export class TaskRoomApplicationService {
|
|
|
179
365
|
return recordTaskTerminalIntentError(input.taskId, input.error, input.recoveryHint);
|
|
180
366
|
}
|
|
181
367
|
async beginTaskRecovery(input) {
|
|
368
|
+
// The deletion-vs-terminal routing decision serializes on the common
|
|
369
|
+
// task-operation lock and must not require configuration: recovery of a
|
|
370
|
+
// deletion-pending task continues the deletion and never resurrects.
|
|
371
|
+
const routed = await withFileLock(taskOperationLockPath(input.taskId), () => {
|
|
372
|
+
const task = readTask(input.taskId);
|
|
373
|
+
if (task.deletion?.status === 'pending')
|
|
374
|
+
return 'deletion';
|
|
375
|
+
if (task.terminal_intent?.status === 'pending')
|
|
376
|
+
return 'terminal';
|
|
377
|
+
return 'none';
|
|
378
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
379
|
+
if (routed === 'deletion')
|
|
380
|
+
return { kind: 'deletion_worker_required', taskId: input.taskId };
|
|
182
381
|
const config = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
183
382
|
this.recovery = { taskId: input.taskId, config };
|
|
184
|
-
|
|
185
|
-
if (task.terminal_intent?.status === 'pending')
|
|
383
|
+
if (routed === 'terminal')
|
|
186
384
|
return { kind: 'terminal_worker_required', taskId: input.taskId };
|
|
187
385
|
return { kind: 'final', result: await this.continueTaskRecovery({
|
|
188
386
|
actor: input.actor, taskId: input.taskId, terminalTimedOut: false,
|
|
@@ -259,8 +457,11 @@ export class TaskRoomApplicationService {
|
|
|
259
457
|
validateTemplates() {
|
|
260
458
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
261
459
|
return listTemplates(cfg.roomTemplates ?? {}).flatMap(template => {
|
|
262
|
-
const issues = template.members.flatMap(member =>
|
|
263
|
-
|
|
460
|
+
const issues = template.members.flatMap(member => {
|
|
461
|
+
const ref = member.agent_template;
|
|
462
|
+
return cfg.agentTemplates?.[ref]
|
|
463
|
+
? [] : [`member ${member.slot}: Agent Template '${ref}' not found`];
|
|
464
|
+
});
|
|
264
465
|
return issues.length ? [{ template: `${template.name}@${template.version}`, issues }] : [];
|
|
265
466
|
});
|
|
266
467
|
}
|
|
@@ -341,6 +542,14 @@ export class TaskRoomApplicationService {
|
|
|
341
542
|
return { kind: 'deletion_worker_required', roomId: input.roomId };
|
|
342
543
|
const room = await adapter.recoverRoom(input.roomId);
|
|
343
544
|
orchestration = getRoomRecord(input.roomId);
|
|
545
|
+
if (orchestration) {
|
|
546
|
+
const policy = storedRoomLaunchPolicy(orchestration.room_policy);
|
|
547
|
+
if ((room.anonymous ?? false) !== policy.anonymous) {
|
|
548
|
+
const error = `Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(policy.anonymous)})`;
|
|
549
|
+
setSagaError(input.roomId, error, 'Do not respawn members. Repair or upgrade Cowork, then recover the Room without changing its policy.', 'waiting_cowork');
|
|
550
|
+
throw new Error(error);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
344
553
|
if (orchestration && !orchestration.owner_seat_cid
|
|
345
554
|
&& (orchestration.provisioning_detail === 'waiting_owner_invite'
|
|
346
555
|
|| orchestration.provisioning_detail === 'owner_cid_mismatch')) {
|
|
@@ -410,69 +619,138 @@ export class TaskRoomApplicationService {
|
|
|
410
619
|
async ensureTaskWork(input) {
|
|
411
620
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
412
621
|
let task = readTask(input.taskId);
|
|
622
|
+
const recordedRoom = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
623
|
+
const recordedPolicy = storedRoomLaunchPolicy(recordedRoom?.room_policy ?? task.execution_plan?.room_policy);
|
|
624
|
+
if (task.room_id && input.anonymous !== undefined && input.anonymous !== recordedPolicy.anonymous)
|
|
625
|
+
throw new TaskRoomApplicationError('template_mismatch', 'anonymous override does not match the existing Room launch policy', { room: task.room_id });
|
|
413
626
|
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
414
627
|
throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: input.taskId, state: task.state });
|
|
415
628
|
if (task.state === 'active' && task.room_id) {
|
|
416
|
-
if (input.template) {
|
|
417
|
-
const
|
|
629
|
+
if (input.template || input.members) {
|
|
630
|
+
const pinned = getRoomRecord(task.room_id)?.template_snapshot ?? task.execution_plan?.snapshot;
|
|
631
|
+
if (!pinned)
|
|
632
|
+
throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
|
|
633
|
+
const templateName = input.template ?? pinned.name;
|
|
634
|
+
if ((!input.template || input.template === pinned.name) && input.members
|
|
635
|
+
&& task.execution_plan?.overrides_hash === hashMemberOverrides(input.members))
|
|
636
|
+
return { task, status: 'already_active' };
|
|
637
|
+
const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
418
638
|
if (!definition)
|
|
419
|
-
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template:
|
|
420
|
-
const requested =
|
|
421
|
-
const
|
|
422
|
-
if (pinned
|
|
639
|
+
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
640
|
+
const requested = prepareExecutionPlan(definition, cfg, input.members ?? {});
|
|
641
|
+
const pinnedPlanHash = task.execution_plan?.plan_hash ?? pinned.content_hash;
|
|
642
|
+
if (pinned.name !== requested.snapshot.name || pinnedPlanHash !== requested.planHash)
|
|
423
643
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
424
|
-
requested: `${requested.name}@${requested.version}`, room: task.room_id,
|
|
644
|
+
requested: `${requested.snapshot.name}@${requested.snapshot.version}`, room: task.room_id,
|
|
425
645
|
provisioned: `${pinned.name}@${pinned.version}`
|
|
426
646
|
});
|
|
427
647
|
}
|
|
428
648
|
return { task, status: 'already_active' };
|
|
429
649
|
}
|
|
430
650
|
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
431
|
-
const durable = room?.template_snapshot;
|
|
651
|
+
const durable = room?.template_snapshot ?? task.execution_plan?.snapshot;
|
|
432
652
|
if (durable && (!task.template || task.template.name !== durable.name
|
|
433
653
|
|| task.template.version !== durable.version || task.template.content_hash !== durable.content_hash))
|
|
434
|
-
throw new Error(`task ${task.task_id} template reference does not match
|
|
654
|
+
throw new Error(`task ${task.task_id} template reference does not match its durable execution snapshot`);
|
|
435
655
|
const templateName = input.template ?? durable?.name ?? task.template?.name
|
|
436
656
|
?? cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'single';
|
|
437
|
-
const
|
|
657
|
+
const storedOverridesMatch = !!(durable && input.members && task.execution_plan?.overrides_hash
|
|
658
|
+
=== hashMemberOverrides(input.members));
|
|
659
|
+
const definition = durable && !input.template && (!input.members || storedOverridesMatch) ? undefined
|
|
438
660
|
: resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
439
661
|
if (input.template && !definition)
|
|
440
662
|
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
441
663
|
if (!durable && !definition)
|
|
442
664
|
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
443
|
-
|
|
665
|
+
let launchDefinitions;
|
|
666
|
+
let preparedPlan = !durable && definition
|
|
667
|
+
? prepareExecutionPlan(definition, cfg, input.members ?? {}) : undefined;
|
|
668
|
+
let snapshot = durable ?? preparedPlan.snapshot;
|
|
669
|
+
const roomPolicy = input.anonymous !== undefined
|
|
670
|
+
? resolveRoomLaunchPolicy(snapshot, input.anonymous)
|
|
671
|
+
: task.execution_plan
|
|
672
|
+
? storedRoomLaunchPolicy(task.execution_plan.room_policy)
|
|
673
|
+
: resolveRoomLaunchPolicy(snapshot, undefined);
|
|
674
|
+
if (preparedPlan)
|
|
675
|
+
launchDefinitions = preparedPlan.launchDefinitions;
|
|
676
|
+
if (input.members && Object.keys(input.members).length && !storedOverridesMatch) {
|
|
677
|
+
if (!definition)
|
|
678
|
+
throw new TaskRoomApplicationError('template_mismatch', 'member overrides cannot change an existing Room execution plan');
|
|
679
|
+
const prepared = preparedPlan ?? prepareExecutionPlan(definition, cfg, input.members);
|
|
680
|
+
preparedPlan = prepared;
|
|
681
|
+
if (durable && prepared.planHash !== durable.content_hash)
|
|
682
|
+
throw new TaskRoomApplicationError('template_mismatch', 'member overrides do not match existing execution plan');
|
|
683
|
+
snapshot = prepared.snapshot;
|
|
684
|
+
launchDefinitions = prepared.launchDefinitions;
|
|
685
|
+
}
|
|
444
686
|
if (durable && input.template && definition) {
|
|
445
|
-
const requested = snapshotTemplate(definition);
|
|
687
|
+
const requested = snapshotTemplate(definition, cfg.agentTemplates);
|
|
446
688
|
if (requested.name !== durable.name || requested.content_hash !== durable.content_hash)
|
|
447
689
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
448
690
|
requested: `${requested.name}@${requested.version}`, room: room.room_id,
|
|
449
691
|
provisioned: `${durable.name}@${durable.version}`
|
|
450
692
|
});
|
|
451
693
|
}
|
|
452
|
-
if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
694
|
+
if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash) {
|
|
695
|
+
// Pre-execution-plan backlog records used the template-only hash. Accept
|
|
696
|
+
// that exact legacy pin once, then upgrade it to the complete sealed plan.
|
|
697
|
+
const legacyHash = definition
|
|
698
|
+
? snapshotTemplate(definition, cfg.agentTemplates).content_hash : undefined;
|
|
699
|
+
if (task.template.content_hash !== legacyHash)
|
|
700
|
+
throw new TaskRoomApplicationError('task_template_drift', 'task template drift', {
|
|
701
|
+
template: `${task.template.name}@${task.template.version}`
|
|
702
|
+
});
|
|
703
|
+
}
|
|
456
704
|
if (task.room_id && room?.template_snapshot
|
|
457
705
|
&& (room.template_snapshot.name !== snapshot.name || room.template_snapshot.content_hash !== snapshot.content_hash))
|
|
458
706
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
459
707
|
requested: `${snapshot.name}@${snapshot.version}`, room: task.room_id,
|
|
460
708
|
provisioned: `${room.template_snapshot.name}@${room.template_snapshot.version}`
|
|
461
709
|
});
|
|
462
|
-
if (
|
|
710
|
+
if (preparedPlan && !task.room_id) {
|
|
711
|
+
const unlock = acquireLaunchSnapshotLock();
|
|
712
|
+
let sealed;
|
|
713
|
+
try {
|
|
714
|
+
sealed = sealTemplateSnapshot(snapshot, cfg.agentTemplates ?? {}, launchDefinitions);
|
|
715
|
+
task = updateTaskExecutionPlan(task.task_id, { schema_version: 1, snapshot: sealed,
|
|
716
|
+
room_policy: roomPolicy,
|
|
717
|
+
overrides: preparedPlan.overrides, overrides_hash: preparedPlan.overridesHash,
|
|
718
|
+
plan_hash: preparedPlan.planHash });
|
|
719
|
+
snapshot = sealed;
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
unlock();
|
|
723
|
+
if (sealed?.launch_snapshot_hash)
|
|
724
|
+
releaseLaunchSnapshot(sealed.launch_snapshot_hash);
|
|
725
|
+
throw error;
|
|
726
|
+
}
|
|
727
|
+
unlock();
|
|
728
|
+
}
|
|
729
|
+
else if (!task.room_id && task.execution_plan && input.anonymous !== undefined) {
|
|
730
|
+
task = updateTaskExecutionPlan(task.task_id, { ...task.execution_plan, room_policy: roomPolicy });
|
|
731
|
+
}
|
|
732
|
+
else if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
|
|
463
733
|
task = updateTaskTemplate(task.task_id, { name: snapshot.name, version: snapshot.version, content_hash: snapshot.content_hash });
|
|
464
|
-
if (task.state === 'backlog')
|
|
734
|
+
if (task.state === 'backlog') {
|
|
465
735
|
task = transitionTask(task.task_id);
|
|
736
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'start', id: task.task_id,
|
|
737
|
+
title: task.title, previousState: 'backlog', newState: task.state,
|
|
738
|
+
revision: task.started_at ?? task.created_at, list: task.list_name ?? 'default',
|
|
739
|
+
template: `${snapshot.name}@${snapshot.version}`, agents: [] });
|
|
740
|
+
}
|
|
466
741
|
if (!task.room_id) {
|
|
467
742
|
try {
|
|
468
743
|
await this.provisionRoom(cfg, task, snapshot, created => {
|
|
469
744
|
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
470
|
-
});
|
|
745
|
+
}, launchDefinitions, roomPolicy);
|
|
471
746
|
task = readTask(task.task_id);
|
|
472
747
|
}
|
|
473
748
|
catch (error) {
|
|
474
749
|
if (error instanceof CoworkUnavailableError)
|
|
475
750
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
751
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
|
|
752
|
+
id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
|
|
753
|
+
eventId: task.room_id ? roomFailureEventId(getRoomRecord(task.room_id)) : task.created_at });
|
|
476
754
|
throw error;
|
|
477
755
|
}
|
|
478
756
|
}
|
|
@@ -491,9 +769,23 @@ export class TaskRoomApplicationService {
|
|
|
491
769
|
catch (error) {
|
|
492
770
|
if (error instanceof CoworkUnavailableError)
|
|
493
771
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
772
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
|
|
773
|
+
id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
|
|
774
|
+
eventId: roomFailureEventId(getRoomRecord(task.room_id)) });
|
|
494
775
|
throw error;
|
|
495
776
|
}
|
|
496
777
|
}
|
|
778
|
+
const resultingRoom = getRoomRecord(task.room_id);
|
|
779
|
+
if (task.state === 'active' && resultingRoom?.state === 'active')
|
|
780
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
|
|
781
|
+
title: task.title, previousState: 'provisioning', newState: task.state,
|
|
782
|
+
revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
|
|
783
|
+
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
784
|
+
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
785
|
+
agents: taskAgents(task) });
|
|
786
|
+
else if (resultingRoom)
|
|
787
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
|
|
788
|
+
id: task.task_id, state: task.state, category: 'provision_pending', eventId: roomFailureEventId(resultingRoom) });
|
|
497
789
|
return { task, status: 'ready' };
|
|
498
790
|
}
|
|
499
791
|
createTemplate(cfg, requested, noRoom) {
|
|
@@ -507,17 +799,17 @@ export class TaskRoomApplicationService {
|
|
|
507
799
|
throw new TaskRoomApplicationError('template_not_found', `template not found: ${requested}`);
|
|
508
800
|
return undefined;
|
|
509
801
|
}
|
|
510
|
-
return snapshotTemplate(definition);
|
|
802
|
+
return snapshotTemplate(definition, cfg.agentTemplates);
|
|
511
803
|
}
|
|
512
804
|
existingTemplate(cfg, task) {
|
|
513
805
|
const ref = task.template;
|
|
514
806
|
const definition = resolveTemplate(ref.name, cfg.roomTemplates ?? {});
|
|
515
|
-
const snapshot = definition ? snapshotTemplate(definition) : undefined;
|
|
807
|
+
const snapshot = definition ? snapshotTemplate(definition, cfg.agentTemplates) : undefined;
|
|
516
808
|
if (!snapshot || snapshot.content_hash !== ref.content_hash)
|
|
517
809
|
throw new TaskRoomApplicationError('task_template_drift', `task template snapshot no longer matches ${ref.name}@${ref.version}`);
|
|
518
810
|
return snapshot;
|
|
519
811
|
}
|
|
520
|
-
async provisionRoom(cfg, task, template, onCreated) {
|
|
812
|
+
async provisionRoom(cfg, task, template, onCreated, launchDefinitions, policy = resolveRoomLaunchPolicy(template, undefined)) {
|
|
521
813
|
const rooms = cfg.rooms;
|
|
522
814
|
if (!rooms)
|
|
523
815
|
throw new ConfigError('rooms: configuration is required');
|
|
@@ -529,17 +821,52 @@ export class TaskRoomApplicationService {
|
|
|
529
821
|
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
530
822
|
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
531
823
|
})();
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
824
|
+
const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
|
|
825
|
+
let launchTemplate;
|
|
826
|
+
let room;
|
|
827
|
+
try {
|
|
828
|
+
launchTemplate = template ? (template.launch_snapshot_hash ? template
|
|
829
|
+
: sealTemplateSnapshot(template, cfg.agentTemplates ?? {}, launchDefinitions)) : undefined;
|
|
830
|
+
// Room publication window: for a task-bound room, hold the common
|
|
831
|
+
// task-operation lock across remote creation, local record creation, and
|
|
832
|
+
// the task link, so an accepted deletion linearizes strictly before
|
|
833
|
+
// (bounded task_deleting abort) or strictly after (record visible to the
|
|
834
|
+
// deletion scan). Lock order stays launch-snapshot → task-operation.
|
|
835
|
+
const publishRoom = async () => {
|
|
836
|
+
if (task.task_id) {
|
|
837
|
+
const fresh = readTask(task.task_id);
|
|
838
|
+
if (fresh.deletion?.status === 'pending')
|
|
839
|
+
throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
|
|
840
|
+
}
|
|
841
|
+
const created = await cowork.createRoom({
|
|
842
|
+
room_name: task.title, goal: task.goal?.trim() || task.title,
|
|
843
|
+
briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
|
|
844
|
+
quiet_membership: launchTemplate?.room?.quiet_membership,
|
|
845
|
+
anonymous: policy.anonymous,
|
|
846
|
+
});
|
|
847
|
+
const record = createRoomRecord({
|
|
848
|
+
room_id: created.room_id, room_name: task.title, room_identity_cid: created.identity_cid,
|
|
849
|
+
task_id: task.task_id, template_snapshot: launchTemplate, room_policy: policy,
|
|
850
|
+
});
|
|
851
|
+
onCreated(record);
|
|
852
|
+
return record;
|
|
853
|
+
};
|
|
854
|
+
room = task.task_id
|
|
855
|
+
? await withFileLock(taskOperationLockPath(task.task_id), publishRoom, {}, TASK_OPERATION_LOCK_STALE_MS)
|
|
856
|
+
: await publishRoom();
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
unlockSnapshot?.();
|
|
860
|
+
if (launchTemplate?.launch_snapshot_hash)
|
|
861
|
+
releaseLaunchSnapshot(launchTemplate.launch_snapshot_hash);
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
unlockSnapshot?.();
|
|
865
|
+
recordFleetAuditPresentation({ kind: 'room', operation: 'create', id: room.room_id,
|
|
866
|
+
name: room.room_name, previousState: 'none', newState: 'provisioning',
|
|
867
|
+
revision: room.created_at, taskId: room.task_id,
|
|
868
|
+
template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
869
|
+
participants: [] });
|
|
543
870
|
room = advanceSaga(room.room_id, 'create_room', 1);
|
|
544
871
|
if (attachOwner) {
|
|
545
872
|
try {
|
|
@@ -553,18 +880,39 @@ export class TaskRoomApplicationService {
|
|
|
553
880
|
const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
|
|
554
881
|
setSagaError(room.room_id, error instanceof Error ? error.message : String(error), mismatch ? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
|
|
555
882
|
: 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
|
|
883
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
|
|
884
|
+
id: room.room_id, state: getRoomRecord(room.room_id)?.state ?? 'provisioning',
|
|
885
|
+
category: 'provision_failed', eventId: roomFailureEventId(getRoomRecord(room.room_id) ?? room) });
|
|
556
886
|
throw error;
|
|
557
887
|
}
|
|
558
888
|
}
|
|
559
889
|
room = advanceSaga(room.room_id, 'create_members', 3);
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
890
|
+
if (launchTemplate?.members.length)
|
|
891
|
+
try {
|
|
892
|
+
room = await (this.deps.provisionMembers ?? provisionMembers)({
|
|
893
|
+
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
|
|
894
|
+
binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
|
|
895
|
+
goal: task.task_id ? task.title : task.goal,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
const failed = getRoomRecord(room.room_id) ?? room;
|
|
900
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: failed.room_id,
|
|
901
|
+
state: failed.state, category: 'provision_failed', eventId: roomFailureEventId(failed) });
|
|
902
|
+
throw error;
|
|
903
|
+
}
|
|
904
|
+
else
|
|
905
|
+
room = activateRoom(room.room_id);
|
|
906
|
+
if (room.state === 'active')
|
|
907
|
+
recordFleetAuditPresentation({ kind: 'room', operation: 'activate', id: room.room_id,
|
|
908
|
+
name: room.room_name, previousState: 'provisioning', newState: 'active',
|
|
909
|
+
revision: room.activated_at ?? room.created_at, taskId: room.task_id,
|
|
910
|
+
template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
911
|
+
participants: roomParticipants(room) });
|
|
912
|
+
else
|
|
913
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: room.room_id,
|
|
914
|
+
state: room.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
|
|
915
|
+
if (task.task_id && !launchTemplate?.members.length)
|
|
568
916
|
activateTask(task.task_id);
|
|
569
917
|
return room;
|
|
570
918
|
}
|