@ours.network/fleet 1.1.0-nightly.8 → 1.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.
Files changed (202) hide show
  1. package/README.md +231 -46
  2. package/dist/application/capabilities.js +18 -6
  3. package/dist/application/fleet-query-service.js +7 -4
  4. package/dist/application/role-command-service.js +9 -0
  5. package/dist/application/role-creation-service.d.ts +4 -4
  6. package/dist/application/role-creation-service.js +9 -2
  7. package/dist/application/role-repository.d.ts +4 -2
  8. package/dist/application/role-repository.js +6 -3
  9. package/dist/application/session-control.d.ts +3 -1
  10. package/dist/application/session-control.js +4 -2
  11. package/dist/application/task-room-service.d.ts +92 -39
  12. package/dist/application/task-room-service.js +579 -173
  13. package/dist/briefing.js +85 -28
  14. package/dist/build-info.json +5 -5
  15. package/dist/canonical-json.d.ts +2 -0
  16. package/dist/canonical-json.js +10 -0
  17. package/dist/cli.js +130 -55
  18. package/dist/config.d.ts +38 -3
  19. package/dist/config.js +193 -28
  20. package/dist/creation.d.ts +1 -1
  21. package/dist/creation.js +7 -4
  22. package/dist/docs.d.ts +1 -1
  23. package/dist/docs.js +193 -55
  24. package/dist/doctor.d.ts +2 -0
  25. package/dist/doctor.js +40 -4
  26. package/dist/fleet-command-audit.d.ts +59 -9
  27. package/dist/fleet-command-audit.js +426 -63
  28. package/dist/fleet-proxy.d.ts +5 -2
  29. package/dist/harness/claude-code-session.js +2 -1
  30. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  31. package/dist/harness/codex-app-server-proxy.js +348 -14
  32. package/dist/harness/codex-session.d.ts +9 -2
  33. package/dist/harness/codex-session.js +18 -2
  34. package/dist/harness/codex.d.ts +5 -1
  35. package/dist/harness/codex.js +104 -10
  36. package/dist/harness/types.d.ts +1 -1
  37. package/dist/index.d.ts +4 -3
  38. package/dist/index.js +3 -2
  39. package/dist/init-guidance.d.ts +1 -1
  40. package/dist/init-guidance.js +1 -1
  41. package/dist/init-wizard.d.ts +98 -0
  42. package/dist/init-wizard.js +631 -0
  43. package/dist/lifecycle-summary.d.ts +134 -0
  44. package/dist/lifecycle-summary.js +179 -0
  45. package/dist/loops/config.d.ts +10 -1
  46. package/dist/loops/config.js +38 -5
  47. package/dist/model-recovery.d.ts +1 -1
  48. package/dist/ops.js +2 -17
  49. package/dist/owner-channel/channel.d.ts +21 -5
  50. package/dist/owner-channel/channel.js +312 -138
  51. package/dist/owner-channel/commands.d.ts +28 -6
  52. package/dist/owner-channel/commands.js +109 -27
  53. package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
  54. package/dist/owner-channel/lifecycle-outbox.js +87 -0
  55. package/dist/owner-channel/ours-client.d.ts +19 -0
  56. package/dist/owner-channel/ours-client.js +3 -0
  57. package/dist/owner-channel/state.d.ts +2 -0
  58. package/dist/owner-channel/state.js +8 -2
  59. package/dist/permissions.js +1 -1
  60. package/dist/preset-migration.d.ts +42 -0
  61. package/dist/preset-migration.js +450 -0
  62. package/dist/reports/artifact.d.ts +10 -0
  63. package/dist/reports/artifact.js +32 -0
  64. package/dist/reports/index.d.ts +5 -0
  65. package/dist/reports/index.js +5 -0
  66. package/dist/reports/render.d.ts +6 -0
  67. package/dist/reports/render.js +240 -0
  68. package/dist/reports/service.d.ts +42 -0
  69. package/dist/reports/service.js +57 -0
  70. package/dist/reports/tasks.d.ts +52 -0
  71. package/dist/reports/tasks.js +177 -0
  72. package/dist/reports/types.d.ts +315 -0
  73. package/dist/reports/types.js +6 -0
  74. package/dist/resolved-plan.js +13 -1
  75. package/dist/rooms-tasks/cli.d.ts +2 -0
  76. package/dist/rooms-tasks/cli.js +434 -279
  77. package/dist/rooms-tasks/close.d.ts +9 -1
  78. package/dist/rooms-tasks/close.js +23 -7
  79. package/dist/rooms-tasks/config.js +16 -25
  80. package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
  81. package/dist/rooms-tasks/cowork-adapter.js +13 -0
  82. package/dist/rooms-tasks/deletion.d.ts +51 -0
  83. package/dist/rooms-tasks/deletion.js +217 -0
  84. package/dist/rooms-tasks/external-worker.d.ts +8 -0
  85. package/dist/rooms-tasks/external-worker.js +38 -0
  86. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  87. package/dist/rooms-tasks/launch-snapshot.js +181 -0
  88. package/dist/rooms-tasks/member-overrides.d.ts +30 -0
  89. package/dist/rooms-tasks/member-overrides.js +204 -0
  90. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  91. package/dist/rooms-tasks/member-startup.js +9 -2
  92. package/dist/rooms-tasks/provision.js +146 -56
  93. package/dist/rooms-tasks/room-state.d.ts +1 -0
  94. package/dist/rooms-tasks/room-state.js +12 -1
  95. package/dist/rooms-tasks/task-room-name.d.ts +12 -0
  96. package/dist/rooms-tasks/task-room-name.js +61 -0
  97. package/dist/rooms-tasks/task-state.d.ts +115 -3
  98. package/dist/rooms-tasks/task-state.js +361 -11
  99. package/dist/rooms-tasks/templates.d.ts +4 -1
  100. package/dist/rooms-tasks/templates.js +34 -3
  101. package/dist/rooms-tasks/terminal.d.ts +3 -0
  102. package/dist/rooms-tasks/terminal.js +10 -4
  103. package/dist/rooms-tasks/types.d.ts +92 -7
  104. package/dist/rooms-tasks/types.js +14 -1
  105. package/dist/runner.d.ts +3 -0
  106. package/dist/runner.js +92 -33
  107. package/dist/session/acp.d.ts +4 -1
  108. package/dist/session/acp.js +29 -13
  109. package/dist/session/arbiter.d.ts +2 -1
  110. package/dist/session/arbiter.js +2 -0
  111. package/dist/session/codex-app-server-transport.d.ts +53 -0
  112. package/dist/session/codex-app-server-transport.js +184 -0
  113. package/dist/session/codex-app-server.d.ts +122 -0
  114. package/dist/session/codex-app-server.js +1031 -0
  115. package/dist/session/control.d.ts +2 -2
  116. package/dist/session/control.js +18 -2
  117. package/dist/session/conversation-types.d.ts +4 -4
  118. package/dist/session/types.d.ts +24 -6
  119. package/dist/session/types.js +28 -0
  120. package/dist/spawn.d.ts +9 -0
  121. package/dist/spawn.js +77 -9
  122. package/dist/web/fleet-config-service.d.ts +1 -0
  123. package/dist/web/fleet-config-service.js +25 -7
  124. package/dist/web/runtime.js +11 -6
  125. package/dist/web/server.js +28 -1
  126. package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
  127. package/dist/web-app/index.html +1 -1
  128. package/package.json +4 -4
  129. package/presets/brain-catalog.json +17 -0
  130. package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
  131. package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
  132. package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
  133. package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
  134. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  135. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  136. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  137. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  138. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  139. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  140. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  141. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  142. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  143. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  144. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  145. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  146. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  147. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  148. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  149. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  150. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  151. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  152. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  153. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  154. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  155. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  156. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  157. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  158. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  159. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
  188. package/presets/fleet/roles/Coordinator.yaml +37 -0
  189. package/presets/fleet/roles/Critic.yaml +23 -1
  190. package/presets/fleet/roles/Developer.yaml +27 -5
  191. package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
  192. package/presets/fleet/room_templates/pair.yaml +9 -7
  193. package/presets/fleet/room_templates/single.yaml +5 -4
  194. package/presets/fleet/room_templates/team.yaml +12 -8
  195. package/presets/manifest.json +2 -1
  196. package/presets/fleet/agents/Architect.yaml +0 -3
  197. package/presets/fleet/agents/Secretary.yaml +0 -3
  198. package/presets/fleet/agents/Tester.yaml +0 -3
  199. package/presets/fleet/roles/Agent.yaml +0 -6
  200. package/presets/fleet/roles/Architect.yaml +0 -6
  201. package/presets/fleet/roles/Secretary.yaml +0 -6
  202. package/presets/fleet/roles/Tester.yaml +0 -6
@@ -1,6 +1,6 @@
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
6
  import { DAEMON_RECOVERY_DEADLINE_MS } from '../daemon-recovery.js';
@@ -11,14 +11,15 @@ import { RoleRepository } from '../application/role-repository.js';
11
11
  import { FleetQueryService } from '../application/fleet-query-service.js';
12
12
  import { interruptSession, queueSessionPrompt } from '../application/session-mutations.js';
13
13
  import { pickBackend } from '../supervisor/index.js';
14
- 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';
15
15
  import { VERSION } from '../version.js';
16
- import { renderMarkdownFailure, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
17
- import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
18
- import { FleetCommandAuditStore, renderFleetAuditInvocation, renderFleetAuditOutcome, } from '../fleet-command-audit.js';
16
+ import { renderMarkdownFailure, renderMarkdownResult, taskStatus, } from '../rooms-tasks/markdown.js';
17
+ import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, ownerTypedCommandCatalog, ownerTypedCommandText, } from './commands.js';
18
+ import { beginFleetAuditCollection, consumeFleetAuditCollection, FleetCommandAuditStore, lifecycleEventDigestBasis, renderFleetLifecycleEvent, setFleetAuditLifecycleCheckpoint, } from '../fleet-command-audit.js';
19
19
  import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
20
20
  import { ownerNotices, } from './notices.js';
21
21
  import { DuplicateSendError, OwnerAuthorizationState, OwnerChannelState, OwnerConversationState, } from './state.js';
22
+ import { FleetLifecycleOutbox } from './lifecycle-outbox.js';
22
23
  import { OwnerTaskState, ownerTaskAuditId, ownerTaskDigest, } from './tasks.js';
23
24
  import { AttachmentRecoveryState, admitAttachments, cleanupAttachmentRoot, parseIncomingAttachments, parseRetrievedAttachments, prepareAttachmentDirectory, recoveredAttachment, removeRequestDirectory, safeField, validateAttachmentSelection, validateAttachmentRelaySelection, writeRecoveredAttachment, } from './attachments.js';
24
25
  import { acquireOwnerBinderLease, OWNER_BIND_HANDOFF_TIMEOUT_MS, } from './binder.js';
@@ -78,6 +79,8 @@ export class OwnerChannel {
78
79
  * (a crash must replay them) but must not be queued twice while live.
79
80
  */
80
81
  inFlight = new Set();
82
+ /** SDK handlers entered during the current getMessages call. */
83
+ typedHandlersEntered = new Set();
81
84
  /** Wires already NACKed to the managed agent, so a history replay stays quiet. */
82
85
  relayNacks = new Set();
83
86
  /**
@@ -112,6 +115,7 @@ export class OwnerChannel {
112
115
  fleetOps;
113
116
  prepareRestart;
114
117
  commandAudits;
118
+ lifecycleOutbox;
115
119
  constructor(options) {
116
120
  this.options = options;
117
121
  this.client = options.client ?? new OursSdkClient(options.env, line => options.log(`[${options.role}] owner channel ${line}`));
@@ -128,6 +132,7 @@ export class OwnerChannel {
128
132
  });
129
133
  this.state = new OwnerChannelState(join(options.stateDir, '.owner-channel-state.json'));
130
134
  this.commandAudits = new FleetCommandAuditStore(join(options.stateDir, '.owner-channel-command-audit.json'));
135
+ this.lifecycleOutbox = new FleetLifecycleOutbox(join(options.stateDir, '.owner-channel-lifecycle-outbox.json'));
131
136
  this.authorizations = new OwnerAuthorizationState(join(options.stateDir, '.owner-channel-owners.json'), options.config.owners);
132
137
  this.conversations = new OwnerConversationState(join(options.stateDir, '.owner-channel-conversations.json'));
133
138
  this.tasks = new OwnerTaskState(join(options.stateDir, '.owner-channel-tasks.json'));
@@ -153,6 +158,8 @@ export class OwnerChannel {
153
158
  options.log(`[${options.role}] owner message recovery state corrupt; message intake disabled`);
154
159
  if (!this.attachmentRecovery.integrity())
155
160
  options.log(`[${options.role}] owner attachment recovery state corrupt; attachments disabled`);
161
+ if (!this.lifecycleOutbox.integrity().ok)
162
+ options.log(`[${options.role}] owner lifecycle outbox corrupt; lifecycle delivery disabled`);
156
163
  }
157
164
  async start() {
158
165
  this.stopping = false;
@@ -168,6 +175,7 @@ export class OwnerChannel {
168
175
  for (;;) {
169
176
  try {
170
177
  await this.client.bindIdentity(this.options.config.identity);
178
+ await this.registerTypedCommands();
171
179
  break;
172
180
  }
173
181
  catch (error) {
@@ -199,6 +207,8 @@ export class OwnerChannel {
199
207
  }
200
208
  this.startedOnce = true;
201
209
  this.ready = true;
210
+ await this.flushPendingFleetCommandAudits();
211
+ await this.flushPendingFleetLifecycle();
202
212
  // Do not make role startup wait for an old owner request to finish a turn.
203
213
  // watchLoop itself drains before every establishment, including this first
204
214
  // one, so there is no drain-to-tip race.
@@ -263,40 +273,57 @@ export class OwnerChannel {
263
273
  return this.queueManagement(async () => {
264
274
  if (!this.ready || this.stopping)
265
275
  throw new Error('owner-channel MCP client is unavailable');
266
- const model = event.model ? `, model ${event.model}` : '';
267
- const monitorPolicy = event.monitor.interrupt === true
268
- ? ' with interruption'
269
- : event.monitor.interrupt === 'after_tool' ? ' with after-tool steering' : '';
270
- const monitor = `${event.monitor.mode} monitor${monitorPolicy}`;
271
276
  const permission = event.permissionMode
272
- ? `; permission ${event.permissionMode.fleetMode}, native ${event.permissionMode.nativeMode}`
273
- : '';
274
- const inherited = event.inherited.length
275
- ? ` Supervisor inherited omitted defaults: ${event.inherited.join(', ')}.` : '';
276
- await this.sendProactiveMessage(`🧑‍💻 ${event.caller} spawned ${event.lifetime} agent ${event.role} `
277
- + `(${event.harness}/${event.session}${model}; ${monitor}${permission}).${inherited}`, `fleet-spawn\0${event.creationActionId}`, 0);
277
+ ? `${event.permissionMode.fleetMode}/${event.permissionMode.nativeMode}` : undefined;
278
+ await this.sendProactiveMessage(renderFleetLifecycleEvent({ kind: 'agent_started', eventId: event.creationActionId,
279
+ id: event.role, name: event.role,
280
+ lifetime: event.lifetime, brain: event.brainSummary, role: event.roleSummary,
281
+ harness: event.harness, session: event.session, model: event.model,
282
+ permissions: permission, parent: event.caller, actionId: event.creationActionId,
283
+ inherited: event.inherited,
284
+ ...(event.configuration ? { configuration: event.configuration } : {}) }), `fleet-spawn\0${event.creationActionId}`, 0);
278
285
  });
279
286
  }
287
+ notifyFleetLifecycle(presentations) {
288
+ const run = this.managementTail.then(async () => {
289
+ // Acceptance is durable before delivery. A detached worker may exit
290
+ // while the Owner channel is reconnecting; startup/recovery flushes the
291
+ // pending event through the existing exactly-once send ledger.
292
+ this.lifecycleOutbox.enqueue(presentations);
293
+ if (this.ready && !this.stopping)
294
+ await this.flushPendingFleetLifecycle();
295
+ });
296
+ this.managementTail = run.then(() => undefined, () => undefined);
297
+ return run;
298
+ }
299
+ async flushPendingFleetLifecycle() {
300
+ for (const entry of this.lifecycleOutbox.pending()) {
301
+ try {
302
+ await this.sendProactiveMessage(renderFleetLifecycleEvent(entry.presentation), `fleet-lifecycle\0${lifecycleEventDigestBasis(entry.presentation)}`, 0);
303
+ this.lifecycleOutbox.finish(entry.digest, 'delivered');
304
+ }
305
+ catch (error) {
306
+ if (error instanceof DuplicateSendError) {
307
+ this.lifecycleOutbox.finish(entry.digest, error.status === 'delivered' ? 'delivered' : 'uncertain');
308
+ this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
309
+ continue;
310
+ }
311
+ // If the send crossed the uncertain boundary, the proactive ledger
312
+ // will refuse it on the next flush. A pre-send outage stays pending.
313
+ this.logError('pending Fleet lifecycle delivery deferred', error);
314
+ return;
315
+ }
316
+ }
317
+ }
280
318
  beginFleetCommandAudit(requestId, argv) {
281
319
  const run = this.managementTail.then(async () => {
282
320
  let attempt = this.commandAudits.begin(requestId, this.options.role, argv);
283
321
  if (attempt.invocation === 'delivered')
284
322
  return attempt;
285
323
  if (attempt.invocation === 'uncertain')
286
- throw new Error('fleet command invocation delivery is uncertain; execution is denied');
287
- if (!this.ready || this.stopping) {
288
- this.commandAudits.invocation(attempt.correlationId, this.options.role, 'uncertain');
289
- this.options.log(`[${this.options.role}] fleet command invocation ${attempt.correlationId} `
290
- + 'delivery unavailable; execution denied');
291
- throw new Error('owner-channel MCP client is unavailable; invocation recorded uncertain');
292
- }
293
- try {
294
- await this.sendProactiveMessage(renderFleetAuditInvocation(attempt), `fleet-command-invocation\0${attempt.correlationId}`, 0);
295
- }
296
- catch (error) {
297
- this.commandAudits.invocation(attempt.correlationId, this.options.role, 'uncertain');
298
- throw error;
299
- }
324
+ return attempt;
325
+ this.options.log(`[${this.options.role}] fleet proxy command ${attempt.correlationId} `
326
+ + `route=${attempt.classification.route} decision=${attempt.classification.decision}`);
300
327
  attempt = this.commandAudits.invocation(attempt.correlationId, this.options.role, 'delivered');
301
328
  return attempt;
302
329
  });
@@ -309,31 +336,56 @@ export class OwnerChannel {
309
336
  class: input.class, effect: input.effect,
310
337
  ...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
311
338
  ...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
312
- ...(input.presentation ? { presentation: input.presentation } : {}),
339
+ ...(input.presentations ? { presentations: input.presentations } : {}),
313
340
  });
314
341
  if (attempt.outcome?.delivery === 'delivered')
315
342
  return attempt;
316
343
  if (attempt.outcome?.delivery === 'uncertain')
317
- throw new Error(`fleet command outcome delivery is uncertain after execution; effect status=${attempt.outcome.effect}`);
318
- if (!this.ready || this.stopping) {
319
- this.commandAudits.outcome(attempt.correlationId, this.options.role, 'uncertain');
320
- this.options.log(`[${this.options.role}] fleet command outcome ${attempt.correlationId} `
321
- + `delivery unavailable after execution; effect status=${attempt.outcome?.effect}`);
322
- throw new Error(`owner-channel unavailable after execution; effect status=${attempt.outcome?.effect}`);
323
- }
324
- try {
325
- await this.sendProactiveMessage(renderFleetAuditOutcome(attempt), `fleet-command-outcome\0${attempt.correlationId}`, 0);
326
- }
327
- catch (error) {
328
- this.commandAudits.outcome(attempt.correlationId, this.options.role, 'uncertain');
329
- throw new Error(`fleet command outcome delivery is uncertain after execution; effect status=${attempt.outcome?.effect}`, { cause: error });
330
- }
331
- attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, 'delivered');
332
- return attempt;
344
+ return attempt;
345
+ // Before the Owner sink is attached, ordinary commands have nothing to
346
+ // deliver. Lifecycle presentations remain in the never-attempted
347
+ // `sending` state and are flushed exactly once when start() makes the
348
+ // authenticated sink ready. A restart converts them to `uncertain`, so
349
+ // effects are never replayed after an unproven delivery boundary.
350
+ if (!attempt.outcome?.presentations?.length)
351
+ return this.commandAudits.outcome(attempt.correlationId, this.options.role, 'delivered');
352
+ if (!this.ready)
353
+ return attempt;
354
+ return this.deliverFleetCommandOutcome(attempt);
333
355
  });
334
356
  this.managementTail = run.then(() => undefined, () => undefined);
335
357
  return run;
336
358
  }
359
+ async flushPendingFleetCommandAudits() {
360
+ for (const attempt of this.commandAudits.list())
361
+ if (attempt.outcome?.delivery === 'sending')
362
+ await this.deliverFleetCommandOutcome(attempt);
363
+ }
364
+ async deliverFleetCommandOutcome(attempt) {
365
+ let uncertain = false;
366
+ try {
367
+ for (const event of attempt.outcome?.presentations ?? []) {
368
+ try {
369
+ await this.sendProactiveMessage(renderFleetLifecycleEvent(event), `fleet-lifecycle\0${lifecycleEventDigestBasis(event)}`, 0);
370
+ }
371
+ catch (error) {
372
+ if (error instanceof DuplicateSendError) {
373
+ if (error.status !== 'delivered')
374
+ uncertain = true;
375
+ this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
376
+ continue;
377
+ }
378
+ throw error;
379
+ }
380
+ }
381
+ }
382
+ catch (error) {
383
+ uncertain = true;
384
+ this.logError(`fleet lifecycle delivery uncertain correlation=${attempt.correlationId}`, error);
385
+ }
386
+ attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, uncertain ? 'uncertain' : 'delivered');
387
+ return attempt;
388
+ }
337
389
  recover(epoch) {
338
390
  if (this.stopping)
339
391
  return Promise.reject(new Error('owner channel is stopping'));
@@ -375,6 +427,9 @@ export class OwnerChannel {
375
427
  if (superseded())
376
428
  throw new Error('owner recovery epoch superseded');
377
429
  await this.recoveryStage('bind', this.client.bindIdentity(this.options.config.identity), deadlineAt);
430
+ if (superseded())
431
+ throw new Error('owner recovery epoch superseded');
432
+ await this.recoveryStage('register_commands', this.registerTypedCommands(), deadlineAt);
378
433
  if (superseded())
379
434
  throw new Error('owner recovery epoch superseded');
380
435
  // Journal-aware drain is the only operation allowed to mark owner mail
@@ -384,6 +439,7 @@ export class OwnerChannel {
384
439
  if (superseded())
385
440
  throw new Error('owner recovery epoch superseded');
386
441
  this.ready = true;
442
+ await this.flushPendingFleetLifecycle();
387
443
  this.watchTask = this.watchLoop(watchGeneration);
388
444
  }
389
445
  catch (error) {
@@ -834,27 +890,61 @@ export class OwnerChannel {
834
890
  throw new Error('the ours daemon returned invalid unread message metadata');
835
891
  const preflight = listed.slice(0, OWNER_MESSAGE_BATCH_LIMIT);
836
892
  const now = Date.now();
837
- const claims = preflight.map(item => this.messageClaim(item, now));
838
- const claimKeys = new Set(claims.map(item => `${item.seq}\0${item.wireId}`));
839
- if (claimKeys.size !== claims.length)
893
+ const metadata = preflight.map(item => ({ item, claim: this.messageClaim(item, now),
894
+ kind: item.message_kind }));
895
+ if (metadata.some(item => !['text', 'command', 'command_result'].includes(item.kind)))
896
+ throw new Error('the ours daemon returned an unknown unread message kind');
897
+ // The SDK marks the entire batch read before invoking typed handlers. Claim
898
+ // every text AND command row first: if an earlier lifecycle handler exits
899
+ // this process, a later command is recovered from persistent history even
900
+ // though its SDK handler was never entered. Command-result rows are inert.
901
+ const recoverable = metadata.filter(item => item.kind !== 'command_result');
902
+ const claims = recoverable.map(item => item.claim);
903
+ const textClaims = metadata.filter(item => item.kind === 'text').map(item => item.claim);
904
+ const textClaimKeys = new Set(textClaims.map(item => `${item.seq}\0${item.wireId}`));
905
+ const preflightKeys = new Set(metadata.map(item => `${item.claim.seq}\0${item.claim.wireId}`));
906
+ if (preflightKeys.size !== metadata.length)
840
907
  throw new Error('the ours daemon returned duplicate unread message metadata');
841
908
  this.messageRecovery.claim(claims);
842
909
  let fresh = [];
843
910
  let remaining = 0;
844
911
  // SDK batchLimit rejects zero. An empty preflight is a read-only drain.
845
- if (claims.length) {
846
- const payload = await this.client.getMessages(claims.length);
847
- if (!payload || !Array.isArray(payload.messages)
912
+ if (preflight.length) {
913
+ const payload = await this.client.getMessages(preflight.length);
914
+ if (!payload || !Array.isArray(payload.messages) || !Array.isArray(payload.command_results)
915
+ || !Number.isSafeInteger(payload.commands_handled) || payload.commands_handled < 0
848
916
  || !Number.isSafeInteger(payload.remaining) || payload.remaining < 0)
849
917
  throw new Error('the ours daemon returned an invalid claimed message batch');
850
918
  fresh = payload.messages.map(message => this.historyMessage(message));
851
919
  remaining = payload.remaining;
852
- const expected = claimKeys;
920
+ const expected = textClaimKeys;
853
921
  const actual = new Set(fresh.map(item => `${item.seq}\0${item.wire_id}`));
854
- if (fresh.length !== claims.length || actual.size !== fresh.length
922
+ if (fresh.length !== textClaims.length || actual.size !== fresh.length
855
923
  || actual.size !== expected.size
856
924
  || [...expected].some(item => !actual.has(item)))
857
925
  throw new Error('the ours daemon claimed a different message batch than fleet journaled');
926
+ const expectedCommands = metadata.filter(item => item.kind === 'command');
927
+ if (payload.commands_handled !== expectedCommands.length)
928
+ throw new Error('the ours daemon handled a different typed-command batch than fleet inspected');
929
+ const expectedResults = new Set(metadata.filter(item => item.kind === 'command_result')
930
+ .map(item => `${item.claim.seq}\0${item.claim.wireId}`));
931
+ const actualResults = new Set(payload.command_results
932
+ .map(item => `${item.seq}\0${item.wire_id}`));
933
+ if (actualResults.size !== payload.command_results.length
934
+ || actualResults.size !== expectedResults.size
935
+ || [...expectedResults].some(item => !actualResults.has(item)))
936
+ throw new Error('the ours daemon returned a different typed-command result batch than fleet inspected');
937
+ for (const item of expectedCommands) {
938
+ // Invalid envelopes and unregistered commands never enter a handler;
939
+ // the SDK has already returned handler_failed, so consume those now.
940
+ // Entered harness commands keep their claim until async completion.
941
+ if (!this.typedHandlersEntered.has(item.claim.wireId))
942
+ this.state.remember(item.claim.wireId);
943
+ this.typedHandlersEntered.delete(item.claim.wireId);
944
+ }
945
+ for (const item of metadata)
946
+ if (item.kind === 'command_result')
947
+ this.state.remember(item.claim.wireId);
858
948
  }
859
949
  const merged = new Map();
860
950
  for (const message of [...recovered, ...fresh]) {
@@ -1110,6 +1200,8 @@ export class OwnerChannel {
1110
1200
  const wireId = this.wireId(message);
1111
1201
  if (!wireId || this.state.has(wireId) || this.inFlight.has(wireId))
1112
1202
  return false;
1203
+ if (message.message_kind === 'command')
1204
+ return this.handleRecoveredTypedCommand(message, wireId);
1113
1205
  const sender = this.sender(message);
1114
1206
  if (this.isAgentSender(sender.id)) {
1115
1207
  try {
@@ -1177,12 +1269,13 @@ export class OwnerChannel {
1177
1269
  catch (error) {
1178
1270
  await rm(outbox, { recursive: true, force: true });
1179
1271
  if (error instanceof SessionControlError
1180
- && error.reasonCode === ACP_CANCEL_DEADLINE_EXCEEDED) {
1272
+ && (error.reasonCode === ACP_CANCEL_DEADLINE_EXCEEDED
1273
+ || error.reasonCode === CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED)) {
1181
1274
  // drainAll journaled this authenticated message before delivery. The
1182
1275
  // adapter generation is terminating, so leave the wire unhandled and
1183
1276
  // body-free: the resumed owner channel recovers it exactly once.
1184
1277
  this.options.log(`[${this.options.role}] owner request ${requestId.slice(0, 12)} `
1185
- + `held for adapter resume reason=${ACP_CANCEL_DEADLINE_EXCEEDED}`);
1278
+ + `held for adapter resume reason=${error.reasonCode}`);
1186
1279
  return false;
1187
1280
  }
1188
1281
  this.logError('request delivery failed', error);
@@ -1222,7 +1315,21 @@ export class OwnerChannel {
1222
1315
  * agent itself can execute /force-restart, /model, or any other command.
1223
1316
  */
1224
1317
  async handleCommand(sender, text, wireId) {
1318
+ const provisioningCommand = async (run) => {
1319
+ beginFleetAuditCollection();
1320
+ setFleetAuditLifecycleCheckpoint(events => this.notifyFleetLifecycle(events));
1321
+ try {
1322
+ return await run();
1323
+ }
1324
+ finally {
1325
+ setFleetAuditLifecycleCheckpoint(undefined);
1326
+ const remaining = consumeFleetAuditCollection().presentations;
1327
+ if (remaining?.length)
1328
+ await this.notifyFleetLifecycle(remaining);
1329
+ }
1330
+ };
1225
1331
  const ctx = {
1332
+ authenticatedCid: sender.id,
1226
1333
  role: this.options.role,
1227
1334
  harness: this.options.harness,
1228
1335
  version: VERSION,
@@ -1240,16 +1347,36 @@ export class OwnerChannel {
1240
1347
  },
1241
1348
  fleetList: () => this.fleetOps.list(),
1242
1349
  closeRoom: roomId => this.closeRoomFromOwner(sender, roomId, wireId),
1243
- recoverRoom: roomId => this.recoverRoomFromOwner(sender, roomId, wireId),
1244
1350
  terminalTask: (taskId, kind, outcome) => this.terminalTaskFromOwner(sender, taskId, kind, outcome, wireId),
1245
- recoverTask: taskId => this.recoverTaskFromOwner(sender, taskId, wireId),
1246
- createTask: input => new TaskRoomApplicationService(this.options.configPath).createTask({
1247
- ...input,
1248
- actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
1351
+ createTask: input => provisioningCommand(async () => {
1352
+ const service = new TaskRoomApplicationService(this.options.configPath);
1353
+ const task = await service.createTask({
1354
+ ...input,
1355
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
1356
+ });
1357
+ if (!input.backlog && !input.noRoom && task.room_id) {
1358
+ const outcome = service.taskProvisioningOutcome(task.task_id);
1359
+ if (outcome.kind === 'in_progress' && !outcome.next_action)
1360
+ await this.fleetOps.provisionTask(task.task_id);
1361
+ }
1362
+ return task;
1249
1363
  }),
1250
- startTask: taskId => new TaskRoomApplicationService(this.options.configPath).startTask({
1251
- actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1364
+ startTask: taskId => provisioningCommand(async () => {
1365
+ const service = new TaskRoomApplicationService(this.options.configPath);
1366
+ const task = await service.startTask({
1367
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1368
+ });
1369
+ // A zero-duration observation repairs a ready notice after a crash or
1370
+ // duplicate start without extending the start command's existing wait.
1371
+ const outcome = await service.awaitTaskProvisioning({
1372
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
1373
+ taskId: task.task_id, waitMs: 0,
1374
+ });
1375
+ if (outcome.kind === 'in_progress' && !outcome.next_action)
1376
+ await this.fleetOps.provisionTask(task.task_id);
1377
+ return outcome;
1252
1378
  }),
1379
+ taskProvisioningOutcome: taskId => new TaskRoomApplicationService(this.options.configPath).taskProvisioningOutcome(taskId),
1253
1380
  listTasks: filter => new TaskRoomApplicationService(this.options.configPath).listTasks(filter),
1254
1381
  groupedTasks: filter => new TaskRoomApplicationService(this.options.configPath).groupedTasks(filter),
1255
1382
  listTaskLists: () => new TaskRoomApplicationService(this.options.configPath).listTaskLists(),
@@ -1275,19 +1402,29 @@ export class OwnerChannel {
1275
1402
  reviewTask: taskId => new TaskRoomApplicationService(this.options.configPath).reviewTask({
1276
1403
  actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1277
1404
  }),
1278
- deleteTask: taskId => new TaskRoomApplicationService(this.options.configPath).deleteTask({
1279
- actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1280
- }),
1405
+ deleteTask: taskId => this.deleteTaskFromOwner(sender, taskId, wireId),
1281
1406
  listRoomQueries: filter => new TaskRoomApplicationService(this.options.configPath).listRooms(filter),
1282
1407
  getRoomQuery: id => new TaskRoomApplicationService(this.options.configPath).getRoomDetail(id),
1283
1408
  listTemplateQueries: () => new TaskRoomApplicationService(this.options.configPath).listTemplates(),
1284
1409
  getTemplateQuery: name => new TaskRoomApplicationService(this.options.configPath).getTemplate(name),
1285
- createRoom: input => new TaskRoomApplicationService(this.options.configPath).createRoom({
1410
+ createRoom: input => provisioningCommand(() => new TaskRoomApplicationService(this.options.configPath).createRoom({
1286
1411
  ...input, actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
1287
- }),
1412
+ })),
1288
1413
  recentEvents: limit => this.options.session.eventsSince(0).slice(-limit),
1289
1414
  readWorklogTail: maxChars => this.readWorklogTail(maxChars),
1290
1415
  reply: async (replyText) => { await this.send(sender.id, replyText, wireId); },
1416
+ replyHtml: async (filename, html) => {
1417
+ const directory = join(this.options.stateDir, '.owner-channel-report-outbox', this.requestId(wireId));
1418
+ const path = join(directory, filename);
1419
+ await mkdir(directory, { recursive: true, mode: 0o700 });
1420
+ try {
1421
+ await writeFile(path, html, { flag: 'wx', mode: 0o600 });
1422
+ await this.client.sendFile({ contact: sender.id, path, filename, replyToWireId: wireId });
1423
+ }
1424
+ finally {
1425
+ await rm(directory, { recursive: true, force: true });
1426
+ }
1427
+ },
1291
1428
  };
1292
1429
  try {
1293
1430
  await dispatchOwnerCommand(text, ctx);
@@ -1300,6 +1437,84 @@ export class OwnerChannel {
1300
1437
  if (!this.inFlight.has(wireId))
1301
1438
  this.state.remember(wireId);
1302
1439
  }
1440
+ /**
1441
+ * Register one typed adapter per primary slash command. The adapter performs
1442
+ * the live CID check before constructing the same slash text and entering the
1443
+ * existing dispatcher; its null SDK result avoids duplicating the ordinary
1444
+ * owner-channel replies that remain the command's result contract.
1445
+ */
1446
+ registerTypedCommands() {
1447
+ const commands = ownerTypedCommandCatalog().map(definition => ({
1448
+ name: definition.name,
1449
+ description: definition.description,
1450
+ input_schema: definition.input_schema,
1451
+ handler: (input, context) => {
1452
+ this.typedHandlersEntered.add(context.request_wire_id);
1453
+ return this.handleTypedCommand(definition.name, input, context);
1454
+ },
1455
+ }));
1456
+ return this.client.registerCommands(commands);
1457
+ }
1458
+ async handleRecoveredTypedCommand(message, wireId) {
1459
+ let payload;
1460
+ try {
1461
+ const parsed = JSON.parse(String(message.body ?? message.text ?? ''));
1462
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
1463
+ || Object.keys(parsed).length !== 2
1464
+ || typeof parsed.command !== 'string'
1465
+ || !('arguments' in parsed))
1466
+ throw new Error('invalid command payload');
1467
+ payload = parsed;
1468
+ }
1469
+ catch {
1470
+ // The original SDK attempt would have emitted handler_failed without
1471
+ // entering Fleet. Recovery mirrors that terminal no-effect outcome.
1472
+ this.state.remember(wireId);
1473
+ return true;
1474
+ }
1475
+ const sender = this.sender(message);
1476
+ try {
1477
+ await this.handleTypedCommand(payload.command, payload.arguments, {
1478
+ sender_cid: sender.id, sender_name: sender.name, request_wire_id: wireId,
1479
+ });
1480
+ }
1481
+ catch (error) {
1482
+ // Expected handler failures (unauthorized/invalid typed input) are
1483
+ // terminal and already recorded, just as the SDK swallows them.
1484
+ if (!this.state.has(wireId))
1485
+ throw error;
1486
+ }
1487
+ return true;
1488
+ }
1489
+ async handleTypedCommand(name, input, context) {
1490
+ const sender = { id: context.sender_cid, name: context.sender_name };
1491
+ const wireId = context.request_wire_id;
1492
+ if (!this.isEffectiveOwner(sender.id)) {
1493
+ this.options.log(`[${this.options.role}] owner channel ignored unauthorized typed-command sender `
1494
+ + `${sender.id || '<unknown>'}`);
1495
+ await this.warnOwnerOfUnauthorizedSender(sender.id);
1496
+ if (wireId)
1497
+ this.state.remember(wireId);
1498
+ throw new Error('typed owner command denied');
1499
+ }
1500
+ try {
1501
+ try {
1502
+ this.conversations.recordInbound(sender.id, wireId);
1503
+ }
1504
+ catch (error) {
1505
+ this.logError('owner conversation route update failed', error);
1506
+ }
1507
+ await this.handleCommand(sender, ownerTypedCommandText(name, input), wireId);
1508
+ return null;
1509
+ }
1510
+ catch (error) {
1511
+ // Structural typed-input failures are terminal handler_failed outcomes,
1512
+ // not indefinitely replayable work.
1513
+ if (wireId)
1514
+ this.state.remember(wireId);
1515
+ throw error;
1516
+ }
1517
+ }
1303
1518
  /** Queue raw slash text to the harness and report the turn's outcome. */
1304
1519
  async runHarnessCommand(sender, command, wireId) {
1305
1520
  const requestId = this.requestId(wireId);
@@ -1347,7 +1562,7 @@ export class OwnerChannel {
1347
1562
  await this.send(sender.id, renderMarkdownFailure({
1348
1563
  kind: 'pending', subject: `/room delete ${roomId} ${roomId}`,
1349
1564
  detail: 'The deletion request was accepted and is still being settled.',
1350
- action: `Run /room recover ${roomId} if deletion remains pending.`,
1565
+ action: `Run /room delete ${roomId} ${roomId} again if deletion remains pending.`,
1351
1566
  }), wireId);
1352
1567
  this.state.remember(wireId);
1353
1568
  try {
@@ -1360,78 +1575,35 @@ export class OwnerChannel {
1360
1575
  throw error;
1361
1576
  }
1362
1577
  }
1363
- async recoverRoomFromOwner(sender, roomId, wireId) {
1364
- const app = new TaskRoomApplicationService(this.options.configPath);
1365
- const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
1366
- const result = await app.recoverRoom({ actor, roomId });
1367
- if (result.kind === 'deletion_worker_required') {
1368
- await this.send(sender.id, renderMarkdownFailure({ kind: 'pending',
1369
- subject: `/room recover ${roomId}`,
1370
- detail: 'The deletion recovery is still being settled.',
1371
- action: `Run /room recover ${roomId} if deletion remains pending.` }), wireId);
1372
- this.state.remember(wireId);
1373
- try {
1374
- await this.fleetOps.closeRoom(roomId);
1375
- }
1376
- catch (error) {
1377
- await app.recordRoomSettlementError({ actor, roomId,
1378
- error: error instanceof Error ? error.message : String(error),
1379
- recoveryHint: `External delete worker failed to start. Retry /room delete ${roomId} ${roomId}.` });
1380
- throw error;
1381
- }
1382
- return;
1383
- }
1384
- const r = result.orchestration;
1385
- await this.send(sender.id, renderMarkdownResult({ icon: '🛟', title: 'Room recovery',
1386
- fields: [{ label: 'Room', value: result.room.room_id, kind: 'code' },
1387
- { label: 'Status', value: roomStatus(result.room.state), kind: 'markdown' },
1388
- ...(r ? [{ label: 'Saga', value: r.saga.phase, kind: 'code' }] : [])],
1389
- sections: result.issues.length ? [{ heading: 'Next steps', items: result.issues }]
1390
- : [{ heading: 'Result', items: ['No recovery action is needed.'] }] }), wireId);
1391
- this.state.remember(wireId);
1392
- }
1393
- /** Carry a task terminal request through a worker that survives this role. */
1394
- async recoverTaskFromOwner(sender, taskId, wireId) {
1578
+ /** Accept a permanent any-state deletion, acknowledge it, then hand cleanup to a durable worker. */
1579
+ async deleteTaskFromOwner(sender, taskId, wireId) {
1395
1580
  const app = new TaskRoomApplicationService(this.options.configPath);
1396
1581
  const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
1397
- const begin = await app.beginTaskRecovery({ actor, taskId });
1398
- if (begin.kind === 'terminal_worker_required') {
1399
- await this.send(sender.id, renderMarkdownFailure({
1400
- kind: 'pending', subject: `/task recover ${taskId}`,
1401
- detail: 'The recovery request was accepted and is still being settled.',
1402
- action: `Run /task recover ${taskId} if it remains pending.`,
1582
+ const accepted = await app.requestTaskDeletion({ actor, taskId });
1583
+ if (accepted.status === 'already_absent') {
1584
+ await this.send(sender.id, renderMarkdownResult({
1585
+ icon: '🗑️', title: 'Task already absent',
1586
+ fields: [{ label: 'ID', value: taskId, kind: 'code' }],
1403
1587
  }), wireId);
1404
1588
  this.state.remember(wireId);
1405
- try {
1406
- await this.fleetOps.recoverTask(taskId);
1407
- }
1408
- catch (error) {
1409
- await app.recordSettlementError({ actor, taskId,
1410
- error: error instanceof Error ? error.message : String(error),
1411
- recoveryHint: `External settle worker failed to start. Retry /task recover ${taskId}.` });
1412
- throw error;
1413
- }
1414
1589
  return;
1415
1590
  }
1416
- const { task, room, issues } = begin.result;
1417
- const hints = issues.map(issue => issue.code === 'waiting_cowork' ? 'Cowork socket unreachable'
1418
- : issue.code === 'waiting_owner_invite' ? 'Owner invite missing or invalid'
1419
- : issue.code === 'owner_cid_mismatch' ? 'Owner CID mismatch'
1420
- : issue.code === 'member_failed' ? `Member failed at step ${issue.stepIndex}`
1421
- : issue.code === 'resume_failed' ? `Resume failed: ${issue.error}`
1422
- : issue.code === 'provisioning_resumed' ? 'Provisioning resumed successfully'
1423
- : issue.code);
1424
- await this.send(sender.id, renderMarkdownResult({
1425
- icon: '🛟', title: 'Task recovery',
1426
- fields: [{ label: 'Task', value: task.task_id, kind: 'code' },
1427
- { label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
1428
- ...(room ? [{ label: 'Room', value: room.room_id, kind: 'code' },
1429
- { label: 'Room status', value: roomStatus(room.state), kind: 'markdown' },
1430
- { label: 'Saga', value: room.saga.phase, kind: 'code' }] : [])],
1431
- sections: hints.length ? [{ heading: 'Next steps', items: hints }]
1432
- : [{ heading: 'Result', items: ['No automated recovery action is available.'] }],
1591
+ await this.send(sender.id, renderMarkdownFailure({
1592
+ kind: 'pending', subject: `/task delete ${taskId} ${taskId}`,
1593
+ detail: 'The deletion was accepted; cleanup is settling in the background.',
1594
+ action: `Repeat /task delete ${taskId} ${taskId} if it remains pending.`,
1433
1595
  }), wireId);
1434
1596
  this.state.remember(wireId);
1597
+ try {
1598
+ await this.fleetOps.settleTaskDeletion(taskId);
1599
+ }
1600
+ catch (error) {
1601
+ await app.recordDeletionError({
1602
+ actor, taskId, error: error instanceof Error ? error.message : String(error),
1603
+ recoveryHint: `External delete worker failed to start. Retry /task delete ${taskId} ${taskId}.`,
1604
+ });
1605
+ throw error;
1606
+ }
1435
1607
  }
1436
1608
  async terminalTaskFromOwner(sender, taskId, kind, outcome, wireId) {
1437
1609
  const app = new TaskRoomApplicationService(this.options.configPath);
@@ -1453,7 +1625,7 @@ export class OwnerChannel {
1453
1625
  await this.send(sender.id, renderMarkdownFailure({
1454
1626
  kind: 'pending', subject: `/task ${kind === 'done' ? 'done' : 'cancel'} ${taskId}`,
1455
1627
  detail: 'The terminal request was accepted and is still being settled.',
1456
- action: `Run /task recover ${taskId} if it remains pending.`,
1628
+ action: `Repeat /task ${kind === 'done' ? 'done' : 'cancel'} ${taskId} if it remains pending.`,
1457
1629
  }), wireId);
1458
1630
  this.state.remember(wireId);
1459
1631
  try {
@@ -1462,7 +1634,7 @@ export class OwnerChannel {
1462
1634
  catch (error) {
1463
1635
  await app.recordSettlementError({
1464
1636
  actor, taskId, error: error instanceof Error ? error.message : String(error),
1465
- recoveryHint: `External settle worker failed to start. Retry the identical task command or run task recover ${taskId}.`,
1637
+ recoveryHint: `External settle worker failed to start. Retry the identical task command.`,
1466
1638
  });
1467
1639
  throw error;
1468
1640
  }
@@ -1920,7 +2092,8 @@ export class OwnerChannel {
1920
2092
  startProgress(event);
1921
2093
  // Automatic commentary is an ACP phase extension. Other backends and
1922
2094
  // older adapters retain their established final-only behavior.
1923
- if (this.options.session.backend === 'acp')
2095
+ if (this.options.session.capabilities?.messagePhases
2096
+ ?? this.options.session.backend === 'acp')
1924
2097
  acceptCommentary(event);
1925
2098
  })
1926
2099
  : () => undefined;
@@ -1962,7 +2135,8 @@ export class OwnerChannel {
1962
2135
  enabled: this.commentsEnabled,
1963
2136
  baseline: this.commentsBaseline,
1964
2137
  // Only the ACP backend emits the phase marker commentary relaying needs.
1965
- supported: this.options.session.backend === 'acp',
2138
+ supported: this.options.session.capabilities?.messagePhases
2139
+ ?? this.options.session.backend === 'acp',
1966
2140
  };
1967
2141
  }
1968
2142
  /** Model-authored commentary only; raw protocol/tool data never reaches here. */