@ours.network/fleet 1.2.0-nightly.2 → 1.2.0-nightly.20
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 +19 -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 +66 -0
- package/presets/brain-catalog.json +7 -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-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,9 +1,12 @@
|
|
|
1
|
+
import { TaskLayouts, assertLayoutOpenable, writeTaskBrief } from './task-layouts.js';
|
|
2
|
+
import { FleetError } from './errors.js';
|
|
1
3
|
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { taskLiveReadiness } from '../rooms-tasks/live-readiness.js';
|
|
2
5
|
import { ConfigError, loadConfig } from '../config.js';
|
|
3
6
|
import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
|
|
4
7
|
import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
|
|
5
8
|
import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
|
|
6
|
-
import { activateTask, blockTask as persistBlockTask, createTask as persistTask, getDeletingTask, getTask as readTask, listTasks as readTasks, taskDeletionState, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, updateTaskExecutionPlan, } from '../rooms-tasks/task-state.js';
|
|
9
|
+
import { activateTask, blockTask as persistBlockTask, createTask as persistTask, getDeletingTask, getTask as readTask, listTasks as readTasks, taskDeletionState, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, updateTaskExecutionPlan, updateTaskBrief, updateTaskLayout, } from '../rooms-tasks/task-state.js';
|
|
7
10
|
import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
|
|
8
11
|
import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
|
|
9
12
|
import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
|
|
@@ -71,7 +74,127 @@ export class TaskRoomApplicationService {
|
|
|
71
74
|
this.configurationPath = configurationPath;
|
|
72
75
|
this.deps = deps;
|
|
73
76
|
}
|
|
77
|
+
async addMember(taskId, request) {
|
|
78
|
+
const { requestMemberAddition } = await import('../rooms-tasks/add-member.js');
|
|
79
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
80
|
+
return requestMemberAddition({ taskId, request, cfg, configPath: this.configurationPath,
|
|
81
|
+
cowork: (this.deps.cowork ?? createCoworkAdapter)(cfg), binPath: (this.deps.binPath ?? getBinPath)(), provision: this.deps.provisionMembers });
|
|
82
|
+
}
|
|
83
|
+
async memberAddition(taskId, requestId) { const { memberAddition } = await import('../rooms-tasks/add-member.js'); return memberAddition(taskId, requestId); }
|
|
84
|
+
layouts() {
|
|
85
|
+
return this.deps.taskLayouts ?? new TaskLayouts(this.configurationPath, { binPath: this.deps.binPath });
|
|
86
|
+
}
|
|
87
|
+
async createLayoutTask(request) {
|
|
88
|
+
if (request.template || request.noRoom || request.members || request.anonymous !== undefined)
|
|
89
|
+
throw new FleetError('invalid_request', 'a layout task cannot be combined with a room template, no-room mode, member overrides or anonymity');
|
|
90
|
+
let layout;
|
|
91
|
+
try {
|
|
92
|
+
layout = this.layouts().resolve(request.layout);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw new FleetError('invalid_request', error.message, { details: { layout: request.layout } });
|
|
96
|
+
}
|
|
97
|
+
const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
|
|
98
|
+
const task = await withTaskListsLock(() => persistTask({
|
|
99
|
+
title: request.title, brief, brief_file: request.briefFile, layout,
|
|
100
|
+
origin: request.origin, idempotency_key: request.idempotencyKey,
|
|
101
|
+
start: !request.backlog, listId: resolveTaskList(request.list ?? 'default').list_id,
|
|
102
|
+
}));
|
|
103
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
|
|
104
|
+
title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
|
|
105
|
+
list: task.list_name ?? 'default', roomId: undefined, template: `layout:${layout.name}`, agents: [] });
|
|
106
|
+
return task.state === 'provisioning' ? (await this.ensureLayoutWork(task)).task : task;
|
|
107
|
+
}
|
|
108
|
+
/** Layout tasks never provision the legacy single room; their run is a local snapshot. */
|
|
109
|
+
async ensureLayoutWork(task) {
|
|
110
|
+
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
111
|
+
throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: task.task_id, state: task.state });
|
|
112
|
+
if (task.state === 'active' || task.state === 'review')
|
|
113
|
+
return { task, status: 'already_active' };
|
|
114
|
+
if (task.state === 'backlog') {
|
|
115
|
+
// Verify the pinned source before leaving Backlog, so a stale choice stays editable on the task page.
|
|
116
|
+
if (!this.layouts().runExists(task.layout) && this.layouts().resolve(task.layout.name).definition_hash !== task.layout.definition_hash)
|
|
117
|
+
throw new FleetError('conflict', `room layout ${task.layout.name} changed after it was chosen for this task; choose it again on the task page to use the edited version`);
|
|
118
|
+
task = transitionTask(task.task_id);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
await this.layouts().ensureRun(task);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const reason = `layout run could not be created: ${error.message}`;
|
|
125
|
+
persistBlockTask(task.task_id, reason);
|
|
126
|
+
throw new FleetError('conflict', reason, { details: { task: task.task_id } });
|
|
127
|
+
}
|
|
128
|
+
return { task: activateTask(task.task_id), status: 'ready' };
|
|
129
|
+
}
|
|
130
|
+
/** Compact room list for task listings; never fails the listing on an unreadable run. */
|
|
131
|
+
withLayoutRooms(task) {
|
|
132
|
+
if (!task.layout)
|
|
133
|
+
return task;
|
|
134
|
+
try {
|
|
135
|
+
const view = this.layouts().view(task);
|
|
136
|
+
return { ...task, layout_rooms: (view?.rooms ?? []).map(({ key, state, room_id, identity_cid }) => ({ key, state, ...(room_id ? { room_id } : {}), ...(identity_cid ? { identity_cid } : {}) })) };
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return { ...task, layout_rooms: [] };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Backlog only: choose, change or clear the task's Room Layout before it starts. */
|
|
143
|
+
async setTaskLayout(input) {
|
|
144
|
+
let next = null;
|
|
145
|
+
if (input.layout !== null) {
|
|
146
|
+
try {
|
|
147
|
+
next = this.layouts().resolve(input.layout);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw new FleetError('invalid_request', error.message, { details: { layout: input.layout } });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const current = readTask(input.taskId);
|
|
154
|
+
if (current.layout && this.layouts().runExists(current.layout))
|
|
155
|
+
throw new FleetError('conflict', 'this task already has a layout run; its room layout is fixed');
|
|
156
|
+
let result;
|
|
157
|
+
try {
|
|
158
|
+
result = updateTaskLayout(input.taskId, input.expectedLayout, next);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error instanceof TaskStateError && !/task not found/.test(error.message))
|
|
162
|
+
throw new FleetError('conflict', error.message);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
// Only after the task stopped referencing it; release re-checks every retained reference under its lock.
|
|
166
|
+
if (result.releasedSnapshot)
|
|
167
|
+
releaseLaunchSnapshot(result.releasedSnapshot);
|
|
168
|
+
return result.task;
|
|
169
|
+
}
|
|
170
|
+
taskLayout(taskId) {
|
|
171
|
+
const task = readTask(taskId);
|
|
172
|
+
if (!task.layout)
|
|
173
|
+
throw new FleetError('resource_not_found', 'task has no room layout');
|
|
174
|
+
return this.layouts().view(task);
|
|
175
|
+
}
|
|
176
|
+
async launchTaskLayoutOperation(input) {
|
|
177
|
+
const task = readTask(input.taskId);
|
|
178
|
+
if (!task.layout)
|
|
179
|
+
throw new FleetError('resource_not_found', 'task has no room layout');
|
|
180
|
+
if (input.operation === 'open')
|
|
181
|
+
assertLayoutOpenable(task);
|
|
182
|
+
else if (task.deletion?.status === 'pending' || task.terminal_intent)
|
|
183
|
+
throw new FleetError('conflict', 'task is closing; its layout rooms are cleaned up by the terminal operation');
|
|
184
|
+
return this.layouts().launch(task, input.operation, input.room);
|
|
185
|
+
}
|
|
186
|
+
/** Worker entry for detached layout operations. */
|
|
187
|
+
async runTaskLayoutOperation(input) {
|
|
188
|
+
const task = readTask(input.taskId);
|
|
189
|
+
// Preconditions are enforced inside the worker before side effects; the worker is directly callable.
|
|
190
|
+
if (input.operation === 'open')
|
|
191
|
+
assertLayoutOpenable(task);
|
|
192
|
+
await this.layouts().run(task, input.operation, input.room, input.operationId);
|
|
193
|
+
return this.layouts().view(task);
|
|
194
|
+
}
|
|
74
195
|
async createTask(request) {
|
|
196
|
+
if (request.layout !== undefined)
|
|
197
|
+
return this.createLayoutTask(request);
|
|
75
198
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
76
199
|
let template = this.createTemplate(cfg, request.template, request.noRoom);
|
|
77
200
|
if (request.noRoom && request.anonymous !== undefined)
|
|
@@ -136,7 +259,7 @@ export class TaskRoomApplicationService {
|
|
|
136
259
|
}
|
|
137
260
|
catch (error) {
|
|
138
261
|
if (error instanceof CoworkUnavailableError)
|
|
139
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
262
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
140
263
|
// Once the durable Room exists, provisioning errors are resumable
|
|
141
264
|
// saga state. Return that explicit state so the command can launch a
|
|
142
265
|
// continuation and report the durable in-progress outcome.
|
|
@@ -219,32 +342,59 @@ export class TaskRoomApplicationService {
|
|
|
219
342
|
return { deleted: source, moved: assigned.length, destination };
|
|
220
343
|
});
|
|
221
344
|
}
|
|
345
|
+
editTaskDescription(input) {
|
|
346
|
+
const task = updateTaskBrief(input.taskId, input.brief, input.expectedBrief);
|
|
347
|
+
// Layout rooms reference the brief file, so rooms opened later read the edited brief.
|
|
348
|
+
if (task.layout)
|
|
349
|
+
writeTaskBrief(task);
|
|
350
|
+
return task;
|
|
351
|
+
}
|
|
222
352
|
getTask(taskId) {
|
|
223
|
-
const task = readTask(taskId);
|
|
353
|
+
const task = this.withLayoutRooms(readTask(taskId));
|
|
224
354
|
return { task, orchestration: task.room_id ? getRoomRecord(task.room_id) : undefined };
|
|
225
355
|
}
|
|
356
|
+
/** Durable provisioning progress only; use active task start/work for a live readiness check. */
|
|
226
357
|
taskProvisioningOutcome(taskId) {
|
|
227
358
|
const task = readTask(taskId);
|
|
359
|
+
if (task.layout) {
|
|
360
|
+
const failed = task.state === 'failed';
|
|
361
|
+
const ready = ['active', 'review', 'done'].includes(task.state);
|
|
362
|
+
const blocker = task.outcome?.summary ?? task.blocked?.reason;
|
|
363
|
+
return {
|
|
364
|
+
kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room: undefined,
|
|
365
|
+
launch: { template: `layout:${task.layout.name}`, anonymous: false, owner_attached: false },
|
|
366
|
+
members: { expected: 0, active: 0, launched: 0 },
|
|
367
|
+
...(blocker ? { blocker } : {}),
|
|
368
|
+
...(!ready && !failed ? { next_action: `Correct the blocker, then run ours-fleet task start ${task.task_id}.` } : {}),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
228
371
|
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
229
|
-
const expected = room?.template_snapshot?.members.reduce((sum, member) => sum + member.count, 0)
|
|
230
|
-
?? task.execution_plan?.snapshot.members.reduce((sum, member) => sum + member.count, 0) ?? 0
|
|
372
|
+
const expected = (room?.template_snapshot?.members.reduce((sum, member) => sum + member.count, 0)
|
|
373
|
+
?? task.execution_plan?.snapshot.members.reduce((sum, member) => sum + member.count, 0) ?? 0)
|
|
374
|
+
+ (room?.additional_member_templates ?? []).reduce((n, t) => n + t.members.reduce((m, s) => m + s.count, 0), 0);
|
|
231
375
|
const active = room?.member_seats.filter(seat => seat.seat_state === 'active').length ?? 0;
|
|
232
376
|
const launched = room?.member_seats.filter(seat => seat.launch?.state === 'launched').length ?? 0;
|
|
233
377
|
const failed = task.state === 'failed' || room?.saga.phase === 'failed';
|
|
234
378
|
const ready = task.state === 'active' && room?.state === 'active'
|
|
235
379
|
&& active === expected && launched === expected;
|
|
236
380
|
const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
|
|
237
|
-
const nextAction =
|
|
238
|
-
? `
|
|
239
|
-
: room?.
|
|
240
|
-
? `
|
|
241
|
-
: room?.provisioning_detail === '
|
|
242
|
-
? `
|
|
243
|
-
: room?.provisioning_detail === '
|
|
244
|
-
? `
|
|
245
|
-
:
|
|
246
|
-
? `
|
|
247
|
-
:
|
|
381
|
+
const nextAction = task.terminal_intent
|
|
382
|
+
? `Complete the accepted ${task.terminal_intent.kind} operation; do not restart provisioning.`
|
|
383
|
+
: room?.state === 'closing' || room?.state === 'closed'
|
|
384
|
+
? `Complete room cleanup; do not restart provisioning.`
|
|
385
|
+
: room?.provisioning_detail === 'member_failed'
|
|
386
|
+
? `Inspect the failed launch, then run ours-fleet task start ${task.task_id}.`
|
|
387
|
+
: room?.provisioning_detail === 'waiting_owner_authorization'
|
|
388
|
+
? `Ensure ours-cowork 1.3.0 or newer is running and available, then run ours-fleet task start ${task.task_id}.`
|
|
389
|
+
: room?.provisioning_detail === 'waiting_owner_invite'
|
|
390
|
+
? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
|
|
391
|
+
: room?.provisioning_detail === 'owner_cid_mismatch'
|
|
392
|
+
? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
|
|
393
|
+
: room?.provisioning_detail === 'waiting_cowork'
|
|
394
|
+
? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
|
|
395
|
+
: failed
|
|
396
|
+
? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
|
|
397
|
+
: undefined;
|
|
248
398
|
return {
|
|
249
399
|
kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
|
|
250
400
|
launch: {
|
|
@@ -265,6 +415,10 @@ export class TaskRoomApplicationService {
|
|
|
265
415
|
const deadline = now() + (input.waitMs ?? 60_000);
|
|
266
416
|
for (;;) {
|
|
267
417
|
const outcome = this.taskProvisioningOutcome(input.taskId);
|
|
418
|
+
if (outcome.kind === 'ready') {
|
|
419
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
420
|
+
await this.assertTaskLiveReadiness(outcome.task, outcome.room, cfg);
|
|
421
|
+
}
|
|
268
422
|
if (outcome.kind !== 'in_progress' || now() >= deadline) {
|
|
269
423
|
recordTaskProvisioningOutcome(outcome);
|
|
270
424
|
return outcome;
|
|
@@ -304,6 +458,15 @@ export class TaskRoomApplicationService {
|
|
|
304
458
|
}
|
|
305
459
|
/** Worker entry: converge an accepted deletion; Cowork is resolved lazily. */
|
|
306
460
|
async settleTaskDeletion(input) {
|
|
461
|
+
// Layout resources close first; a failure stays a retryable pending deletion.
|
|
462
|
+
const deleting = (() => { try {
|
|
463
|
+
return getDeletingTask(input.taskId);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return undefined;
|
|
467
|
+
} })();
|
|
468
|
+
if (deleting?.layout)
|
|
469
|
+
await this.layouts().closeAndForget(deleting);
|
|
307
470
|
const result = await settleTaskDeletion({
|
|
308
471
|
taskId: input.taskId,
|
|
309
472
|
cowork: () => {
|
|
@@ -332,7 +495,7 @@ export class TaskRoomApplicationService {
|
|
|
332
495
|
*/
|
|
333
496
|
async launchTaskDeletionWorker(input) {
|
|
334
497
|
const launch = this.deps.launchDeletionWorker
|
|
335
|
-
?? ((taskId) => launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, this.configurationPath));
|
|
498
|
+
?? ((taskId) => launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, this.configurationPath, this.deps.binPath?.()));
|
|
336
499
|
const errorAtBefore = (() => {
|
|
337
500
|
try {
|
|
338
501
|
return getDeletingTask(input.taskId).deletion?.error_at;
|
|
@@ -385,6 +548,20 @@ export class TaskRoomApplicationService {
|
|
|
385
548
|
}
|
|
386
549
|
return this.acceptTerminal(input.taskId, 'done', roomId, input.outcome);
|
|
387
550
|
}
|
|
551
|
+
/** Terminal layout tasks archive their rooms and retire only agents their run created. */
|
|
552
|
+
async withLayoutCleanup(plan) {
|
|
553
|
+
// The task is already terminal; a worker launch failure is recorded on the run for retry.
|
|
554
|
+
if (plan.task.layout && this.layouts().runExists(plan.task.layout))
|
|
555
|
+
await this.layouts().launch(plan.task, 'close').catch(() => { });
|
|
556
|
+
return plan;
|
|
557
|
+
}
|
|
558
|
+
/** Retry terminal layout cleanup for known resources (e.g. after a failed close). */
|
|
559
|
+
async retryTaskLayoutCleanup(input) {
|
|
560
|
+
const task = readTask(input.taskId);
|
|
561
|
+
if (!task.layout || !TASK_TERMINAL_STATES.includes(task.state))
|
|
562
|
+
throw new TaskStateError('layout cleanup applies to finished or cancelled layout tasks');
|
|
563
|
+
return this.layouts().launch(task, 'close');
|
|
564
|
+
}
|
|
388
565
|
async cancelTask(input) {
|
|
389
566
|
const task = readTask(input.taskId);
|
|
390
567
|
if (!TASK_CANCELLABLE_STATES.includes(task.state))
|
|
@@ -393,6 +570,23 @@ export class TaskRoomApplicationService {
|
|
|
393
570
|
(this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
394
571
|
return this.acceptTerminal(input.taskId, 'cancelled', task.room_id);
|
|
395
572
|
}
|
|
573
|
+
/** Continue accepted operations outside the HTTP connection's lifetime. */
|
|
574
|
+
async launchTaskSettlement(taskId) {
|
|
575
|
+
try {
|
|
576
|
+
await launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, this.configurationPath, this.deps.binPath?.());
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
await recordTaskTerminalIntentError(taskId, error instanceof Error ? error.message : String(error), 'The settlement worker could not start. Inspect the task before retrying.');
|
|
580
|
+
}
|
|
581
|
+
return readTask(taskId);
|
|
582
|
+
}
|
|
583
|
+
async launchTaskProvisioning(taskId) {
|
|
584
|
+
const outcome = this.taskProvisioningOutcome(taskId);
|
|
585
|
+
if (outcome.kind === 'in_progress' && !outcome.next_action) {
|
|
586
|
+
await launchFleetWorker(['task', '_provision', taskId], `task-provision-${taskId}`, this.configurationPath, this.deps.binPath?.());
|
|
587
|
+
}
|
|
588
|
+
return outcome;
|
|
589
|
+
}
|
|
396
590
|
async settleTask(input) {
|
|
397
591
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
398
592
|
if (!cfg.rooms)
|
|
@@ -410,7 +604,9 @@ export class TaskRoomApplicationService {
|
|
|
410
604
|
let task = readTask(input.taskId);
|
|
411
605
|
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
412
606
|
const issues = [];
|
|
413
|
-
if (task.state !== 'provisioning'
|
|
607
|
+
if (task.state !== 'provisioning' || task.terminal_intent
|
|
608
|
+
|| room?.state === 'closing' || room?.state === 'closed'
|
|
609
|
+
|| room?.provisioning_detail === 'member_failed')
|
|
414
610
|
return {
|
|
415
611
|
kind: 'no_op', task, room, issues,
|
|
416
612
|
};
|
|
@@ -486,7 +682,7 @@ export class TaskRoomApplicationService {
|
|
|
486
682
|
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
487
683
|
const cowork = this.deps.cowork ? this.deps.cowork(cfg) : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
488
684
|
await (this.deps.provisionMembers ?? provisionMembers)({
|
|
489
|
-
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template,
|
|
685
|
+
configPath: this.configurationPath, cfg, cowork, roomId: room.room_id, taskId: task.task_id, template,
|
|
490
686
|
binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief, goal: task.title,
|
|
491
687
|
});
|
|
492
688
|
task = readTask(task.task_id);
|
|
@@ -549,7 +745,8 @@ export class TaskRoomApplicationService {
|
|
|
549
745
|
}
|
|
550
746
|
async acceptTerminal(taskId, kind, roomId, outcome) {
|
|
551
747
|
const task = await acceptTaskTerminalIntent({ taskId, kind, roomId, outcome });
|
|
552
|
-
|
|
748
|
+
// Every terminal path (complete, finish, cancel) closes a layout task's rooms.
|
|
749
|
+
return this.withLayoutCleanup({ task, settlementRequired: task.terminal_intent?.status === 'pending' && !!roomId });
|
|
553
750
|
}
|
|
554
751
|
listTemplates() {
|
|
555
752
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
@@ -624,7 +821,7 @@ export class TaskRoomApplicationService {
|
|
|
624
821
|
createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
625
822
|
const room = getRoomRecord(input.roomId);
|
|
626
823
|
if (!room) {
|
|
627
|
-
|
|
824
|
+
return { room: undefined, settlementRequired: true };
|
|
628
825
|
}
|
|
629
826
|
return { room: await acceptManagedRoomClose(input.roomId), settlementRequired: true };
|
|
630
827
|
}
|
|
@@ -649,9 +846,42 @@ export class TaskRoomApplicationService {
|
|
|
649
846
|
(this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
650
847
|
return this.acceptTerminal(task.task_id, 'done', task.room_id, input.outcome);
|
|
651
848
|
}
|
|
849
|
+
/** Point-in-time health, separate from durable provisioning and lifecycle. */
|
|
850
|
+
async observeTaskReadiness(taskId) {
|
|
851
|
+
const task = readTask(taskId);
|
|
852
|
+
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
853
|
+
try {
|
|
854
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
855
|
+
return await this.observeLiveReadiness(task, room, cfg) ?? { state: 'ready' };
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
return { state: 'unknown', reason: 'readiness_unavailable' };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async observeLiveReadiness(task, room, cfg) {
|
|
862
|
+
const cowork = this.deps.cowork ? this.deps.cowork(cfg, { timeoutMs: 2_000 })
|
|
863
|
+
: createCoworkAdapter({ configPath: cfg.rooms?.cowork?.config, timeoutMs: 2_000 });
|
|
864
|
+
const issue = await taskLiveReadiness(task, room, cowork, this.deps.liveReadiness);
|
|
865
|
+
if (issue)
|
|
866
|
+
return issue;
|
|
867
|
+
if (JSON.stringify(readTask(task.task_id)) !== JSON.stringify(task)
|
|
868
|
+
|| (room && JSON.stringify(getRoomRecord(room.room_id)) !== JSON.stringify(room)))
|
|
869
|
+
return { state: 'unknown', reason: 'records_changed' };
|
|
870
|
+
}
|
|
871
|
+
async assertTaskLiveReadiness(task, room, cfg) {
|
|
872
|
+
const issue = await this.observeLiveReadiness(task, room, cfg);
|
|
873
|
+
if (issue)
|
|
874
|
+
throw new TaskRoomApplicationError('task_not_ready', `Task readiness is ${issue.state}: ${issue.reason}. Ask Fleet Coordinator to inspect the room and member status and choose supported recovery; do not blindly relaunch or reuse invites.`, { task: task.task_id, room: task.room_id ?? 'missing', readiness: issue.state,
|
|
875
|
+
reason: issue.reason, ...(issue.member ? { member: issue.member } : {}) });
|
|
876
|
+
}
|
|
652
877
|
async ensureTaskWork(input) {
|
|
653
878
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
654
879
|
let task = readTask(input.taskId);
|
|
880
|
+
if (task.layout) {
|
|
881
|
+
if (input.template || input.members || input.anonymous !== undefined)
|
|
882
|
+
throw new FleetError('invalid_request', 'a layout task cannot take a room template or member overrides');
|
|
883
|
+
return this.ensureLayoutWork(task);
|
|
884
|
+
}
|
|
655
885
|
const recordedRoom = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
656
886
|
const recordedPolicy = storedRoomLaunchPolicy(recordedRoom?.room_policy ?? task.execution_plan?.room_policy);
|
|
657
887
|
if (task.room_id && input.anonymous !== undefined && input.anonymous !== recordedPolicy.anonymous)
|
|
@@ -673,8 +903,10 @@ export class TaskRoomApplicationService {
|
|
|
673
903
|
const pinned = durableTemplate;
|
|
674
904
|
if (!pinned)
|
|
675
905
|
throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
|
|
676
|
-
if (durableRequestMatches)
|
|
906
|
+
if (durableRequestMatches) {
|
|
907
|
+
await this.assertTaskLiveReadiness(task, recordedRoom, cfg);
|
|
677
908
|
return { task, status: 'already_active' };
|
|
909
|
+
}
|
|
678
910
|
const templateName = input.template ?? pinned.name;
|
|
679
911
|
const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
680
912
|
if (!definition)
|
|
@@ -687,6 +919,7 @@ export class TaskRoomApplicationService {
|
|
|
687
919
|
provisioned: `${pinned.name}@${pinned.version}`
|
|
688
920
|
});
|
|
689
921
|
}
|
|
922
|
+
await this.assertTaskLiveReadiness(task, recordedRoom, cfg);
|
|
690
923
|
return { task, status: 'already_active' };
|
|
691
924
|
}
|
|
692
925
|
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
@@ -784,7 +1017,7 @@ export class TaskRoomApplicationService {
|
|
|
784
1017
|
}
|
|
785
1018
|
catch (error) {
|
|
786
1019
|
if (error instanceof CoworkUnavailableError)
|
|
787
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
1020
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
788
1021
|
const current = readTask(task.task_id);
|
|
789
1022
|
if (!current.room_id || !getRoomRecord(current.room_id))
|
|
790
1023
|
throw error;
|
|
@@ -802,7 +1035,7 @@ export class TaskRoomApplicationService {
|
|
|
802
1035
|
}
|
|
803
1036
|
catch (error) {
|
|
804
1037
|
if (error instanceof CoworkUnavailableError)
|
|
805
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
1038
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
806
1039
|
task = readTask(task.task_id);
|
|
807
1040
|
return { task, status: 'in_progress' };
|
|
808
1041
|
}
|
|
@@ -813,19 +1046,23 @@ export class TaskRoomApplicationService {
|
|
|
813
1046
|
if (!cfg.rooms)
|
|
814
1047
|
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
815
1048
|
const cowork = this.deps.cowork ? this.deps.cowork(cfg) : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
816
|
-
await (this.deps.provisionMembers ?? provisionMembers)({ cfg, cowork, roomId: room.room_id,
|
|
1049
|
+
await (this.deps.provisionMembers ?? provisionMembers)({ configPath: this.configurationPath, cfg, cowork, roomId: room.room_id,
|
|
817
1050
|
taskId: task.task_id, template: snapshot, binPath: (this.deps.binPath ?? getBinPath)(),
|
|
818
1051
|
brief: task.brief, goal: task.title });
|
|
819
1052
|
task = readTask(task.task_id);
|
|
820
1053
|
}
|
|
821
1054
|
catch (error) {
|
|
822
1055
|
if (error instanceof CoworkUnavailableError)
|
|
823
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
1056
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
824
1057
|
task = readTask(task.task_id);
|
|
825
1058
|
}
|
|
826
1059
|
}
|
|
827
1060
|
const resultingRoom = getRoomRecord(task.room_id);
|
|
828
|
-
|
|
1061
|
+
if (task.state === 'active' && resultingRoom?.state === 'active') {
|
|
1062
|
+
await this.assertTaskLiveReadiness(task, resultingRoom, cfg);
|
|
1063
|
+
return { task, status: 'ready' };
|
|
1064
|
+
}
|
|
1065
|
+
return { task, status: 'in_progress' };
|
|
829
1066
|
}
|
|
830
1067
|
createTemplate(cfg, requested, noRoom) {
|
|
831
1068
|
if (noRoom)
|
|
@@ -864,6 +1101,7 @@ export class TaskRoomApplicationService {
|
|
|
864
1101
|
const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
|
|
865
1102
|
let launchTemplate;
|
|
866
1103
|
let room;
|
|
1104
|
+
let reusedRoom = false;
|
|
867
1105
|
try {
|
|
868
1106
|
launchTemplate = template ? (template.launch_snapshot_hash ? template
|
|
869
1107
|
: sealTemplateSnapshot(template, cfg.agentTemplates ?? {}, launchDefinitions)) : undefined;
|
|
@@ -877,19 +1115,45 @@ export class TaskRoomApplicationService {
|
|
|
877
1115
|
const fresh = readTask(task.task_id);
|
|
878
1116
|
if (fresh.deletion?.status === 'pending')
|
|
879
1117
|
throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
|
|
1118
|
+
// A concurrent caller or a restart after local Room publication must
|
|
1119
|
+
// reuse the task's one durable room, never create a second workspace/room.
|
|
1120
|
+
const candidates = listRoomRecords().filter(record => record.task_id === task.task_id);
|
|
1121
|
+
if (candidates.length > 1)
|
|
1122
|
+
throw new Error('ambiguous task room ownership');
|
|
1123
|
+
const existing = fresh.room_id ? getRoomRecord(fresh.room_id) : candidates[0];
|
|
1124
|
+
if (fresh.room_id && !existing)
|
|
1125
|
+
throw new Error('recorded task room is missing; refusing duplicate creation');
|
|
1126
|
+
if (existing) {
|
|
1127
|
+
reusedRoom = true;
|
|
1128
|
+
// Heal a crash between durable room publication and task linkage.
|
|
1129
|
+
// Return a resumable phase, leaving owner admission/member launch to
|
|
1130
|
+
// the existing continuation rather than racing the original creator.
|
|
1131
|
+
const resumable = ['persist_intent', 'create_room'].includes(existing.saga.phase)
|
|
1132
|
+
? advanceSaga(existing.room_id, attachOwner ? 'attach_owner' : 'create_members', attachOwner ? 2 : 3)
|
|
1133
|
+
: existing;
|
|
1134
|
+
onCreated(resumable);
|
|
1135
|
+
return resumable;
|
|
1136
|
+
}
|
|
880
1137
|
}
|
|
1138
|
+
const requiredRoles = new Map();
|
|
1139
|
+
if (attachOwner)
|
|
1140
|
+
requiredRoles.set(rooms.owner.role, 1);
|
|
1141
|
+
for (const member of launchTemplate?.members ?? [])
|
|
1142
|
+
requiredRoles.set(member.role, (requiredRoles.get(member.role) ?? 0) + member.count);
|
|
881
1143
|
const created = await cowork.createRoom({
|
|
882
1144
|
room_name: roomName, goal: task.goal?.trim() || task.title,
|
|
883
1145
|
briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
|
|
884
1146
|
quiet_membership: launchTemplate?.room?.quiet_membership,
|
|
885
1147
|
anonymous: policy.anonymous,
|
|
1148
|
+
activation_requirements: [...requiredRoles].map(([role, count]) => ({ role, count })),
|
|
886
1149
|
});
|
|
887
1150
|
const record = createRoomRecord({
|
|
888
1151
|
room_id: created.room_id, room_name: roomName, room_identity_cid: created.identity_cid,
|
|
889
1152
|
task_id: task.task_id, template_snapshot: launchTemplate, room_policy: policy,
|
|
890
1153
|
});
|
|
891
|
-
|
|
892
|
-
|
|
1154
|
+
const resumable = advanceSaga(record.room_id, attachOwner ? 'attach_owner' : 'create_members', attachOwner ? 2 : 3);
|
|
1155
|
+
onCreated(resumable);
|
|
1156
|
+
return resumable;
|
|
893
1157
|
};
|
|
894
1158
|
room = task.task_id
|
|
895
1159
|
? await withFileLock(taskOperationLockPath(task.task_id), publishRoom, {}, TASK_OPERATION_LOCK_STALE_MS)
|
|
@@ -902,7 +1166,8 @@ export class TaskRoomApplicationService {
|
|
|
902
1166
|
throw error;
|
|
903
1167
|
}
|
|
904
1168
|
unlockSnapshot?.();
|
|
905
|
-
|
|
1169
|
+
if (reusedRoom)
|
|
1170
|
+
return room;
|
|
906
1171
|
if (attachOwner) {
|
|
907
1172
|
room = advanceSaga(room.room_id, 'attach_owner', 2);
|
|
908
1173
|
await this.setOwnerRoomCommands(cowork, room.room_id, rooms.owner.role);
|
|
@@ -927,7 +1192,7 @@ export class TaskRoomApplicationService {
|
|
|
927
1192
|
if (launchTemplate && (launchTemplate.members.length > 0 || attachOwner))
|
|
928
1193
|
try {
|
|
929
1194
|
room = await (this.deps.provisionMembers ?? provisionMembers)({
|
|
930
|
-
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
|
|
1195
|
+
configPath: this.configurationPath, cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
|
|
931
1196
|
binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
|
|
932
1197
|
goal: task.task_id ? task.title : task.goal,
|
|
933
1198
|
});
|
package/dist/atomic-file.d.ts
CHANGED
|
@@ -36,3 +36,8 @@ export declare function withFileLock<T>(lockPath: string, fn: () => T | Promise<
|
|
|
36
36
|
* intact.
|
|
37
37
|
*/
|
|
38
38
|
export declare function replaceFileAtomically(path: string, contents: string, mode?: number, deps?: WriteDeps): void;
|
|
39
|
+
/** Synchronous Lamport bakery lock: unique claims are never reused, so stale
|
|
40
|
+
* reclamation cannot remove a successor's lock. Atomic claim replacement keeps
|
|
41
|
+
* choosing/ticket reads complete. Dead claims can be removed by any contender.
|
|
42
|
+
*/
|
|
43
|
+
export declare function withSynchronousFileLock<T>(path: string, work: () => T): T;
|
package/dist/atomic-file.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, writeSync, } from 'node:fs';
|
|
1
|
+
import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, writeSync, } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join } from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
const DEFAULT_STALE_MS = 10_000;
|
|
@@ -136,3 +136,64 @@ export function replaceFileAtomically(path, contents, mode = 0o600, deps = {}) {
|
|
|
136
136
|
}
|
|
137
137
|
catch { /* platform does not allow fsync on a directory */ }
|
|
138
138
|
}
|
|
139
|
+
/** Synchronous Lamport bakery lock: unique claims are never reused, so stale
|
|
140
|
+
* reclamation cannot remove a successor's lock. Atomic claim replacement keeps
|
|
141
|
+
* choosing/ticket reads complete. Dead claims can be removed by any contender.
|
|
142
|
+
*/
|
|
143
|
+
export function withSynchronousFileLock(path, work) {
|
|
144
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
145
|
+
const token = `${process.pid}-${randomUUID()}.json`;
|
|
146
|
+
const own = join(path, token);
|
|
147
|
+
const deadline = Date.now() + 10_000;
|
|
148
|
+
const claims = () => {
|
|
149
|
+
const result = [];
|
|
150
|
+
for (const name of readdirSync(path).filter(name => /^[0-9]+-[a-f0-9-]{36}\.json$/.test(name))) {
|
|
151
|
+
const file = join(path, name);
|
|
152
|
+
let value;
|
|
153
|
+
try {
|
|
154
|
+
value = JSON.parse(readFileSync(file, 'utf8'));
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (error.code === 'ENOENT')
|
|
158
|
+
continue;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
if (!Number.isSafeInteger(value.pid) || value.pid <= 0 || !Number.isSafeInteger(value.ticket) || value.ticket < 0)
|
|
162
|
+
throw new Error('Invalid synchronous lock claim');
|
|
163
|
+
if (name !== token) {
|
|
164
|
+
try {
|
|
165
|
+
process.kill(value.pid, 0);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error.code === 'ESRCH') {
|
|
169
|
+
// This dead contender alone owned this UUID path. Never remove
|
|
170
|
+
// the namespace or a successor's differently named claim.
|
|
171
|
+
rmSync(file, { force: true });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
result.push({ name, value });
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
};
|
|
180
|
+
replaceFileAtomically(own, JSON.stringify({ pid: process.pid, choosing: true, ticket: 0 }));
|
|
181
|
+
try {
|
|
182
|
+
const ticket = Math.max(0, ...claims().map(claim => claim.value.ticket)) + 1;
|
|
183
|
+
if (!Number.isSafeInteger(ticket))
|
|
184
|
+
throw new Error('Synchronous lock ticket overflow');
|
|
185
|
+
replaceFileAtomically(own, JSON.stringify({ pid: process.pid, choosing: false, ticket }));
|
|
186
|
+
for (;;) {
|
|
187
|
+
const preceding = claims().some(claim => claim.name !== token && (claim.value.choosing
|
|
188
|
+
|| claim.value.ticket < ticket || (claim.value.ticket === ticket && claim.name < token)));
|
|
189
|
+
if (!preceding)
|
|
190
|
+
return work();
|
|
191
|
+
if (Date.now() >= deadline)
|
|
192
|
+
throw new Error('Timed out acquiring synchronous journal lock');
|
|
193
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
rmSync(own, { force: true });
|
|
198
|
+
}
|
|
199
|
+
}
|