@ours.network/fleet 2.0.3 → 2.1.0

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 CHANGED
@@ -485,7 +485,6 @@ Additional operational field inventory (schematic, not one YAML document):
485
485
  owners: [owner-contact-cid] # authenticated ours contact IDs, never display names
486
486
  agent: managed-agent-cid # exact role CID allowed to relay messages/files outward
487
487
  interrupt: false # false queues; true cancels current work first
488
- progress_interval_ms: 30000 # fleet-generated progress notices; 0 disables
489
488
  comments: true # relay live "🟡 Live update:" structured commentary (default true);
490
489
  # restart baseline for /comments on|off
491
490
  attachments: # secure inbound documents, images, and voice
@@ -1962,3 +1961,13 @@ App released. The currently pinned Fleet nightly.41 does not support v2 grants.
1962
1961
  These source changes do not publish, merge, or deploy any component.
1963
1962
 
1964
1963
  See [ACP file sending](docs/acp-files.md) for the same-chat `send_file` flow, available to every Agent by default, and its supported sessions.
1964
+
1965
+ ### Owner-channel activity display
1966
+
1967
+ Routine tool activity is shown as one updating status in the app, outside message
1968
+ history. Fleet no longer sends periodic tool-counter messages. The
1969
+ `owner_channel.progress_interval_ms` setting has been removed; remove it from
1970
+ existing role/default configuration when applying this change. Meaningful agent
1971
+ commentary, attention requests and final answers continue to be delivered.
1972
+ The app associates activity with the channel's bound CID, never a display name.
1973
+ It shows unavailable status when the directory observation disconnects.
@@ -165,6 +165,8 @@ export class FleetQueryService {
165
165
  return {
166
166
  backend: intended, reachability: snapshot.alive ? 'online' : 'offline',
167
167
  readiness: snapshot.readiness, evidence: 'authoritative',
168
+ ...(typeof snapshot.ownerChannelCid === 'string' && /^[a-f0-9]{64}$/i.test(snapshot.ownerChannelCid)
169
+ ? { ownerChannelCid: snapshot.ownerChannelCid.toLowerCase() } : {}),
168
170
  sessionId: snapshot.sessionId, lastError: snapshot.lastError,
169
171
  pendingPermissionId: snapshot.pendingPermissionId,
170
172
  protocolVersion: snapshot.protocolVersion, features: snapshot.features,
@@ -14,6 +14,7 @@ import { loadConfig, splitRootFor } from '../config.js';
14
14
  import { assertLayoutFile, validLayoutKey, validateRoomLayout } from '../rooms-tasks/layout-config.js';
15
15
  import { FleetError } from './errors.js';
16
16
  import { layoutDefinitionHash } from './task-layouts.js';
17
+ import { EMPTY_TASK_LAYOUT } from '../rooms-tasks/empty-layout.js';
17
18
  export const ABSENT_REVISION = 'absent';
18
19
  const revisionOf = (bytes) => createHash('sha256').update(bytes).digest('hex');
19
20
  export class RoomLayoutDefinitions {
@@ -25,6 +26,8 @@ export class RoomLayoutDefinitions {
25
26
  }
26
27
  root() { return join(splitRootFor(this.configPath ?? defaultConfigPath()), 'room_layouts'); }
27
28
  pathFor(name) {
29
+ if (name === EMPTY_TASK_LAYOUT)
30
+ throw new FleetError('invalid_request', 'the built-in empty layout cannot be edited or deleted');
28
31
  if (!validLayoutKey(name))
29
32
  throw new FleetError('invalid_request', 'layout name must start with a letter and use letters, digits, - or _');
30
33
  return join(this.root(), `${name}.yaml`);
@@ -68,6 +71,8 @@ export class RoomLayoutDefinitions {
68
71
  return readdirSync(root).filter(file => /\.ya?ml$/.test(file)).sort().map(file => {
69
72
  const name = basename(file, extname(file)), path = join(root, file);
70
73
  try {
74
+ if (name === EMPTY_TASK_LAYOUT)
75
+ throw Error('reserved built-in layout name');
71
76
  if (!validLayoutKey(name))
72
77
  throw Error(`invalid room layout id: ${name}`);
73
78
  assertLayoutFile(path);
@@ -1,7 +1,7 @@
1
1
  import { type RoomLayoutDefinition } from '../rooms-tasks/layout-config.js';
2
2
  import { RoomLayoutService } from '../rooms-tasks/layout-service.js';
3
3
  import type { TaskLayoutLink, TaskRecord } from '../rooms-tasks/types.js';
4
- export type TaskLayoutOperation = 'open' | 'close-room' | 'close';
4
+ export type TaskLayoutOperation = 'start' | 'open' | 'close-room' | 'close';
5
5
  export interface TaskLayoutOperationRecord {
6
6
  /** Only the worker holding the current ID may update the record. */
7
7
  id: string;
@@ -73,6 +73,15 @@ export declare class TaskLayouts {
73
73
  runExists(link: TaskLayoutLink): boolean;
74
74
  /** Idempotent: reuse a run whose provenance matches this task, or snapshot one from the pinned source. */
75
75
  ensureRun(task: TaskRecord, precondition?: (current: TaskRecord) => void): Promise<void>;
76
+ /** Starting a mission initializes every room and its required agents before Active. */
77
+ initializeRooms(task: TaskRecord): Promise<void>;
78
+ addMember(task: TaskRecord, input: {
79
+ key: string;
80
+ role: string;
81
+ requestId: string;
82
+ hash: string;
83
+ definition: import('../config.js').AgentTemplateDefinition;
84
+ }): Promise<void>;
76
85
  private verifyProvenance;
77
86
  readOperation(runId: string): TaskLayoutOperationRecord | undefined;
78
87
  /** Update only the still-current operation; a superseded or forgotten one is never resurrected. */
@@ -11,7 +11,7 @@ import { join } from 'node:path';
11
11
  import { canonicalJson } from '../canonical-json.js';
12
12
  import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
13
13
  import { assertLayoutFile } from '../rooms-tasks/layout-config.js';
14
- import { getTask as readTask } from '../rooms-tasks/task-state.js';
14
+ import { getTask as readTask, activateTask, blockTask } from '../rooms-tasks/task-state.js';
15
15
  import { stateRoot } from '../paths.js';
16
16
  import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
17
17
  import { layoutSupervisorId } from '../rooms-tasks/layout-control.js';
@@ -20,6 +20,7 @@ import { FleetError } from './errors.js';
20
20
  import { ownedLayoutRetirementSeats } from '../rooms-tasks/layout-member-retirement.js';
21
21
  import { eraseMemberArtifacts } from '../rooms-tasks/erasure.js';
22
22
  import { assertTaskRegistrationsAbsent } from '../supervisor/catalog.js';
23
+ import { EMPTY_TASK_LAYOUT, EMPTY_TASK_ROOM } from '../rooms-tasks/empty-layout.js';
23
24
  /** Hash of the authorable definition only (never the source file path). */
24
25
  export function layoutDefinitionHash(definition) {
25
26
  const { version, description, participants, rooms } = definition;
@@ -95,12 +96,35 @@ export class TaskLayouts {
95
96
  if (layoutDefinitionHash(source) !== link.definition_hash)
96
97
  throw Error(`room layout ${link.name} changed before this task's layout run was created; create a new task to use the edited layout`);
97
98
  const scoped = taskScopedDefinition(structuredClone(source), current);
99
+ if (link.name === EMPTY_TASK_LAYOUT)
100
+ scoped.rooms[EMPTY_TASK_ROOM].goal = current.brief?.trim() || current.title;
98
101
  const provenance = { task_id: current.task_id, source_hash: link.definition_hash, scoped_hash: layoutDefinitionHash(scoped) };
99
102
  // Provenance precedes the run: a crash in between is re-derived from the same pinned source.
100
103
  replaceFileAtomically(this.provenancePath(link.run_id), JSON.stringify(provenance), 0o600);
101
104
  await service.create(link.name, {}, link.run_id, { source: scoped, cwd: current.workspace?.path });
102
105
  });
103
106
  }
107
+ /** Starting a mission initializes every room and its required agents before Active. */
108
+ async initializeRooms(task) {
109
+ await this.withTaskLock(task.layout.run_id, async () => {
110
+ const current = readTask(task.task_id);
111
+ if (current.deletion?.status === 'pending' || current.terminal_intent || !['provisioning', 'active'].includes(current.state))
112
+ throw new FleetError('conflict', 'task is no longer starting; its room cannot be opened');
113
+ this.verifyProvenance(current);
114
+ const engine = this.service().open(task.layout.run_id);
115
+ for (const key of Object.keys(engine.snapshot().definition.rooms))
116
+ await engine.activate(layoutSupervisorId(), key);
117
+ });
118
+ }
119
+ async addMember(task, input) {
120
+ const link = task.layout;
121
+ await this.withTaskLock(link.run_id, async () => {
122
+ const current = readTask(task.task_id);
123
+ assertLayoutOpenable(current);
124
+ this.verifyProvenance(current);
125
+ await this.service().open(link.run_id).addMember(layoutSupervisorId(), { ...input, room: EMPTY_TASK_ROOM });
126
+ });
127
+ }
104
128
  verifyProvenance(task) {
105
129
  const link = task.layout, path = this.provenancePath(link.run_id);
106
130
  let provenance;
@@ -151,15 +175,18 @@ export class TaskLayouts {
151
175
  ...(state.uncertain ? { uncertain: state.uncertain } : {}),
152
176
  rooms: Object.entries(state.definition.rooms).map(([key, spec]) => {
153
177
  const room = state.rooms[key];
178
+ const additions = Object.entries(state.additional_members ?? {}).filter(([, member]) => member.room === key);
179
+ const roles = { ...spec.roles, ...Object.fromEntries(additions.map(([name, member]) => [name, member.role])) };
154
180
  return {
155
- key, goal: spec.goal, members: [...spec.members], ...(spec.roles ? { roles: { ...spec.roles } } : {}),
181
+ key, goal: spec.goal, members: [...spec.members, ...additions.map(([name]) => name)],
182
+ ...(Object.keys(roles).length ? { roles } : {}),
156
183
  state: room?.state ?? 'declared', ready: [...(room?.ready ?? [])],
157
184
  ...(room?.native ? { room_id: room.native.room_id, identity_cid: room.native.identity_cid } : {}),
158
185
  };
159
186
  }),
160
187
  participants: Object.entries(state.participants).map(([key, p]) => ({
161
188
  key, ...(p.participant ? { participant: p.participant, room: p.room } : {}),
162
- agent_template: state.definition.participants[p.participant ?? key]?.agent_template, owned: p.owned, retired: Boolean(p.retired),
189
+ agent_template: state.definition.participants[p.participant ?? key]?.agent_template ?? state.additional_members?.[key]?.template, owned: p.owned, retired: Boolean(p.retired),
163
190
  ...(p.instance?.agent ? { agent: p.instance.agent } : {}), ...(p.instance?.cid ? { cid: p.instance.cid } : {}),
164
191
  })),
165
192
  };
@@ -169,7 +196,7 @@ export class TaskLayouts {
169
196
  const link = task.layout;
170
197
  if (!link)
171
198
  throw Error(`task ${task.task_id} has no layout`);
172
- if (operation !== 'close' && (!room || !ROOM_KEY.test(room)))
199
+ if (!['start', 'close'].includes(operation) && (!room || !ROOM_KEY.test(room)))
173
200
  throw new FleetError('invalid_request', 'a valid room key is required');
174
201
  if (room) {
175
202
  const definition = this.runExists(link) ? this.service().open(link.run_id, true).snapshot().definition : undefined;
@@ -177,13 +204,20 @@ export class TaskLayouts {
177
204
  throw new FleetError('resource_not_found', `unknown layout room: ${room}`);
178
205
  }
179
206
  const record = { id: randomUUID(), operation, ...(room ? { room } : {}), status: 'launching' };
180
- await withFileLock(this.operationPath(link.run_id) + '.lock', () => {
207
+ let accepted = false;
208
+ const selected = await withFileLock(this.operationPath(link.run_id) + '.lock', () => {
181
209
  const current = this.readOperation(link.run_id);
182
210
  // Full close always proceeds: terminal cleanup must not wait behind an open; the engine lock serializes them.
211
+ if (operation === 'start' && current?.operation === 'start' && ['launching', 'running'].includes(current.status) && Date.now() - Date.parse(current.updated_at) < 10 * 60_000)
212
+ return current;
183
213
  if (operation !== 'close' && current && ['launching', 'running'].includes(current.status) && Date.now() - Date.parse(current.updated_at) < 10 * 60_000)
184
214
  throw new FleetError('conflict', `layout operation already in progress: ${current.operation}${current.room ? ' ' + current.room : ''}`);
185
215
  this.writeOperation(link.run_id, record);
216
+ accepted = true;
217
+ return record;
186
218
  });
219
+ if (!accepted)
220
+ return selected;
187
221
  const args = ['task', '_layout', task.task_id, operation, ...(room ? [room] : []), '--operation-id', record.id];
188
222
  const launch = this.deps.launch
189
223
  ?? ((cli, name) => launchFleetWorker(cli, name, this.configPath, this.deps.binPath?.()));
@@ -218,6 +252,20 @@ export class TaskLayouts {
218
252
  }
219
253
  async perform(task, operation, room) {
220
254
  const link = task.layout;
255
+ if (operation === 'start') {
256
+ try {
257
+ await this.initializeRooms(task);
258
+ if (readTask(task.task_id).state !== 'active')
259
+ activateTask(task.task_id);
260
+ }
261
+ catch (error) {
262
+ const current = readTask(task.task_id);
263
+ if (current.state === 'provisioning' && !current.terminal_intent && current.deletion?.status !== 'pending')
264
+ blockTask(task.task_id, `layout startup failed: ${error.message}`);
265
+ throw error;
266
+ }
267
+ return;
268
+ }
221
269
  if (operation === 'open') {
222
270
  // Re-read under the task-layout lock: a deletion or terminal action accepted after HTTP admission wins.
223
271
  await this.ensureRun(task, assertLayoutOpenable);
@@ -79,6 +79,8 @@ export interface CreateTaskRequest {
79
79
  anonymous?: boolean;
80
80
  /** Room Layout name: the task's rooms and participants come from a layout run. */
81
81
  layout?: string;
82
+ /** HTTP acceptance returns while a detached worker initializes the layout. */
83
+ deferLayoutStart?: boolean;
82
84
  }
83
85
  export interface CreateRoomRequest {
84
86
  actor: TaskRoomActor;
@@ -167,7 +169,7 @@ export declare class TaskRoomApplicationService {
167
169
  runTaskLayoutOperation(input: {
168
170
  actor: TaskRoomActor;
169
171
  taskId: string;
170
- operation: 'open' | 'close-room' | 'close';
172
+ operation: 'start' | 'open' | 'close-room' | 'close';
171
173
  room?: string;
172
174
  operationId?: string;
173
175
  }): Promise<import("./task-layouts.js").TaskLayoutView>;
@@ -179,6 +181,7 @@ export declare class TaskRoomApplicationService {
179
181
  template?: string;
180
182
  members?: MemberOverrides;
181
183
  anonymous?: boolean;
184
+ deferLayoutStart?: boolean;
182
185
  }): Promise<TaskRecord>;
183
186
  listTasks(filter?: {
184
187
  state?: TaskState | TaskState[];
@@ -404,6 +407,7 @@ export declare class TaskRoomApplicationService {
404
407
  template?: string;
405
408
  members?: MemberOverrides;
406
409
  anonymous?: boolean;
410
+ deferLayoutStart?: boolean;
407
411
  }): Promise<{
408
412
  task: TaskRecord;
409
413
  status: 'ready' | 'already_active' | 'in_progress';
@@ -1,4 +1,5 @@
1
1
  import { TaskLayouts, assertLayoutOpenable, writeTaskBrief } from './task-layouts.js';
2
+ import { EMPTY_TASK_LAYOUT } from '../rooms-tasks/empty-layout.js';
2
3
  import { FleetError } from './errors.js';
3
4
  import { readFileSync } from 'node:fs';
4
5
  import { taskLiveReadiness } from '../rooms-tasks/live-readiness.js';
@@ -78,7 +79,7 @@ export class TaskRoomApplicationService {
78
79
  const { requestMemberAddition } = await import('../rooms-tasks/add-member.js');
79
80
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
80
81
  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
+ cowork: readTask(taskId).layout ? undefined : (this.deps.cowork ?? createCoworkAdapter)(cfg), binPath: (this.deps.binPath ?? getBinPath)(), provision: this.deps.provisionMembers, layouts: this.layouts() });
82
83
  }
83
84
  async memberAddition(taskId, requestId) { const { memberAddition } = await import('../rooms-tasks/add-member.js'); return memberAddition(taskId, requestId); }
84
85
  layouts() {
@@ -103,10 +104,10 @@ export class TaskRoomApplicationService {
103
104
  recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
104
105
  title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
105
106
  list: task.list_name ?? 'default', roomId: undefined, template: `layout:${layout.name}`, agents: [] });
106
- return task.state === 'provisioning' ? (await this.ensureLayoutWork(task)).task : task;
107
+ return task.state === 'provisioning' ? (await this.ensureLayoutWork(task, request.deferLayoutStart)).task : task;
107
108
  }
108
109
  /** Layout tasks never provision the legacy single room; their run is a local snapshot. */
109
- async ensureLayoutWork(task) {
110
+ async ensureLayoutWork(task, defer = false) {
110
111
  if (TASK_TERMINAL_STATES.includes(task.state))
111
112
  throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: task.task_id, state: task.state });
112
113
  if (task.state === 'active' || task.state === 'review')
@@ -119,6 +120,11 @@ export class TaskRoomApplicationService {
119
120
  }
120
121
  try {
121
122
  await this.layouts().ensureRun(task);
123
+ if (defer) {
124
+ await this.layouts().launch(task, 'start');
125
+ return { task: readTask(task.task_id), status: 'in_progress' };
126
+ }
127
+ await this.layouts().initializeRooms(task);
122
128
  }
123
129
  catch (error) {
124
130
  const reason = `layout run could not be created: ${error.message}`;
@@ -362,10 +368,11 @@ export class TaskRoomApplicationService {
362
368
  const blocker = task.outcome?.summary ?? task.blocked?.reason;
363
369
  return {
364
370
  kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room: undefined,
365
- launch: { template: `layout:${task.layout.name}`, anonymous: false, owner_attached: false },
371
+ launch: { template: `layout:${task.layout.name}`, anonymous: false,
372
+ owner_attached: task.layout.name === EMPTY_TASK_LAYOUT && this.layouts().view(task)?.rooms.some(room => room.state === 'active') === true },
366
373
  members: { expected: 0, active: 0, launched: 0 },
367
374
  ...(blocker ? { blocker } : {}),
368
- ...(!ready && !failed ? { next_action: `Correct the blocker, then run ours-fleet task start ${task.task_id}.` } : {}),
375
+ ...(!ready && !failed && blocker ? { next_action: `Correct the blocker, then run ours-fleet task start ${task.task_id}.` } : {}),
369
376
  };
370
377
  }
371
378
  const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
@@ -584,6 +591,8 @@ export class TaskRoomApplicationService {
584
591
  }
585
592
  async launchTaskProvisioning(taskId) {
586
593
  const outcome = this.taskProvisioningOutcome(taskId);
594
+ if (outcome.task.layout)
595
+ return outcome; // layout startup already has its own durable operation/worker
587
596
  if (outcome.kind === 'in_progress' && !outcome.next_action) {
588
597
  await launchFleetWorker(['task', '_provision', taskId], `task-provision-${taskId}`, this.configurationPath, this.deps.binPath?.());
589
598
  }
@@ -882,7 +891,7 @@ export class TaskRoomApplicationService {
882
891
  if (task.layout) {
883
892
  if (input.template || input.members || input.anonymous !== undefined)
884
893
  throw new FleetError('invalid_request', 'a layout task cannot take a room template or member overrides');
885
- return this.ensureLayoutWork(task);
894
+ return this.ensureLayoutWork(task, input.deferLayoutStart);
886
895
  }
887
896
  const recordedRoom = task.room_id ? getRoomRecord(task.room_id) : undefined;
888
897
  const recordedPolicy = storedRoomLaunchPolicy(recordedRoom?.room_policy ?? task.execution_plan?.room_policy);
@@ -62,6 +62,8 @@ export interface RoleStatus {
62
62
  detail: string;
63
63
  };
64
64
  session: {
65
+ /** Supervisor-bound owner mailbox, distinct from the managed agent identity. */
66
+ ownerChannelCid?: string;
65
67
  backend: SessionBackendId | 'unknown';
66
68
  reachability: 'online' | 'offline' | 'unavailable' | 'unknown';
67
69
  readiness: 'starting' | 'idle' | 'running' | 'awaiting_permission' | 'failed' | 'unknown';
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "2.0.3",
3
- "buildId": "495dcfb38c4e",
4
- "commit": "6bc5bab0c0b088acd40d1a79fe5244730dde4708",
2
+ "version": "2.1.0",
3
+ "buildId": "697e8bf9e53c",
4
+ "commit": "2805e0e84c2a3bad05efdbe895fd8fb6e9d2aff4",
5
5
  "dirty": false,
6
- "builtAt": "2026-10-23T08:54:42.810Z",
6
+ "builtAt": "2026-10-27T12:22:22.859Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
package/dist/config.d.ts CHANGED
@@ -101,8 +101,6 @@ export interface OwnerChannelConfig {
101
101
  agent?: string;
102
102
  /** Cancel active work before each owner request instead of queueing it. */
103
103
  interrupt: boolean;
104
- /** Deterministic in-progress notice interval; 0 disables progress notices. */
105
- progress_interval_ms: number;
106
104
  /**
107
105
  * Relay the agent's live ACP commentary to the owner while a turn runs.
108
106
  * This is the RESTART BASELINE only: `/comments on|off` changes the running
package/dist/config.js CHANGED
@@ -882,8 +882,11 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
882
882
  ...(role ?? {}),
883
883
  };
884
884
  const allowed = [
885
- 'identity', 'owners', 'agent', 'interrupt', 'progress_interval_ms', 'comments', 'attachments',
885
+ 'identity', 'owners', 'agent', 'interrupt', 'comments', 'attachments',
886
886
  ];
887
+ if ('progress_interval_ms' in merged)
888
+ throw new ConfigError(`${file}: role '${name}' owner_channel.progress_interval_ms has been removed; delete it. `
889
+ + 'Routine tool activity is shown as one status in the app instead of periodic chat messages');
887
890
  const bad = Object.keys(merged).filter(key => !allowed.includes(key));
888
891
  if (bad.length)
889
892
  throw new ConfigError(`${file}: role '${name}' owner_channel: unknown key(s) ${bad.join(', ')}`);
@@ -905,10 +908,6 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
905
908
  throw new ConfigError(`${file}: role '${name}' owner_channel.owners must contain exact 64-hex CIDs when agent relay is configured`);
906
909
  if (merged.interrupt !== undefined && typeof merged.interrupt !== 'boolean')
907
910
  throw new ConfigError(`${file}: role '${name}' owner_channel.interrupt must be true or false`);
908
- if (merged.progress_interval_ms !== undefined
909
- && (typeof merged.progress_interval_ms !== 'number'
910
- || !Number.isFinite(merged.progress_interval_ms) || merged.progress_interval_ms < 0))
911
- throw new ConfigError(`${file}: role '${name}' owner_channel.progress_interval_ms must be a non-negative number`);
912
911
  if (merged.comments !== undefined && typeof merged.comments !== 'boolean')
913
912
  throw new ConfigError(`${file}: role '${name}' owner_channel.comments must be true or false`);
914
913
  if (defaultInput.attachments !== undefined && !isPlainObject(defaultInput.attachments))
@@ -949,7 +948,6 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
949
948
  owners,
950
949
  ...(agent ? { agent } : {}),
951
950
  interrupt: merged.interrupt ?? false,
952
- progress_interval_ms: merged.progress_interval_ms ?? 30_000,
953
951
  // Default true preserves the established live-commentary behavior; an
954
952
  // upgrade never silently goes quiet on an owner who relied on it.
955
953
  comments: merged.comments ?? true,