@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +204 -55
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.js +18 -6
  5. package/dist/application/fleet-query-service.js +20 -5
  6. package/dist/application/role-command-service.js +9 -0
  7. package/dist/application/role-creation-service.d.ts +6 -25
  8. package/dist/application/role-creation-service.js +34 -72
  9. package/dist/application/role-repository.d.ts +4 -2
  10. package/dist/application/role-repository.js +6 -3
  11. package/dist/application/session-control.d.ts +3 -1
  12. package/dist/application/session-control.js +4 -2
  13. package/dist/application/task-room-service.d.ts +58 -6
  14. package/dist/application/task-room-service.js +435 -79
  15. package/dist/briefing.js +74 -24
  16. package/dist/build-info.json +4 -4
  17. package/dist/canonical-json.d.ts +2 -0
  18. package/dist/canonical-json.js +10 -0
  19. package/dist/cli.js +241 -76
  20. package/dist/config-yaml.d.ts +1 -1
  21. package/dist/config.d.ts +75 -3
  22. package/dist/config.js +274 -34
  23. package/dist/creation.d.ts +1 -1
  24. package/dist/creation.js +3 -2
  25. package/dist/daemon-recovery.d.ts +93 -0
  26. package/dist/daemon-recovery.js +328 -0
  27. package/dist/docs.d.ts +1 -1
  28. package/dist/docs.js +192 -66
  29. package/dist/doctor.d.ts +2 -0
  30. package/dist/doctor.js +54 -4
  31. package/dist/fleet-command-audit.d.ts +165 -0
  32. package/dist/fleet-command-audit.js +555 -0
  33. package/dist/fleet-proxy.d.ts +7 -2
  34. package/dist/fleet-proxy.js +30 -8
  35. package/dist/harness/agent-session.d.ts +5 -0
  36. package/dist/harness/claude-code-session.d.ts +2 -0
  37. package/dist/harness/claude-code-session.js +6 -1
  38. package/dist/harness/claude-code.js +2 -0
  39. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  40. package/dist/harness/codex-app-server-proxy.js +348 -14
  41. package/dist/harness/codex-session.d.ts +11 -2
  42. package/dist/harness/codex-session.js +22 -2
  43. package/dist/harness/codex.d.ts +5 -1
  44. package/dist/harness/codex.js +110 -10
  45. package/dist/harness/types.d.ts +1 -1
  46. package/dist/index.d.ts +5 -4
  47. package/dist/index.js +4 -3
  48. package/dist/init-guidance.d.ts +1 -1
  49. package/dist/init-guidance.js +4 -3
  50. package/dist/init-wizard.d.ts +98 -0
  51. package/dist/init-wizard.js +629 -0
  52. package/dist/lifecycle-summary.d.ts +134 -0
  53. package/dist/lifecycle-summary.js +179 -0
  54. package/dist/loops/config.d.ts +10 -1
  55. package/dist/loops/config.js +38 -5
  56. package/dist/model-env.d.ts +2 -2
  57. package/dist/model-env.js +1 -1
  58. package/dist/model-recovery.d.ts +1 -1
  59. package/dist/ops.js +2 -17
  60. package/dist/owner-channel/channel.d.ts +58 -0
  61. package/dist/owner-channel/channel.js +321 -42
  62. package/dist/owner-channel/commands.d.ts +6 -1
  63. package/dist/owner-channel/commands.js +34 -19
  64. package/dist/owner-channel/state.d.ts +2 -0
  65. package/dist/owner-channel/state.js +8 -2
  66. package/dist/permissions.js +1 -1
  67. package/dist/preset-bootstrap.d.ts +10 -0
  68. package/dist/preset-bootstrap.js +85 -0
  69. package/dist/preset-migration.d.ts +42 -0
  70. package/dist/preset-migration.js +414 -0
  71. package/dist/reports/artifact.d.ts +10 -0
  72. package/dist/reports/artifact.js +32 -0
  73. package/dist/reports/index.d.ts +5 -0
  74. package/dist/reports/index.js +5 -0
  75. package/dist/reports/render.d.ts +6 -0
  76. package/dist/reports/render.js +240 -0
  77. package/dist/reports/service.d.ts +42 -0
  78. package/dist/reports/service.js +57 -0
  79. package/dist/reports/tasks.d.ts +52 -0
  80. package/dist/reports/tasks.js +177 -0
  81. package/dist/reports/types.d.ts +315 -0
  82. package/dist/reports/types.js +6 -0
  83. package/dist/resolved-plan.js +13 -1
  84. package/dist/rooms-tasks/cli.d.ts +2 -0
  85. package/dist/rooms-tasks/cli.js +514 -75
  86. package/dist/rooms-tasks/close.d.ts +9 -1
  87. package/dist/rooms-tasks/close.js +20 -4
  88. package/dist/rooms-tasks/config.js +16 -14
  89. package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
  90. package/dist/rooms-tasks/cowork-adapter.js +6 -0
  91. package/dist/rooms-tasks/deletion.d.ts +51 -0
  92. package/dist/rooms-tasks/deletion.js +217 -0
  93. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  94. package/dist/rooms-tasks/external-worker.js +14 -9
  95. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  96. package/dist/rooms-tasks/launch-snapshot.js +181 -0
  97. package/dist/rooms-tasks/member-overrides.d.ts +30 -0
  98. package/dist/rooms-tasks/member-overrides.js +204 -0
  99. package/dist/rooms-tasks/provision.js +184 -49
  100. package/dist/rooms-tasks/room-state.d.ts +1 -0
  101. package/dist/rooms-tasks/room-state.js +12 -1
  102. package/dist/rooms-tasks/task-state.d.ts +115 -3
  103. package/dist/rooms-tasks/task-state.js +361 -11
  104. package/dist/rooms-tasks/templates.d.ts +4 -2
  105. package/dist/rooms-tasks/templates.js +42 -78
  106. package/dist/rooms-tasks/terminal.d.ts +3 -0
  107. package/dist/rooms-tasks/terminal.js +7 -3
  108. package/dist/rooms-tasks/types.d.ts +96 -17
  109. package/dist/rooms-tasks/types.js +14 -4
  110. package/dist/runner.d.ts +12 -2
  111. package/dist/runner.js +124 -43
  112. package/dist/session/acp.d.ts +9 -1
  113. package/dist/session/acp.js +35 -9
  114. package/dist/session/arbiter.d.ts +2 -1
  115. package/dist/session/arbiter.js +2 -0
  116. package/dist/session/codex-app-server-transport.d.ts +53 -0
  117. package/dist/session/codex-app-server-transport.js +184 -0
  118. package/dist/session/codex-app-server.d.ts +122 -0
  119. package/dist/session/codex-app-server.js +1031 -0
  120. package/dist/session/control.d.ts +14 -1
  121. package/dist/session/control.js +38 -0
  122. package/dist/session/conversation-types.d.ts +4 -4
  123. package/dist/session/types.d.ts +24 -6
  124. package/dist/session/types.js +28 -0
  125. package/dist/spawn.d.ts +15 -31
  126. package/dist/spawn.js +117 -237
  127. package/dist/temp-lifecycle.d.ts +1 -1
  128. package/dist/watchdog/config.d.ts +10 -7
  129. package/dist/watchdog/config.js +29 -19
  130. package/dist/watchdog/run.js +4 -13
  131. package/dist/web/fleet-config-service.d.ts +1 -0
  132. package/dist/web/fleet-config-service.js +27 -8
  133. package/dist/web/runtime.js +11 -6
  134. package/dist/web/server.js +28 -1
  135. package/dist/web/topology-promote.js +8 -1
  136. package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
  137. package/dist/web-app/index.html +1 -1
  138. package/package.json +3 -2
  139. package/presets/brain-catalog.json +16 -0
  140. package/presets/fleet/agent_templates/Critic.yaml +4 -0
  141. package/presets/fleet/agent_templates/Developer.yaml +4 -0
  142. package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
  143. package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
  144. package/presets/fleet/brains/claude-default.yaml +5 -0
  145. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  146. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  147. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  148. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  149. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  150. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  151. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  152. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  153. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  154. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  155. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  156. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  157. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  158. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  159. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  188. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  189. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  190. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  191. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  192. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  193. package/presets/fleet/roles/Coordinator.yaml +30 -0
  194. package/presets/fleet/roles/Critic.yaml +19 -0
  195. package/presets/fleet/roles/Developer.yaml +19 -0
  196. package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
  197. package/presets/fleet/room_templates/pair.yaml +10 -0
  198. package/presets/fleet/room_templates/single.yaml +7 -0
  199. package/presets/fleet/room_templates/team.yaml +12 -0
  200. package/presets/fleet.yaml +7 -0
  201. package/presets/manifest.json +6 -0
  202. package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
@@ -1,8 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
- import { mkdir, readFile, readdir, rm } from 'node:fs/promises';
3
+ import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { canonicalCid } from '../config.js';
6
+ import { DAEMON_RECOVERY_DEADLINE_MS } from '../daemon-recovery.js';
6
7
  import { replaceFileAtomically } from '../atomic-file.js';
7
8
  import { TaskRoomApplicationService } from '../application/task-room-service.js';
8
9
  import { RoleLifecycleService } from '../application/role-command-service.js';
@@ -10,10 +11,11 @@ import { RoleRepository } from '../application/role-repository.js';
10
11
  import { FleetQueryService } from '../application/fleet-query-service.js';
11
12
  import { interruptSession, queueSessionPrompt } from '../application/session-mutations.js';
12
13
  import { pickBackend } from '../supervisor/index.js';
13
- import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, } from '../session/types.js';
14
+ import { ACP_CANCEL_DEADLINE_EXCEEDED, CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED, SessionControlError, } from '../session/types.js';
14
15
  import { VERSION } from '../version.js';
15
16
  import { renderMarkdownFailure, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
16
17
  import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
18
+ import { FleetCommandAuditStore, lifecycleEventDigestBasis, renderFleetLifecycleEvent, } from '../fleet-command-audit.js';
17
19
  import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
18
20
  import { ownerNotices, } from './notices.js';
19
21
  import { DuplicateSendError, OwnerAuthorizationState, OwnerChannelState, OwnerConversationState, } from './state.js';
@@ -21,6 +23,24 @@ import { OwnerTaskState, ownerTaskAuditId, ownerTaskDigest, } from './tasks.js';
21
23
  import { AttachmentRecoveryState, admitAttachments, cleanupAttachmentRoot, parseIncomingAttachments, parseRetrievedAttachments, prepareAttachmentDirectory, recoveredAttachment, removeRequestDirectory, safeField, validateAttachmentSelection, validateAttachmentRelaySelection, writeRecoveredAttachment, } from './attachments.js';
22
24
  import { acquireOwnerBinderLease, OWNER_BIND_HANDOFF_TIMEOUT_MS, } from './binder.js';
23
25
  import { MessageRecoveryState } from './message-recovery.js';
26
+ export const OWNER_RECOVERY_QUEUE_CAPACITY = 32;
27
+ export const OWNER_RECOVERY_DEGRADED = 'OWNER_RECOVERY_DEGRADED';
28
+ export class OwnerRecoveryDegradedError extends Error {
29
+ code = OWNER_RECOVERY_DEGRADED;
30
+ constructor() {
31
+ super('owner channel recovery is degraded');
32
+ this.name = 'OwnerRecoveryDegradedError';
33
+ }
34
+ }
35
+ export class OwnerRecoveryTimeoutError extends Error {
36
+ stage;
37
+ code = OWNER_RECOVERY_DEGRADED;
38
+ constructor(stage) {
39
+ super(`owner channel recovery timed out at ${stage}`);
40
+ this.stage = stage;
41
+ this.name = 'OwnerRecoveryTimeoutError';
42
+ }
43
+ }
24
44
  const OWNER_UPDATE_MIN_INTERVAL_MS = 5_000;
25
45
  const OWNER_UPDATE_MAX_COUNT = 20;
26
46
  const OWNER_UPDATE_MAX_CHARS = 280;
@@ -76,11 +96,22 @@ export class OwnerChannel {
76
96
  completionTasks = new Set();
77
97
  activeRequests = new Map();
78
98
  managementTail = Promise.resolve();
99
+ recoveryEpoch;
100
+ recoveryTask;
101
+ recoveryToken = 0;
102
+ recoveryQueued = 0;
103
+ watchGeneration = 0;
104
+ /** A timed-out client operation that must settle before replacement is safe. */
105
+ recoveryQuiescence;
106
+ releaseSafe = true;
79
107
  ready = false;
108
+ startedOnce = false;
109
+ shutdownDegraded = false;
80
110
  binder;
81
111
  binderOwnedInternally = false;
82
112
  fleetOps;
83
113
  prepareRestart;
114
+ commandAudits;
84
115
  constructor(options) {
85
116
  this.options = options;
86
117
  this.client = options.client ?? new OursSdkClient(options.env, line => options.log(`[${options.role}] owner channel ${line}`));
@@ -96,6 +127,7 @@ export class OwnerChannel {
96
127
  await lifecycle.prepareRestart({ roleIds: [role], mode });
97
128
  });
98
129
  this.state = new OwnerChannelState(join(options.stateDir, '.owner-channel-state.json'));
130
+ this.commandAudits = new FleetCommandAuditStore(join(options.stateDir, '.owner-channel-command-audit.json'));
99
131
  this.authorizations = new OwnerAuthorizationState(join(options.stateDir, '.owner-channel-owners.json'), options.config.owners);
100
132
  this.conversations = new OwnerConversationState(join(options.stateDir, '.owner-channel-conversations.json'));
101
133
  this.tasks = new OwnerTaskState(join(options.stateDir, '.owner-channel-tasks.json'));
@@ -124,6 +156,7 @@ export class OwnerChannel {
124
156
  }
125
157
  async start() {
126
158
  this.stopping = false;
159
+ this.shutdownDegraded = false;
127
160
  this.binderOwnedInternally = !this.options.binderLease;
128
161
  this.binder = this.options.binderLease ?? await acquireOwnerBinderLease(this.options.stateDir, this.options.role, this.options.config.identity, this.options.binderDeps);
129
162
  try {
@@ -164,11 +197,12 @@ export class OwnerChannel {
164
197
  this.attachmentRecovery.cleanup(Date.now(), this.attachmentConfig.retention_ms);
165
198
  void cleanupAttachmentRoot(this.attachmentRoot, Date.now(), this.attachmentConfig.retention_ms).catch(error => this.logError('attachment crash cleanup failed', error));
166
199
  }
200
+ this.startedOnce = true;
167
201
  this.ready = true;
168
202
  // Do not make role startup wait for an old owner request to finish a turn.
169
203
  // watchLoop itself drains before every establishment, including this first
170
204
  // one, so there is no drain-to-tip race.
171
- this.watchTask = this.watchLoop();
205
+ this.watchTask = this.watchLoop(++this.watchGeneration);
172
206
  }
173
207
  drain() {
174
208
  this.drainRequested = true;
@@ -183,46 +217,240 @@ export class OwnerChannel {
183
217
  return this.drainTask;
184
218
  }
185
219
  async close() {
220
+ if (this.recoveryTask)
221
+ this.shutdownDegraded = true;
186
222
  this.stopping = true;
187
223
  this.ready = false;
188
- this.watchAbort?.abort();
189
- await this.watchTask?.catch(error => this.logError('watch shutdown failed', error));
224
+ this.recoveryToken++;
225
+ this.watchGeneration++;
226
+ const staleWatch = this.watchTask;
190
227
  this.watchTask = undefined;
191
- await this.managementTail;
228
+ this.watchAbort?.abort();
229
+ const now = this.options.recoveryDeps?.now ?? (() => Date.now());
230
+ const deadlineAt = now()
231
+ + (this.options.recoveryDeps?.deadlineMs ?? DAEMON_RECOVERY_DEADLINE_MS);
232
+ let disposed = false;
192
233
  try {
193
- await this.client.close();
234
+ await this.recoveryStage('shutdown_quiescence', Promise.all([
235
+ staleWatch?.catch(error => this.logError('watch shutdown failed', error)),
236
+ this.drainTask?.catch(error => this.logError('drain shutdown failed', error)),
237
+ this.managementTail.then(() => undefined),
238
+ this.recoveryQuiescence,
239
+ ]), deadlineAt);
240
+ await this.recoveryStage('shutdown_close', this.client.close(), deadlineAt);
241
+ disposed = true;
242
+ this.writeShutdownState('closed', 'OWNER_SHUTDOWN_CLOSED', now());
243
+ }
244
+ catch (error) {
245
+ this.shutdownDegraded = true;
246
+ this.releaseSafe = false;
247
+ const stage = error instanceof OwnerRecoveryTimeoutError ? error.stage : 'shutdown_unknown';
248
+ this.writeShutdownState('degraded', `OWNER_${stage.toUpperCase()}_TIMEOUT`, now());
249
+ this.options.log(`[${this.options.role}] owner shutdown degraded stage=${stage}`);
194
250
  }
195
251
  finally {
196
- if (this.binderOwnedInternally)
252
+ if (disposed && this.binderOwnedInternally)
197
253
  this.binder?.release();
198
- this.binder = undefined;
254
+ if (disposed)
255
+ this.binder = undefined;
199
256
  }
200
257
  }
258
+ binderReleaseSafe() { return this.releaseSafe; }
201
259
  manage(request) {
202
- const run = this.managementTail.then(() => this.manageNow(request));
203
- this.managementTail = run.then(() => undefined, () => undefined);
204
- return run;
260
+ return this.queueManagement(() => this.manageNow(request));
205
261
  }
206
262
  notifyFleetSpawn(event) {
207
- const run = this.managementTail.then(async () => {
263
+ return this.queueManagement(async () => {
208
264
  if (!this.ready || this.stopping)
209
265
  throw new Error('owner-channel MCP client is unavailable');
210
- const model = event.model ? `, model ${event.model}` : '';
211
- const monitorPolicy = event.monitor.interrupt === true
212
- ? ' with interruption'
213
- : event.monitor.interrupt === 'after_tool' ? ' with after-tool steering' : '';
214
- const monitor = `${event.monitor.mode} monitor${monitorPolicy}`;
215
266
  const permission = event.permissionMode
216
- ? `; permission ${event.permissionMode.fleetMode}, native ${event.permissionMode.nativeMode}`
217
- : '';
218
- const inherited = event.inherited.length
219
- ? ` Supervisor inherited omitted defaults: ${event.inherited.join(', ')}.` : '';
220
- await this.sendProactiveMessage(`🧑‍💻 ${event.caller} spawned ${event.lifetime} agent ${event.role} `
221
- + `(${event.harness}/${event.session}${model}; ${monitor}${permission}).${inherited}`, `fleet-spawn\0${event.creationActionId}`, 0);
267
+ ? `${event.permissionMode.fleetMode}/${event.permissionMode.nativeMode}` : undefined;
268
+ await this.sendProactiveMessage(renderFleetLifecycleEvent({ kind: 'agent_started', eventId: event.creationActionId,
269
+ id: event.role, name: event.role,
270
+ lifetime: event.lifetime, brain: event.brainSummary, role: event.roleSummary,
271
+ harness: event.harness, session: event.session, model: event.model,
272
+ permissions: permission, parent: event.caller, actionId: event.creationActionId,
273
+ inherited: event.inherited,
274
+ ...(event.configuration ? { configuration: event.configuration } : {}) }), `fleet-spawn\0${event.creationActionId}`, 0);
275
+ });
276
+ }
277
+ beginFleetCommandAudit(requestId, argv) {
278
+ const run = this.managementTail.then(async () => {
279
+ let attempt = this.commandAudits.begin(requestId, this.options.role, argv);
280
+ if (attempt.invocation === 'delivered')
281
+ return attempt;
282
+ if (attempt.invocation === 'uncertain')
283
+ return attempt;
284
+ this.options.log(`[${this.options.role}] fleet proxy command ${attempt.correlationId} `
285
+ + `route=${attempt.classification.route} decision=${attempt.classification.decision}`);
286
+ attempt = this.commandAudits.invocation(attempt.correlationId, this.options.role, 'delivered');
287
+ return attempt;
222
288
  });
223
289
  this.managementTail = run.then(() => undefined, () => undefined);
224
290
  return run;
225
291
  }
292
+ finishFleetCommandAudit(input) {
293
+ const run = this.managementTail.then(async () => {
294
+ let attempt = this.commandAudits.finish(input.correlationId, this.options.role, {
295
+ class: input.class, effect: input.effect,
296
+ ...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
297
+ ...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
298
+ ...(input.presentations ? { presentations: input.presentations } : {}),
299
+ });
300
+ if (attempt.outcome?.delivery === 'delivered')
301
+ return attempt;
302
+ if (attempt.outcome?.delivery === 'uncertain')
303
+ return attempt;
304
+ let uncertain = false;
305
+ try {
306
+ for (const event of attempt.outcome?.presentations ?? []) {
307
+ try {
308
+ await this.sendProactiveMessage(renderFleetLifecycleEvent(event), `fleet-lifecycle\0${lifecycleEventDigestBasis(event)}`, 0);
309
+ }
310
+ catch (error) {
311
+ if (error instanceof DuplicateSendError) {
312
+ if (error.status !== 'delivered')
313
+ uncertain = true;
314
+ this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
315
+ continue;
316
+ }
317
+ throw error;
318
+ }
319
+ }
320
+ }
321
+ catch (error) {
322
+ uncertain = true;
323
+ this.logError(`fleet lifecycle delivery uncertain correlation=${attempt.correlationId}`, error);
324
+ }
325
+ attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, uncertain ? 'uncertain' : 'delivered');
326
+ return attempt;
327
+ });
328
+ this.managementTail = run.then(() => undefined, () => undefined);
329
+ return run;
330
+ }
331
+ recover(epoch) {
332
+ if (this.stopping)
333
+ return Promise.reject(new Error('owner channel is stopping'));
334
+ if (this.recoveryEpoch === epoch && this.recoveryTask)
335
+ return this.recoveryTask;
336
+ this.recoveryEpoch = epoch;
337
+ this.ready = false;
338
+ const token = ++this.recoveryToken;
339
+ const watchGeneration = ++this.watchGeneration;
340
+ const staleWatch = this.watchTask;
341
+ this.watchTask = undefined;
342
+ this.watchAbort?.abort();
343
+ const superseded = () => this.stopping || token !== this.recoveryToken;
344
+ const now = this.options.recoveryDeps?.now ?? (() => Date.now());
345
+ const deadlineAt = now()
346
+ + (this.options.recoveryDeps?.deadlineMs ?? DAEMON_RECOVERY_DEADLINE_MS);
347
+ const run = this.managementTail.then(async () => {
348
+ if (superseded())
349
+ throw new Error('owner recovery epoch superseded');
350
+ if (this.recoveryQuiescence) {
351
+ const debt = this.recoveryQuiescence;
352
+ await this.recoveryStage('prior_quiescence', debt, deadlineAt);
353
+ if (this.recoveryQuiescence === debt)
354
+ this.recoveryQuiescence = undefined;
355
+ }
356
+ // Quiesce every stale-client reader before replacing its transport. A
357
+ // watch can be inside the shared drain task when its abort lands.
358
+ await this.recoveryStage('stale_quiescence', Promise.all([
359
+ staleWatch?.catch(error => this.logError('stale watch shutdown failed', error)),
360
+ this.drainTask?.catch(error => this.logError('stale drain shutdown failed', error)),
361
+ ]), deadlineAt);
362
+ if (superseded())
363
+ throw new Error('owner recovery epoch superseded');
364
+ await this.recoveryStage('close', this.client.close(), deadlineAt);
365
+ if (superseded())
366
+ throw new Error('owner recovery epoch superseded');
367
+ try {
368
+ await this.recoveryStage('start', this.client.start(), deadlineAt);
369
+ if (superseded())
370
+ throw new Error('owner recovery epoch superseded');
371
+ await this.recoveryStage('bind', this.client.bindIdentity(this.options.config.identity), deadlineAt);
372
+ if (superseded())
373
+ throw new Error('owner recovery epoch superseded');
374
+ // Journal-aware drain is the only operation allowed to mark owner mail
375
+ // read. The final listing is a typed, read-only channel probe.
376
+ await this.recoveryStage('drain', this.drain(), deadlineAt);
377
+ await this.recoveryStage('list', this.client.listIncomingMessages(), deadlineAt);
378
+ if (superseded())
379
+ throw new Error('owner recovery epoch superseded');
380
+ this.ready = true;
381
+ this.watchTask = this.watchLoop(watchGeneration);
382
+ }
383
+ catch (error) {
384
+ this.ready = false;
385
+ if (token === this.recoveryToken)
386
+ this.recoveryToken++;
387
+ // A timed-out operation may still be using this client. Never overlap
388
+ // its disposal; the recorded quiescence debt gates the next retry.
389
+ if (!this.recoveryQuiescence) {
390
+ try {
391
+ await this.recoveryStage('close_after_failure', this.client.close(), deadlineAt);
392
+ }
393
+ catch (closeError) {
394
+ this.logError('recovery client close failed', closeError);
395
+ }
396
+ }
397
+ throw error;
398
+ }
399
+ });
400
+ this.managementTail = run.then(() => undefined, () => undefined);
401
+ const task = run.finally(() => {
402
+ if (this.recoveryTask === task)
403
+ this.recoveryTask = undefined;
404
+ });
405
+ this.recoveryTask = task;
406
+ return task;
407
+ }
408
+ recoveryStage(stage, operation, deadlineAt) {
409
+ const now = this.options.recoveryDeps?.now ?? (() => Date.now());
410
+ const remaining = Math.max(0, deadlineAt - now());
411
+ const setTimer = this.options.recoveryDeps?.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
412
+ const clearTimer = this.options.recoveryDeps?.clearTimer ?? (timer => clearTimeout(timer));
413
+ let timer;
414
+ const timeout = new Promise((_resolve, reject) => {
415
+ timer = setTimer(() => reject(new OwnerRecoveryTimeoutError(stage)), remaining);
416
+ });
417
+ return Promise.race([operation, timeout]).catch(error => {
418
+ if (error instanceof OwnerRecoveryTimeoutError) {
419
+ const debt = operation.then(() => undefined, () => undefined);
420
+ this.recoveryQuiescence = debt;
421
+ }
422
+ throw error;
423
+ }).finally(() => clearTimer(timer));
424
+ }
425
+ writeShutdownState(state, reason, at) {
426
+ try {
427
+ replaceFileAtomically(join(this.options.stateDir, '.owner-channel-shutdown.json'), JSON.stringify({ version: 1, state, reason, at: new Date(at).toISOString() }, null, 2) + '\n');
428
+ }
429
+ catch (error) {
430
+ this.logError('shutdown status write failed', error);
431
+ }
432
+ }
433
+ queueManagement(operation) {
434
+ const deferred = this.recoveryTask !== undefined && !this.ready;
435
+ if (!this.ready && !deferred)
436
+ return Promise.reject(!this.startedOnce || (this.stopping && !this.shutdownDegraded)
437
+ ? new Error('owner-channel MCP client is unavailable')
438
+ : new OwnerRecoveryDegradedError());
439
+ if (deferred && this.recoveryQueued >= OWNER_RECOVERY_QUEUE_CAPACITY)
440
+ return Promise.reject(new Error('owner recovery queue is full'));
441
+ if (deferred)
442
+ this.recoveryQueued++;
443
+ const run = this.managementTail.then(() => {
444
+ if (!this.ready || this.stopping)
445
+ throw new OwnerRecoveryDegradedError();
446
+ return operation();
447
+ });
448
+ this.managementTail = run.then(() => undefined, () => undefined);
449
+ return run.finally(() => {
450
+ if (deferred)
451
+ this.recoveryQueued = Math.max(0, this.recoveryQueued - 1);
452
+ });
453
+ }
226
454
  async manageNow(request) {
227
455
  if (!this.ready || this.stopping)
228
456
  throw new Error('owner-channel MCP client is unavailable');
@@ -943,12 +1171,13 @@ export class OwnerChannel {
943
1171
  catch (error) {
944
1172
  await rm(outbox, { recursive: true, force: true });
945
1173
  if (error instanceof SessionControlError
946
- && error.reasonCode === ACP_CANCEL_DEADLINE_EXCEEDED) {
1174
+ && (error.reasonCode === ACP_CANCEL_DEADLINE_EXCEEDED
1175
+ || error.reasonCode === CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED)) {
947
1176
  // drainAll journaled this authenticated message before delivery. The
948
1177
  // adapter generation is terminating, so leave the wire unhandled and
949
1178
  // body-free: the resumed owner channel recovers it exactly once.
950
1179
  this.options.log(`[${this.options.role}] owner request ${requestId.slice(0, 12)} `
951
- + `held for adapter resume reason=${ACP_CANCEL_DEADLINE_EXCEEDED}`);
1180
+ + `held for adapter resume reason=${error.reasonCode}`);
952
1181
  return false;
953
1182
  }
954
1183
  this.logError('request delivery failed', error);
@@ -989,6 +1218,7 @@ export class OwnerChannel {
989
1218
  */
990
1219
  async handleCommand(sender, text, wireId) {
991
1220
  const ctx = {
1221
+ authenticatedCid: sender.id,
992
1222
  role: this.options.role,
993
1223
  harness: this.options.harness,
994
1224
  version: VERSION,
@@ -1041,9 +1271,7 @@ export class OwnerChannel {
1041
1271
  reviewTask: taskId => new TaskRoomApplicationService(this.options.configPath).reviewTask({
1042
1272
  actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1043
1273
  }),
1044
- deleteTask: taskId => new TaskRoomApplicationService(this.options.configPath).deleteTask({
1045
- actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1046
- }),
1274
+ deleteTask: taskId => this.deleteTaskFromOwner(sender, taskId, wireId),
1047
1275
  listRoomQueries: filter => new TaskRoomApplicationService(this.options.configPath).listRooms(filter),
1048
1276
  getRoomQuery: id => new TaskRoomApplicationService(this.options.configPath).getRoomDetail(id),
1049
1277
  listTemplateQueries: () => new TaskRoomApplicationService(this.options.configPath).listTemplates(),
@@ -1054,6 +1282,18 @@ export class OwnerChannel {
1054
1282
  recentEvents: limit => this.options.session.eventsSince(0).slice(-limit),
1055
1283
  readWorklogTail: maxChars => this.readWorklogTail(maxChars),
1056
1284
  reply: async (replyText) => { await this.send(sender.id, replyText, wireId); },
1285
+ replyHtml: async (filename, html) => {
1286
+ const directory = join(this.options.stateDir, '.owner-channel-report-outbox', this.requestId(wireId));
1287
+ const path = join(directory, filename);
1288
+ await mkdir(directory, { recursive: true, mode: 0o700 });
1289
+ try {
1290
+ await writeFile(path, html, { flag: 'wx', mode: 0o600 });
1291
+ await this.client.sendFile({ contact: sender.id, path, filename, replyToWireId: wireId });
1292
+ }
1293
+ finally {
1294
+ await rm(directory, { recursive: true, force: true });
1295
+ }
1296
+ },
1057
1297
  };
1058
1298
  try {
1059
1299
  await dispatchOwnerCommand(text, ctx);
@@ -1161,10 +1401,13 @@ export class OwnerChannel {
1161
1401
  const app = new TaskRoomApplicationService(this.options.configPath);
1162
1402
  const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
1163
1403
  const begin = await app.beginTaskRecovery({ actor, taskId });
1164
- if (begin.kind === 'terminal_worker_required') {
1404
+ if (begin.kind !== 'final') {
1405
+ const deleting = begin.kind === 'deletion_worker_required';
1165
1406
  await this.send(sender.id, renderMarkdownFailure({
1166
1407
  kind: 'pending', subject: `/task recover ${taskId}`,
1167
- detail: 'The recovery request was accepted and is still being settled.',
1408
+ detail: deleting
1409
+ ? 'The task is pending deletion; recovery continues its cleanup.'
1410
+ : 'The recovery request was accepted and is still being settled.',
1168
1411
  action: `Run /task recover ${taskId} if it remains pending.`,
1169
1412
  }), wireId);
1170
1413
  this.state.remember(wireId);
@@ -1172,9 +1415,13 @@ export class OwnerChannel {
1172
1415
  await this.fleetOps.recoverTask(taskId);
1173
1416
  }
1174
1417
  catch (error) {
1175
- await app.recordSettlementError({ actor, taskId,
1176
- error: error instanceof Error ? error.message : String(error),
1177
- recoveryHint: `External settle worker failed to start. Retry /task recover ${taskId}.` });
1418
+ const failure = {
1419
+ actor, taskId, error: error instanceof Error ? error.message : String(error),
1420
+ recoveryHint: deleting
1421
+ ? `External delete worker failed to start. Retry /task delete ${taskId} ${taskId}.`
1422
+ : `External settle worker failed to start. Retry /task recover ${taskId}.`,
1423
+ };
1424
+ await (deleting ? app.recordDeletionError(failure) : app.recordSettlementError(failure));
1178
1425
  throw error;
1179
1426
  }
1180
1427
  return;
@@ -1199,6 +1446,36 @@ export class OwnerChannel {
1199
1446
  }), wireId);
1200
1447
  this.state.remember(wireId);
1201
1448
  }
1449
+ /** Accept a permanent any-state deletion, acknowledge it, then hand cleanup to a durable worker. */
1450
+ async deleteTaskFromOwner(sender, taskId, wireId) {
1451
+ const app = new TaskRoomApplicationService(this.options.configPath);
1452
+ const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
1453
+ const accepted = await app.requestTaskDeletion({ actor, taskId });
1454
+ if (accepted.status === 'already_absent') {
1455
+ await this.send(sender.id, renderMarkdownResult({
1456
+ icon: '🗑️', title: 'Task already absent',
1457
+ fields: [{ label: 'ID', value: taskId, kind: 'code' }],
1458
+ }), wireId);
1459
+ this.state.remember(wireId);
1460
+ return;
1461
+ }
1462
+ await this.send(sender.id, renderMarkdownFailure({
1463
+ kind: 'pending', subject: `/task delete ${taskId} ${taskId}`,
1464
+ detail: 'The deletion was accepted; cleanup is settling in the background.',
1465
+ action: `Run /task recover ${taskId} or repeat /task delete ${taskId} ${taskId} if it remains pending.`,
1466
+ }), wireId);
1467
+ this.state.remember(wireId);
1468
+ try {
1469
+ await this.fleetOps.settleTaskDeletion(taskId);
1470
+ }
1471
+ catch (error) {
1472
+ await app.recordDeletionError({
1473
+ actor, taskId, error: error instanceof Error ? error.message : String(error),
1474
+ recoveryHint: `External delete worker failed to start. Retry /task delete ${taskId} ${taskId}.`,
1475
+ });
1476
+ throw error;
1477
+ }
1478
+ }
1202
1479
  async terminalTaskFromOwner(sender, taskId, kind, outcome, wireId) {
1203
1480
  const app = new TaskRoomApplicationService(this.options.configPath);
1204
1481
  const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
@@ -1686,7 +1963,8 @@ export class OwnerChannel {
1686
1963
  startProgress(event);
1687
1964
  // Automatic commentary is an ACP phase extension. Other backends and
1688
1965
  // older adapters retain their established final-only behavior.
1689
- if (this.options.session.backend === 'acp')
1966
+ if (this.options.session.capabilities?.messagePhases
1967
+ ?? this.options.session.backend === 'acp')
1690
1968
  acceptCommentary(event);
1691
1969
  })
1692
1970
  : () => undefined;
@@ -1728,7 +2006,8 @@ export class OwnerChannel {
1728
2006
  enabled: this.commentsEnabled,
1729
2007
  baseline: this.commentsBaseline,
1730
2008
  // Only the ACP backend emits the phase marker commentary relaying needs.
1731
- supported: this.options.session.backend === 'acp',
2009
+ supported: this.options.session.capabilities?.messagePhases
2010
+ ?? this.options.session.backend === 'acp',
1732
2011
  };
1733
2012
  }
1734
2013
  /** Model-authored commentary only; raw protocol/tool data never reaches here. */
@@ -1876,7 +2155,7 @@ export class OwnerChannel {
1876
2155
  }
1877
2156
  return undefined;
1878
2157
  }
1879
- async watchLoop() {
2158
+ async watchLoop(generation) {
1880
2159
  const sleep = this.options.binderDeps?.sleep
1881
2160
  ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
1882
2161
  const restored = this.readWatchState();
@@ -1885,7 +2164,7 @@ export class OwnerChannel {
1885
2164
  state = this.writeWatchState(state, 'OWNER_WATCH_STATE_RECOVERED');
1886
2165
  let delayMs = 1_000;
1887
2166
  let attempts = 0;
1888
- while (!this.stopping) {
2167
+ while (!this.stopping && generation === this.watchGeneration) {
1889
2168
  const ctrl = new AbortController();
1890
2169
  this.watchAbort = ctrl;
1891
2170
  try {
@@ -1895,12 +2174,12 @@ export class OwnerChannel {
1895
2174
  // fall into a gap. Persistent history plus fleet's body-free claim
1896
2175
  // journal is authoritative; durable wire-ID dedupe makes hints harmless.
1897
2176
  await this.drain();
1898
- if (this.stopping)
2177
+ if (this.stopping || generation !== this.watchGeneration)
1899
2178
  return;
1900
2179
  state = this.writeWatchState(state, 'OWNER_WATCH_CONNECTING', { reconnected: attempts > 0 });
1901
2180
  attempts++;
1902
2181
  for await (const _event of this.client.watchNotifications(this.options.config.identity, { since: 0, signal: ctrl.signal })) {
1903
- if (this.stopping)
2182
+ if (this.stopping || generation !== this.watchGeneration)
1904
2183
  return;
1905
2184
  state = this.writeWatchState(state, 'OWNER_WATCH_CONNECTED', { resetFailures: true });
1906
2185
  delayMs = 1_000;
@@ -1909,11 +2188,11 @@ export class OwnerChannel {
1909
2188
  // recorded before a managed request is dispatched.
1910
2189
  await this.drain();
1911
2190
  }
1912
- if (!this.stopping)
2191
+ if (!this.stopping && generation === this.watchGeneration)
1913
2192
  throw new Error('ours SDK notification stream ended');
1914
2193
  }
1915
2194
  catch (error) {
1916
- if (this.stopping)
2195
+ if (this.stopping || generation !== this.watchGeneration)
1917
2196
  return;
1918
2197
  // SDK 2 deliberately hides transport status behind its typed stream.
1919
2198
  // Do not parse error prose to rediscover it: every failure follows the
@@ -17,6 +17,8 @@ export interface OwnerFleetOps {
17
17
  closeRoom(roomId: string): Promise<void>;
18
18
  /** Resume a task terminal intent outside the caller role's supervisor lifecycle. */
19
19
  settleTask(taskId: string): Promise<void>;
20
+ /** Settle an accepted task deletion outside the caller role's supervisor lifecycle. */
21
+ settleTaskDeletion(taskId: string): Promise<void>;
20
22
  recoverTask(taskId: string): Promise<void>;
21
23
  }
22
24
  /**
@@ -25,6 +27,7 @@ export interface OwnerFleetOps {
25
27
  * cannot name another agent or another recipient.
26
28
  */
27
29
  export interface OwnerCommandContext {
30
+ authenticatedCid?: string;
28
31
  role: string;
29
32
  /** Harness id of the role (e.g. 'claude-code', 'codex'); gates forwarding. */
30
33
  harness: string;
@@ -83,7 +86,8 @@ export interface OwnerCommandContext {
83
86
  blockTask(taskId: string, reason: string): TaskRecord;
84
87
  unblockTask(taskId: string): TaskRecord;
85
88
  reviewTask(taskId: string): TaskRecord;
86
- deleteTask(taskId: string): boolean;
89
+ /** Accept a permanent any-state deletion, acknowledge it, then launch the external delete worker. */
90
+ deleteTask(taskId: string): Promise<void>;
87
91
  listRoomQueries(filter?: {
88
92
  state?: 'active' | 'provisioning';
89
93
  }): ReturnType<TaskRoomApplicationService['listRooms']>;
@@ -94,6 +98,7 @@ export interface OwnerCommandContext {
94
98
  recentEvents(limit: number): SessionEvent[];
95
99
  readWorklogTail(maxChars: number): Promise<string | undefined>;
96
100
  reply(text: string): Promise<void>;
101
+ replyHtml(filename: string, html: string): Promise<void>;
97
102
  }
98
103
  export interface OwnerCommand {
99
104
  /** Primary name without the leading slash. */