@ours.network/fleet 1.2.0-nightly.5 → 1.2.0-nightly.50
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 +263 -53
- package/dist/account-origin.d.ts +2 -0
- package/dist/account-origin.js +6 -0
- package/dist/agent-ours/bridge.js +9 -0
- package/dist/agent-ours/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -0
- package/dist/agent-ours/harness.js +8 -0
- package/dist/agent-ours/runtime.d.ts +6 -0
- package/dist/agent-ours/runtime.js +36 -0
- package/dist/agent-ours/service.js +33 -13
- package/dist/application/codex-models.d.ts +15 -0
- package/dist/application/codex-models.js +56 -0
- package/dist/application/conversation-history.d.ts +25 -0
- package/dist/application/conversation-history.js +189 -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/role-creation-service.js +1 -1
- package/dist/application/role-removal-service.js +2 -0
- package/dist/application/role-repository.js +2 -2
- 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 +30 -0
- package/dist/application/session-control.js +72 -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 +91 -0
- package/dist/application/task-layouts.js +249 -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.d.ts +2 -0
- package/dist/briefing.js +8 -1
- package/dist/build-info.json +7 -4
- package/dist/capabilities.d.ts +6 -1
- package/dist/capabilities.js +8 -0
- package/dist/cli.js +376 -34
- package/dist/client-profile.d.ts +5 -17
- package/dist/client-profile.js +4 -127
- package/dist/config.d.ts +28 -0
- package/dist/config.js +41 -6
- package/dist/creation.js +2 -2
- 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 +126 -22
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +55 -60
- package/dist/erased-resources.d.ts +9 -0
- package/dist/erased-resources.js +87 -0
- package/dist/fleet-command-audit.d.ts +7 -0
- package/dist/fleet-command-audit.js +26 -9
- package/dist/harness/claude-code.js +9 -1
- package/dist/harness/claude-runtime.d.ts +12 -0
- package/dist/harness/claude-runtime.js +43 -0
- 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-reasoning.d.ts +5 -0
- package/dist/harness/codex-reasoning.js +12 -0
- package/dist/harness/codex-runtime.d.ts +11 -1
- package/dist/harness/codex-runtime.js +31 -4
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +20 -11
- package/dist/init-wizard.d.ts +2 -0
- package/dist/init-wizard.js +38 -19
- package/dist/isolation/policy.js +2 -0
- package/dist/isolation/types.d.ts +5 -0
- package/dist/managed-cli.d.ts +253 -0
- package/dist/managed-cli.js +923 -0
- package/dist/minimal-setup.d.ts +2 -0
- package/dist/minimal-setup.js +44 -0
- package/dist/monitor.d.ts +1 -34
- package/dist/monitor.js +24 -151
- package/dist/notifications/fleet-producer.d.ts +19 -0
- package/dist/notifications/fleet-producer.js +208 -0
- package/dist/notifications/outbox.d.ts +46 -0
- package/dist/notifications/outbox.js +158 -0
- package/dist/notifications/target-cleanup.d.ts +8 -0
- package/dist/notifications/target-cleanup.js +41 -0
- package/dist/ops.js +5 -1
- 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/paths.d.ts +2 -0
- package/dist/paths.js +2 -0
- package/dist/preset-migration.js +25 -0
- package/dist/resolved-plan.js +1 -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.d.ts +1 -1
- package/dist/rooms-tasks/config.js +11 -4
- package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
- package/dist/rooms-tasks/cowork-adapter.js +8 -121
- 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-binding-client.d.ts +11 -0
- package/dist/rooms-tasks/layout-binding-client.js +56 -0
- package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
- package/dist/rooms-tasks/layout-binding-grants.js +128 -0
- package/dist/rooms-tasks/layout-cli.d.ts +4 -0
- package/dist/rooms-tasks/layout-cli.js +107 -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 +42 -0
- package/dist/rooms-tasks/layout-service.js +185 -0
- package/dist/rooms-tasks/layout.d.ts +114 -0
- package/dist/rooms-tasks/layout.js +250 -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 +173 -46
- 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 +19 -3
- package/dist/runner.js +128 -45
- package/dist/session/acp.d.ts +9 -1
- package/dist/session/acp.js +31 -6
- package/dist/session/arbiter.d.ts +5 -0
- package/dist/session/arbiter.js +28 -2
- package/dist/session/codex-app-server.d.ts +5 -0
- package/dist/session/codex-app-server.js +36 -3
- package/dist/session/control.d.ts +12 -1
- package/dist/session/control.js +68 -3
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-store.d.ts +1 -1
- package/dist/session/conversation-store.js +7 -11
- package/dist/session/conversation-types.d.ts +7 -3
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +6 -0
- package/dist/setup-tunnel-preflight.d.ts +34 -0
- package/dist/setup-tunnel-preflight.js +62 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/spawn.js +8 -1
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/subscriptions/cli.d.ts +91 -0
- package/dist/subscriptions/cli.js +223 -0
- package/dist/subscriptions/launch.d.ts +37 -0
- package/dist/subscriptions/launch.js +93 -0
- package/dist/subscriptions/login.d.ts +75 -0
- package/dist/subscriptions/login.js +182 -0
- package/dist/subscriptions/observed.d.ts +7 -0
- package/dist/subscriptions/observed.js +44 -0
- package/dist/subscriptions/probe.d.ts +10 -0
- package/dist/subscriptions/probe.js +73 -0
- package/dist/subscriptions/service.d.ts +102 -0
- package/dist/subscriptions/service.js +364 -0
- package/dist/subscriptions/store.d.ts +71 -0
- package/dist/subscriptions/store.js +140 -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/tunnel-setup-completion.d.ts +7 -0
- package/dist/tunnel-setup-completion.js +13 -0
- package/dist/tunnel-setup-input.d.ts +19 -0
- package/dist/tunnel-setup-input.js +124 -0
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +29 -1
- package/dist/web/auth.js +82 -7
- package/dist/web/available-port.d.ts +2 -0
- package/dist/web/available-port.js +23 -0
- package/dist/web/control.d.ts +3 -3
- package/dist/web/control.js +10 -8
- 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 +32 -0
- package/dist/web/prefix-gateway.js +234 -0
- package/dist/web/runtime.d.ts +3 -1
- package/dist/web/runtime.js +116 -31
- package/dist/web/server.d.ts +16 -0
- package/dist/web/server.js +588 -53
- package/dist/web/service.d.ts +11 -2
- package/dist/web/service.js +30 -4
- package/dist/web/workspace-devices.d.ts +36 -0
- package/dist/web/workspace-devices.js +101 -0
- package/dist/web/workspace-tunnel.d.ts +1 -0
- package/dist/web/workspace-tunnel.js +34 -0
- package/dist/workspace-enrollment.d.ts +82 -0
- package/dist/workspace-enrollment.js +566 -0
- package/dist/workspace-replacement.d.ts +66 -0
- package/dist/workspace-replacement.js +220 -0
- package/docs/live-voice.md +13 -0
- package/docs/room-layouts.md +227 -0
- package/docs/task-workspaces.md +130 -0
- package/docs/validation/managed-cli-permissions.md +421 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/scoped.yaml +21 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +15 -18
- package/presets/README.md +78 -0
- package/presets/brain-catalog.json +8 -2
- package/presets/fleet/agent_templates/Critic.yaml +1 -1
- package/presets/fleet/agent_templates/Developer.yaml +1 -1
- package/presets/fleet/agent_templates/Engineer.yaml +5 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +1 -1
- package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
- 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 +34 -11
- 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
|
@@ -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)
|
|
@@ -3,6 +3,7 @@ import type { RoomOrchestrationRecord, TemplateSnapshot } from './types.js';
|
|
|
3
3
|
import { type FleetConfig } from '../config.js';
|
|
4
4
|
export declare function getBinPath(): string;
|
|
5
5
|
export interface ProvisionMembersInput {
|
|
6
|
+
configPath?: string;
|
|
6
7
|
cfg: FleetConfig;
|
|
7
8
|
cowork: CoworkAdapter;
|
|
8
9
|
roomId: string;
|
|
@@ -12,6 +13,8 @@ export interface ProvisionMembersInput {
|
|
|
12
13
|
brief?: string;
|
|
13
14
|
goal?: string;
|
|
14
15
|
startupWait?: Partial<StartupWaitPolicy>;
|
|
16
|
+
/** Append only the supplied new slots to an already active room. */
|
|
17
|
+
append?: boolean;
|
|
15
18
|
}
|
|
16
19
|
export interface StartupWaitPolicy {
|
|
17
20
|
timeoutMs: number;
|
|
@@ -21,6 +24,17 @@ export interface StartupWaitPolicy {
|
|
|
21
24
|
sleep(ms: number): Promise<void>;
|
|
22
25
|
}
|
|
23
26
|
export declare function provisionMembers(input: ProvisionMembersInput): Promise<RoomOrchestrationRecord>;
|
|
27
|
+
/** Reconcile proven, admitted launches without any spawn, archive, invite or recovery path. */
|
|
28
|
+
export declare function reconcileExistingTaskMembers(input: {
|
|
29
|
+
taskId: string;
|
|
30
|
+
checkOnly?: boolean;
|
|
31
|
+
cowork: Pick<CoworkAdapter, 'getRoom'>;
|
|
32
|
+
expectedLaunches: ReadonlyArray<{
|
|
33
|
+
role_name: string;
|
|
34
|
+
launch_id: string;
|
|
35
|
+
identity_cid: string;
|
|
36
|
+
}>;
|
|
37
|
+
}): Promise<RoomOrchestrationRecord>;
|
|
24
38
|
export declare function cleanupMembers(input: {
|
|
25
39
|
roomId: string;
|
|
26
40
|
taskId?: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readRoomReadiness } from '../agent-ours/service.js';
|
|
2
|
+
import { ensureWorkspace, validateWorkspace } from './workspace.js';
|
|
2
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
5
|
import { join } from 'node:path';
|
|
@@ -14,7 +15,7 @@ import { readLaunchSnapshot, redactLaunchDefinition } from './launch-snapshot.js
|
|
|
14
15
|
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
|
|
15
16
|
import { controlRequest } from '../session/control.js';
|
|
16
17
|
import { SessionControlError } from '../session/types.js';
|
|
17
|
-
import { closeManagedRoom } from './close.js';
|
|
18
|
+
import { closeManagedRoom, roomCloseLockPath, CLOSE_LOCK_STALE_MS } from './close.js';
|
|
18
19
|
import { withFileLock } from '../atomic-file.js';
|
|
19
20
|
import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
|
|
20
21
|
import { TaskStateError, taskDeletionState } from './task-state.js';
|
|
@@ -116,6 +117,7 @@ function settingsFor(member, cfg, sealed) {
|
|
|
116
117
|
}
|
|
117
118
|
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid, anonymous) {
|
|
118
119
|
return buildRoomMemberTask({
|
|
120
|
+
workspace: getRoomRecord(input.roomId)?.workspace?.path,
|
|
119
121
|
taskId: input.taskId,
|
|
120
122
|
roomId: input.roomId,
|
|
121
123
|
roomIdentityCid,
|
|
@@ -129,7 +131,7 @@ function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCi
|
|
|
129
131
|
cowork_role: member.coworkRole,
|
|
130
132
|
persona: settings.persona,
|
|
131
133
|
},
|
|
132
|
-
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 => ({
|
|
133
135
|
role_name: candidate.name,
|
|
134
136
|
cowork_role: candidate.coworkRole,
|
|
135
137
|
})),
|
|
@@ -142,6 +144,10 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
|
|
|
142
144
|
try {
|
|
143
145
|
const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
|
|
144
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;
|
|
145
151
|
return role.identity === member.name
|
|
146
152
|
&& startup?.room_id === roomId
|
|
147
153
|
&& startup.room_identity_cid === roomIdentityCid
|
|
@@ -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,6 +336,8 @@ 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;
|
|
348
341
|
const readiness = readRoomReadiness(current.room_identity_cid, member.name);
|
|
349
342
|
if (!readiness || readiness.room !== roomId || readiness.invite !== seat.invite_id) {
|
|
350
343
|
complete = false;
|
|
@@ -371,9 +364,41 @@ function assertCoworkRoomPolicy(room, expectedAnonymous) {
|
|
|
371
364
|
if ((room.anonymous ?? false) !== expectedAnonymous)
|
|
372
365
|
throw new Error(`Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(expectedAnonymous)})`);
|
|
373
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
|
+
}
|
|
374
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) || 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 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) {
|
|
375
400
|
const { cfg, cowork, roomId, taskId, template } = input;
|
|
376
|
-
//
|
|
401
|
+
// The common task-operation lock protects this epoch through all member launches.
|
|
377
402
|
if (taskId && taskDeletionState(taskId) !== 'none')
|
|
378
403
|
throw new Error(`task ${taskId} is pending deletion; refusing to provision members`);
|
|
379
404
|
const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
|
|
@@ -385,6 +410,12 @@ export async function provisionMembers(input) {
|
|
|
385
410
|
const existing = getRoomRecord(roomId);
|
|
386
411
|
if (!existing?.room_identity_cid)
|
|
387
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
|
+
}
|
|
388
419
|
const roomIdentityCid = existing.room_identity_cid;
|
|
389
420
|
const ownerSeatCid = existing.owner_seat_cid ?? null;
|
|
390
421
|
const roomPolicy = storedRoomLaunchPolicy(existing.room_policy);
|
|
@@ -392,22 +423,23 @@ export async function provisionMembers(input) {
|
|
|
392
423
|
const resuming = members.length > 0
|
|
393
424
|
&& members.every(member => persistedNames.has(member.name));
|
|
394
425
|
if (!resuming) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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);
|
|
411
443
|
}
|
|
412
444
|
else {
|
|
413
445
|
for (const member of members) {
|
|
@@ -425,7 +457,8 @@ export async function provisionMembers(input) {
|
|
|
425
457
|
now: input.startupWait?.now ?? Date.now,
|
|
426
458
|
sleep: input.startupWait?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
427
459
|
};
|
|
428
|
-
|
|
460
|
+
if (!input.append)
|
|
461
|
+
advanceSaga(roomId, 'join_role_groups', 4);
|
|
429
462
|
try {
|
|
430
463
|
const initialRoom = await cowork.recoverRoom(roomId);
|
|
431
464
|
assertCoworkRoomPolicy(initialRoom, roomPolicy.anonymous);
|
|
@@ -446,17 +479,31 @@ export async function provisionMembers(input) {
|
|
|
446
479
|
provision: input, member, settings: settings.get(member.name), task, roomIdentityCid,
|
|
447
480
|
}))
|
|
448
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
|
+
}
|
|
449
494
|
const issued = await cowork.issueInvite(roomId, {
|
|
450
495
|
mode: 'one_time', role: member.coworkRole, min_accepts: 1,
|
|
451
496
|
});
|
|
452
|
-
const seats = getRoomRecord(roomId).member_seats.map(seat => seat.role_name === member.name ? { ...seat, invite_id: issued.invite_id } : seat);
|
|
453
|
-
updateMemberSeats(roomId, seats);
|
|
454
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);
|
|
455
501
|
await launchMember({
|
|
456
502
|
provision: input,
|
|
457
503
|
member,
|
|
458
504
|
settings: settings.get(member.name),
|
|
459
505
|
startup: {
|
|
506
|
+
workspace: existing.workspace,
|
|
460
507
|
room_id: roomId,
|
|
461
508
|
room_identity_cid: roomIdentityCid,
|
|
462
509
|
identity_name: member.name,
|
|
@@ -470,7 +517,12 @@ export async function provisionMembers(input) {
|
|
|
470
517
|
});
|
|
471
518
|
}
|
|
472
519
|
catch (error) {
|
|
473
|
-
|
|
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
|
+
}
|
|
474
526
|
throw error;
|
|
475
527
|
}
|
|
476
528
|
}
|
|
@@ -491,20 +543,23 @@ export async function provisionMembers(input) {
|
|
|
491
543
|
}
|
|
492
544
|
throw error;
|
|
493
545
|
}
|
|
494
|
-
|
|
546
|
+
if (!input.append)
|
|
547
|
+
advanceSaga(roomId, 'wait_seats', 5);
|
|
495
548
|
const deadline = policy.now() + policy.timeoutMs;
|
|
496
549
|
let delay = policy.initialDelayMs;
|
|
497
550
|
for (;;) {
|
|
498
551
|
const remote = await cowork.recoverRoom(roomId);
|
|
499
552
|
assertCoworkRoomPolicy(remote, roomPolicy.anonymous);
|
|
500
553
|
const reconciled = reconcileMemberSeats(roomId, members, remote.seats, ownerSeatCid ?? undefined);
|
|
501
|
-
if (reconciled.complete)
|
|
554
|
+
if (reconciled.complete && remote.state === 'active')
|
|
502
555
|
break;
|
|
503
556
|
if (policy.now() >= deadline) {
|
|
504
|
-
|
|
557
|
+
if (!input.append)
|
|
558
|
+
advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
|
|
505
559
|
return getRoomRecord(roomId);
|
|
506
560
|
}
|
|
507
|
-
|
|
561
|
+
if (!input.append)
|
|
562
|
+
advanceSaga(roomId, 'wait_seats', 5, 'waiting_seats');
|
|
508
563
|
await policy.sleep(delay);
|
|
509
564
|
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
510
565
|
}
|
|
@@ -529,15 +584,87 @@ export async function provisionMembers(input) {
|
|
|
529
584
|
};
|
|
530
585
|
});
|
|
531
586
|
updateTaskMembers(taskId, taskMembers);
|
|
532
|
-
if (getTask(taskId).blocked)
|
|
587
|
+
if (!input.append && getTask(taskId).blocked)
|
|
533
588
|
unblockTask(taskId);
|
|
534
589
|
}
|
|
590
|
+
if (input.append)
|
|
591
|
+
return getRoomRecord(roomId);
|
|
535
592
|
advanceSaga(roomId, 'activate', 6);
|
|
536
593
|
const record = activateRoom(roomId);
|
|
537
594
|
if (taskId)
|
|
538
595
|
activateTask(taskId);
|
|
539
596
|
return record;
|
|
540
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.toLowerCase() === current.owner_seat_cid.toLowerCase() && 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
|
+
}
|
|
541
668
|
export async function cleanupMembers(input) {
|
|
542
669
|
const room = getRoomRecord(input.roomId);
|
|
543
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 {};
|