@ours.network/fleet 1.2.0-nightly.2 → 1.2.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 +152 -29
- 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/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -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 +72 -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 +59 -0
- package/dist/agent-ours/runtime.js +283 -0
- package/dist/agent-ours/service.d.ts +23 -0
- package/dist/agent-ours/service.js +372 -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/conversation-history.d.ts +6 -0
- package/dist/application/conversation-history.js +65 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- package/dist/application/model-catalog.d.ts +1 -1
- package/dist/application/model-catalog.js +5 -2
- package/dist/application/preset-provenance.d.ts +19 -0
- package/dist/application/preset-provenance.js +59 -0
- package/dist/application/room-layout-definitions.d.ts +45 -0
- package/dist/application/room-layout-definitions.js +130 -0
- package/dist/application/session-control.d.ts +8 -0
- package/dist/application/session-control.js +21 -3
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +113 -0
- package/dist/application/task-layouts.d.ts +89 -0
- package/dist/application/task-layouts.js +248 -0
- package/dist/application/task-room-service.d.ts +99 -3
- package/dist/application/task-room-service.js +296 -31
- package/dist/atomic-file.d.ts +5 -0
- package/dist/atomic-file.js +62 -1
- 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 +65 -2
- package/dist/client-profile.d.ts +5 -16
- package/dist/client-profile.js +4 -112
- package/dist/config.d.ts +4 -0
- package/dist/config.js +7 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +5 -16
- package/dist/daemon-recovery.d.ts +1 -8
- package/dist/daemon-recovery.js +8 -113
- package/dist/docs.d.ts +3 -3
- package/dist/docs.js +43 -23
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +46 -59
- package/dist/erased-resources.d.ts +9 -0
- package/dist/erased-resources.js +87 -0
- package/dist/fleet-command-audit.d.ts +1 -0
- package/dist/fleet-command-audit.js +12 -7
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +4 -2
- package/dist/harness/codex-app-server-proxy.d.ts +4 -1
- package/dist/harness/codex-app-server-proxy.js +41 -5
- 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/init-wizard.js +20 -9
- package/dist/monitor.d.ts +1 -34
- package/dist/monitor.js +24 -151
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +34 -6
- package/dist/owner-channel/commands.d.ts +4 -0
- package/dist/owner-channel/commands.js +8 -0
- package/dist/owner-channel/lifecycle-outbox.js +4 -3
- package/dist/owner-channel/ours-client.js +2 -17
- package/dist/preset-migration.js +15 -0
- package/dist/rooms-tasks/add-member.d.ts +48 -0
- package/dist/rooms-tasks/add-member.js +83 -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 +34 -5
- package/dist/rooms-tasks/close.d.ts +5 -2
- package/dist/rooms-tasks/close.js +208 -28
- package/dist/rooms-tasks/config.js +3 -1
- package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
- package/dist/rooms-tasks/cowork-adapter.js +11 -117
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- package/dist/rooms-tasks/deletion.d.ts +1 -1
- package/dist/rooms-tasks/deletion.js +74 -5
- package/dist/rooms-tasks/erasure.d.ts +6 -0
- package/dist/rooms-tasks/erasure.js +169 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -1
- package/dist/rooms-tasks/external-worker.js +3 -4
- package/dist/rooms-tasks/layout-cli.d.ts +2 -0
- package/dist/rooms-tasks/layout-cli.js +81 -0
- package/dist/rooms-tasks/layout-config.d.ts +11 -0
- package/dist/rooms-tasks/layout-config.js +89 -0
- package/dist/rooms-tasks/layout-control.d.ts +21 -0
- package/dist/rooms-tasks/layout-control.js +52 -0
- package/dist/rooms-tasks/layout-service.d.ts +39 -0
- package/dist/rooms-tasks/layout-service.js +173 -0
- package/dist/rooms-tasks/layout.d.ts +102 -0
- package/dist/rooms-tasks/layout.js +227 -0
- package/dist/rooms-tasks/live-readiness.d.ts +18 -0
- package/dist/rooms-tasks/live-readiness.js +85 -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 +14 -0
- package/dist/rooms-tasks/provision.js +182 -48
- package/dist/rooms-tasks/room-state.d.ts +4 -2
- package/dist/rooms-tasks/room-state.js +53 -8
- package/dist/rooms-tasks/task-state.d.ts +42 -7
- package/dist/rooms-tasks/task-state.js +153 -23
- package/dist/rooms-tasks/types.d.ts +28 -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 +23 -3
- package/dist/runner.js +574 -494
- package/dist/session/acp.d.ts +3 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +3 -0
- package/dist/session/arbiter.js +20 -2
- package/dist/session/codex-app-server-transport.js +1 -1
- package/dist/session/codex-app-server.d.ts +3 -0
- package/dist/session/codex-app-server.js +25 -1
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +36 -2
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-types.d.ts +5 -3
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +3 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/spawn.js +19 -16
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/supervisor/launchd.js +2 -1
- package/dist/supervisor/systemd.js +9 -23
- package/dist/temp-idle.d.ts +17 -0
- package/dist/temp-idle.js +42 -0
- package/dist/temp-lifecycle.d.ts +6 -1
- package/dist/temp-lifecycle.js +29 -13
- 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/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +4 -1
- package/dist/web/auth.js +23 -2
- package/dist/web/control.js +2 -1
- package/dist/web/fleet-config-service.d.ts +4 -1
- package/dist/web/fleet-config-service.js +46 -15
- package/dist/web/prefix-gateway.d.ts +20 -0
- package/dist/web/prefix-gateway.js +150 -0
- package/dist/web/runtime.d.ts +4 -0
- package/dist/web/runtime.js +19 -14
- package/dist/web/server.d.ts +7 -0
- package/dist/web/server.js +293 -25
- package/docs/room-layouts.md +92 -0
- package/docs/task-workspaces.md +130 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +17 -13
- package/presets/README.md +78 -0
- package/presets/brain-catalog.json +8 -2
- package/presets/fleet/agent_templates/Engineer.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
- package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +22 -0
- package/presets/fleet/roles/Engineer.yaml +40 -0
- package/presets/fleet/room_layouts/engineering.yaml +25 -0
- package/presets/fleet/room_layouts/pair.yaml +24 -0
- package/presets/fleet/room_layouts/single.yaml +17 -0
- package/presets/fleet/room_layouts/team.yaml +30 -0
- package/presets/fleet/room_templates/engineering.yaml +19 -0
- package/presets/manifest.json +2 -2
- package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
- package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
- package/dist/web-app/icons/ours-fleet.svg +0 -4
- package/dist/web-app/index.html +0 -17
- package/dist/web-app/manifest.webmanifest +0 -15
- package/dist/web-app/offline.html +0 -18
- package/dist/web-app/sw.js +0 -51
|
@@ -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,
|
|
@@ -128,7 +131,7 @@ function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCi
|
|
|
128
131
|
cowork_role: member.coworkRole,
|
|
129
132
|
persona: settings.persona,
|
|
130
133
|
},
|
|
131
|
-
roster: members.map(candidate => ({
|
|
134
|
+
roster: [...(input.append ? (getRoomRecord(input.roomId)?.member_seats ?? []).filter(seat => !members.some(m => m.name === seat.role_name)).map(seat => ({ name: seat.role_name, coworkRole: seat.cowork_role })) : []), ...members].map(candidate => ({
|
|
132
135
|
role_name: candidate.name,
|
|
133
136
|
cowork_role: candidate.coworkRole,
|
|
134
137
|
})),
|
|
@@ -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);
|
|
@@ -274,6 +264,7 @@ async function launchMemberUnlocked(input) {
|
|
|
274
264
|
try {
|
|
275
265
|
const launched = await spawnRoomMember({
|
|
276
266
|
name: member.name,
|
|
267
|
+
configPath: provision.configPath,
|
|
277
268
|
temp: true,
|
|
278
269
|
identity: member.name,
|
|
279
270
|
agentDefinition: settings.definition,
|
|
@@ -345,7 +336,16 @@ function reconcileMemberSeats(roomId, members, observed, ownerSeatCid) {
|
|
|
345
336
|
&& seat.seat_state === 'active');
|
|
346
337
|
const seats = current.member_seats.map(seat => {
|
|
347
338
|
const member = members.find(candidate => candidate.name === seat.role_name);
|
|
339
|
+
if (!member)
|
|
340
|
+
return seat;
|
|
341
|
+
const readiness = readRoomReadiness(current.room_identity_cid, member.name);
|
|
342
|
+
if (!readiness || readiness.room !== roomId || readiness.invite !== seat.invite_id) {
|
|
343
|
+
complete = false;
|
|
344
|
+
return seat;
|
|
345
|
+
}
|
|
348
346
|
const found = exactCoworkSeat(observed, member, seat.invite_id);
|
|
347
|
+
if (found && found.identity_cid !== readiness.cid)
|
|
348
|
+
throw new Error('Cowork seat CID does not match supervisor provisioned identity');
|
|
349
349
|
if (!found || found.seat_state !== 'active') {
|
|
350
350
|
complete = false;
|
|
351
351
|
return seat;
|
|
@@ -364,9 +364,41 @@ function assertCoworkRoomPolicy(room, expectedAnonymous) {
|
|
|
364
364
|
if ((room.anonymous ?? false) !== expectedAnonymous)
|
|
365
365
|
throw new Error(`Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(expectedAnonymous)})`);
|
|
366
366
|
}
|
|
367
|
+
function assertProvisioningOpen(input) {
|
|
368
|
+
if (input.taskId && getTask(input.taskId).terminal_intent)
|
|
369
|
+
throw new Error(`task ${input.taskId} has an accepted terminal intent; refusing to provision members`);
|
|
370
|
+
const room = getRoomRecord(input.roomId);
|
|
371
|
+
if (room?.state === 'closing' || room?.state === 'closed')
|
|
372
|
+
throw new Error(`room ${input.roomId} is ${room.state}; refusing to provision members`);
|
|
373
|
+
}
|
|
367
374
|
export async function provisionMembers(input) {
|
|
375
|
+
// Same lock order as terminal/deletion settlement. Serialize concurrent retries
|
|
376
|
+
// and standalone retirement as well as member publication. Seat waits are bounded.
|
|
377
|
+
const run = () => withFileLock(roomCloseLockPath(input.roomId), () => {
|
|
378
|
+
assertProvisioningOpen(input);
|
|
379
|
+
if (input.append) {
|
|
380
|
+
const task = input.taskId ? getTask(input.taskId) : undefined;
|
|
381
|
+
const room = getRoomRecord(input.roomId);
|
|
382
|
+
if (!task || !['active', 'review'].includes(task.state) || task.blocked || room?.state !== 'active'
|
|
383
|
+
|| room.task_id !== task.task_id || room.room_identity_cid !== task.room_identity_cid)
|
|
384
|
+
throw new Error('Adding an agent requires the exact active, unblocked task room');
|
|
385
|
+
const names = expandMembers(input.template, shortId(task.task_id)).map(m => m.name);
|
|
386
|
+
if (room.member_seats.some(seat => names.includes(seat.role_name)))
|
|
387
|
+
throw new Error('Member slot is already recorded; inspect its existing launch');
|
|
388
|
+
if (room.member_seats.some(seat => seat.seat_state !== 'active'))
|
|
389
|
+
throw new Error('Existing member admission must finish before adding an agent');
|
|
390
|
+
}
|
|
391
|
+
if (!getRoomRecord(input.roomId))
|
|
392
|
+
throw new Error(`room ${input.roomId} is missing; refusing provisioning`);
|
|
393
|
+
return provisionMembersUnlocked(input);
|
|
394
|
+
}, {}, CLOSE_LOCK_STALE_MS);
|
|
395
|
+
return input.taskId
|
|
396
|
+
? withFileLock(taskOperationLockPath(input.taskId), run, {}, TASK_OPERATION_LOCK_STALE_MS)
|
|
397
|
+
: run();
|
|
398
|
+
}
|
|
399
|
+
async function provisionMembersUnlocked(input) {
|
|
368
400
|
const { cfg, cowork, roomId, taskId, template } = input;
|
|
369
|
-
//
|
|
401
|
+
// The common task-operation lock protects this epoch through all member launches.
|
|
370
402
|
if (taskId && taskDeletionState(taskId) !== 'none')
|
|
371
403
|
throw new Error(`task ${taskId} is pending deletion; refusing to provision members`);
|
|
372
404
|
const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
|
|
@@ -378,6 +410,12 @@ export async function provisionMembers(input) {
|
|
|
378
410
|
const existing = getRoomRecord(roomId);
|
|
379
411
|
if (!existing?.room_identity_cid)
|
|
380
412
|
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
413
|
+
if (existing.workspace) {
|
|
414
|
+
validateWorkspace(existing.workspace, taskId ? 'task' : 'room', taskId ?? roomId);
|
|
415
|
+
ensureWorkspace(existing.workspace);
|
|
416
|
+
for (const setting of settings.values())
|
|
417
|
+
setting.definition.cwd = existing.workspace.path;
|
|
418
|
+
}
|
|
381
419
|
const roomIdentityCid = existing.room_identity_cid;
|
|
382
420
|
const ownerSeatCid = existing.owner_seat_cid ?? null;
|
|
383
421
|
const roomPolicy = storedRoomLaunchPolicy(existing.room_policy);
|
|
@@ -385,22 +423,23 @@ export async function provisionMembers(input) {
|
|
|
385
423
|
const resuming = members.length > 0
|
|
386
424
|
&& members.every(member => persistedNames.has(member.name));
|
|
387
425
|
if (!resuming) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
426
|
+
if (!input.append)
|
|
427
|
+
advanceSaga(roomId, 'create_members', 3);
|
|
428
|
+
updateMemberSeats(roomId, [...(input.append ? existing.member_seats : []), ...members.map(member => {
|
|
429
|
+
const memberSettings = settings.get(member.name);
|
|
430
|
+
const evidence = launchDefinition(memberSettings.definition);
|
|
431
|
+
return ({
|
|
432
|
+
role_name: member.name,
|
|
433
|
+
slot: member.slot,
|
|
434
|
+
cowork_role: member.coworkRole,
|
|
435
|
+
seat_state: 'pending',
|
|
436
|
+
launch: { state: 'pending', attempt: 0,
|
|
437
|
+
agent_definition: evidence.projection, agent_fingerprint: evidence.fingerprint,
|
|
438
|
+
agent_template: memberSettings.template,
|
|
439
|
+
agent_template_hash: memberSettings.templateHash,
|
|
440
|
+
updated_at: new Date().toISOString() },
|
|
441
|
+
});
|
|
442
|
+
})], input.append ? template : undefined);
|
|
404
443
|
}
|
|
405
444
|
else {
|
|
406
445
|
for (const member of members) {
|
|
@@ -418,7 +457,8 @@ export async function provisionMembers(input) {
|
|
|
418
457
|
now: input.startupWait?.now ?? Date.now,
|
|
419
458
|
sleep: input.startupWait?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
420
459
|
};
|
|
421
|
-
|
|
460
|
+
if (!input.append)
|
|
461
|
+
advanceSaga(roomId, 'join_role_groups', 4);
|
|
422
462
|
try {
|
|
423
463
|
const initialRoom = await cowork.recoverRoom(roomId);
|
|
424
464
|
assertCoworkRoomPolicy(initialRoom, roomPolicy.anonymous);
|
|
@@ -439,17 +479,31 @@ export async function provisionMembers(input) {
|
|
|
439
479
|
provision: input, member, settings: settings.get(member.name), task, roomIdentityCid,
|
|
440
480
|
}))
|
|
441
481
|
continue;
|
|
482
|
+
// A previous failed launch keeps its invite pointer durably. Never
|
|
483
|
+
// overwrite that requirement until the supported revoke has succeeded;
|
|
484
|
+
// a transport failure must fence subsequent invite issuance too.
|
|
485
|
+
if (currentSeat.invite_id) {
|
|
486
|
+
const observed = await cowork.getRoom(roomId);
|
|
487
|
+
if (!observed || observed.identity_cid !== roomIdentityCid)
|
|
488
|
+
throw new Error('cannot verify room before failed-attempt invite cleanup');
|
|
489
|
+
if (observed.seats.some(seat => seat.invite_id === currentSeat.invite_id
|
|
490
|
+
&& seat.seat_state !== 'removed'))
|
|
491
|
+
throw new Error('failed-attempt invite has an admitted seat; reconcile before replacing the member');
|
|
492
|
+
await cowork.revokeInvite(roomId, currentSeat.invite_id);
|
|
493
|
+
}
|
|
442
494
|
const issued = await cowork.issueInvite(roomId, {
|
|
443
495
|
mode: 'one_time', role: member.coworkRole, min_accepts: 1,
|
|
444
496
|
});
|
|
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
497
|
try {
|
|
498
|
+
assertProvisioningOpen(input);
|
|
499
|
+
const seats = getRoomRecord(roomId).member_seats.map(seat => seat.role_name === member.name ? { ...seat, invite_id: issued.invite_id } : seat);
|
|
500
|
+
updateMemberSeats(roomId, seats);
|
|
448
501
|
await launchMember({
|
|
449
502
|
provision: input,
|
|
450
503
|
member,
|
|
451
504
|
settings: settings.get(member.name),
|
|
452
505
|
startup: {
|
|
506
|
+
workspace: existing.workspace,
|
|
453
507
|
room_id: roomId,
|
|
454
508
|
room_identity_cid: roomIdentityCid,
|
|
455
509
|
identity_name: member.name,
|
|
@@ -463,7 +517,12 @@ export async function provisionMembers(input) {
|
|
|
463
517
|
});
|
|
464
518
|
}
|
|
465
519
|
catch (error) {
|
|
466
|
-
|
|
520
|
+
try {
|
|
521
|
+
await cowork.revokeInvite(roomId, issued.invite_id);
|
|
522
|
+
}
|
|
523
|
+
catch (cleanupError) {
|
|
524
|
+
throw new AggregateError([error, cleanupError], 'member launch failed and invite cleanup is unresolved; retry must revoke the retained requirement first');
|
|
525
|
+
}
|
|
467
526
|
throw error;
|
|
468
527
|
}
|
|
469
528
|
}
|
|
@@ -484,20 +543,23 @@ export async function provisionMembers(input) {
|
|
|
484
543
|
}
|
|
485
544
|
throw error;
|
|
486
545
|
}
|
|
487
|
-
|
|
546
|
+
if (!input.append)
|
|
547
|
+
advanceSaga(roomId, 'wait_seats', 5);
|
|
488
548
|
const deadline = policy.now() + policy.timeoutMs;
|
|
489
549
|
let delay = policy.initialDelayMs;
|
|
490
550
|
for (;;) {
|
|
491
551
|
const remote = await cowork.recoverRoom(roomId);
|
|
492
552
|
assertCoworkRoomPolicy(remote, roomPolicy.anonymous);
|
|
493
553
|
const reconciled = reconcileMemberSeats(roomId, members, remote.seats, ownerSeatCid ?? undefined);
|
|
494
|
-
if (reconciled.complete)
|
|
554
|
+
if (reconciled.complete && remote.state === 'active')
|
|
495
555
|
break;
|
|
496
556
|
if (policy.now() >= deadline) {
|
|
497
|
-
|
|
557
|
+
if (!input.append)
|
|
558
|
+
advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
|
|
498
559
|
return getRoomRecord(roomId);
|
|
499
560
|
}
|
|
500
|
-
|
|
561
|
+
if (!input.append)
|
|
562
|
+
advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
|
|
501
563
|
await policy.sleep(delay);
|
|
502
564
|
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
503
565
|
}
|
|
@@ -522,15 +584,87 @@ export async function provisionMembers(input) {
|
|
|
522
584
|
};
|
|
523
585
|
});
|
|
524
586
|
updateTaskMembers(taskId, taskMembers);
|
|
525
|
-
if (getTask(taskId).blocked)
|
|
587
|
+
if (!input.append && getTask(taskId).blocked)
|
|
526
588
|
unblockTask(taskId);
|
|
527
589
|
}
|
|
590
|
+
if (input.append)
|
|
591
|
+
return getRoomRecord(roomId);
|
|
528
592
|
advanceSaga(roomId, 'activate', 6);
|
|
529
593
|
const record = activateRoom(roomId);
|
|
530
594
|
if (taskId)
|
|
531
595
|
activateTask(taskId);
|
|
532
596
|
return record;
|
|
533
597
|
}
|
|
598
|
+
/** Reconcile proven, admitted launches without any spawn, archive, invite or recovery path. */
|
|
599
|
+
export async function reconcileExistingTaskMembers(input) {
|
|
600
|
+
return withFileLock(taskOperationLockPath(input.taskId), async () => {
|
|
601
|
+
const task = getTask(input.taskId);
|
|
602
|
+
if (!['provisioning', 'active'].includes(task.state) || task.terminal_intent || taskDeletionState(input.taskId) !== 'none'
|
|
603
|
+
|| !task.room_id)
|
|
604
|
+
throw new Error('task is not open for existing-member reconciliation');
|
|
605
|
+
const roomId = task.room_id;
|
|
606
|
+
return withFileLock(roomCloseLockPath(roomId), async () => {
|
|
607
|
+
const current = getRoomRecord(roomId);
|
|
608
|
+
if (!current || !['provisioning', 'active'].includes(current.state) || current.close
|
|
609
|
+
|| current.task_id !== input.taskId || !current.room_identity_cid
|
|
610
|
+
|| task.room_identity_cid !== current.room_identity_cid)
|
|
611
|
+
throw new Error('room is not open for existing-member reconciliation');
|
|
612
|
+
const names = new Set(input.expectedLaunches.map(seat => seat.role_name));
|
|
613
|
+
if (!names.size || names.size !== input.expectedLaunches.length
|
|
614
|
+
|| current.member_seats.length !== names.size
|
|
615
|
+
|| current.member_seats.some(seat => !names.has(seat.role_name)))
|
|
616
|
+
throw new Error('expected launch roster does not match the persisted room');
|
|
617
|
+
const remote = await input.cowork.getRoom(roomId);
|
|
618
|
+
if (!remote || remote.room_id !== roomId || remote.identity_cid !== current.room_identity_cid
|
|
619
|
+
|| remote.state !== 'active')
|
|
620
|
+
throw new Error('Cowork room is not the exact active room');
|
|
621
|
+
assertCoworkRoomPolicy(remote, storedRoomLaunchPolicy(current.room_policy).anonymous);
|
|
622
|
+
if (current.owner_seat_cid && !remote.seats.some(seat => seat.identity_cid === current.owner_seat_cid && seat.seat_state === 'active'))
|
|
623
|
+
throw new Error('expected Owner seat is not active');
|
|
624
|
+
const seats = [];
|
|
625
|
+
for (const seat of current.member_seats) {
|
|
626
|
+
const expected = input.expectedLaunches.find(item => item.role_name === seat.role_name);
|
|
627
|
+
const launch = seat.launch;
|
|
628
|
+
if (seat.seat_state === 'removed' || !seat.invite_id || launch?.state !== 'launched'
|
|
629
|
+
|| launch.launch_id !== expected.launch_id || !launch.action_id || !launch.mission_sha256
|
|
630
|
+
|| (seat.identity_cid && seat.identity_cid !== expected.identity_cid))
|
|
631
|
+
throw new Error('persisted member launch does not match reconciliation evidence');
|
|
632
|
+
const dir = agentDir(seat.role_name, true);
|
|
633
|
+
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))
|
|
634
|
+
throw new Error('member startup provenance mismatch');
|
|
635
|
+
const supervisor = readTempSupervisor(dir);
|
|
636
|
+
if (!supervisor || supervisor.role !== seat.role_name || supervisor.launchId !== expected.launch_id
|
|
637
|
+
|| await tempSupervisorLiveness(dir) !== 'running')
|
|
638
|
+
throw new Error('expected member supervisor is not running');
|
|
639
|
+
const ready = readRoomReadiness(current.room_identity_cid, seat.role_name);
|
|
640
|
+
const matches = remote.seats.filter(item => item.display_name === seat.role_name
|
|
641
|
+
&& item.seat_state !== 'removed');
|
|
642
|
+
const found = matches[0];
|
|
643
|
+
if (!ready || ready.room !== roomId || ready.invite !== seat.invite_id
|
|
644
|
+
|| ready.cid !== expected.identity_cid || matches.length !== 1 || !found
|
|
645
|
+
|| found.identity_cid !== expected.identity_cid || found.role !== seat.cowork_role
|
|
646
|
+
|| found.seat_state !== 'active' || found.invite_id !== seat.invite_id)
|
|
647
|
+
throw new Error('member readiness or authenticated Cowork seat mismatch');
|
|
648
|
+
seats.push({ ...seat, identity_cid: expected.identity_cid, seat_state: 'active' });
|
|
649
|
+
}
|
|
650
|
+
if (input.checkOnly)
|
|
651
|
+
return current;
|
|
652
|
+
// No mutation until the complete roster has passed; these writes are replayable
|
|
653
|
+
// under the same task/room lifecycle locks if interrupted between files.
|
|
654
|
+
updateMemberSeats(roomId, seats);
|
|
655
|
+
updateTaskMembers(input.taskId, seats.map(seat => ({ name: seat.role_name,
|
|
656
|
+
identity_cid: seat.identity_cid, slot: seat.slot, cowork_role: seat.cowork_role })));
|
|
657
|
+
if (getTask(input.taskId).blocked)
|
|
658
|
+
unblockTask(input.taskId);
|
|
659
|
+
if (current.state !== 'active')
|
|
660
|
+
advanceSaga(roomId, 'activate', 6);
|
|
661
|
+
const activated = current.state === 'active' ? getRoomRecord(roomId) : activateRoom(roomId);
|
|
662
|
+
if (task.state !== 'active')
|
|
663
|
+
activateTask(input.taskId);
|
|
664
|
+
return activated;
|
|
665
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
666
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
667
|
+
}
|
|
534
668
|
export async function cleanupMembers(input) {
|
|
535
669
|
const room = getRoomRecord(input.roomId);
|
|
536
670
|
if (!room)
|
|
@@ -22,7 +22,7 @@ export declare function advanceSaga(id: string, phase: SagaPhase, stepIndex?: nu
|
|
|
22
22
|
export declare function setSagaError(id: string, error: string, hint?: string, detail?: ProvisioningDetail): RoomOrchestrationRecord;
|
|
23
23
|
export declare function setRoomIdentity(id: string, roomIdentityCid: string): RoomOrchestrationRecord;
|
|
24
24
|
export declare function setOwnerSeat(id: string, ownerSeatCid: string, inviteFingerprint: string): RoomOrchestrationRecord;
|
|
25
|
-
export declare function updateMemberSeats(id: string, seats: RoomMemberSeat[]): RoomOrchestrationRecord;
|
|
25
|
+
export declare function updateMemberSeats(id: string, seats: RoomMemberSeat[], additionalTemplate?: import('./types.js').TemplateSnapshot): RoomOrchestrationRecord;
|
|
26
26
|
export declare function updateRoomRoleBriefing(id: string, role: string, definition: RoomRoleBriefingDefinition): RoomOrchestrationRecord;
|
|
27
27
|
export declare function updateRoomHistoryCursor(id: string, cursor: number): RoomOrchestrationRecord;
|
|
28
28
|
export declare function updateMemberStartup(id: string, roleName: string, update: {
|
|
@@ -31,6 +31,8 @@ export declare function updateMemberStartup(id: string, roleName: string, update
|
|
|
31
31
|
export declare function activateRoom(id: string): RoomOrchestrationRecord;
|
|
32
32
|
export declare function closeRoom(id: string): RoomOrchestrationRecord;
|
|
33
33
|
export declare function beginRoomClose(id: string): RoomOrchestrationRecord;
|
|
34
|
-
export declare function advanceMemberRetirement(id: string, roleName: string, phase: MemberRetirementPhase, launchId: string, archivePath?: string): RoomOrchestrationRecord;
|
|
34
|
+
export declare function advanceMemberRetirement(id: string, roleName: string, phase: MemberRetirementPhase, launchId: string, archivePath?: string, absenceVerified?: boolean): RoomOrchestrationRecord;
|
|
35
35
|
export declare function advanceRoomClose(id: string, phase: RoomClosePhase): RoomOrchestrationRecord;
|
|
36
36
|
export declare function setRoomCloseError(id: string, error: string, recoveryHint: string): RoomOrchestrationRecord;
|
|
37
|
+
/** Settlement-only binding recovery; never publishes a seat or changes a launch. */
|
|
38
|
+
export declare function recordRetirementMemberCids(id: string, recovered: ReadonlyArray<RoomMemberSeat>): RoomOrchestrationRecord;
|
|
@@ -2,17 +2,28 @@ 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');
|
|
8
|
-
function roomPath(id) {
|
|
10
|
+
function roomPath(id) {
|
|
11
|
+
if (!/^[a-zA-Z0-9_-]{1,120}$/.test(id))
|
|
12
|
+
throw new RoomStateError('Invalid room ID');
|
|
13
|
+
return join(roomsDir(), `${id}.json`);
|
|
14
|
+
}
|
|
9
15
|
export class RoomStateError extends Error {
|
|
10
16
|
}
|
|
11
17
|
function readRoom(id) {
|
|
12
18
|
const p = roomPath(id);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === 'ENOENT')
|
|
24
|
+
throw new RoomStateError(`room not found: ${id}`);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
16
27
|
}
|
|
17
28
|
function writeRoom(record) {
|
|
18
29
|
mkdirSync(roomsDir(), { recursive: true });
|
|
@@ -25,6 +36,8 @@ export function createRoomRecord(input) {
|
|
|
25
36
|
const requested = storedRoomLaunchPolicy(input.room_policy);
|
|
26
37
|
if (JSON.stringify(before) !== JSON.stringify(requested))
|
|
27
38
|
throw new RoomStateError(`room ${input.room_id} launch policy mismatch`);
|
|
39
|
+
if (existing.workspace)
|
|
40
|
+
ensureWorkspace(existing.workspace);
|
|
28
41
|
return existing;
|
|
29
42
|
}
|
|
30
43
|
const record = {
|
|
@@ -40,15 +53,20 @@ export function createRoomRecord(input) {
|
|
|
40
53
|
state: 'provisioning',
|
|
41
54
|
created_at: new Date().toISOString(),
|
|
42
55
|
};
|
|
56
|
+
record.workspace = input.task_id ? getTask(input.task_id).workspace : planWorkspace('room', input.room_id);
|
|
43
57
|
writeRoom(record);
|
|
58
|
+
if (record.workspace)
|
|
59
|
+
ensureWorkspace(record.workspace);
|
|
44
60
|
return record;
|
|
45
61
|
}
|
|
46
62
|
export function getRoomRecord(id) {
|
|
47
63
|
try {
|
|
48
64
|
return readRoom(id);
|
|
49
65
|
}
|
|
50
|
-
catch {
|
|
51
|
-
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof RoomStateError && error.message === `room not found: ${id}`)
|
|
68
|
+
return undefined;
|
|
69
|
+
throw error;
|
|
52
70
|
}
|
|
53
71
|
}
|
|
54
72
|
/** Remove a terminal orchestration record from the live room inventory. */
|
|
@@ -110,9 +128,13 @@ export function setOwnerSeat(id, ownerSeatCid, inviteFingerprint) {
|
|
|
110
128
|
writeRoom(r);
|
|
111
129
|
return r;
|
|
112
130
|
}
|
|
113
|
-
export function updateMemberSeats(id, seats) {
|
|
131
|
+
export function updateMemberSeats(id, seats, additionalTemplate) {
|
|
114
132
|
const r = readRoom(id);
|
|
133
|
+
if (r.state === 'closing' || r.state === 'closed')
|
|
134
|
+
throw new RoomStateError(`room ${id} is ${r.state}; member provisioning is fenced`);
|
|
115
135
|
r.member_seats = seats;
|
|
136
|
+
if (additionalTemplate)
|
|
137
|
+
r.additional_member_templates = [...(r.additional_member_templates ?? []), additionalTemplate];
|
|
116
138
|
writeRoom(r);
|
|
117
139
|
return r;
|
|
118
140
|
}
|
|
@@ -135,6 +157,8 @@ const LAUNCH_ORDER = [
|
|
|
135
157
|
];
|
|
136
158
|
export function updateMemberStartup(id, roleName, update) {
|
|
137
159
|
const r = readRoom(id);
|
|
160
|
+
if (r.state === 'closing' || r.state === 'closed')
|
|
161
|
+
throw new RoomStateError(`room ${id} is ${r.state}; member provisioning is fenced`);
|
|
138
162
|
const seat = r.member_seats.find(candidate => candidate.role_name === roleName);
|
|
139
163
|
if (!seat)
|
|
140
164
|
throw new RoomStateError(`room ${id} has no recorded member ${roleName}`);
|
|
@@ -151,6 +175,8 @@ export function updateMemberStartup(id, roleName, update) {
|
|
|
151
175
|
}
|
|
152
176
|
export function activateRoom(id) {
|
|
153
177
|
const r = readRoom(id);
|
|
178
|
+
if (r.state === 'closing' || r.state === 'closed')
|
|
179
|
+
throw new RoomStateError(`room ${id} is ${r.state}; activation is fenced`);
|
|
154
180
|
r.state = 'active';
|
|
155
181
|
r.saga = { phase: 'completed', step_index: 0 };
|
|
156
182
|
delete r.provisioning_detail;
|
|
@@ -198,7 +224,7 @@ export function beginRoomClose(id) {
|
|
|
198
224
|
const RETIREMENT_ORDER = [
|
|
199
225
|
'stop_requested', 'liveness_absent', 'archive_secured', 'identity_absent',
|
|
200
226
|
];
|
|
201
|
-
export function advanceMemberRetirement(id, roleName, phase, launchId, archivePath) {
|
|
227
|
+
export function advanceMemberRetirement(id, roleName, phase, launchId, archivePath, absenceVerified = false) {
|
|
202
228
|
const r = readRoom(id);
|
|
203
229
|
if (r.state !== 'closing')
|
|
204
230
|
throw new RoomStateError(`room ${id} is not closing`);
|
|
@@ -215,6 +241,7 @@ export function advanceMemberRetirement(id, roleName, phase, launchId, archivePa
|
|
|
215
241
|
throw new RoomStateError(`room ${id} member ${roleName} retirement cannot move backward to ${phase}`);
|
|
216
242
|
}
|
|
217
243
|
seat.retirement = {
|
|
244
|
+
...(absenceVerified || previous?.absence_verified ? { absence_verified: true } : {}),
|
|
218
245
|
phase,
|
|
219
246
|
launch_id: launchId,
|
|
220
247
|
updated_at: new Date().toISOString(),
|
|
@@ -267,3 +294,21 @@ export function setRoomCloseError(id, error, recoveryHint) {
|
|
|
267
294
|
writeRoom(r);
|
|
268
295
|
return r;
|
|
269
296
|
}
|
|
297
|
+
/** Settlement-only binding recovery; never publishes a seat or changes a launch. */
|
|
298
|
+
export function recordRetirementMemberCids(id, recovered) {
|
|
299
|
+
const room = readRoom(id);
|
|
300
|
+
if (room.state !== 'closing')
|
|
301
|
+
throw new RoomStateError('Room is not closing');
|
|
302
|
+
for (const proof of recovered) {
|
|
303
|
+
const seat = room.member_seats.find(value => value.role_name === proof.role_name);
|
|
304
|
+
if (!seat || seat.launch?.action_id !== proof.launch?.action_id || seat.invite_id !== proof.invite_id)
|
|
305
|
+
throw new RoomStateError('Member changed during identity recovery');
|
|
306
|
+
if (!proof.identity_cid)
|
|
307
|
+
continue;
|
|
308
|
+
if (seat.identity_cid && seat.identity_cid.toLowerCase() !== proof.identity_cid.toLowerCase())
|
|
309
|
+
throw new RoomStateError('Member identity changed during recovery');
|
|
310
|
+
seat.identity_cid = proof.identity_cid;
|
|
311
|
+
}
|
|
312
|
+
writeRoom(room);
|
|
313
|
+
return room;
|
|
314
|
+
}
|
|
@@ -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 {
|
|
@@ -17,6 +18,10 @@ export interface CreateTaskInput {
|
|
|
17
18
|
idempotency_key?: string;
|
|
18
19
|
start?: boolean;
|
|
19
20
|
no_room?: boolean;
|
|
21
|
+
layout?: {
|
|
22
|
+
name: string;
|
|
23
|
+
definition_hash: string;
|
|
24
|
+
};
|
|
20
25
|
room_id?: string;
|
|
21
26
|
listId?: string;
|
|
22
27
|
}
|
|
@@ -61,9 +66,8 @@ export declare function getDeletingTask(id: string): TaskRecord;
|
|
|
61
66
|
export declare function taskDeletionState(id: string): 'none' | 'pending' | 'absent';
|
|
62
67
|
/**
|
|
63
68
|
* Durable, surface-independent audit evidence for a permanent deletion. The
|
|
64
|
-
* receipt
|
|
65
|
-
*
|
|
66
|
-
* room content.
|
|
69
|
+
* receipt supports in-flight recovery only: written with acceptance intent,
|
|
70
|
+
* then erased before settlement is reported. It may contain a bounded title.
|
|
67
71
|
*/
|
|
68
72
|
export interface TaskDeletionReceipt {
|
|
69
73
|
schema_version: 1;
|
|
@@ -74,6 +78,7 @@ export interface TaskDeletionReceipt {
|
|
|
74
78
|
original_state: TaskState;
|
|
75
79
|
room_id?: string;
|
|
76
80
|
member_count: number;
|
|
81
|
+
archived_absences?: ArchivedMemberAbsence[];
|
|
77
82
|
settled_at?: string;
|
|
78
83
|
result?: 'deleted';
|
|
79
84
|
}
|
|
@@ -86,7 +91,7 @@ export declare function readTaskDeletionReceipt(id: string): TaskDeletionReceipt
|
|
|
86
91
|
* settlement rather than deleting resources without audit evidence.
|
|
87
92
|
*/
|
|
88
93
|
export declare function ensureTaskDeletionReceipt(id: string): void;
|
|
89
|
-
/**
|
|
94
|
+
/** Erase the in-flight receipt; also heals a crash after task unlink. */
|
|
90
95
|
export declare function completeTaskDeletionReceipt(id: string): void;
|
|
91
96
|
export type TaskDeletionAcceptance = {
|
|
92
97
|
status: 'accepted' | 'pending';
|
|
@@ -122,7 +127,12 @@ export declare function advanceTaskDeletionMember(id: string, name: string, phas
|
|
|
122
127
|
interface SeatEvidence {
|
|
123
128
|
role_name: string;
|
|
124
129
|
identity_cid?: string;
|
|
130
|
+
launch?: {
|
|
131
|
+
launch_id?: string;
|
|
132
|
+
action_id?: string;
|
|
133
|
+
};
|
|
125
134
|
retirement?: {
|
|
135
|
+
absence_verified?: boolean;
|
|
126
136
|
phase: TaskDeletionMemberPhase;
|
|
127
137
|
launch_id: string;
|
|
128
138
|
archive_path?: string;
|
|
@@ -146,14 +156,39 @@ export declare function upsertTaskDeletionMembersFromSeats(id: string, seats: Re
|
|
|
146
156
|
*
|
|
147
157
|
* The room saga is trusted for phase jumps, but partial or corrupt retained
|
|
148
158
|
* records must not become false success: every seat must be identity_absent;
|
|
149
|
-
* a real launch requires archive evidence; an identity-less seat
|
|
150
|
-
*
|
|
159
|
+
* a real launch requires archive evidence; an identity-less seat requires
|
|
160
|
+
* never-launched proof or a freshly verified archived-absence checkpoint.
|
|
151
161
|
*/
|
|
152
|
-
export declare function importTaskDeletionRetirementEvidence(id: string, seats: ReadonlyArray<SeatEvidence>): TaskRecord;
|
|
162
|
+
export declare function importTaskDeletionRetirementEvidence(id: string, seats: ReadonlyArray<SeatEvidence>, proofs?: ReadonlyArray<ArchivedMemberAbsence>): TaskRecord;
|
|
163
|
+
/** Settlement-only checkpoint, under the task-operation lock after fresh archive verification.
|
|
164
|
+
* From here retries still verify live absence, but owned archives may be consumed.
|
|
165
|
+
*/
|
|
166
|
+
export declare function beginTaskWorkspaceCleanup(id: string): void;
|
|
153
167
|
/**
|
|
154
168
|
* Physically remove a deletion-pending task record. Settlement-only: callers
|
|
155
169
|
* must have completed member retirement and room cleanup first. Missing
|
|
156
170
|
* records are the idempotent already-settled outcome.
|
|
157
171
|
*/
|
|
158
172
|
export declare function unlinkDeletedTask(id: string): boolean;
|
|
173
|
+
/** Called only after current temp and exact identity absence have been checked. */
|
|
174
|
+
export declare function settleAbsentTaskDeletionMember(id: string, name: string): void;
|
|
175
|
+
/** Room-only erasure keeps the parent Task but removes its dead room/member links. */
|
|
176
|
+
export declare function detachDeletedRoom(taskId: string, roomId: string): void;
|
|
177
|
+
/** A completed room may consume its archives while other task members still retire. */
|
|
178
|
+
export declare function beginTaskArchiveCleanup(id: string): void;
|
|
179
|
+
/** Metadata-only edit; compare under the task lock to prevent lost updates. */
|
|
180
|
+
/**
|
|
181
|
+
* Choose, change or clear a Backlog task's Room Layout before it starts.
|
|
182
|
+
* `expected` is the layout name the caller saw (null = none); a different
|
|
183
|
+
* current choice is a stale edit. Choosing a layout drops the sealed room
|
|
184
|
+
* template plan; the caller releases the returned launch snapshot hash.
|
|
185
|
+
*/
|
|
186
|
+
export declare function updateTaskLayout(id: string, expected: string | null, next: {
|
|
187
|
+
name: string;
|
|
188
|
+
definition_hash: string;
|
|
189
|
+
} | null): {
|
|
190
|
+
task: TaskRecord;
|
|
191
|
+
releasedSnapshot?: string;
|
|
192
|
+
};
|
|
193
|
+
export declare function updateTaskBrief(id: string, brief: string, expectedBrief: string): TaskRecord;
|
|
159
194
|
export {};
|