@ours.network/fleet 1.1.0-nightly.12 → 1.1.0-nightly.14

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.
Files changed (39) hide show
  1. package/README.md +7 -0
  2. package/dist/application/role-creation-service.js +8 -1
  3. package/dist/application/task-room-service.d.ts +51 -5
  4. package/dist/application/task-room-service.js +175 -27
  5. package/dist/briefing.js +3 -3
  6. package/dist/build-info.json +4 -4
  7. package/dist/config.d.ts +1 -0
  8. package/dist/docs.d.ts +1 -1
  9. package/dist/docs.js +25 -5
  10. package/dist/fleet-command-audit.d.ts +4 -0
  11. package/dist/fleet-command-audit.js +126 -22
  12. package/dist/fleet-proxy.d.ts +3 -0
  13. package/dist/lifecycle-summary.d.ts +118 -0
  14. package/dist/lifecycle-summary.js +161 -0
  15. package/dist/owner-channel/channel.d.ts +2 -0
  16. package/dist/owner-channel/channel.js +45 -9
  17. package/dist/owner-channel/commands.d.ts +4 -1
  18. package/dist/owner-channel/commands.js +3 -6
  19. package/dist/rooms-tasks/cli.js +170 -19
  20. package/dist/rooms-tasks/close.d.ts +8 -0
  21. package/dist/rooms-tasks/close.js +10 -3
  22. package/dist/rooms-tasks/config.js +5 -1
  23. package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
  24. package/dist/rooms-tasks/cowork-adapter.js +6 -0
  25. package/dist/rooms-tasks/deletion.d.ts +51 -0
  26. package/dist/rooms-tasks/deletion.js +217 -0
  27. package/dist/rooms-tasks/launch-snapshot.d.ts +9 -0
  28. package/dist/rooms-tasks/launch-snapshot.js +18 -1
  29. package/dist/rooms-tasks/provision.js +94 -14
  30. package/dist/rooms-tasks/room-state.d.ts +1 -0
  31. package/dist/rooms-tasks/room-state.js +8 -1
  32. package/dist/rooms-tasks/task-state.d.ts +113 -3
  33. package/dist/rooms-tasks/task-state.js +345 -13
  34. package/dist/rooms-tasks/terminal.d.ts +3 -0
  35. package/dist/rooms-tasks/terminal.js +7 -3
  36. package/dist/rooms-tasks/types.d.ts +58 -0
  37. package/dist/rooms-tasks/types.js +12 -0
  38. package/dist/web/server.js +28 -1
  39. package/package.json +1 -1
package/README.md CHANGED
@@ -525,6 +525,13 @@ Cowork role, and task. The agent creates that identity itself with ours MCP
525
525
  work immediately. Fleet activates the room from Cowork's authenticated seat; there
526
526
  is no briefing hash, startup ACK, or separate role-briefing readiness gate.
527
527
 
528
+ Set `room.anonymous: true` on a room template, or pass `--anonymous` to
529
+ `task create`, `task start`, `task work`, or `room create`, to create an
530
+ anonymous Cowork room. `--no-anonymous` explicitly overrides an anonymous
531
+ template. Fleet records the resolved value before room creation so retries keep
532
+ the same choice. Temporary members of an anonymous room are instructed to call
533
+ `create_temporary_identity` with `expose_local=false`.
534
+
528
535
  Human task and room results use the same compact Markdown presentation in the
529
536
  CLI and authenticated owner channel: a short heading, icon-plus-word status,
530
537
  code-formatted identifiers, bounded summaries, and actionable recovery or error
@@ -10,6 +10,7 @@ import { FleetError, normalizeError } from './errors.js';
10
10
  import { inheritCallerSpawnDefaults } from '../fleet-proxy.js';
11
11
  import { effectivePermissionMode } from '../permissions.js';
12
12
  import { effectiveRoleModel } from '../model-env.js';
13
+ import { selectionOrigin, summarizeResolvedLaunch } from '../lifecycle-summary.js';
13
14
  const canonical = (value) => {
14
15
  if (Array.isArray(value))
15
16
  return `[${value.map(canonical).join(',')}]`;
@@ -71,15 +72,21 @@ export class RoleCreationService {
71
72
  const fingerprint = hash(value.inline).slice(0, 16);
72
73
  return `inline:sha256:${fingerprint} (${provenance})`;
73
74
  };
75
+ const permissionMode = effectivePermissionMode(plan.preview.resolvedRole);
74
76
  return {
75
77
  caller: plan.caller, role: plan.options.name,
76
78
  lifetime: plan.options.temp ? 'temporary' : 'permanent', statePath,
77
79
  harness: plan.preview.resolvedRole.harness, session: plan.preview.resolvedRole.session,
78
80
  ...(effectiveRoleModel(plan.preview.resolvedRole) ? { model: effectiveRoleModel(plan.preview.resolvedRole) } : {}),
79
81
  monitor: { mode: plan.preview.resolvedRole.monitor.mode, interrupt: plan.preview.resolvedRole.monitor.interrupt },
80
- permissionMode: effectivePermissionMode(plan.preview.resolvedRole), inherited: plan.inherited,
82
+ permissionMode, inherited: plan.inherited,
81
83
  creationActionId,
82
84
  brainSummary: selectionSummary('brain'), roleSummary: selectionSummary('role'),
85
+ configuration: summarizeResolvedLaunch(plan.preview.resolvedRole, {
86
+ role: selectionOrigin(plan.options.role ?? plan.options.agentDefinition?.role),
87
+ brain: selectionOrigin(plan.options.brain ?? plan.options.agentDefinition?.brain),
88
+ permissionMode,
89
+ }),
83
90
  };
84
91
  }
85
92
  launchSync(options, creation) {
@@ -3,7 +3,9 @@ import { type CoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
3
3
  import { provisionMembers } from '../rooms-tasks/provision.js';
4
4
  import { moveTaskToList } from '../rooms-tasks/task-state.js';
5
5
  import { type MemberOverrides } from '../rooms-tasks/member-overrides.js';
6
- import type { RoomOrchestrationRecord, TaskListRecord, TaskOrigin, TaskOutcome, TaskRecord, TaskState } from '../rooms-tasks/types.js';
6
+ import { type TaskDeletionSettleResult } from '../rooms-tasks/deletion.js';
7
+ import type { RoomLaunchPolicy, RoomOrchestrationRecord, TaskListRecord, TaskOrigin, TaskOutcome, TaskRecord, TaskState, TemplateSnapshot } from '../rooms-tasks/types.js';
8
+ import type { TaskDeletionAcceptance } from '../rooms-tasks/task-state.js';
7
9
  export type TaskRoomActor = {
8
10
  kind: 'local_control';
9
11
  surface: 'cli' | 'web';
@@ -38,14 +40,17 @@ export interface TaskRecoveryResult {
38
40
  export type TaskRecoveryBegin = {
39
41
  kind: 'terminal_worker_required';
40
42
  taskId: string;
43
+ } | {
44
+ kind: 'deletion_worker_required';
45
+ taskId: string;
41
46
  } | {
42
47
  kind: 'final';
43
48
  result: TaskRecoveryResult;
44
49
  };
45
50
  export declare class TaskRoomApplicationError extends Error {
46
- readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'room_not_found' | 'room_record_not_found';
51
+ readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found';
47
52
  readonly fields: Readonly<Record<string, string>>;
48
- constructor(code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'room_not_found' | 'room_record_not_found', message: string, fields?: Readonly<Record<string, string>>);
53
+ constructor(code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found', message: string, fields?: Readonly<Record<string, string>>);
49
54
  }
50
55
  export interface CreateTaskRequest {
51
56
  actor: TaskRoomActor;
@@ -59,6 +64,7 @@ export interface CreateTaskRequest {
59
64
  origin: TaskOrigin;
60
65
  list?: string;
61
66
  members?: MemberOverrides;
67
+ anonymous?: boolean;
62
68
  }
63
69
  export interface CreateRoomRequest {
64
70
  actor: TaskRoomActor;
@@ -68,13 +74,18 @@ export interface CreateRoomRequest {
68
74
  brief?: string;
69
75
  briefFile?: string;
70
76
  members?: MemberOverrides;
77
+ anonymous?: boolean;
71
78
  }
79
+ export declare function resolveRoomLaunchPolicy(template: TemplateSnapshot | undefined, override: boolean | undefined): RoomLaunchPolicy;
72
80
  export interface TaskRoomServiceDeps {
73
81
  loadConfiguration?(path?: string): FleetConfig;
74
82
  cowork?(config: FleetConfig): CoworkAdapter;
75
83
  binPath?(): string;
76
84
  provisionMembers?: typeof provisionMembers;
77
85
  moveTaskToList?: typeof moveTaskToList;
86
+ launchDeletionWorker?(taskId: string): Promise<void>;
87
+ sleep?(ms: number): Promise<void>;
88
+ now?(): number;
78
89
  }
79
90
  /** Exact extraction of the previously CLI-owned task create/start behavior. */
80
91
  export declare class TaskRoomApplicationService {
@@ -89,15 +100,18 @@ export declare class TaskRoomApplicationService {
89
100
  taskId: string;
90
101
  template?: string;
91
102
  members?: MemberOverrides;
103
+ anonymous?: boolean;
92
104
  }): Promise<TaskRecord>;
93
105
  listTasks(filter?: {
94
106
  state?: TaskState | TaskState[];
95
107
  list?: string;
108
+ includeDeleting?: boolean;
96
109
  }): TaskRecord[];
97
110
  listTaskLists(): TaskListRecord[];
98
111
  groupedTasks(filter?: {
99
112
  state?: TaskState | TaskState[];
100
113
  list?: string;
114
+ includeDeleting?: boolean;
101
115
  }): Array<{
102
116
  list: TaskListRecord;
103
117
  tasks: TaskRecord[];
@@ -142,10 +156,40 @@ export declare class TaskRoomApplicationService {
142
156
  actor: TaskRoomActor;
143
157
  taskId: string;
144
158
  }): TaskRecord;
145
- deleteTask(input: {
159
+ private deletionActor;
160
+ /** Accept a permanent deletion in any lifecycle state; audit at acceptance. */
161
+ requestTaskDeletion(input: {
146
162
  actor: TaskRoomActor;
147
163
  taskId: string;
148
- }): boolean;
164
+ }): Promise<TaskDeletionAcceptance>;
165
+ /** Worker entry: converge an accepted deletion; Cowork is resolved lazily. */
166
+ settleTaskDeletion(input: {
167
+ actor: {
168
+ kind: 'internal_worker';
169
+ surface: 'cli';
170
+ };
171
+ taskId: string;
172
+ }): Promise<TaskDeletionSettleResult>;
173
+ /**
174
+ * Launch the external deletion worker outside the caller's lifecycle and
175
+ * wait boundedly for physical absence. Timeouts and launch failures report
176
+ * a pending, recoverable state — never success. A concurrent worker that
177
+ * already removed the record reads as settled.
178
+ */
179
+ launchTaskDeletionWorker(input: {
180
+ taskId: string;
181
+ waitMs?: number;
182
+ }): Promise<{
183
+ deleted: boolean;
184
+ pending: boolean;
185
+ error?: string;
186
+ }>;
187
+ recordDeletionError(input: {
188
+ actor: TaskRoomActor;
189
+ taskId: string;
190
+ error: string;
191
+ recoveryHint: string;
192
+ }): Promise<TaskRecord>;
149
193
  completeTask(input: {
150
194
  actor: TaskRoomActor;
151
195
  taskId: string;
@@ -202,6 +246,7 @@ export declare class TaskRoomApplicationService {
202
246
  identity_cid: string;
203
247
  room_name: string;
204
248
  state: "provisioning" | "active" | "closing" | "closed";
249
+ anonymous: boolean;
205
250
  seats: import("../rooms-tasks/cowork-adapter.js").CoworkSeatInfo[];
206
251
  goal?: string;
207
252
  briefing?: string;
@@ -258,6 +303,7 @@ export declare class TaskRoomApplicationService {
258
303
  taskId: string;
259
304
  template?: string;
260
305
  members?: MemberOverrides;
306
+ anonymous?: boolean;
261
307
  }): Promise<{
262
308
  task: TaskRecord;
263
309
  status: 'ready' | 'already_active';
@@ -4,15 +4,18 @@ import { ConfigError, loadConfig } from '../config.js';
4
4
  import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
5
5
  import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
6
6
  import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
7
- import { activateTask, blockTask as persistBlockTask, createTask as persistTask, deleteTask as persistDeleteTask, getTask as readTask, listTasks as readTasks, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, updateTaskExecutionPlan, } from '../rooms-tasks/task-state.js';
7
+ 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';
8
8
  import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
9
9
  import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
10
10
  import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
11
11
  import { hashMemberOverrides, prepareExecutionPlan } from '../rooms-tasks/member-overrides.js';
12
12
  import { recordFleetAuditPresentation } from '../fleet-command-audit.js';
13
13
  import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
14
- import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from '../rooms-tasks/terminal.js';
15
- import { TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
14
+ import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath, } from '../rooms-tasks/terminal.js';
15
+ import { acceptTaskDeletion, recordTaskDeletionError, settleTaskDeletion, } from '../rooms-tasks/deletion.js';
16
+ import { withFileLock } from '../atomic-file.js';
17
+ import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
18
+ import { storedRoomLaunchPolicy, TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
16
19
  export class TaskRoomApplicationError extends Error {
17
20
  code;
18
21
  fields;
@@ -23,6 +26,10 @@ export class TaskRoomApplicationError extends Error {
23
26
  this.name = 'TaskRoomApplicationError';
24
27
  }
25
28
  }
29
+ export function resolveRoomLaunchPolicy(template, override) {
30
+ const anonymous = override ?? template?.room?.anonymous ?? false;
31
+ return { anonymous };
32
+ }
26
33
  function launchSelection(definition, key) {
27
34
  const selected = definition?.[key];
28
35
  if (!selected || typeof selected !== 'object' || Array.isArray(selected))
@@ -48,6 +55,7 @@ function roomParticipants(room) {
48
55
  brain: launchSelection(seat.launch?.agent_definition, 'brain'),
49
56
  role: launchSelection(seat.launch?.agent_definition, 'role') ?? seat.cowork_role,
50
57
  permissions: launchPermissions(seat.launch?.agent_definition),
58
+ ...(seat.launch?.presentation ? { configuration: seat.launch.presentation } : {}),
51
59
  }));
52
60
  }
53
61
  function taskAgents(task) {
@@ -57,7 +65,8 @@ function taskAgents(task) {
57
65
  const seat = seats.get(member.name);
58
66
  return { name: member.name, brain: launchSelection(seat?.launch?.agent_definition, 'brain'),
59
67
  role: launchSelection(seat?.launch?.agent_definition, 'role') ?? member.cowork_role,
60
- permissions: launchPermissions(seat?.launch?.agent_definition) };
68
+ permissions: launchPermissions(seat?.launch?.agent_definition),
69
+ ...(seat?.launch?.presentation ? { configuration: seat.launch.presentation } : {}) };
61
70
  });
62
71
  }
63
72
  function roomFailureEventId(room) {
@@ -76,6 +85,9 @@ export class TaskRoomApplicationService {
76
85
  async createTask(request) {
77
86
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
78
87
  let template = this.createTemplate(cfg, request.template, request.noRoom);
88
+ if (request.noRoom && request.anonymous !== undefined)
89
+ throw new ConfigError('--anonymous/--no-anonymous cannot be combined with --no-room');
90
+ const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
79
91
  let launchDefinitions;
80
92
  let executionPlan;
81
93
  let preparedPlan;
@@ -92,7 +104,8 @@ export class TaskRoomApplicationService {
92
104
  if (preparedPlan) {
93
105
  template = sealTemplateSnapshot(preparedPlan.snapshot, cfg.agentTemplates ?? {}, preparedPlan.launchDefinitions);
94
106
  sealedHash = template.launch_snapshot_hash;
95
- executionPlan = { schema_version: 1, snapshot: template, overrides: preparedPlan.overrides,
107
+ executionPlan = { schema_version: 1, snapshot: template, room_policy: roomPolicy,
108
+ overrides: preparedPlan.overrides,
96
109
  overrides_hash: preparedPlan.overridesHash,
97
110
  plan_hash: preparedPlan.planHash };
98
111
  }
@@ -124,7 +137,7 @@ export class TaskRoomApplicationService {
124
137
  try {
125
138
  const room = await this.provisionRoom(cfg, task, template, created => {
126
139
  task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
127
- }, launchDefinitions);
140
+ }, launchDefinitions, roomPolicy);
128
141
  task = readTask(task.task_id);
129
142
  if (task.state === 'active' && room.state === 'active')
130
143
  recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
@@ -159,20 +172,22 @@ export class TaskRoomApplicationService {
159
172
  if (request.members && Object.keys(request.members).length) {
160
173
  const prepared = prepareExecutionPlan(definition, cfg, request.members);
161
174
  template = prepared.snapshot;
162
- return this.provisionRoom(cfg, { title: request.name, brief, goal: request.goal }, template, () => { }, prepared.launchDefinitions);
175
+ const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
176
+ return this.provisionRoom(cfg, { title: request.name, brief, goal: request.goal }, template, () => { }, prepared.launchDefinitions, roomPolicy);
163
177
  }
164
178
  template = snapshotTemplate(definition, cfg.agentTemplates);
165
179
  }
180
+ const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
166
181
  return this.provisionRoom(cfg, {
167
182
  title: request.name, brief, goal: request.goal,
168
- }, template, () => { });
183
+ }, template, () => { }, undefined, roomPolicy);
169
184
  }
170
185
  async startTask(input) {
171
186
  return (await this.ensureTaskWork(input)).task;
172
187
  }
173
188
  listTasks(filter) {
174
189
  const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
175
- return readTasks({ state: filter?.state, listId });
190
+ return readTasks({ state: filter?.state, listId, includeDeleting: filter?.includeDeleting });
176
191
  }
177
192
  listTaskLists() { return readTaskLists(); }
178
193
  groupedTasks(filter) {
@@ -225,8 +240,93 @@ export class TaskRoomApplicationService {
225
240
  reviewTask(input) {
226
241
  return persistReviewTask(input.taskId);
227
242
  }
228
- deleteTask(input) {
229
- return persistDeleteTask(input.taskId);
243
+ deletionActor(actor) {
244
+ if (actor.kind === 'local_control')
245
+ return { kind: 'local_control', surface: actor.surface };
246
+ if (actor.kind === 'authenticated_owner')
247
+ return { kind: 'authenticated_owner', surface: 'messenger', cid: actor.cid };
248
+ throw new Error('internal workers cannot originate a task deletion acceptance');
249
+ }
250
+ /** Accept a permanent deletion in any lifecycle state; audit at acceptance. */
251
+ async requestTaskDeletion(input) {
252
+ const result = await acceptTaskDeletion(input.taskId, this.deletionActor(input.actor));
253
+ if (result.status === 'accepted') {
254
+ const task = result.task;
255
+ recordFleetAuditPresentation({ kind: 'task', operation: 'delete', id: task.task_id,
256
+ title: task.title, previousState: task.state, newState: 'deleting',
257
+ revision: task.deletion?.accepted_at ?? task.created_at, list: task.list_name ?? 'default',
258
+ roomId: task.room_id,
259
+ template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
260
+ agents: [] });
261
+ }
262
+ return result;
263
+ }
264
+ /** Worker entry: converge an accepted deletion; Cowork is resolved lazily. */
265
+ async settleTaskDeletion(input) {
266
+ const result = await settleTaskDeletion({
267
+ taskId: input.taskId,
268
+ cowork: () => {
269
+ // Config resolves only when room work exists: a no-room deletion must
270
+ // settle even with missing or invalid rooms configuration.
271
+ const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
272
+ if (!cfg.rooms)
273
+ throw new ConfigError('rooms: configuration is required before deleting task rooms');
274
+ return this.deps.cowork ? this.deps.cowork(cfg)
275
+ : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
276
+ },
277
+ });
278
+ if (result.deleted && result.previous_state) {
279
+ recordFleetAuditPresentation({ kind: 'task', operation: 'delete', id: result.task_id,
280
+ title: result.title, previousState: result.previous_state, newState: 'deleted',
281
+ revision: new Date().toISOString(), list: 'default', roomId: undefined,
282
+ template: undefined, agents: [] });
283
+ }
284
+ return result;
285
+ }
286
+ /**
287
+ * Launch the external deletion worker outside the caller's lifecycle and
288
+ * wait boundedly for physical absence. Timeouts and launch failures report
289
+ * a pending, recoverable state — never success. A concurrent worker that
290
+ * already removed the record reads as settled.
291
+ */
292
+ async launchTaskDeletionWorker(input) {
293
+ const launch = this.deps.launchDeletionWorker
294
+ ?? ((taskId) => launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, this.configurationPath));
295
+ const errorAtBefore = (() => {
296
+ try {
297
+ return getDeletingTask(input.taskId).deletion?.error_at;
298
+ }
299
+ catch {
300
+ return undefined;
301
+ }
302
+ })();
303
+ try {
304
+ await launch(input.taskId);
305
+ }
306
+ catch (error) {
307
+ const message = error instanceof Error ? error.message : String(error);
308
+ await recordTaskDeletionError(input.taskId, message, `External delete worker failed to start. Repeat the delete request for ${input.taskId}.`).catch(() => { });
309
+ return { deleted: false, pending: true, error: message };
310
+ }
311
+ const now = this.deps.now ?? Date.now;
312
+ const sleep = this.deps.sleep
313
+ ?? ((ms) => new Promise(resolve => { setTimeout(resolve, ms); }));
314
+ const deadline = now() + (input.waitMs ?? 10_000);
315
+ while (now() < deadline) {
316
+ if (taskDeletionState(input.taskId) === 'absent')
317
+ return { deleted: true, pending: false };
318
+ try {
319
+ const current = getDeletingTask(input.taskId).deletion;
320
+ if (current?.error_at !== errorAtBefore && current?.error)
321
+ return { deleted: false, pending: true, error: current.error };
322
+ }
323
+ catch { /* re-checked as absence on the next iteration */ }
324
+ await sleep(100);
325
+ }
326
+ return { deleted: false, pending: true };
327
+ }
328
+ recordDeletionError(input) {
329
+ return recordTaskDeletionError(input.taskId, input.error, input.recoveryHint);
230
330
  }
231
331
  async completeTask(input) {
232
332
  const task = readTask(input.taskId);
@@ -265,10 +365,22 @@ export class TaskRoomApplicationService {
265
365
  return recordTaskTerminalIntentError(input.taskId, input.error, input.recoveryHint);
266
366
  }
267
367
  async beginTaskRecovery(input) {
368
+ // The deletion-vs-terminal routing decision serializes on the common
369
+ // task-operation lock and must not require configuration: recovery of a
370
+ // deletion-pending task continues the deletion and never resurrects.
371
+ const routed = await withFileLock(taskOperationLockPath(input.taskId), () => {
372
+ const task = readTask(input.taskId);
373
+ if (task.deletion?.status === 'pending')
374
+ return 'deletion';
375
+ if (task.terminal_intent?.status === 'pending')
376
+ return 'terminal';
377
+ return 'none';
378
+ }, {}, TASK_OPERATION_LOCK_STALE_MS);
379
+ if (routed === 'deletion')
380
+ return { kind: 'deletion_worker_required', taskId: input.taskId };
268
381
  const config = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
269
382
  this.recovery = { taskId: input.taskId, config };
270
- const task = readTask(input.taskId);
271
- if (task.terminal_intent?.status === 'pending')
383
+ if (routed === 'terminal')
272
384
  return { kind: 'terminal_worker_required', taskId: input.taskId };
273
385
  return { kind: 'final', result: await this.continueTaskRecovery({
274
386
  actor: input.actor, taskId: input.taskId, terminalTimedOut: false,
@@ -430,6 +542,14 @@ export class TaskRoomApplicationService {
430
542
  return { kind: 'deletion_worker_required', roomId: input.roomId };
431
543
  const room = await adapter.recoverRoom(input.roomId);
432
544
  orchestration = getRoomRecord(input.roomId);
545
+ if (orchestration) {
546
+ const policy = storedRoomLaunchPolicy(orchestration.room_policy);
547
+ if ((room.anonymous ?? false) !== policy.anonymous) {
548
+ const error = `Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(policy.anonymous)})`;
549
+ setSagaError(input.roomId, error, 'Do not respawn members. Repair or upgrade Cowork, then recover the Room without changing its policy.', 'waiting_cowork');
550
+ throw new Error(error);
551
+ }
552
+ }
433
553
  if (orchestration && !orchestration.owner_seat_cid
434
554
  && (orchestration.provisioning_detail === 'waiting_owner_invite'
435
555
  || orchestration.provisioning_detail === 'owner_cid_mismatch')) {
@@ -499,6 +619,10 @@ export class TaskRoomApplicationService {
499
619
  async ensureTaskWork(input) {
500
620
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
501
621
  let task = readTask(input.taskId);
622
+ const recordedRoom = task.room_id ? getRoomRecord(task.room_id) : undefined;
623
+ const recordedPolicy = storedRoomLaunchPolicy(recordedRoom?.room_policy ?? task.execution_plan?.room_policy);
624
+ if (task.room_id && input.anonymous !== undefined && input.anonymous !== recordedPolicy.anonymous)
625
+ throw new TaskRoomApplicationError('template_mismatch', 'anonymous override does not match the existing Room launch policy', { room: task.room_id });
502
626
  if (TASK_TERMINAL_STATES.includes(task.state))
503
627
  throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: input.taskId, state: task.state });
504
628
  if (task.state === 'active' && task.room_id) {
@@ -542,6 +666,11 @@ export class TaskRoomApplicationService {
542
666
  let preparedPlan = !durable && definition
543
667
  ? prepareExecutionPlan(definition, cfg, input.members ?? {}) : undefined;
544
668
  let snapshot = durable ?? preparedPlan.snapshot;
669
+ const roomPolicy = input.anonymous !== undefined
670
+ ? resolveRoomLaunchPolicy(snapshot, input.anonymous)
671
+ : task.execution_plan
672
+ ? storedRoomLaunchPolicy(task.execution_plan.room_policy)
673
+ : resolveRoomLaunchPolicy(snapshot, undefined);
545
674
  if (preparedPlan)
546
675
  launchDefinitions = preparedPlan.launchDefinitions;
547
676
  if (input.members && Object.keys(input.members).length && !storedOverridesMatch) {
@@ -584,6 +713,7 @@ export class TaskRoomApplicationService {
584
713
  try {
585
714
  sealed = sealTemplateSnapshot(snapshot, cfg.agentTemplates ?? {}, launchDefinitions);
586
715
  task = updateTaskExecutionPlan(task.task_id, { schema_version: 1, snapshot: sealed,
716
+ room_policy: roomPolicy,
587
717
  overrides: preparedPlan.overrides, overrides_hash: preparedPlan.overridesHash,
588
718
  plan_hash: preparedPlan.planHash });
589
719
  snapshot = sealed;
@@ -596,6 +726,9 @@ export class TaskRoomApplicationService {
596
726
  }
597
727
  unlock();
598
728
  }
729
+ else if (!task.room_id && task.execution_plan && input.anonymous !== undefined) {
730
+ task = updateTaskExecutionPlan(task.task_id, { ...task.execution_plan, room_policy: roomPolicy });
731
+ }
599
732
  else if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
600
733
  task = updateTaskTemplate(task.task_id, { name: snapshot.name, version: snapshot.version, content_hash: snapshot.content_hash });
601
734
  if (task.state === 'backlog') {
@@ -609,7 +742,7 @@ export class TaskRoomApplicationService {
609
742
  try {
610
743
  await this.provisionRoom(cfg, task, snapshot, created => {
611
744
  task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
612
- }, launchDefinitions);
745
+ }, launchDefinitions, roomPolicy);
613
746
  task = readTask(task.task_id);
614
747
  }
615
748
  catch (error) {
@@ -676,7 +809,7 @@ export class TaskRoomApplicationService {
676
809
  throw new TaskRoomApplicationError('task_template_drift', `task template snapshot no longer matches ${ref.name}@${ref.version}`);
677
810
  return snapshot;
678
811
  }
679
- async provisionRoom(cfg, task, template, onCreated, launchDefinitions) {
812
+ async provisionRoom(cfg, task, template, onCreated, launchDefinitions, policy = resolveRoomLaunchPolicy(template, undefined)) {
680
813
  const rooms = cfg.rooms;
681
814
  if (!rooms)
682
815
  throw new ConfigError('rooms: configuration is required');
@@ -690,21 +823,37 @@ export class TaskRoomApplicationService {
690
823
  })();
691
824
  const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
692
825
  let launchTemplate;
693
- let created;
694
826
  let room;
695
827
  try {
696
828
  launchTemplate = template ? (template.launch_snapshot_hash ? template
697
829
  : sealTemplateSnapshot(template, cfg.agentTemplates ?? {}, launchDefinitions)) : undefined;
698
- created = await cowork.createRoom({
699
- room_name: task.title, goal: task.goal?.trim() || task.title,
700
- briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
701
- quiet_membership: launchTemplate?.room?.quiet_membership,
702
- anonymous: launchTemplate?.room?.anonymous,
703
- });
704
- room = createRoomRecord({
705
- room_id: created.room_id, room_name: task.title, room_identity_cid: created.identity_cid,
706
- task_id: task.task_id, template_snapshot: launchTemplate,
707
- });
830
+ // Room publication window: for a task-bound room, hold the common
831
+ // task-operation lock across remote creation, local record creation, and
832
+ // the task link, so an accepted deletion linearizes strictly before
833
+ // (bounded task_deleting abort) or strictly after (record visible to the
834
+ // deletion scan). Lock order stays launch-snapshot → task-operation.
835
+ const publishRoom = async () => {
836
+ if (task.task_id) {
837
+ const fresh = readTask(task.task_id);
838
+ if (fresh.deletion?.status === 'pending')
839
+ throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
840
+ }
841
+ const created = await cowork.createRoom({
842
+ room_name: task.title, goal: task.goal?.trim() || task.title,
843
+ briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
844
+ quiet_membership: launchTemplate?.room?.quiet_membership,
845
+ anonymous: policy.anonymous,
846
+ });
847
+ const record = createRoomRecord({
848
+ room_id: created.room_id, room_name: task.title, room_identity_cid: created.identity_cid,
849
+ task_id: task.task_id, template_snapshot: launchTemplate, room_policy: policy,
850
+ });
851
+ onCreated(record);
852
+ return record;
853
+ };
854
+ room = task.task_id
855
+ ? await withFileLock(taskOperationLockPath(task.task_id), publishRoom, {}, TASK_OPERATION_LOCK_STALE_MS)
856
+ : await publishRoom();
708
857
  }
709
858
  catch (error) {
710
859
  unlockSnapshot?.();
@@ -718,7 +867,6 @@ export class TaskRoomApplicationService {
718
867
  revision: room.created_at, taskId: room.task_id,
719
868
  template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
720
869
  participants: [] });
721
- onCreated(room);
722
870
  room = advanceSaga(room.room_id, 'create_room', 1);
723
871
  if (attachOwner) {
724
872
  try {
package/dist/briefing.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { userInfo } from 'node:os';
2
2
  import { oversightTaxonomyLines } from './session/control.js';
3
- function temporaryIdentityBootstrap(id, v) {
3
+ function temporaryIdentityBootstrap(id, v, anonymous = false) {
4
4
  return [
5
5
  `2. CREATE your ours identity now: call **${v.temporaryCreateTool}** through ours MCP`,
6
- ` with the exact assigned name "${id}". The ours connector owns its cleanup when this`,
6
+ ` with the exact assigned name "${id}"${anonymous ? ' and expose_local=false' : ''}. The ours connector owns its cleanup when this`,
7
7
  ' connector session lifecycle ends.',
8
8
  ' Do not inspect, preserve, adopt, or use any pre-existing or persistent identity.',
9
9
  ' On a collision, missing tool, or creation error, STOP and',
@@ -24,7 +24,7 @@ function generateRoomMemberBriefing(role, v, opts, prefix) {
24
24
  L.push('', '### One-time room invite', '', '```text', startup.invite, '```');
25
25
  L.push('', '## Do these NOW, in order');
26
26
  L.push(`1. ${v.launchNote(role.name)}`);
27
- L.push(...temporaryIdentityBootstrap(startup.identity_name, v));
27
+ L.push(...temporaryIdentityBootstrap(startup.identity_name, v, startup.anonymous));
28
28
  L.push('3. Call **add_contact** through ours MCP with the exact one-time invite above. Confirm');
29
29
  L.push(` that it resolves to room CID \`${startup.room_identity_cid}\`. The contact may remain`);
30
30
  L.push(' pending while the room finishes its asynchronous verification.');
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.1.0-nightly.12",
3
- "buildId": "6da038d91a6d",
4
- "commit": "57faa7ff79b4097ac94797b9a591ce54aad01c0a",
2
+ "version": "1.1.0-nightly.14",
3
+ "buildId": "3651cb00af5a",
4
+ "commit": "974e9150cbb9d24f92412866de7a53a0cd6fde8f",
5
5
  "dirty": true,
6
- "builtAt": "2026-08-31T21:40:11.904Z",
6
+ "builtAt": "2026-09-01T08:58:08.364Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
package/dist/config.d.ts CHANGED
@@ -175,6 +175,7 @@ export interface RoomMemberStartup {
175
175
  role: string;
176
176
  task: string;
177
177
  owner_seat_cid: string | null;
178
+ anonymous?: boolean;
178
179
  }
179
180
  export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel' | 'worklog'> {
180
181
  name: string;