@ours.network/fleet 0.19.0-nightly.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -11
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +12 -0
- package/dist/application/role-creation-service.js +3 -1
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +103 -10
- package/dist/build-info.json +5 -5
- package/dist/cli.js +41 -11
- package/dist/config.d.ts +21 -7
- package/dist/config.js +32 -18
- package/dist/creation.d.ts +1 -1
- package/dist/docs.d.ts +2 -2
- package/dist/docs.js +145 -24
- package/dist/doctor.js +10 -10
- package/dist/duration.js +1 -1
- package/dist/fleet-proxy.d.ts +5 -0
- package/dist/harness/acp-agent.js +11 -6
- package/dist/harness/claude-code.js +201 -8
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +72 -13
- package/dist/harness/types.d.ts +57 -6
- package/dist/isolation/bubblewrap.d.ts +4 -4
- package/dist/isolation/bubblewrap.js +4 -4
- package/dist/isolation/policy.d.ts +1 -1
- package/dist/isolation/policy.js +2 -2
- package/dist/isolation/registry.d.ts +3 -3
- package/dist/isolation/registry.js +5 -5
- package/dist/isolation/resources.d.ts +1 -1
- package/dist/isolation/resources.js +1 -1
- package/dist/isolation/types.d.ts +3 -3
- package/dist/loops/manager.d.ts +30 -1
- package/dist/loops/manager.js +69 -6
- package/dist/loops/state.d.ts +18 -0
- package/dist/loops/state.js +4 -0
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/monitor.d.ts +2 -2
- package/dist/monitor.js +4 -4
- package/dist/ops.d.ts +2 -2
- package/dist/ops.js +9 -9
- package/dist/owner-channel/channel.d.ts +17 -0
- package/dist/owner-channel/channel.js +104 -23
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +226 -99
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/rooms-tasks/cli.js +733 -221
- package/dist/rooms-tasks/close.d.ts +35 -0
- package/dist/rooms-tasks/close.js +182 -0
- package/dist/rooms-tasks/config.js +8 -5
- package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
- package/dist/rooms-tasks/cowork-adapter.js +116 -0
- package/dist/rooms-tasks/external-worker.d.ts +2 -0
- package/dist/rooms-tasks/external-worker.js +40 -0
- package/dist/rooms-tasks/index.d.ts +2 -0
- package/dist/rooms-tasks/index.js +2 -0
- package/dist/rooms-tasks/markdown.d.ts +49 -0
- package/dist/rooms-tasks/markdown.js +206 -0
- package/dist/rooms-tasks/member-startup.d.ts +51 -0
- package/dist/rooms-tasks/member-startup.js +151 -0
- package/dist/rooms-tasks/provision.d.ts +8 -0
- package/dist/rooms-tasks/provision.js +363 -97
- package/dist/rooms-tasks/room-state.d.ts +13 -1
- package/dist/rooms-tasks/room-state.js +149 -1
- package/dist/rooms-tasks/task-state.d.ts +13 -2
- package/dist/rooms-tasks/task-state.js +131 -3
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +56 -0
- package/dist/rooms-tasks/types.d.ts +109 -4
- package/dist/rooms-tasks/types.js +1 -1
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +252 -100
- package/dist/session/acp.d.ts +106 -2
- package/dist/session/acp.js +217 -14
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/control.d.ts +1 -1
- package/dist/session/conversation-normalizer.d.ts +7 -1
- package/dist/session/conversation-normalizer.js +154 -11
- package/dist/session/conversation-store.js +1 -1
- package/dist/session/conversation-types.d.ts +25 -6
- package/dist/session/types.d.ts +35 -0
- package/dist/spawn.d.ts +4 -2
- package/dist/spawn.js +39 -26
- package/dist/supervisor/launchd.d.ts +2 -2
- package/dist/supervisor/launchd.js +4 -4
- package/dist/supervisor/systemd.js +4 -4
- package/dist/supervisor/types.d.ts +1 -1
- package/dist/temp-lifecycle.d.ts +14 -2
- package/dist/temp-lifecycle.js +90 -0
- package/dist/watchdog/alerts.d.ts +2 -2
- package/dist/watchdog/alerts.js +2 -2
- package/dist/watchdog/briefing.d.ts +5 -5
- package/dist/watchdog/briefing.js +13 -6
- package/dist/watchdog/config.js +1 -1
- package/dist/watchdog/query.d.ts +2 -2
- package/dist/watchdog/query.js +2 -2
- package/dist/watchdog/report.js +2 -2
- package/dist/watchdog/run.d.ts +2 -2
- package/dist/watchdog/run.js +6 -6
- package/dist/watchdog/scheduler.d.ts +7 -7
- package/dist/watchdog/scheduler.js +10 -10
- package/dist/watchdog/service.d.ts +4 -4
- package/dist/watchdog/service.js +5 -6
- package/dist/watchdog/store.d.ts +4 -4
- package/dist/watchdog/store.js +4 -4
- package/dist/web/runtime.js +1 -1
- package/dist/web/server.js +1 -1
- package/dist/web/topology-promote.js +1 -1
- package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
- package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +7 -1
- package/dist/worklog.js +191 -39
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
4
4
|
import { stateRoot } from '../paths.js';
|
|
@@ -43,6 +43,12 @@ export function getRoomRecord(id) {
|
|
|
43
43
|
return undefined;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
/** Remove a terminal orchestration record from the live room inventory. */
|
|
47
|
+
export function deleteRoomRecord(id) {
|
|
48
|
+
const path = roomPath(id);
|
|
49
|
+
if (existsSync(path))
|
|
50
|
+
rmSync(path);
|
|
51
|
+
}
|
|
46
52
|
export function listRoomRecords(filter) {
|
|
47
53
|
const dir = roomsDir();
|
|
48
54
|
if (!existsSync(dir))
|
|
@@ -99,6 +105,48 @@ export function updateMemberSeats(id, seats) {
|
|
|
99
105
|
writeRoom(r);
|
|
100
106
|
return r;
|
|
101
107
|
}
|
|
108
|
+
export function updateRoomRoleBriefing(id, role, definition) {
|
|
109
|
+
const r = readRoom(id);
|
|
110
|
+
r.role_briefings = { ...(r.role_briefings ?? {}), [role]: definition };
|
|
111
|
+
writeRoom(r);
|
|
112
|
+
return r;
|
|
113
|
+
}
|
|
114
|
+
export function updateRoomHistoryCursor(id, cursor) {
|
|
115
|
+
const r = readRoom(id);
|
|
116
|
+
if (!Number.isSafeInteger(cursor) || cursor < (r.history_cursor ?? 0))
|
|
117
|
+
throw new RoomStateError(`room ${id} history cursor cannot move backward`);
|
|
118
|
+
r.history_cursor = cursor;
|
|
119
|
+
writeRoom(r);
|
|
120
|
+
return r;
|
|
121
|
+
}
|
|
122
|
+
const LAUNCH_ORDER = [
|
|
123
|
+
'pending', 'intent', 'launched', 'stopped', 'failed',
|
|
124
|
+
];
|
|
125
|
+
const BRIEFING_ORDER = [
|
|
126
|
+
'pending', 'relay_queued', 'relay_failed', 'acknowledged',
|
|
127
|
+
];
|
|
128
|
+
export function updateMemberStartup(id, roleName, update) {
|
|
129
|
+
const r = readRoom(id);
|
|
130
|
+
const seat = r.member_seats.find(candidate => candidate.role_name === roleName);
|
|
131
|
+
if (!seat)
|
|
132
|
+
throw new RoomStateError(`room ${id} has no recorded member ${roleName}`);
|
|
133
|
+
if (update.launch && seat.launch
|
|
134
|
+
&& LAUNCH_ORDER.indexOf(update.launch.state) < LAUNCH_ORDER.indexOf(seat.launch.state)
|
|
135
|
+
&& !(seat.launch.state === 'stopped' && update.launch.state === 'intent')
|
|
136
|
+
&& !(seat.launch.state === 'failed' && update.launch.state === 'intent')) {
|
|
137
|
+
throw new RoomStateError(`room ${id} member ${roleName} launch cannot move backward to ${update.launch.state}`);
|
|
138
|
+
}
|
|
139
|
+
if (update.briefing && seat.briefing
|
|
140
|
+
&& BRIEFING_ORDER.indexOf(update.briefing.state) < BRIEFING_ORDER.indexOf(seat.briefing.state)) {
|
|
141
|
+
throw new RoomStateError(`room ${id} member ${roleName} briefing cannot move backward to ${update.briefing.state}`);
|
|
142
|
+
}
|
|
143
|
+
if (update.launch)
|
|
144
|
+
seat.launch = update.launch;
|
|
145
|
+
if (update.briefing)
|
|
146
|
+
seat.briefing = update.briefing;
|
|
147
|
+
writeRoom(r);
|
|
148
|
+
return r;
|
|
149
|
+
}
|
|
102
150
|
export function activateRoom(id) {
|
|
103
151
|
const r = readRoom(id);
|
|
104
152
|
r.state = 'active';
|
|
@@ -114,6 +162,106 @@ export function closeRoom(id) {
|
|
|
114
162
|
return r;
|
|
115
163
|
r.state = 'closed';
|
|
116
164
|
r.closed_at = new Date().toISOString();
|
|
165
|
+
r.close = {
|
|
166
|
+
phase: 'completed',
|
|
167
|
+
accepted_at: r.close?.accepted_at ?? r.closed_at,
|
|
168
|
+
...(r.close?.first_failure ? { first_failure: r.close.first_failure } : {}),
|
|
169
|
+
...(r.close?.first_recovery_hint
|
|
170
|
+
? { first_recovery_hint: r.close.first_recovery_hint }
|
|
171
|
+
: {}),
|
|
172
|
+
};
|
|
173
|
+
writeRoom(r);
|
|
174
|
+
return r;
|
|
175
|
+
}
|
|
176
|
+
export function beginRoomClose(id) {
|
|
177
|
+
const r = readRoom(id);
|
|
178
|
+
if (r.state === 'closed')
|
|
179
|
+
return r;
|
|
180
|
+
const acceptedAt = r.close?.accepted_at ?? new Date().toISOString();
|
|
181
|
+
r.state = 'closing';
|
|
182
|
+
r.close = {
|
|
183
|
+
phase: r.close?.phase ?? 'retire_members',
|
|
184
|
+
accepted_at: acceptedAt,
|
|
185
|
+
...(r.close?.error ? { error: r.close.error } : {}),
|
|
186
|
+
...(r.close?.error_at ? { error_at: r.close.error_at } : {}),
|
|
187
|
+
...(r.close?.recovery_hint ? { recovery_hint: r.close.recovery_hint } : {}),
|
|
188
|
+
...(r.close?.first_failure ? { first_failure: r.close.first_failure } : {}),
|
|
189
|
+
...(r.close?.first_recovery_hint
|
|
190
|
+
? { first_recovery_hint: r.close.first_recovery_hint }
|
|
191
|
+
: {}),
|
|
192
|
+
};
|
|
193
|
+
writeRoom(r);
|
|
194
|
+
return r;
|
|
195
|
+
}
|
|
196
|
+
const RETIREMENT_ORDER = [
|
|
197
|
+
'stop_requested', 'liveness_absent', 'archive_secured', 'identity_absent',
|
|
198
|
+
];
|
|
199
|
+
export function advanceMemberRetirement(id, roleName, phase, launchId, archivePath) {
|
|
200
|
+
const r = readRoom(id);
|
|
201
|
+
if (r.state !== 'closing')
|
|
202
|
+
throw new RoomStateError(`room ${id} is not closing`);
|
|
203
|
+
const seat = r.member_seats.find(candidate => candidate.role_name === roleName);
|
|
204
|
+
if (!seat)
|
|
205
|
+
throw new RoomStateError(`room ${id} has no recorded member ${roleName}`);
|
|
206
|
+
const previous = seat.retirement;
|
|
207
|
+
if (previous && previous.launch_id !== launchId) {
|
|
208
|
+
throw new RoomStateError(`room ${id} member ${roleName} launch changed from ${previous.launch_id} to ${launchId}`);
|
|
209
|
+
}
|
|
210
|
+
const nextIndex = RETIREMENT_ORDER.indexOf(phase);
|
|
211
|
+
const previousIndex = previous ? RETIREMENT_ORDER.indexOf(previous.phase) : -1;
|
|
212
|
+
if (nextIndex < previousIndex) {
|
|
213
|
+
throw new RoomStateError(`room ${id} member ${roleName} retirement cannot move backward to ${phase}`);
|
|
214
|
+
}
|
|
215
|
+
seat.retirement = {
|
|
216
|
+
phase,
|
|
217
|
+
launch_id: launchId,
|
|
218
|
+
updated_at: new Date().toISOString(),
|
|
219
|
+
...(archivePath ?? previous?.archive_path
|
|
220
|
+
? { archive_path: archivePath ?? previous?.archive_path }
|
|
221
|
+
: {}),
|
|
222
|
+
};
|
|
223
|
+
if (phase === 'identity_absent')
|
|
224
|
+
seat.seat_state = 'removed';
|
|
225
|
+
writeRoom(r);
|
|
226
|
+
return r;
|
|
227
|
+
}
|
|
228
|
+
export function advanceRoomClose(id, phase) {
|
|
229
|
+
const r = readRoom(id);
|
|
230
|
+
if (r.state === 'closed')
|
|
231
|
+
return r;
|
|
232
|
+
if (r.state !== 'closing' || !r.close)
|
|
233
|
+
throw new RoomStateError(`room ${id} is not closing`);
|
|
234
|
+
const order = ['retire_members', 'close_cowork', 'completed'];
|
|
235
|
+
if (order.indexOf(phase) < order.indexOf(r.close.phase)) {
|
|
236
|
+
throw new RoomStateError(`room ${id} close cannot move backward to ${phase}`);
|
|
237
|
+
}
|
|
238
|
+
const progressed = order.indexOf(phase) > order.indexOf(r.close.phase);
|
|
239
|
+
r.close = {
|
|
240
|
+
phase,
|
|
241
|
+
accepted_at: r.close.accepted_at,
|
|
242
|
+
...(!progressed && r.close.error ? { error: r.close.error } : {}),
|
|
243
|
+
...(!progressed && r.close.error_at ? { error_at: r.close.error_at } : {}),
|
|
244
|
+
...(!progressed && r.close.recovery_hint
|
|
245
|
+
? { recovery_hint: r.close.recovery_hint }
|
|
246
|
+
: {}),
|
|
247
|
+
...(r.close.first_failure ? { first_failure: r.close.first_failure } : {}),
|
|
248
|
+
...(r.close.first_recovery_hint
|
|
249
|
+
? { first_recovery_hint: r.close.first_recovery_hint }
|
|
250
|
+
: {}),
|
|
251
|
+
};
|
|
252
|
+
writeRoom(r);
|
|
253
|
+
return r;
|
|
254
|
+
}
|
|
255
|
+
export function setRoomCloseError(id, error, recoveryHint) {
|
|
256
|
+
const r = readRoom(id);
|
|
257
|
+
if ((r.state !== 'closing' && r.state !== 'closed') || !r.close)
|
|
258
|
+
throw new RoomStateError(`room ${id} is not closing or awaiting deletion`);
|
|
259
|
+
r.close.first_failure ??= error;
|
|
260
|
+
r.close.first_recovery_hint ??= recoveryHint;
|
|
261
|
+
r.close.error = error;
|
|
262
|
+
const previousErrorAt = Date.parse(r.close.error_at ?? '');
|
|
263
|
+
r.close.error_at = new Date(Math.max(Date.now(), Number.isFinite(previousErrorAt) ? previousErrorAt + 1 : 0)).toISOString();
|
|
264
|
+
r.close.recovery_hint = recoveryHint;
|
|
117
265
|
writeRoom(r);
|
|
118
266
|
return r;
|
|
119
267
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole } from './types.js';
|
|
1
|
+
import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent } from './types.js';
|
|
2
2
|
export declare const tasksDir: () => string;
|
|
3
3
|
export declare class TaskStateError extends Error {
|
|
4
4
|
}
|
|
@@ -26,8 +26,19 @@ export declare function unblockTask(id: string): TaskRecord;
|
|
|
26
26
|
export declare function reviewTask(id: string): TaskRecord;
|
|
27
27
|
export declare function completeTask(id: string, outcome?: TaskOutcome): TaskRecord;
|
|
28
28
|
export declare function cancelTask(id: string): TaskRecord;
|
|
29
|
+
/** Persist the first terminal request. Callers serialize this with the task-operation lock. */
|
|
30
|
+
export declare function beginTaskTerminalIntent(id: string, input: {
|
|
31
|
+
kind: TaskTerminalIntent['kind'];
|
|
32
|
+
roomId?: string;
|
|
33
|
+
outcome?: TaskOutcome;
|
|
34
|
+
}): TaskRecord;
|
|
35
|
+
/** Record an actionable failure without claiming the requested terminal state. */
|
|
36
|
+
export declare function setTaskTerminalIntentError(id: string, error: string, recoveryHint: string): TaskRecord;
|
|
37
|
+
/** Atomically publish the task terminal state and settled audit cursor. */
|
|
38
|
+
export declare function finishTaskTerminalIntent(id: string): TaskRecord;
|
|
29
39
|
export declare function failTask(id: string, error: string): TaskRecord;
|
|
30
40
|
export declare function updateTaskRoom(id: string, roomId: string, roomIdentityCid: string): TaskRecord;
|
|
31
41
|
export declare function updateTaskTemplate(id: string, template: TaskTemplateRef): TaskRecord;
|
|
32
42
|
export declare function updateTaskMembers(id: string, members: TaskMemberRole[]): TaskRecord;
|
|
33
|
-
|
|
43
|
+
/** Remove a completed task from Fleet's backlog. Missing tasks are an idempotent no-op. */
|
|
44
|
+
export declare function deleteTask(id: string): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync,
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
@@ -11,8 +11,18 @@ function generateTaskId() {
|
|
|
11
11
|
const rand = randomUUID().replace(/-/g, '').slice(0, 8);
|
|
12
12
|
return `${ts}${rand}`;
|
|
13
13
|
}
|
|
14
|
+
const TASK_ID_PATTERN = /^[0-9a-z]{9}[0-9a-f]{8}$/;
|
|
14
15
|
export class TaskStateError extends Error {
|
|
15
16
|
}
|
|
17
|
+
function assertCanonicalTaskId(id) {
|
|
18
|
+
if (!TASK_ID_PATTERN.test(id)) {
|
|
19
|
+
throw new TaskStateError('invalid task ID: expected the canonical 17-character lowercase ID');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function isNotFoundError(error) {
|
|
23
|
+
return error instanceof Error && 'code' in error
|
|
24
|
+
&& error.code === 'ENOENT';
|
|
25
|
+
}
|
|
16
26
|
function readTask(id) {
|
|
17
27
|
const p = taskPath(id);
|
|
18
28
|
if (!existsSync(p))
|
|
@@ -37,6 +47,11 @@ function assertTransition(from, to) {
|
|
|
37
47
|
if (!VALID_TRANSITIONS[from].includes(to))
|
|
38
48
|
throw new TaskStateError(`cannot transition from '${from}' to '${to}'`);
|
|
39
49
|
}
|
|
50
|
+
function assertNoPendingTerminalIntent(task) {
|
|
51
|
+
if (task.terminal_intent?.status === 'pending') {
|
|
52
|
+
throw new TaskStateError(`task ${task.task_id} has a pending '${task.terminal_intent.kind}' terminal intent`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
40
55
|
export function createTask(input) {
|
|
41
56
|
const key = input.idempotency_key ?? randomUUID();
|
|
42
57
|
const existing = findByIdempotencyKey(key);
|
|
@@ -85,6 +100,7 @@ export function findByIdempotencyKey(key) {
|
|
|
85
100
|
}
|
|
86
101
|
export function startTask(id) {
|
|
87
102
|
const t = readTask(id);
|
|
103
|
+
assertNoPendingTerminalIntent(t);
|
|
88
104
|
assertTransition(t.state, 'provisioning');
|
|
89
105
|
t.state = 'provisioning';
|
|
90
106
|
t.started_at = new Date().toISOString();
|
|
@@ -93,6 +109,7 @@ export function startTask(id) {
|
|
|
93
109
|
}
|
|
94
110
|
export function activateTask(id) {
|
|
95
111
|
const t = readTask(id);
|
|
112
|
+
assertNoPendingTerminalIntent(t);
|
|
96
113
|
assertTransition(t.state, 'active');
|
|
97
114
|
t.state = 'active';
|
|
98
115
|
delete t.blocked;
|
|
@@ -101,6 +118,7 @@ export function activateTask(id) {
|
|
|
101
118
|
}
|
|
102
119
|
export function blockTask(id, reason) {
|
|
103
120
|
const t = readTask(id);
|
|
121
|
+
assertNoPendingTerminalIntent(t);
|
|
104
122
|
if (TASK_TERMINAL_STATES.includes(t.state))
|
|
105
123
|
throw new TaskStateError(`cannot block a '${t.state}' task`);
|
|
106
124
|
t.blocked = { reason, at: new Date().toISOString() };
|
|
@@ -109,6 +127,7 @@ export function blockTask(id, reason) {
|
|
|
109
127
|
}
|
|
110
128
|
export function unblockTask(id) {
|
|
111
129
|
const t = readTask(id);
|
|
130
|
+
assertNoPendingTerminalIntent(t);
|
|
112
131
|
if (!t.blocked)
|
|
113
132
|
throw new TaskStateError('task is not blocked');
|
|
114
133
|
delete t.blocked;
|
|
@@ -117,6 +136,7 @@ export function unblockTask(id) {
|
|
|
117
136
|
}
|
|
118
137
|
export function reviewTask(id) {
|
|
119
138
|
const t = readTask(id);
|
|
139
|
+
assertNoPendingTerminalIntent(t);
|
|
120
140
|
assertTransition(t.state, 'review');
|
|
121
141
|
t.state = 'review';
|
|
122
142
|
delete t.blocked;
|
|
@@ -125,6 +145,7 @@ export function reviewTask(id) {
|
|
|
125
145
|
}
|
|
126
146
|
export function completeTask(id, outcome) {
|
|
127
147
|
const t = readTask(id);
|
|
148
|
+
assertNoPendingTerminalIntent(t);
|
|
128
149
|
assertTransition(t.state, 'done');
|
|
129
150
|
t.state = 'done';
|
|
130
151
|
t.ended_at = new Date().toISOString();
|
|
@@ -136,6 +157,7 @@ export function completeTask(id, outcome) {
|
|
|
136
157
|
}
|
|
137
158
|
export function cancelTask(id) {
|
|
138
159
|
const t = readTask(id);
|
|
160
|
+
assertNoPendingTerminalIntent(t);
|
|
139
161
|
if (!TASK_CANCELLABLE_STATES.includes(t.state))
|
|
140
162
|
throw new TaskStateError(`cannot cancel a '${t.state}' task`);
|
|
141
163
|
t.state = 'cancelled';
|
|
@@ -144,8 +166,90 @@ export function cancelTask(id) {
|
|
|
144
166
|
writeTask(t);
|
|
145
167
|
return t;
|
|
146
168
|
}
|
|
169
|
+
function sameOutcome(left, right) {
|
|
170
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
171
|
+
}
|
|
172
|
+
/** Persist the first terminal request. Callers serialize this with the task-operation lock. */
|
|
173
|
+
export function beginTaskTerminalIntent(id, input) {
|
|
174
|
+
const t = readTask(id);
|
|
175
|
+
const existing = t.terminal_intent;
|
|
176
|
+
if (existing) {
|
|
177
|
+
if (existing.kind !== input.kind || existing.room_id !== input.roomId
|
|
178
|
+
|| !sameOutcome(existing.outcome, input.outcome)) {
|
|
179
|
+
throw new TaskStateError(`task ${id} already has a conflicting '${existing.kind}' terminal intent`);
|
|
180
|
+
}
|
|
181
|
+
return t;
|
|
182
|
+
}
|
|
183
|
+
if (TASK_TERMINAL_STATES.includes(t.state)) {
|
|
184
|
+
throw new TaskStateError(`task ${id} is already in terminal state '${t.state}'`);
|
|
185
|
+
}
|
|
186
|
+
if (input.kind === 'done' && t.state !== 'review') {
|
|
187
|
+
throw new TaskStateError(`cannot transition from '${t.state}' to 'done'`);
|
|
188
|
+
}
|
|
189
|
+
if (input.kind === 'cancelled' && !TASK_CANCELLABLE_STATES.includes(t.state)) {
|
|
190
|
+
throw new TaskStateError(`cannot cancel a '${t.state}' task`);
|
|
191
|
+
}
|
|
192
|
+
if (input.roomId !== undefined && t.room_id !== input.roomId) {
|
|
193
|
+
throw new TaskStateError(`task ${id} is not tied to room ${input.roomId}`);
|
|
194
|
+
}
|
|
195
|
+
t.terminal_intent = {
|
|
196
|
+
kind: input.kind,
|
|
197
|
+
status: 'pending',
|
|
198
|
+
room_id: input.roomId,
|
|
199
|
+
outcome: input.outcome,
|
|
200
|
+
accepted_at: new Date().toISOString(),
|
|
201
|
+
};
|
|
202
|
+
writeTask(t);
|
|
203
|
+
return t;
|
|
204
|
+
}
|
|
205
|
+
/** Record an actionable failure without claiming the requested terminal state. */
|
|
206
|
+
export function setTaskTerminalIntentError(id, error, recoveryHint) {
|
|
207
|
+
const t = readTask(id);
|
|
208
|
+
if (!t.terminal_intent)
|
|
209
|
+
throw new TaskStateError(`task ${id} has no terminal intent`);
|
|
210
|
+
if (t.terminal_intent.status === 'settled')
|
|
211
|
+
return t;
|
|
212
|
+
t.terminal_intent.first_failure ??= error;
|
|
213
|
+
t.terminal_intent.first_recovery_hint ??= recoveryHint;
|
|
214
|
+
t.terminal_intent.error = error;
|
|
215
|
+
const previousErrorAt = Date.parse(t.terminal_intent.error_at ?? '');
|
|
216
|
+
t.terminal_intent.error_at = new Date(Math.max(Date.now(), Number.isFinite(previousErrorAt) ? previousErrorAt + 1 : 0)).toISOString();
|
|
217
|
+
t.terminal_intent.recovery_hint = recoveryHint;
|
|
218
|
+
writeTask(t);
|
|
219
|
+
return t;
|
|
220
|
+
}
|
|
221
|
+
/** Atomically publish the task terminal state and settled audit cursor. */
|
|
222
|
+
export function finishTaskTerminalIntent(id) {
|
|
223
|
+
const t = readTask(id);
|
|
224
|
+
const intent = t.terminal_intent;
|
|
225
|
+
if (!intent)
|
|
226
|
+
throw new TaskStateError(`task ${id} has no terminal intent`);
|
|
227
|
+
if (intent.status === 'settled')
|
|
228
|
+
return t;
|
|
229
|
+
if (TASK_TERMINAL_STATES.includes(t.state)) {
|
|
230
|
+
throw new TaskStateError(`task ${id} reached terminal state '${t.state}' outside its intent`);
|
|
231
|
+
}
|
|
232
|
+
if (intent.kind === 'done')
|
|
233
|
+
assertTransition(t.state, 'done');
|
|
234
|
+
else if (!TASK_CANCELLABLE_STATES.includes(t.state)) {
|
|
235
|
+
throw new TaskStateError(`cannot cancel a '${t.state}' task`);
|
|
236
|
+
}
|
|
237
|
+
t.state = intent.kind;
|
|
238
|
+
t.ended_at = new Date().toISOString();
|
|
239
|
+
delete t.blocked;
|
|
240
|
+
if (intent.outcome)
|
|
241
|
+
t.outcome = intent.outcome;
|
|
242
|
+
intent.status = 'settled';
|
|
243
|
+
intent.settled_at = t.ended_at;
|
|
244
|
+
delete intent.error;
|
|
245
|
+
delete intent.error_at;
|
|
246
|
+
delete intent.recovery_hint;
|
|
247
|
+
writeTask(t);
|
|
248
|
+
return t;
|
|
249
|
+
}
|
|
147
250
|
export function failTask(id, error) {
|
|
148
251
|
const t = readTask(id);
|
|
252
|
+
assertNoPendingTerminalIntent(t);
|
|
149
253
|
assertTransition(t.state, 'failed');
|
|
150
254
|
t.state = 'failed';
|
|
151
255
|
t.ended_at = new Date().toISOString();
|
|
@@ -156,6 +260,7 @@ export function failTask(id, error) {
|
|
|
156
260
|
}
|
|
157
261
|
export function updateTaskRoom(id, roomId, roomIdentityCid) {
|
|
158
262
|
const t = readTask(id);
|
|
263
|
+
assertNoPendingTerminalIntent(t);
|
|
159
264
|
t.room_id = roomId;
|
|
160
265
|
t.room_identity_cid = roomIdentityCid;
|
|
161
266
|
writeTask(t);
|
|
@@ -163,18 +268,41 @@ export function updateTaskRoom(id, roomId, roomIdentityCid) {
|
|
|
163
268
|
}
|
|
164
269
|
export function updateTaskTemplate(id, template) {
|
|
165
270
|
const t = readTask(id);
|
|
271
|
+
assertNoPendingTerminalIntent(t);
|
|
166
272
|
t.template = template;
|
|
167
273
|
writeTask(t);
|
|
168
274
|
return t;
|
|
169
275
|
}
|
|
170
276
|
export function updateTaskMembers(id, members) {
|
|
171
277
|
const t = readTask(id);
|
|
278
|
+
assertNoPendingTerminalIntent(t);
|
|
172
279
|
t.member_roles = members;
|
|
173
280
|
writeTask(t);
|
|
174
281
|
return t;
|
|
175
282
|
}
|
|
283
|
+
/** Remove a completed task from Fleet's backlog. Missing tasks are an idempotent no-op. */
|
|
176
284
|
export function deleteTask(id) {
|
|
285
|
+
assertCanonicalTaskId(id);
|
|
177
286
|
const p = taskPath(id);
|
|
178
|
-
|
|
179
|
-
|
|
287
|
+
let task;
|
|
288
|
+
try {
|
|
289
|
+
task = JSON.parse(readFileSync(p, 'utf8'));
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (isNotFoundError(error))
|
|
293
|
+
return false;
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
if (task.state !== 'done') {
|
|
297
|
+
throw new TaskStateError(`cannot delete a '${task.state}' task; only 'done' tasks can be deleted`);
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
unlinkSync(p);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (isNotFoundError(error))
|
|
304
|
+
return false;
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
180
308
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type RoomCloseDeps } from './close.js';
|
|
2
|
+
import type { CoworkAdapter } from './cowork-adapter.js';
|
|
3
|
+
import type { TaskOutcome, TaskRecord, TaskTerminalIntent } from './types.js';
|
|
4
|
+
export interface TaskTerminalInput {
|
|
5
|
+
taskId: string;
|
|
6
|
+
kind: TaskTerminalIntent['kind'];
|
|
7
|
+
roomId?: string;
|
|
8
|
+
outcome?: TaskOutcome;
|
|
9
|
+
}
|
|
10
|
+
/** Persist first-wins terminal intent before any acknowledgement or close effect. */
|
|
11
|
+
export declare function acceptTaskTerminalIntent(input: TaskTerminalInput): Promise<TaskRecord>;
|
|
12
|
+
export interface SettleTaskTerminalDeps {
|
|
13
|
+
roomClose?: RoomCloseDeps;
|
|
14
|
+
afterRoomClosed?(): void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** Delete the recorded room, then atomically settle the persisted task intent. */
|
|
17
|
+
export declare function settleTaskTerminalIntent(input: {
|
|
18
|
+
taskId: string;
|
|
19
|
+
cowork: Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'>;
|
|
20
|
+
deps?: SettleTaskTerminalDeps;
|
|
21
|
+
}): Promise<TaskRecord>;
|
|
22
|
+
/** Persist failure to launch an external settle worker under the same task lock. */
|
|
23
|
+
export declare function recordTaskTerminalIntentError(taskId: string, error: string, recoveryHint: string): Promise<TaskRecord>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { withFileLock } from '../atomic-file.js';
|
|
3
|
+
import { stateRoot } from '../paths.js';
|
|
4
|
+
import { deleteManagedRoom } from './close.js';
|
|
5
|
+
import { beginTaskTerminalIntent, finishTaskTerminalIntent, getTask, setTaskTerminalIntentError, } from './task-state.js';
|
|
6
|
+
import { getRoomRecord } from './room-state.js';
|
|
7
|
+
const TASK_OPERATION_LOCK_STALE_MS = 10 * 60_000;
|
|
8
|
+
function taskOperationLockPath(taskId) {
|
|
9
|
+
return join(stateRoot(), 'locks', 'task-terminal', encodeURIComponent(taskId));
|
|
10
|
+
}
|
|
11
|
+
function errorText(error) {
|
|
12
|
+
return error instanceof Error ? error.message : String(error);
|
|
13
|
+
}
|
|
14
|
+
/** Persist first-wins terminal intent before any acknowledgement or close effect. */
|
|
15
|
+
export function acceptTaskTerminalIntent(input) {
|
|
16
|
+
return withFileLock(taskOperationLockPath(input.taskId), () => {
|
|
17
|
+
const accepted = beginTaskTerminalIntent(input.taskId, input);
|
|
18
|
+
if (!input.roomId)
|
|
19
|
+
return finishTaskTerminalIntent(input.taskId);
|
|
20
|
+
return accepted;
|
|
21
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
22
|
+
}
|
|
23
|
+
/** Delete the recorded room, then atomically settle the persisted task intent. */
|
|
24
|
+
export function settleTaskTerminalIntent(input) {
|
|
25
|
+
return withFileLock(taskOperationLockPath(input.taskId), async () => {
|
|
26
|
+
const current = getTask(input.taskId);
|
|
27
|
+
const intent = current.terminal_intent;
|
|
28
|
+
if (!intent)
|
|
29
|
+
throw new Error(`task ${input.taskId} has no accepted terminal intent`);
|
|
30
|
+
if (intent.status === 'settled')
|
|
31
|
+
return current;
|
|
32
|
+
try {
|
|
33
|
+
if (intent.room_id) {
|
|
34
|
+
if (getRoomRecord(intent.room_id)) {
|
|
35
|
+
const deleted = await deleteManagedRoom({
|
|
36
|
+
roomId: intent.room_id,
|
|
37
|
+
cowork: input.cowork,
|
|
38
|
+
deps: input.deps?.roomClose,
|
|
39
|
+
});
|
|
40
|
+
if (!deleted.deleted)
|
|
41
|
+
throw new Error(`room ${intent.room_id} was not deleted`);
|
|
42
|
+
}
|
|
43
|
+
await input.deps?.afterRoomClosed?.();
|
|
44
|
+
}
|
|
45
|
+
return finishTaskTerminalIntent(input.taskId);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
setTaskTerminalIntentError(input.taskId, errorText(error), `Retry 'ours-fleet task recover ${input.taskId}'.`);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
52
|
+
}
|
|
53
|
+
/** Persist failure to launch an external settle worker under the same task lock. */
|
|
54
|
+
export function recordTaskTerminalIntentError(taskId, error, recoveryHint) {
|
|
55
|
+
return withFileLock(taskOperationLockPath(taskId), () => setTaskTerminalIntentError(taskId, error, recoveryHint), {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
56
|
+
}
|
|
@@ -31,6 +31,19 @@ export interface TaskOutcome {
|
|
|
31
31
|
summary: string;
|
|
32
32
|
artifacts?: string[];
|
|
33
33
|
}
|
|
34
|
+
export interface TaskTerminalIntent {
|
|
35
|
+
kind: 'done' | 'cancelled';
|
|
36
|
+
status: 'pending' | 'settled';
|
|
37
|
+
room_id?: string;
|
|
38
|
+
outcome?: TaskOutcome;
|
|
39
|
+
accepted_at: string;
|
|
40
|
+
settled_at?: string;
|
|
41
|
+
error?: string;
|
|
42
|
+
error_at?: string;
|
|
43
|
+
recovery_hint?: string;
|
|
44
|
+
first_failure?: string;
|
|
45
|
+
first_recovery_hint?: string;
|
|
46
|
+
}
|
|
34
47
|
export interface TaskRecord {
|
|
35
48
|
task_id: string;
|
|
36
49
|
title: string;
|
|
@@ -49,22 +62,112 @@ export interface TaskRecord {
|
|
|
49
62
|
started_at?: string;
|
|
50
63
|
ended_at?: string;
|
|
51
64
|
outcome?: TaskOutcome;
|
|
65
|
+
terminal_intent?: TaskTerminalIntent;
|
|
52
66
|
}
|
|
53
67
|
export type RoomOrchestrationState = 'provisioning' | 'active' | 'closing' | 'closed';
|
|
54
|
-
export type SagaPhase = 'persist_intent' | 'create_room' | 'attach_owner' | 'create_members' | 'join_role_groups' | 'wait_seats' | 'launch_work' | 'activate' | 'completed' | 'failed';
|
|
68
|
+
export type SagaPhase = 'persist_intent' | 'create_room' | 'attach_owner' | 'create_members' | 'configure_briefings' | 'join_role_groups' | 'wait_seats' | 'launch_work' | 'wait_briefing_acks' | 'activate' | 'completed' | 'failed';
|
|
55
69
|
export interface SagaCursor {
|
|
56
70
|
phase: SagaPhase;
|
|
57
71
|
step_index: number;
|
|
58
72
|
error?: string;
|
|
59
73
|
recovery_hint?: string;
|
|
60
74
|
}
|
|
61
|
-
export type ProvisioningDetail = 'waiting_cowork' | 'waiting_owner_invite' | 'owner_cid_mismatch' | 'member_failed' | 'waiting_seats' | 'uncertain';
|
|
75
|
+
export type ProvisioningDetail = 'waiting_cowork' | 'waiting_owner_invite' | 'owner_cid_mismatch' | 'member_failed' | 'waiting_seats' | 'waiting_briefing_delivery' | 'waiting_briefing_acks' | 'briefing_delivery_failed' | 'uncertain';
|
|
76
|
+
export interface RoomRoleBriefingDefinition {
|
|
77
|
+
role: string;
|
|
78
|
+
text: string;
|
|
79
|
+
sha256: string;
|
|
80
|
+
version?: number;
|
|
81
|
+
state: 'pending' | 'configured' | 'failed';
|
|
82
|
+
attempts: number;
|
|
83
|
+
updated_at: string;
|
|
84
|
+
last_error?: string;
|
|
85
|
+
}
|
|
86
|
+
export interface RoomMemberLaunchState {
|
|
87
|
+
state: 'pending' | 'intent' | 'launched' | 'stopped' | 'failed';
|
|
88
|
+
attempt: number;
|
|
89
|
+
action_id?: string;
|
|
90
|
+
mission_sha256?: string;
|
|
91
|
+
launch_id?: string;
|
|
92
|
+
updated_at: string;
|
|
93
|
+
error?: string;
|
|
94
|
+
}
|
|
95
|
+
export interface RoomMemberBriefingState {
|
|
96
|
+
role: string;
|
|
97
|
+
state: 'pending' | 'relay_queued' | 'relay_failed' | 'acknowledged';
|
|
98
|
+
message_id?: string;
|
|
99
|
+
relay_intent_record_id?: string;
|
|
100
|
+
relay_result_record_id?: string;
|
|
101
|
+
relay_wire_id?: string;
|
|
102
|
+
checked_at?: string;
|
|
103
|
+
acknowledged_at?: string;
|
|
104
|
+
acknowledgement_message_id?: string;
|
|
105
|
+
acknowledgement_seq?: number;
|
|
106
|
+
rejected_ack_count: number;
|
|
107
|
+
last_rejected_ack_reason?: string;
|
|
108
|
+
last_rejected_ack_seq?: number;
|
|
109
|
+
last_processed_seq?: number;
|
|
110
|
+
}
|
|
111
|
+
export type RoomHistoryEvidence = {
|
|
112
|
+
kind: 'message';
|
|
113
|
+
seq: number;
|
|
114
|
+
record_id: string;
|
|
115
|
+
at: string;
|
|
116
|
+
message_id: string;
|
|
117
|
+
category: 'role_briefing' | 'chat';
|
|
118
|
+
author: {
|
|
119
|
+
identity: string;
|
|
120
|
+
display_name: string;
|
|
121
|
+
role: string;
|
|
122
|
+
};
|
|
123
|
+
text: string;
|
|
124
|
+
recipient_identities: string[];
|
|
125
|
+
briefing_role?: string;
|
|
126
|
+
briefing_version?: number;
|
|
127
|
+
} | {
|
|
128
|
+
kind: 'relay_intent';
|
|
129
|
+
seq: number;
|
|
130
|
+
record_id: string;
|
|
131
|
+
at: string;
|
|
132
|
+
message_id: string;
|
|
133
|
+
recipient_identity: string;
|
|
134
|
+
} | {
|
|
135
|
+
kind: 'relay_result';
|
|
136
|
+
seq: number;
|
|
137
|
+
record_id: string;
|
|
138
|
+
at: string;
|
|
139
|
+
intent_record_id: string;
|
|
140
|
+
message_id: string;
|
|
141
|
+
recipient_identity: string;
|
|
142
|
+
status: 'queued' | 'send_failed' | 'skipped_removed';
|
|
143
|
+
wire_id?: string;
|
|
144
|
+
};
|
|
145
|
+
export type MemberRetirementPhase = 'stop_requested' | 'liveness_absent' | 'archive_secured' | 'identity_absent';
|
|
146
|
+
export interface MemberRetirement {
|
|
147
|
+
phase: MemberRetirementPhase;
|
|
148
|
+
launch_id: string;
|
|
149
|
+
updated_at: string;
|
|
150
|
+
archive_path?: string;
|
|
151
|
+
}
|
|
152
|
+
export type RoomClosePhase = 'retire_members' | 'close_cowork' | 'completed';
|
|
153
|
+
export interface RoomCloseCursor {
|
|
154
|
+
phase: RoomClosePhase;
|
|
155
|
+
accepted_at: string;
|
|
156
|
+
error?: string;
|
|
157
|
+
error_at?: string;
|
|
158
|
+
recovery_hint?: string;
|
|
159
|
+
first_failure?: string;
|
|
160
|
+
first_recovery_hint?: string;
|
|
161
|
+
}
|
|
62
162
|
export interface RoomMemberSeat {
|
|
63
163
|
role_name: string;
|
|
64
164
|
identity_cid: string;
|
|
65
165
|
slot: string;
|
|
66
166
|
cowork_role: string;
|
|
67
167
|
seat_state: 'pending' | 'active' | 'removed';
|
|
168
|
+
launch?: RoomMemberLaunchState;
|
|
169
|
+
briefing?: RoomMemberBriefingState;
|
|
170
|
+
retirement?: MemberRetirement;
|
|
68
171
|
}
|
|
69
172
|
export interface RoomOrchestrationRecord {
|
|
70
173
|
room_id: string;
|
|
@@ -77,11 +180,14 @@ export interface RoomOrchestrationRecord {
|
|
|
77
180
|
provisioning_detail?: ProvisioningDetail;
|
|
78
181
|
owner_seat_cid?: string;
|
|
79
182
|
owner_invite_fingerprint?: string;
|
|
183
|
+
role_briefings?: Record<string, RoomRoleBriefingDefinition>;
|
|
184
|
+
history_cursor?: number;
|
|
80
185
|
member_seats: RoomMemberSeat[];
|
|
81
186
|
state: RoomOrchestrationState;
|
|
82
187
|
created_at: string;
|
|
83
188
|
activated_at?: string;
|
|
84
189
|
closed_at?: string;
|
|
190
|
+
close?: RoomCloseCursor;
|
|
85
191
|
}
|
|
86
192
|
export interface TemplateMemberSlot {
|
|
87
193
|
slot: string;
|
|
@@ -133,7 +239,6 @@ export interface RoomsDefaults {
|
|
|
133
239
|
close_when_task_done?: boolean;
|
|
134
240
|
}
|
|
135
241
|
export interface RoomsConfig {
|
|
136
|
-
provider: string;
|
|
137
242
|
cowork?: RoomsCoworkConfig;
|
|
138
243
|
owner: RoomsOwnerConfig;
|
|
139
244
|
defaults?: RoomsDefaults;
|
|
@@ -145,7 +250,7 @@ export interface TasksConfig {
|
|
|
145
250
|
retain_completed_for?: string;
|
|
146
251
|
}
|
|
147
252
|
export type RoomTemplatesConfig = Record<string, TemplateDefinition>;
|
|
148
|
-
export declare const ROOMS_KEYS: readonly ["
|
|
253
|
+
export declare const ROOMS_KEYS: readonly ["cowork", "owner", "defaults"];
|
|
149
254
|
export declare const ROOMS_OWNER_KEYS: readonly ["provider", "public_invite", "public_invite_file", "expected_cid", "role"];
|
|
150
255
|
export declare const ROOMS_COWORK_KEYS: readonly ["config"];
|
|
151
256
|
export declare const ROOMS_DEFAULTS_KEYS: readonly ["template", "attach_owner", "close_when_task_done"];
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export const TASK_TERMINAL_STATES = ['done', 'cancelled', 'failed'];
|
|
9
9
|
export const TASK_CANCELLABLE_STATES = ['backlog', 'provisioning', 'active', 'review'];
|
|
10
10
|
// ── Validation keys ─────────────────────────────────────────────────────
|
|
11
|
-
export const ROOMS_KEYS = ['
|
|
11
|
+
export const ROOMS_KEYS = ['cowork', 'owner', 'defaults'];
|
|
12
12
|
export const ROOMS_OWNER_KEYS = ['provider', 'public_invite', 'public_invite_file', 'expected_cid', 'role'];
|
|
13
13
|
export const ROOMS_COWORK_KEYS = ['config'];
|
|
14
14
|
export const ROOMS_DEFAULTS_KEYS = ['template', 'attach_owner', 'close_when_task_done'];
|