@ours.network/fleet 1.2.0-nightly.1 → 1.2.0-nightly.11
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 +110 -21
- package/dist/agent-ours/bridge.d.ts +3 -0
- package/dist/agent-ours/bridge.js +178 -0
- package/dist/agent-ours/controller.d.ts +14 -0
- package/dist/agent-ours/controller.js +60 -0
- package/dist/agent-ours/file-rpc.d.ts +23 -0
- package/dist/agent-ours/file-rpc.js +123 -0
- package/dist/agent-ours/harness.d.ts +14 -0
- package/dist/agent-ours/harness.js +64 -0
- package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
- package/dist/agent-ours/mcp-endpoint.js +111 -0
- package/dist/agent-ours/runtime.d.ts +53 -0
- package/dist/agent-ours/runtime.js +247 -0
- package/dist/agent-ours/service.d.ts +23 -0
- package/dist/agent-ours/service.js +364 -0
- package/dist/agent-ours/state.d.ts +45 -0
- package/dist/agent-ours/state.js +95 -0
- package/dist/agent-ours/wire.d.ts +14 -0
- package/dist/agent-ours/wire.js +55 -0
- package/dist/agent-recovery-gate.js +1 -1
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +121 -0
- package/dist/application/task-room-service.d.ts +6 -2
- package/dist/application/task-room-service.js +78 -21
- package/dist/briefing.js +10 -59
- package/dist/build-info.json +5 -4
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +63 -2
- package/dist/client-profile.d.ts +1 -0
- package/dist/client-profile.js +18 -3
- package/dist/config.d.ts +1 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +3 -14
- package/dist/docs.d.ts +3 -3
- package/dist/docs.js +32 -23
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +29 -10
- package/dist/fleet-command-audit.js +3 -2
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +4 -2
- package/dist/harness/codex-session.js +9 -2
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +4 -3
- package/dist/harness/hermes-session.js +7 -1
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +24 -4
- package/dist/owner-channel/commands.js +5 -0
- package/dist/rooms-tasks/archived-absence.d.ts +8 -0
- package/dist/rooms-tasks/archived-absence.js +48 -0
- package/dist/rooms-tasks/cli.js +6 -1
- package/dist/rooms-tasks/close.d.ts +3 -0
- package/dist/rooms-tasks/close.js +95 -17
- package/dist/rooms-tasks/cowork-adapter.d.ts +4 -0
- package/dist/rooms-tasks/cowork-adapter.js +10 -2
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- package/dist/rooms-tasks/deletion.js +40 -3
- package/dist/rooms-tasks/live-readiness.d.ts +18 -0
- package/dist/rooms-tasks/live-readiness.js +75 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +2 -0
- package/dist/rooms-tasks/provision.d.ts +11 -0
- package/dist/rooms-tasks/provision.js +138 -26
- package/dist/rooms-tasks/room-state.js +13 -0
- package/dist/rooms-tasks/task-state.d.ts +13 -3
- package/dist/rooms-tasks/task-state.js +42 -6
- package/dist/rooms-tasks/types.d.ts +12 -0
- package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
- package/dist/rooms-tasks/workspace-artifacts.js +93 -0
- package/dist/rooms-tasks/workspace.d.ts +26 -0
- package/dist/rooms-tasks/workspace.js +195 -0
- package/dist/runner.d.ts +14 -0
- package/dist/runner.js +527 -495
- package/dist/session/codex-app-server-transport.js +1 -1
- package/dist/spawn.js +19 -16
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/supervisor/systemd.js +5 -0
- package/dist/temp-supervisor-recovery.d.ts +3 -0
- package/dist/temp-supervisor-recovery.js +46 -0
- package/dist/watchdog/briefing.js +3 -16
- package/dist/watchdog/run.js +19 -5
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +18 -1
- package/docs/task-workspaces.md +98 -0
- package/package.json +6 -3
- package/presets/fleet/roles/Coordinator.yaml +22 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { realExec } from '../exec.js';
|
|
2
|
+
import { agentDir } from '../paths.js';
|
|
3
|
+
import { readRoomReadiness } from '../agent-ours/service.js';
|
|
4
|
+
import { controlRequest } from '../session/control.js';
|
|
5
|
+
import { readTempSupervisor, tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
6
|
+
/** Read-only observation. A failed probe never grants authority to replace a seat. */
|
|
7
|
+
export async function taskLiveReadiness(task, room, cowork, deps = {}) {
|
|
8
|
+
const degraded = (reason, member) => ({ state: 'degraded', reason, member });
|
|
9
|
+
const unknown = (reason, member) => ({ state: 'unknown', reason, member });
|
|
10
|
+
if (!room || task.state !== 'active' || task.room_id !== room.room_id || room.state !== 'active' || room.close || task.terminal_intent
|
|
11
|
+
|| room.task_id !== task.task_id || !room.room_identity_cid
|
|
12
|
+
|| task.room_identity_cid !== room.room_identity_cid)
|
|
13
|
+
return degraded('room_record_mismatch');
|
|
14
|
+
const expected = room.template_snapshot?.members.reduce((n, member) => n + member.count, 0);
|
|
15
|
+
if (expected === undefined || room.member_seats.length !== expected
|
|
16
|
+
|| new Set(room.member_seats.map(seat => seat.role_name)).size !== expected)
|
|
17
|
+
return degraded('member_roster_mismatch');
|
|
18
|
+
let remote;
|
|
19
|
+
try {
|
|
20
|
+
remote = await cowork.getRoom(room.room_id);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return unknown('room_probe_unavailable');
|
|
24
|
+
}
|
|
25
|
+
if (!remote || remote.room_id !== room.room_id || remote.identity_cid !== room.room_identity_cid
|
|
26
|
+
|| remote.state !== 'active')
|
|
27
|
+
return degraded('room_identity_or_state_mismatch');
|
|
28
|
+
if (room.owner_seat_cid && !remote.seats.some(seat => seat.identity_cid === room.owner_seat_cid && seat.seat_state === 'active'))
|
|
29
|
+
return degraded('owner_seat_missing');
|
|
30
|
+
for (const seat of room.member_seats) {
|
|
31
|
+
const member = seat.role_name;
|
|
32
|
+
if (seat.seat_state !== 'active' || !seat.identity_cid || !seat.invite_id
|
|
33
|
+
|| seat.launch?.state !== 'launched' || !seat.launch.launch_id || seat.retirement)
|
|
34
|
+
return degraded('member_record_incomplete', member);
|
|
35
|
+
const matches = remote.seats.filter(item => item.display_name === member && item.seat_state !== 'removed');
|
|
36
|
+
if (matches.length !== 1 || matches[0].identity_cid !== seat.identity_cid
|
|
37
|
+
|| matches[0].invite_id !== seat.invite_id || matches[0].role !== seat.cowork_role
|
|
38
|
+
|| matches[0].seat_state !== 'active')
|
|
39
|
+
return degraded('member_seat_mismatch', member);
|
|
40
|
+
const dir = agentDir(member, true);
|
|
41
|
+
try {
|
|
42
|
+
const supervisor = (deps.supervisor ?? readTempSupervisor)(dir);
|
|
43
|
+
if (!supervisor)
|
|
44
|
+
return unknown('supervisor_record_missing', member);
|
|
45
|
+
if (supervisor.role !== member || supervisor.launchId !== seat.launch.launch_id)
|
|
46
|
+
return degraded('supervisor_launch_mismatch', member);
|
|
47
|
+
const live = await (deps.liveness ?? tempSupervisorLiveness)(dir, {
|
|
48
|
+
exec: (cmd, args, opts) => realExec(cmd, args, { ...opts, timeout: 2_000 }),
|
|
49
|
+
});
|
|
50
|
+
if (live === 'stopped')
|
|
51
|
+
return degraded('supervisor_stopped', member);
|
|
52
|
+
if (live !== 'running')
|
|
53
|
+
return unknown('supervisor_liveness_unknown', member);
|
|
54
|
+
const ready = (deps.readiness ?? readRoomReadiness)(room.room_identity_cid, member);
|
|
55
|
+
if (!ready || ready.room !== room.room_id || ready.invite !== seat.invite_id || ready.cid !== seat.identity_cid)
|
|
56
|
+
return degraded('member_readiness_mismatch', member);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return unknown('supervisor_probe_unavailable', member);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const response = await (deps.control ?? controlRequest)(dir, { command: 'status' }, 2_000);
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
return unknown('control_unavailable', member);
|
|
65
|
+
const status = response.result;
|
|
66
|
+
if (status?.alive === false || status?.readiness === 'failed')
|
|
67
|
+
return degraded('agent_not_ready', member);
|
|
68
|
+
if (status?.alive !== true || !['idle', 'running', 'awaiting_permission'].includes(status.readiness ?? ''))
|
|
69
|
+
return unknown('agent_readiness_unknown', member);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return unknown('control_unavailable', member);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -6,6 +6,7 @@ export interface RoomTaskMember {
|
|
|
6
6
|
}
|
|
7
7
|
/** Build the complete task text Fleet places directly in a temporary member's briefing. */
|
|
8
8
|
export declare function buildRoomMemberTask(input: {
|
|
9
|
+
workspace?: string;
|
|
9
10
|
taskId?: string;
|
|
10
11
|
roomId: string;
|
|
11
12
|
roomIdentityCid: string;
|
|
@@ -7,6 +7,8 @@ export function buildRoomMemberTask(input) {
|
|
|
7
7
|
`Fleet Task ${input.taskId ?? '(standalone)'} — ${member.cowork_role} in room ${input.roomId}`,
|
|
8
8
|
'',
|
|
9
9
|
];
|
|
10
|
+
if (input.workspace)
|
|
11
|
+
lines.push(`Task-owned workspace: ${input.workspace}`, 'Start and keep all execution artifacts, dependency checkouts, and Git worktrees inside this workspace.', 'Clone repositories inside this workspace before adding worktrees; never attach to an external Git common directory.', 'Use distinct per-agent branches/directories for concurrent edits. Configured cwd is source context, not artifact ownership.', 'Completion, cancellation and room retirement retain artifacts. Only explicit owner deletion removes them.', 'Do not delete the workspace or its ownership marker yourself.', '');
|
|
10
12
|
if (input.goal)
|
|
11
13
|
lines.push(`Goal: ${input.goal}`);
|
|
12
14
|
if (input.brief)
|
|
@@ -21,6 +21,17 @@ export interface StartupWaitPolicy {
|
|
|
21
21
|
sleep(ms: number): Promise<void>;
|
|
22
22
|
}
|
|
23
23
|
export declare function provisionMembers(input: ProvisionMembersInput): Promise<RoomOrchestrationRecord>;
|
|
24
|
+
/** Reconcile proven, admitted launches without any spawn, archive, invite or recovery path. */
|
|
25
|
+
export declare function reconcileExistingTaskMembers(input: {
|
|
26
|
+
taskId: string;
|
|
27
|
+
checkOnly?: boolean;
|
|
28
|
+
cowork: Pick<CoworkAdapter, 'getRoom'>;
|
|
29
|
+
expectedLaunches: ReadonlyArray<{
|
|
30
|
+
role_name: string;
|
|
31
|
+
launch_id: string;
|
|
32
|
+
identity_cid: string;
|
|
33
|
+
}>;
|
|
34
|
+
}): Promise<RoomOrchestrationRecord>;
|
|
24
35
|
export declare function cleanupMembers(input: {
|
|
25
36
|
roomId: string;
|
|
26
37
|
taskId?: string;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readRoomReadiness } from '../agent-ours/service.js';
|
|
2
|
+
import { ensureWorkspace, validateWorkspace } from './workspace.js';
|
|
1
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
4
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
5
|
import { join } from 'node:path';
|
|
@@ -13,7 +15,7 @@ import { readLaunchSnapshot, redactLaunchDefinition } from './launch-snapshot.js
|
|
|
13
15
|
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
|
|
14
16
|
import { controlRequest } from '../session/control.js';
|
|
15
17
|
import { SessionControlError } from '../session/types.js';
|
|
16
|
-
import { closeManagedRoom } from './close.js';
|
|
18
|
+
import { closeManagedRoom, roomCloseLockPath, CLOSE_LOCK_STALE_MS } from './close.js';
|
|
17
19
|
import { withFileLock } from '../atomic-file.js';
|
|
18
20
|
import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
|
|
19
21
|
import { TaskStateError, taskDeletionState } from './task-state.js';
|
|
@@ -115,6 +117,7 @@ function settingsFor(member, cfg, sealed) {
|
|
|
115
117
|
}
|
|
116
118
|
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid, anonymous) {
|
|
117
119
|
return buildRoomMemberTask({
|
|
120
|
+
workspace: getRoomRecord(input.roomId)?.workspace?.path,
|
|
118
121
|
taskId: input.taskId,
|
|
119
122
|
roomId: input.roomId,
|
|
120
123
|
roomIdentityCid,
|
|
@@ -141,6 +144,10 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
|
|
|
141
144
|
try {
|
|
142
145
|
const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
|
|
143
146
|
const startup = role.roomMemberStartup;
|
|
147
|
+
const workspace = getRoomRecord(roomId)?.workspace;
|
|
148
|
+
if (workspace && (role.cwd !== workspace.path
|
|
149
|
+
|| JSON.stringify(startup?.workspace) !== JSON.stringify(workspace)))
|
|
150
|
+
return false;
|
|
144
151
|
return role.identity === member.name
|
|
145
152
|
&& startup?.room_id === roomId
|
|
146
153
|
&& startup.room_identity_cid === roomIdentityCid
|
|
@@ -149,8 +156,7 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
|
|
|
149
156
|
&& (startup.anonymous ?? false) === anonymous
|
|
150
157
|
&& sha256Text(startup.task ?? '') === taskSha
|
|
151
158
|
&& (expectedInviteId === undefined || startup.invite_id === expectedInviteId)
|
|
152
|
-
&&
|
|
153
|
-
&& startup.invite.length > 0;
|
|
159
|
+
&& startup.invite === '';
|
|
154
160
|
}
|
|
155
161
|
catch {
|
|
156
162
|
return false;
|
|
@@ -234,25 +240,9 @@ async function retainRunningLaunch(input) {
|
|
|
234
240
|
}
|
|
235
241
|
return false;
|
|
236
242
|
}
|
|
237
|
-
/**
|
|
238
|
-
* Member-launch publication window: for a task-bound room, the durable launch
|
|
239
|
-
* intent, spawn, and launch record publish under the common task-operation
|
|
240
|
-
* lock so an accepted deletion linearizes strictly before (bounded abort — no
|
|
241
|
-
* spawn happens) or strictly after (the seat's launch record is visible to
|
|
242
|
-
* deletion retirement). The lock is held per member only, never across
|
|
243
|
-
* seat-wait loops.
|
|
244
|
-
*/
|
|
245
243
|
async function launchMember(input) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return launchMemberUnlocked(input);
|
|
249
|
-
return withFileLock(taskOperationLockPath(taskId), () => {
|
|
250
|
-
if (taskDeletionState(taskId) !== 'none')
|
|
251
|
-
throw new Error(`task ${taskId} is pending deletion; aborting member launch for ${input.member.name}`);
|
|
252
|
-
return launchMemberUnlocked(input);
|
|
253
|
-
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
254
|
-
}
|
|
255
|
-
async function launchMemberUnlocked(input) {
|
|
244
|
+
// provisionMembers holds the task and room lifecycle locks through every launch.
|
|
245
|
+
assertProvisioningOpen(input.provision);
|
|
256
246
|
const { provision, member, settings, startup } = input;
|
|
257
247
|
const seat = getRoomRecord(provision.roomId).member_seats
|
|
258
248
|
.find(candidate => candidate.role_name === member.name);
|
|
@@ -345,7 +335,14 @@ function reconcileMemberSeats(roomId, members, observed, ownerSeatCid) {
|
|
|
345
335
|
&& seat.seat_state === 'active');
|
|
346
336
|
const seats = current.member_seats.map(seat => {
|
|
347
337
|
const member = members.find(candidate => candidate.name === seat.role_name);
|
|
338
|
+
const readiness = readRoomReadiness(current.room_identity_cid, member.name);
|
|
339
|
+
if (!readiness || readiness.room !== roomId || readiness.invite !== seat.invite_id) {
|
|
340
|
+
complete = false;
|
|
341
|
+
return seat;
|
|
342
|
+
}
|
|
348
343
|
const found = exactCoworkSeat(observed, member, seat.invite_id);
|
|
344
|
+
if (found && found.identity_cid !== readiness.cid)
|
|
345
|
+
throw new Error('Cowork seat CID does not match supervisor provisioned identity');
|
|
349
346
|
if (!found || found.seat_state !== 'active') {
|
|
350
347
|
complete = false;
|
|
351
348
|
return seat;
|
|
@@ -364,9 +361,29 @@ function assertCoworkRoomPolicy(room, expectedAnonymous) {
|
|
|
364
361
|
if ((room.anonymous ?? false) !== expectedAnonymous)
|
|
365
362
|
throw new Error(`Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(expectedAnonymous)})`);
|
|
366
363
|
}
|
|
364
|
+
function assertProvisioningOpen(input) {
|
|
365
|
+
if (input.taskId && getTask(input.taskId).terminal_intent)
|
|
366
|
+
throw new Error(`task ${input.taskId} has an accepted terminal intent; refusing to provision members`);
|
|
367
|
+
const room = getRoomRecord(input.roomId);
|
|
368
|
+
if (room?.state === 'closing' || room?.state === 'closed')
|
|
369
|
+
throw new Error(`room ${input.roomId} is ${room.state}; refusing to provision members`);
|
|
370
|
+
}
|
|
367
371
|
export async function provisionMembers(input) {
|
|
372
|
+
// Same lock order as terminal/deletion settlement. Serialize concurrent retries
|
|
373
|
+
// and standalone retirement as well as member publication. Seat waits are bounded.
|
|
374
|
+
const run = () => withFileLock(roomCloseLockPath(input.roomId), () => {
|
|
375
|
+
assertProvisioningOpen(input);
|
|
376
|
+
if (!getRoomRecord(input.roomId))
|
|
377
|
+
throw new Error(`room ${input.roomId} is missing; refusing provisioning`);
|
|
378
|
+
return provisionMembersUnlocked(input);
|
|
379
|
+
}, {}, CLOSE_LOCK_STALE_MS);
|
|
380
|
+
return input.taskId
|
|
381
|
+
? withFileLock(taskOperationLockPath(input.taskId), run, {}, TASK_OPERATION_LOCK_STALE_MS)
|
|
382
|
+
: run();
|
|
383
|
+
}
|
|
384
|
+
async function provisionMembersUnlocked(input) {
|
|
368
385
|
const { cfg, cowork, roomId, taskId, template } = input;
|
|
369
|
-
//
|
|
386
|
+
// The common task-operation lock protects this epoch through all member launches.
|
|
370
387
|
if (taskId && taskDeletionState(taskId) !== 'none')
|
|
371
388
|
throw new Error(`task ${taskId} is pending deletion; refusing to provision members`);
|
|
372
389
|
const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
|
|
@@ -378,6 +395,12 @@ export async function provisionMembers(input) {
|
|
|
378
395
|
const existing = getRoomRecord(roomId);
|
|
379
396
|
if (!existing?.room_identity_cid)
|
|
380
397
|
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
398
|
+
if (existing.workspace) {
|
|
399
|
+
validateWorkspace(existing.workspace, taskId ? 'task' : 'room', taskId ?? roomId);
|
|
400
|
+
ensureWorkspace(existing.workspace);
|
|
401
|
+
for (const setting of settings.values())
|
|
402
|
+
setting.definition.cwd = existing.workspace.path;
|
|
403
|
+
}
|
|
381
404
|
const roomIdentityCid = existing.room_identity_cid;
|
|
382
405
|
const ownerSeatCid = existing.owner_seat_cid ?? null;
|
|
383
406
|
const roomPolicy = storedRoomLaunchPolicy(existing.room_policy);
|
|
@@ -439,17 +462,31 @@ export async function provisionMembers(input) {
|
|
|
439
462
|
provision: input, member, settings: settings.get(member.name), task, roomIdentityCid,
|
|
440
463
|
}))
|
|
441
464
|
continue;
|
|
465
|
+
// A previous failed launch keeps its invite pointer durably. Never
|
|
466
|
+
// overwrite that requirement until the supported revoke has succeeded;
|
|
467
|
+
// a transport failure must fence subsequent invite issuance too.
|
|
468
|
+
if (currentSeat.invite_id) {
|
|
469
|
+
const observed = await cowork.getRoom(roomId);
|
|
470
|
+
if (!observed || observed.identity_cid !== roomIdentityCid)
|
|
471
|
+
throw new Error('cannot verify room before failed-attempt invite cleanup');
|
|
472
|
+
if (observed.seats.some(seat => seat.invite_id === currentSeat.invite_id
|
|
473
|
+
&& seat.seat_state !== 'removed'))
|
|
474
|
+
throw new Error('failed-attempt invite has an admitted seat; reconcile before replacing the member');
|
|
475
|
+
await cowork.revokeInvite(roomId, currentSeat.invite_id);
|
|
476
|
+
}
|
|
442
477
|
const issued = await cowork.issueInvite(roomId, {
|
|
443
478
|
mode: 'one_time', role: member.coworkRole, min_accepts: 1,
|
|
444
479
|
});
|
|
445
|
-
const seats = getRoomRecord(roomId).member_seats.map(seat => seat.role_name === member.name ? { ...seat, invite_id: issued.invite_id } : seat);
|
|
446
|
-
updateMemberSeats(roomId, seats);
|
|
447
480
|
try {
|
|
481
|
+
assertProvisioningOpen(input);
|
|
482
|
+
const seats = getRoomRecord(roomId).member_seats.map(seat => seat.role_name === member.name ? { ...seat, invite_id: issued.invite_id } : seat);
|
|
483
|
+
updateMemberSeats(roomId, seats);
|
|
448
484
|
await launchMember({
|
|
449
485
|
provision: input,
|
|
450
486
|
member,
|
|
451
487
|
settings: settings.get(member.name),
|
|
452
488
|
startup: {
|
|
489
|
+
workspace: existing.workspace,
|
|
453
490
|
room_id: roomId,
|
|
454
491
|
room_identity_cid: roomIdentityCid,
|
|
455
492
|
identity_name: member.name,
|
|
@@ -463,7 +500,12 @@ export async function provisionMembers(input) {
|
|
|
463
500
|
});
|
|
464
501
|
}
|
|
465
502
|
catch (error) {
|
|
466
|
-
|
|
503
|
+
try {
|
|
504
|
+
await cowork.revokeInvite(roomId, issued.invite_id);
|
|
505
|
+
}
|
|
506
|
+
catch (cleanupError) {
|
|
507
|
+
throw new AggregateError([error, cleanupError], 'member launch failed and invite cleanup is unresolved; retry must revoke the retained requirement first');
|
|
508
|
+
}
|
|
467
509
|
throw error;
|
|
468
510
|
}
|
|
469
511
|
}
|
|
@@ -491,7 +533,7 @@ export async function provisionMembers(input) {
|
|
|
491
533
|
const remote = await cowork.recoverRoom(roomId);
|
|
492
534
|
assertCoworkRoomPolicy(remote, roomPolicy.anonymous);
|
|
493
535
|
const reconciled = reconcileMemberSeats(roomId, members, remote.seats, ownerSeatCid ?? undefined);
|
|
494
|
-
if (reconciled.complete)
|
|
536
|
+
if (reconciled.complete && remote.state === 'active')
|
|
495
537
|
break;
|
|
496
538
|
if (policy.now() >= deadline) {
|
|
497
539
|
advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
|
|
@@ -531,6 +573,76 @@ export async function provisionMembers(input) {
|
|
|
531
573
|
activateTask(taskId);
|
|
532
574
|
return record;
|
|
533
575
|
}
|
|
576
|
+
/** Reconcile proven, admitted launches without any spawn, archive, invite or recovery path. */
|
|
577
|
+
export async function reconcileExistingTaskMembers(input) {
|
|
578
|
+
return withFileLock(taskOperationLockPath(input.taskId), async () => {
|
|
579
|
+
const task = getTask(input.taskId);
|
|
580
|
+
if (!['provisioning', 'active'].includes(task.state) || task.terminal_intent || taskDeletionState(input.taskId) !== 'none'
|
|
581
|
+
|| !task.room_id)
|
|
582
|
+
throw new Error('task is not open for existing-member reconciliation');
|
|
583
|
+
const roomId = task.room_id;
|
|
584
|
+
return withFileLock(roomCloseLockPath(roomId), async () => {
|
|
585
|
+
const current = getRoomRecord(roomId);
|
|
586
|
+
if (!current || !['provisioning', 'active'].includes(current.state) || current.close
|
|
587
|
+
|| current.task_id !== input.taskId || !current.room_identity_cid
|
|
588
|
+
|| task.room_identity_cid !== current.room_identity_cid)
|
|
589
|
+
throw new Error('room is not open for existing-member reconciliation');
|
|
590
|
+
const names = new Set(input.expectedLaunches.map(seat => seat.role_name));
|
|
591
|
+
if (!names.size || names.size !== input.expectedLaunches.length
|
|
592
|
+
|| current.member_seats.length !== names.size
|
|
593
|
+
|| current.member_seats.some(seat => !names.has(seat.role_name)))
|
|
594
|
+
throw new Error('expected launch roster does not match the persisted room');
|
|
595
|
+
const remote = await input.cowork.getRoom(roomId);
|
|
596
|
+
if (!remote || remote.room_id !== roomId || remote.identity_cid !== current.room_identity_cid
|
|
597
|
+
|| remote.state !== 'active')
|
|
598
|
+
throw new Error('Cowork room is not the exact active room');
|
|
599
|
+
assertCoworkRoomPolicy(remote, storedRoomLaunchPolicy(current.room_policy).anonymous);
|
|
600
|
+
if (current.owner_seat_cid && !remote.seats.some(seat => seat.identity_cid === current.owner_seat_cid && seat.seat_state === 'active'))
|
|
601
|
+
throw new Error('expected Owner seat is not active');
|
|
602
|
+
const seats = [];
|
|
603
|
+
for (const seat of current.member_seats) {
|
|
604
|
+
const expected = input.expectedLaunches.find(item => item.role_name === seat.role_name);
|
|
605
|
+
const launch = seat.launch;
|
|
606
|
+
if (seat.seat_state === 'removed' || !seat.invite_id || launch?.state !== 'launched'
|
|
607
|
+
|| launch.launch_id !== expected.launch_id || !launch.action_id || !launch.mission_sha256
|
|
608
|
+
|| (seat.identity_cid && seat.identity_cid !== expected.identity_cid))
|
|
609
|
+
throw new Error('persisted member launch does not match reconciliation evidence');
|
|
610
|
+
const dir = agentDir(seat.role_name, true);
|
|
611
|
+
if (!launchMatches(dir, { name: seat.role_name, coworkRole: seat.cowork_role }, launch.action_id, launch.mission_sha256, roomId, current.room_identity_cid, seat.invite_id, storedRoomLaunchPolicy(current.room_policy).anonymous))
|
|
612
|
+
throw new Error('member startup provenance mismatch');
|
|
613
|
+
const supervisor = readTempSupervisor(dir);
|
|
614
|
+
if (!supervisor || supervisor.role !== seat.role_name || supervisor.launchId !== expected.launch_id
|
|
615
|
+
|| await tempSupervisorLiveness(dir) !== 'running')
|
|
616
|
+
throw new Error('expected member supervisor is not running');
|
|
617
|
+
const ready = readRoomReadiness(current.room_identity_cid, seat.role_name);
|
|
618
|
+
const matches = remote.seats.filter(item => item.display_name === seat.role_name
|
|
619
|
+
&& item.seat_state !== 'removed');
|
|
620
|
+
const found = matches[0];
|
|
621
|
+
if (!ready || ready.room !== roomId || ready.invite !== seat.invite_id
|
|
622
|
+
|| ready.cid !== expected.identity_cid || matches.length !== 1 || !found
|
|
623
|
+
|| found.identity_cid !== expected.identity_cid || found.role !== seat.cowork_role
|
|
624
|
+
|| found.seat_state !== 'active' || found.invite_id !== seat.invite_id)
|
|
625
|
+
throw new Error('member readiness or authenticated Cowork seat mismatch');
|
|
626
|
+
seats.push({ ...seat, identity_cid: expected.identity_cid, seat_state: 'active' });
|
|
627
|
+
}
|
|
628
|
+
if (input.checkOnly)
|
|
629
|
+
return current;
|
|
630
|
+
// No mutation until the complete roster has passed; these writes are replayable
|
|
631
|
+
// under the same task/room lifecycle locks if interrupted between files.
|
|
632
|
+
updateMemberSeats(roomId, seats);
|
|
633
|
+
updateTaskMembers(input.taskId, seats.map(seat => ({ name: seat.role_name,
|
|
634
|
+
identity_cid: seat.identity_cid, slot: seat.slot, cowork_role: seat.cowork_role })));
|
|
635
|
+
if (getTask(input.taskId).blocked)
|
|
636
|
+
unblockTask(input.taskId);
|
|
637
|
+
if (current.state !== 'active')
|
|
638
|
+
advanceSaga(roomId, 'activate', 6);
|
|
639
|
+
const activated = current.state === 'active' ? getRoomRecord(roomId) : activateRoom(roomId);
|
|
640
|
+
if (task.state !== 'active')
|
|
641
|
+
activateTask(input.taskId);
|
|
642
|
+
return activated;
|
|
643
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
644
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
645
|
+
}
|
|
534
646
|
export async function cleanupMembers(input) {
|
|
535
647
|
const room = getRoomRecord(input.roomId);
|
|
536
648
|
if (!room)
|
|
@@ -2,6 +2,8 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:f
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
4
4
|
import { stateRoot } from '../paths.js';
|
|
5
|
+
import { planWorkspace, ensureWorkspace } from './workspace.js';
|
|
6
|
+
import { getTask } from './task-state.js';
|
|
5
7
|
import { storedRoomLaunchPolicy } from './types.js';
|
|
6
8
|
import { releaseLaunchSnapshot } from './launch-snapshot.js';
|
|
7
9
|
export const roomsDir = () => join(stateRoot(), 'rooms');
|
|
@@ -25,6 +27,8 @@ export function createRoomRecord(input) {
|
|
|
25
27
|
const requested = storedRoomLaunchPolicy(input.room_policy);
|
|
26
28
|
if (JSON.stringify(before) !== JSON.stringify(requested))
|
|
27
29
|
throw new RoomStateError(`room ${input.room_id} launch policy mismatch`);
|
|
30
|
+
if (existing.workspace)
|
|
31
|
+
ensureWorkspace(existing.workspace);
|
|
28
32
|
return existing;
|
|
29
33
|
}
|
|
30
34
|
const record = {
|
|
@@ -40,7 +44,10 @@ export function createRoomRecord(input) {
|
|
|
40
44
|
state: 'provisioning',
|
|
41
45
|
created_at: new Date().toISOString(),
|
|
42
46
|
};
|
|
47
|
+
record.workspace = input.task_id ? getTask(input.task_id).workspace : planWorkspace('room', input.room_id);
|
|
43
48
|
writeRoom(record);
|
|
49
|
+
if (record.workspace)
|
|
50
|
+
ensureWorkspace(record.workspace);
|
|
44
51
|
return record;
|
|
45
52
|
}
|
|
46
53
|
export function getRoomRecord(id) {
|
|
@@ -112,6 +119,8 @@ export function setOwnerSeat(id, ownerSeatCid, inviteFingerprint) {
|
|
|
112
119
|
}
|
|
113
120
|
export function updateMemberSeats(id, seats) {
|
|
114
121
|
const r = readRoom(id);
|
|
122
|
+
if (r.state === 'closing' || r.state === 'closed')
|
|
123
|
+
throw new RoomStateError(`room ${id} is ${r.state}; member provisioning is fenced`);
|
|
115
124
|
r.member_seats = seats;
|
|
116
125
|
writeRoom(r);
|
|
117
126
|
return r;
|
|
@@ -135,6 +144,8 @@ const LAUNCH_ORDER = [
|
|
|
135
144
|
];
|
|
136
145
|
export function updateMemberStartup(id, roleName, update) {
|
|
137
146
|
const r = readRoom(id);
|
|
147
|
+
if (r.state === 'closing' || r.state === 'closed')
|
|
148
|
+
throw new RoomStateError(`room ${id} is ${r.state}; member provisioning is fenced`);
|
|
138
149
|
const seat = r.member_seats.find(candidate => candidate.role_name === roleName);
|
|
139
150
|
if (!seat)
|
|
140
151
|
throw new RoomStateError(`room ${id} has no recorded member ${roleName}`);
|
|
@@ -151,6 +162,8 @@ export function updateMemberStartup(id, roleName, update) {
|
|
|
151
162
|
}
|
|
152
163
|
export function activateRoom(id) {
|
|
153
164
|
const r = readRoom(id);
|
|
165
|
+
if (r.state === 'closing' || r.state === 'closed')
|
|
166
|
+
throw new RoomStateError(`room ${id} is ${r.state}; activation is fenced`);
|
|
154
167
|
r.state = 'active';
|
|
155
168
|
r.saga = { phase: 'completed', step_index: 0 };
|
|
156
169
|
delete r.provisioning_detail;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ArchivedMemberAbsence } from './types.js';
|
|
1
2
|
import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent, TaskDeletionActor, TaskDeletionMemberPhase } from './types.js';
|
|
2
3
|
export declare const tasksDir: () => string;
|
|
3
4
|
export declare class TaskStateError extends Error {
|
|
@@ -74,6 +75,7 @@ export interface TaskDeletionReceipt {
|
|
|
74
75
|
original_state: TaskState;
|
|
75
76
|
room_id?: string;
|
|
76
77
|
member_count: number;
|
|
78
|
+
archived_absences?: ArchivedMemberAbsence[];
|
|
77
79
|
settled_at?: string;
|
|
78
80
|
result?: 'deleted';
|
|
79
81
|
}
|
|
@@ -122,6 +124,10 @@ export declare function advanceTaskDeletionMember(id: string, name: string, phas
|
|
|
122
124
|
interface SeatEvidence {
|
|
123
125
|
role_name: string;
|
|
124
126
|
identity_cid?: string;
|
|
127
|
+
launch?: {
|
|
128
|
+
launch_id?: string;
|
|
129
|
+
action_id?: string;
|
|
130
|
+
};
|
|
125
131
|
retirement?: {
|
|
126
132
|
phase: TaskDeletionMemberPhase;
|
|
127
133
|
launch_id: string;
|
|
@@ -146,10 +152,14 @@ export declare function upsertTaskDeletionMembersFromSeats(id: string, seats: Re
|
|
|
146
152
|
*
|
|
147
153
|
* The room saga is trusted for phase jumps, but partial or corrupt retained
|
|
148
154
|
* records must not become false success: every seat must be identity_absent;
|
|
149
|
-
* a real launch requires archive evidence; an identity-less seat
|
|
150
|
-
*
|
|
155
|
+
* a real launch requires archive evidence; an identity-less seat requires
|
|
156
|
+
* never-launched proof or a freshly verified archived-absence checkpoint.
|
|
157
|
+
*/
|
|
158
|
+
export declare function importTaskDeletionRetirementEvidence(id: string, seats: ReadonlyArray<SeatEvidence>, proofs?: ReadonlyArray<ArchivedMemberAbsence>): TaskRecord;
|
|
159
|
+
/** Settlement-only checkpoint, under the task-operation lock after fresh archive verification.
|
|
160
|
+
* From here retries still verify live absence, but owned archives may be consumed.
|
|
151
161
|
*/
|
|
152
|
-
export declare function
|
|
162
|
+
export declare function beginTaskWorkspaceCleanup(id: string): void;
|
|
153
163
|
/**
|
|
154
164
|
* Physically remove a deletion-pending task record. Settlement-only: callers
|
|
155
165
|
* must have completed member retirement and room cleanup first. Missing
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
5
5
|
import { stateRoot } from '../paths.js';
|
|
6
|
+
import { planWorkspace, ensureWorkspace } from './workspace.js';
|
|
6
7
|
import { storedRoomLaunchPolicy, TASK_TERMINAL_STATES, TASK_CANCELLABLE_STATES } from './types.js';
|
|
7
8
|
import { DEFAULT_TASK_LIST_ID, readTaskLists } from './task-lists.js';
|
|
8
9
|
export const tasksDir = () => join(stateRoot(), 'tasks');
|
|
@@ -127,7 +128,7 @@ export function assertNoPendingDeletion(task) {
|
|
|
127
128
|
}
|
|
128
129
|
export function createTask(input) {
|
|
129
130
|
const key = input.idempotency_key ?? randomUUID();
|
|
130
|
-
const existing =
|
|
131
|
+
const existing = listTasks({ includeDeleting: true }).find(task => task.idempotency_key === key);
|
|
131
132
|
if (existing) {
|
|
132
133
|
const existingPlan = existing.execution_plan?.plan_hash;
|
|
133
134
|
const requestedPlan = input.execution_plan?.plan_hash;
|
|
@@ -135,7 +136,13 @@ export function createTask(input) {
|
|
|
135
136
|
const requestedPolicy = storedRoomLaunchPolicy(input.execution_plan?.room_policy);
|
|
136
137
|
if (existingPlan !== requestedPlan || JSON.stringify(existingPolicy) !== JSON.stringify(requestedPolicy))
|
|
137
138
|
throw new TaskStateError(`idempotency key '${key}' was already used with a different execution plan`);
|
|
138
|
-
return existing
|
|
139
|
+
return withTaskLock(existing.task_id, () => {
|
|
140
|
+
const fresh = readTask(existing.task_id);
|
|
141
|
+
assertNoPendingDeletion(fresh);
|
|
142
|
+
if (fresh.workspace)
|
|
143
|
+
ensureWorkspace(fresh.workspace);
|
|
144
|
+
return fresh;
|
|
145
|
+
});
|
|
139
146
|
}
|
|
140
147
|
const state = input.start === false ? 'backlog' : 'provisioning';
|
|
141
148
|
const record = {
|
|
@@ -155,7 +162,9 @@ export function createTask(input) {
|
|
|
155
162
|
created_at: new Date().toISOString(),
|
|
156
163
|
started_at: state === 'provisioning' ? new Date().toISOString() : undefined,
|
|
157
164
|
};
|
|
165
|
+
record.workspace = planWorkspace('task', record.task_id);
|
|
158
166
|
writeTask(record);
|
|
167
|
+
ensureWorkspace(record.workspace);
|
|
159
168
|
return readTask(record.task_id);
|
|
160
169
|
}
|
|
161
170
|
export function getTask(id) { return withTaskLock(id, () => readTask(id)); }
|
|
@@ -505,7 +514,8 @@ function writeDeletionReceiptForIntent(stored) {
|
|
|
505
514
|
actor: deletion.actor,
|
|
506
515
|
original_state: stored.state,
|
|
507
516
|
room_id: deletion.room_id,
|
|
508
|
-
member_count: deletion.members.length,
|
|
517
|
+
member_count: deletion.members.length + (deletion.archived_absences?.length ?? 0),
|
|
518
|
+
...(deletion.archived_absences?.length ? { archived_absences: deletion.archived_absences } : {}),
|
|
509
519
|
};
|
|
510
520
|
replaceFileAtomically(deletionReceiptPath(stored.task_id), JSON.stringify(receipt, null, 2) + '\n');
|
|
511
521
|
}
|
|
@@ -688,10 +698,10 @@ export function upsertTaskDeletionMembersFromSeats(id, seats) {
|
|
|
688
698
|
*
|
|
689
699
|
* The room saga is trusted for phase jumps, but partial or corrupt retained
|
|
690
700
|
* records must not become false success: every seat must be identity_absent;
|
|
691
|
-
* a real launch requires archive evidence; an identity-less seat
|
|
692
|
-
*
|
|
701
|
+
* a real launch requires archive evidence; an identity-less seat requires
|
|
702
|
+
* never-launched proof or a freshly verified archived-absence checkpoint.
|
|
693
703
|
*/
|
|
694
|
-
export function importTaskDeletionRetirementEvidence(id, seats) {
|
|
704
|
+
export function importTaskDeletionRetirementEvidence(id, seats, proofs = []) {
|
|
695
705
|
return withTaskLock(id, () => {
|
|
696
706
|
assertCanonicalTaskId(id);
|
|
697
707
|
const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
|
|
@@ -708,6 +718,18 @@ export function importTaskDeletionRetirementEvidence(id, seats) {
|
|
|
708
718
|
if (!seat.identity_cid) {
|
|
709
719
|
if (neverLaunched)
|
|
710
720
|
continue; // provably never held a managed identity
|
|
721
|
+
const proof = proofs.find(value => value.name === seat.role_name);
|
|
722
|
+
if (proof && proof.launch_id === evidence.launch_id && proof.archive_path === evidence.archive_path
|
|
723
|
+
&& proof.launch_id === seat.launch?.launch_id && proof.action_id === seat.launch?.action_id) {
|
|
724
|
+
const existing = stored.deletion.archived_absences?.find(value => value.name === proof.name);
|
|
725
|
+
if (existing && (existing.launch_id !== proof.launch_id || existing.action_id !== proof.action_id
|
|
726
|
+
|| existing.archive_path !== proof.archive_path))
|
|
727
|
+
throw new TaskStateError('Archived absence checkpoint ownership is immutable');
|
|
728
|
+
stored.deletion.archived_absences ??= [];
|
|
729
|
+
if (!existing)
|
|
730
|
+
stored.deletion.archived_absences.push({ ...proof });
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
711
733
|
throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' has retirement evidence but no identity CID`);
|
|
712
734
|
}
|
|
713
735
|
let cursor = findCursorForSeat(id, stored, seat);
|
|
@@ -729,9 +751,23 @@ export function importTaskDeletionRetirementEvidence(id, seats) {
|
|
|
729
751
|
cursor.updated_at = now;
|
|
730
752
|
}
|
|
731
753
|
writeTask(stored);
|
|
754
|
+
writeDeletionReceiptForIntent(stored);
|
|
732
755
|
return presentTaskLenient(stored);
|
|
733
756
|
});
|
|
734
757
|
}
|
|
758
|
+
/** Settlement-only checkpoint, under the task-operation lock after fresh archive verification.
|
|
759
|
+
* From here retries still verify live absence, but owned archives may be consumed.
|
|
760
|
+
*/
|
|
761
|
+
export function beginTaskWorkspaceCleanup(id) {
|
|
762
|
+
withTaskLock(id, () => {
|
|
763
|
+
const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
|
|
764
|
+
if (stored.deletion?.status !== 'pending' || !stored.workspace
|
|
765
|
+
|| stored.deletion.members.some(member => member.phase !== 'identity_absent'))
|
|
766
|
+
throw new TaskStateError('Task is not ready for workspace cleanup');
|
|
767
|
+
stored.deletion.workspace_cleanup_started_at ??= new Date().toISOString();
|
|
768
|
+
writeTask(stored);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
735
771
|
/**
|
|
736
772
|
* Physically remove a deletion-pending task record. Settlement-only: callers
|
|
737
773
|
* must have completed member retirement and room cleanup first. Missing
|
|
@@ -73,6 +73,13 @@ export interface TaskDeletionMemberCursor {
|
|
|
73
73
|
* evidence. While pending, the task is hidden from normal operation and every
|
|
74
74
|
* lifecycle mutation or room publication is rejected.
|
|
75
75
|
*/
|
|
76
|
+
export interface ArchivedMemberAbsence {
|
|
77
|
+
name: string;
|
|
78
|
+
launch_id: string;
|
|
79
|
+
action_id: string;
|
|
80
|
+
archive_path: string;
|
|
81
|
+
checked_at: string;
|
|
82
|
+
}
|
|
76
83
|
export interface TaskDeletionIntent {
|
|
77
84
|
status: 'pending';
|
|
78
85
|
accepted_at: string;
|
|
@@ -80,6 +87,9 @@ export interface TaskDeletionIntent {
|
|
|
80
87
|
room_id?: string;
|
|
81
88
|
/** Snapshot of managed members at acceptance; the missing-room retirement evidence. */
|
|
82
89
|
members: TaskDeletionMemberCursor[];
|
|
90
|
+
archived_absences?: ArchivedMemberAbsence[];
|
|
91
|
+
/** Archive proofs were verified before owned workspace cleanup may consume them. */
|
|
92
|
+
workspace_cleanup_started_at?: string;
|
|
83
93
|
error?: string;
|
|
84
94
|
error_at?: string;
|
|
85
95
|
recovery_hint?: string;
|
|
@@ -87,6 +97,7 @@ export interface TaskDeletionIntent {
|
|
|
87
97
|
first_recovery_hint?: string;
|
|
88
98
|
}
|
|
89
99
|
export interface TaskRecord {
|
|
100
|
+
workspace?: import('./workspace.js').OwnedWorkspace;
|
|
90
101
|
task_id: string;
|
|
91
102
|
/** Stable organizational list identifier. Missing legacy values mean `default`. */
|
|
92
103
|
list_id: string;
|
|
@@ -227,6 +238,7 @@ export interface RoomMemberSeat {
|
|
|
227
238
|
retirement?: MemberRetirement;
|
|
228
239
|
}
|
|
229
240
|
export interface RoomOrchestrationRecord {
|
|
241
|
+
workspace?: import('./workspace.js').OwnedWorkspace;
|
|
230
242
|
room_id: string;
|
|
231
243
|
room_identity_cid?: string;
|
|
232
244
|
room_name: string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type OwnedWorkspace } from './workspace.js';
|
|
2
|
+
/** Runtime control state stays in Fleet's live roster and recovery store. Its
|
|
3
|
+
* durable launch descriptor, not a name prefix or caller-supplied path, proves
|
|
4
|
+
* artifact ownership. Audit receipts/journals intentionally outlive deletion.
|
|
5
|
+
*/
|
|
6
|
+
export declare function collectWorkspaceArchives(workspace: OwnedWorkspace): void;
|