@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.21
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 +204 -55
- 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 +435 -79
- 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 +241 -76
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +75 -3
- package/dist/config.js +274 -34
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +3 -2
- 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 +192 -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 +555 -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 +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-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 +13 -1
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +514 -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 +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/provision.js +184 -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 +96 -17
- package/dist/rooms-tasks/types.js +14 -4
- package/dist/runner.d.ts +12 -2
- package/dist/runner.js +124 -43
- 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 +15 -31
- package/dist/spawn.js +117 -237
- 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,146 @@ 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 });
|
|
628
|
+
const durableTemplate = task.room_id
|
|
629
|
+
? getRoomRecord(task.room_id)?.template_snapshot ?? task.execution_plan?.snapshot
|
|
630
|
+
: task.execution_plan?.snapshot;
|
|
631
|
+
const templateRequestMatches = !input.template || (durableTemplate !== undefined
|
|
632
|
+
&& (input.template === durableTemplate.name
|
|
633
|
+
|| input.template === `${durableTemplate.name}@${durableTemplate.version}`));
|
|
634
|
+
const memberRequestMatches = input.members === undefined || (task.execution_plan !== undefined
|
|
635
|
+
&& task.execution_plan.overrides_hash === hashMemberOverrides(input.members));
|
|
636
|
+
const durableRequestMatches = durableTemplate !== undefined
|
|
637
|
+
&& templateRequestMatches && memberRequestMatches;
|
|
415
638
|
if (task.state === 'active' && task.room_id) {
|
|
416
|
-
if (input.template) {
|
|
417
|
-
const
|
|
639
|
+
if (input.template || input.members) {
|
|
640
|
+
const pinned = durableTemplate;
|
|
641
|
+
if (!pinned)
|
|
642
|
+
throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
|
|
643
|
+
if (durableRequestMatches)
|
|
644
|
+
return { task, status: 'already_active' };
|
|
645
|
+
const templateName = input.template ?? pinned.name;
|
|
646
|
+
const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
418
647
|
if (!definition)
|
|
419
|
-
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template:
|
|
420
|
-
const requested =
|
|
421
|
-
const
|
|
422
|
-
if (pinned
|
|
648
|
+
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
649
|
+
const requested = prepareExecutionPlan(definition, cfg, input.members ?? {});
|
|
650
|
+
const pinnedPlanHash = task.execution_plan?.plan_hash ?? pinned.content_hash;
|
|
651
|
+
if (pinned.name !== requested.snapshot.name || pinnedPlanHash !== requested.planHash)
|
|
423
652
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
424
|
-
requested: `${requested.name}@${requested.version}`, room: task.room_id,
|
|
653
|
+
requested: `${requested.snapshot.name}@${requested.snapshot.version}`, room: task.room_id,
|
|
425
654
|
provisioned: `${pinned.name}@${pinned.version}`
|
|
426
655
|
});
|
|
427
656
|
}
|
|
428
657
|
return { task, status: 'already_active' };
|
|
429
658
|
}
|
|
430
659
|
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
431
|
-
const durable = room?.template_snapshot;
|
|
660
|
+
const durable = room?.template_snapshot ?? task.execution_plan?.snapshot;
|
|
432
661
|
if (durable && (!task.template || task.template.name !== durable.name
|
|
433
662
|
|| 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
|
|
663
|
+
throw new Error(`task ${task.task_id} template reference does not match its durable execution snapshot`);
|
|
435
664
|
const templateName = input.template ?? durable?.name ?? task.template?.name
|
|
436
665
|
?? cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'single';
|
|
437
|
-
const
|
|
666
|
+
const storedOverridesMatch = !!(durable && memberRequestMatches);
|
|
667
|
+
const definition = durable && durableRequestMatches ? undefined
|
|
438
668
|
: resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
439
|
-
if (input.template && !definition)
|
|
669
|
+
if (input.template && !definition && !durableRequestMatches)
|
|
440
670
|
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
441
671
|
if (!durable && !definition)
|
|
442
672
|
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
443
|
-
|
|
673
|
+
let launchDefinitions;
|
|
674
|
+
let preparedPlan = !durable && definition
|
|
675
|
+
? prepareExecutionPlan(definition, cfg, input.members ?? {}) : undefined;
|
|
676
|
+
let snapshot = durable ?? preparedPlan.snapshot;
|
|
677
|
+
const roomPolicy = input.anonymous !== undefined
|
|
678
|
+
? resolveRoomLaunchPolicy(snapshot, input.anonymous)
|
|
679
|
+
: task.execution_plan
|
|
680
|
+
? storedRoomLaunchPolicy(task.execution_plan.room_policy)
|
|
681
|
+
: resolveRoomLaunchPolicy(snapshot, undefined);
|
|
682
|
+
if (preparedPlan)
|
|
683
|
+
launchDefinitions = preparedPlan.launchDefinitions;
|
|
684
|
+
if (input.members && Object.keys(input.members).length && !storedOverridesMatch) {
|
|
685
|
+
if (!definition)
|
|
686
|
+
throw new TaskRoomApplicationError('template_mismatch', 'member overrides cannot change an existing Room execution plan');
|
|
687
|
+
const prepared = preparedPlan ?? prepareExecutionPlan(definition, cfg, input.members);
|
|
688
|
+
preparedPlan = prepared;
|
|
689
|
+
if (durable && prepared.planHash !== durable.content_hash)
|
|
690
|
+
throw new TaskRoomApplicationError('template_mismatch', 'member overrides do not match existing execution plan');
|
|
691
|
+
snapshot = prepared.snapshot;
|
|
692
|
+
launchDefinitions = prepared.launchDefinitions;
|
|
693
|
+
}
|
|
444
694
|
if (durable && input.template && definition) {
|
|
445
|
-
const requested = snapshotTemplate(definition);
|
|
695
|
+
const requested = snapshotTemplate(definition, cfg.agentTemplates);
|
|
446
696
|
if (requested.name !== durable.name || requested.content_hash !== durable.content_hash)
|
|
447
697
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
448
698
|
requested: `${requested.name}@${requested.version}`, room: room.room_id,
|
|
449
699
|
provisioned: `${durable.name}@${durable.version}`
|
|
450
700
|
});
|
|
451
701
|
}
|
|
452
|
-
if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
702
|
+
if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash) {
|
|
703
|
+
// Pre-execution-plan backlog records used the template-only hash. Accept
|
|
704
|
+
// that exact legacy pin once, then upgrade it to the complete sealed plan.
|
|
705
|
+
const legacyHash = definition
|
|
706
|
+
? snapshotTemplate(definition, cfg.agentTemplates).content_hash : undefined;
|
|
707
|
+
if (task.template.content_hash !== legacyHash)
|
|
708
|
+
throw new TaskRoomApplicationError('task_template_drift', 'task template drift', {
|
|
709
|
+
template: `${task.template.name}@${task.template.version}`
|
|
710
|
+
});
|
|
711
|
+
}
|
|
456
712
|
if (task.room_id && room?.template_snapshot
|
|
457
713
|
&& (room.template_snapshot.name !== snapshot.name || room.template_snapshot.content_hash !== snapshot.content_hash))
|
|
458
714
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
459
715
|
requested: `${snapshot.name}@${snapshot.version}`, room: task.room_id,
|
|
460
716
|
provisioned: `${room.template_snapshot.name}@${room.template_snapshot.version}`
|
|
461
717
|
});
|
|
462
|
-
if (
|
|
718
|
+
if (preparedPlan && !task.room_id) {
|
|
719
|
+
const unlock = acquireLaunchSnapshotLock();
|
|
720
|
+
let sealed;
|
|
721
|
+
try {
|
|
722
|
+
sealed = sealTemplateSnapshot(snapshot, cfg.agentTemplates ?? {}, launchDefinitions);
|
|
723
|
+
task = updateTaskExecutionPlan(task.task_id, { schema_version: 1, snapshot: sealed,
|
|
724
|
+
room_policy: roomPolicy,
|
|
725
|
+
overrides: preparedPlan.overrides, overrides_hash: preparedPlan.overridesHash,
|
|
726
|
+
plan_hash: preparedPlan.planHash });
|
|
727
|
+
snapshot = sealed;
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
unlock();
|
|
731
|
+
if (sealed?.launch_snapshot_hash)
|
|
732
|
+
releaseLaunchSnapshot(sealed.launch_snapshot_hash);
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
unlock();
|
|
736
|
+
}
|
|
737
|
+
else if (!task.room_id && task.execution_plan && input.anonymous !== undefined) {
|
|
738
|
+
task = updateTaskExecutionPlan(task.task_id, { ...task.execution_plan, room_policy: roomPolicy });
|
|
739
|
+
}
|
|
740
|
+
else if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
|
|
463
741
|
task = updateTaskTemplate(task.task_id, { name: snapshot.name, version: snapshot.version, content_hash: snapshot.content_hash });
|
|
464
|
-
if (task.state === 'backlog')
|
|
742
|
+
if (task.state === 'backlog') {
|
|
465
743
|
task = transitionTask(task.task_id);
|
|
744
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'start', id: task.task_id,
|
|
745
|
+
title: task.title, previousState: 'backlog', newState: task.state,
|
|
746
|
+
revision: task.started_at ?? task.created_at, list: task.list_name ?? 'default',
|
|
747
|
+
template: `${snapshot.name}@${snapshot.version}`, agents: [] });
|
|
748
|
+
}
|
|
466
749
|
if (!task.room_id) {
|
|
467
750
|
try {
|
|
468
751
|
await this.provisionRoom(cfg, task, snapshot, created => {
|
|
469
752
|
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
470
|
-
});
|
|
753
|
+
}, launchDefinitions, roomPolicy);
|
|
471
754
|
task = readTask(task.task_id);
|
|
472
755
|
}
|
|
473
756
|
catch (error) {
|
|
474
757
|
if (error instanceof CoworkUnavailableError)
|
|
475
758
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
759
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
|
|
760
|
+
id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
|
|
761
|
+
eventId: task.room_id ? roomFailureEventId(getRoomRecord(task.room_id)) : task.created_at });
|
|
476
762
|
throw error;
|
|
477
763
|
}
|
|
478
764
|
}
|
|
@@ -491,9 +777,23 @@ export class TaskRoomApplicationService {
|
|
|
491
777
|
catch (error) {
|
|
492
778
|
if (error instanceof CoworkUnavailableError)
|
|
493
779
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
780
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
|
|
781
|
+
id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
|
|
782
|
+
eventId: roomFailureEventId(getRoomRecord(task.room_id)) });
|
|
494
783
|
throw error;
|
|
495
784
|
}
|
|
496
785
|
}
|
|
786
|
+
const resultingRoom = getRoomRecord(task.room_id);
|
|
787
|
+
if (task.state === 'active' && resultingRoom?.state === 'active')
|
|
788
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
|
|
789
|
+
title: task.title, previousState: 'provisioning', newState: task.state,
|
|
790
|
+
revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
|
|
791
|
+
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
792
|
+
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
793
|
+
agents: taskAgents(task) });
|
|
794
|
+
else if (resultingRoom)
|
|
795
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
|
|
796
|
+
id: task.task_id, state: task.state, category: 'provision_pending', eventId: roomFailureEventId(resultingRoom) });
|
|
497
797
|
return { task, status: 'ready' };
|
|
498
798
|
}
|
|
499
799
|
createTemplate(cfg, requested, noRoom) {
|
|
@@ -507,17 +807,17 @@ export class TaskRoomApplicationService {
|
|
|
507
807
|
throw new TaskRoomApplicationError('template_not_found', `template not found: ${requested}`);
|
|
508
808
|
return undefined;
|
|
509
809
|
}
|
|
510
|
-
return snapshotTemplate(definition);
|
|
810
|
+
return snapshotTemplate(definition, cfg.agentTemplates);
|
|
511
811
|
}
|
|
512
812
|
existingTemplate(cfg, task) {
|
|
513
813
|
const ref = task.template;
|
|
514
814
|
const definition = resolveTemplate(ref.name, cfg.roomTemplates ?? {});
|
|
515
|
-
const snapshot = definition ? snapshotTemplate(definition) : undefined;
|
|
815
|
+
const snapshot = definition ? snapshotTemplate(definition, cfg.agentTemplates) : undefined;
|
|
516
816
|
if (!snapshot || snapshot.content_hash !== ref.content_hash)
|
|
517
817
|
throw new TaskRoomApplicationError('task_template_drift', `task template snapshot no longer matches ${ref.name}@${ref.version}`);
|
|
518
818
|
return snapshot;
|
|
519
819
|
}
|
|
520
|
-
async provisionRoom(cfg, task, template, onCreated) {
|
|
820
|
+
async provisionRoom(cfg, task, template, onCreated, launchDefinitions, policy = resolveRoomLaunchPolicy(template, undefined)) {
|
|
521
821
|
const rooms = cfg.rooms;
|
|
522
822
|
if (!rooms)
|
|
523
823
|
throw new ConfigError('rooms: configuration is required');
|
|
@@ -529,17 +829,52 @@ export class TaskRoomApplicationService {
|
|
|
529
829
|
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
530
830
|
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
531
831
|
})();
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
832
|
+
const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
|
|
833
|
+
let launchTemplate;
|
|
834
|
+
let room;
|
|
835
|
+
try {
|
|
836
|
+
launchTemplate = template ? (template.launch_snapshot_hash ? template
|
|
837
|
+
: sealTemplateSnapshot(template, cfg.agentTemplates ?? {}, launchDefinitions)) : undefined;
|
|
838
|
+
// Room publication window: for a task-bound room, hold the common
|
|
839
|
+
// task-operation lock across remote creation, local record creation, and
|
|
840
|
+
// the task link, so an accepted deletion linearizes strictly before
|
|
841
|
+
// (bounded task_deleting abort) or strictly after (record visible to the
|
|
842
|
+
// deletion scan). Lock order stays launch-snapshot → task-operation.
|
|
843
|
+
const publishRoom = async () => {
|
|
844
|
+
if (task.task_id) {
|
|
845
|
+
const fresh = readTask(task.task_id);
|
|
846
|
+
if (fresh.deletion?.status === 'pending')
|
|
847
|
+
throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
|
|
848
|
+
}
|
|
849
|
+
const created = await cowork.createRoom({
|
|
850
|
+
room_name: task.title, goal: task.goal?.trim() || task.title,
|
|
851
|
+
briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
|
|
852
|
+
quiet_membership: launchTemplate?.room?.quiet_membership,
|
|
853
|
+
anonymous: policy.anonymous,
|
|
854
|
+
});
|
|
855
|
+
const record = createRoomRecord({
|
|
856
|
+
room_id: created.room_id, room_name: task.title, room_identity_cid: created.identity_cid,
|
|
857
|
+
task_id: task.task_id, template_snapshot: launchTemplate, room_policy: policy,
|
|
858
|
+
});
|
|
859
|
+
onCreated(record);
|
|
860
|
+
return record;
|
|
861
|
+
};
|
|
862
|
+
room = task.task_id
|
|
863
|
+
? await withFileLock(taskOperationLockPath(task.task_id), publishRoom, {}, TASK_OPERATION_LOCK_STALE_MS)
|
|
864
|
+
: await publishRoom();
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
unlockSnapshot?.();
|
|
868
|
+
if (launchTemplate?.launch_snapshot_hash)
|
|
869
|
+
releaseLaunchSnapshot(launchTemplate.launch_snapshot_hash);
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
unlockSnapshot?.();
|
|
873
|
+
recordFleetAuditPresentation({ kind: 'room', operation: 'create', id: room.room_id,
|
|
874
|
+
name: room.room_name, previousState: 'none', newState: 'provisioning',
|
|
875
|
+
revision: room.created_at, taskId: room.task_id,
|
|
876
|
+
template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
877
|
+
participants: [] });
|
|
543
878
|
room = advanceSaga(room.room_id, 'create_room', 1);
|
|
544
879
|
if (attachOwner) {
|
|
545
880
|
try {
|
|
@@ -553,18 +888,39 @@ export class TaskRoomApplicationService {
|
|
|
553
888
|
const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
|
|
554
889
|
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
890
|
: 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
|
|
891
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
|
|
892
|
+
id: room.room_id, state: getRoomRecord(room.room_id)?.state ?? 'provisioning',
|
|
893
|
+
category: 'provision_failed', eventId: roomFailureEventId(getRoomRecord(room.room_id) ?? room) });
|
|
556
894
|
throw error;
|
|
557
895
|
}
|
|
558
896
|
}
|
|
559
897
|
room = advanceSaga(room.room_id, 'create_members', 3);
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
898
|
+
if (launchTemplate?.members.length)
|
|
899
|
+
try {
|
|
900
|
+
room = await (this.deps.provisionMembers ?? provisionMembers)({
|
|
901
|
+
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
|
|
902
|
+
binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
|
|
903
|
+
goal: task.task_id ? task.title : task.goal,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
const failed = getRoomRecord(room.room_id) ?? room;
|
|
908
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: failed.room_id,
|
|
909
|
+
state: failed.state, category: 'provision_failed', eventId: roomFailureEventId(failed) });
|
|
910
|
+
throw error;
|
|
911
|
+
}
|
|
912
|
+
else
|
|
913
|
+
room = activateRoom(room.room_id);
|
|
914
|
+
if (room.state === 'active')
|
|
915
|
+
recordFleetAuditPresentation({ kind: 'room', operation: 'activate', id: room.room_id,
|
|
916
|
+
name: room.room_name, previousState: 'provisioning', newState: 'active',
|
|
917
|
+
revision: room.activated_at ?? room.created_at, taskId: room.task_id,
|
|
918
|
+
template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
919
|
+
participants: roomParticipants(room) });
|
|
920
|
+
else
|
|
921
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: room.room_id,
|
|
922
|
+
state: room.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
|
|
923
|
+
if (task.task_id && !launchTemplate?.members.length)
|
|
568
924
|
activateTask(task.task_id);
|
|
569
925
|
return room;
|
|
570
926
|
}
|