@ours.network/fleet 0.19.0-nightly.9 → 1.0.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 (114) hide show
  1. package/README.md +66 -11
  2. package/dist/application/fleet-query-service.d.ts +1 -1
  3. package/dist/application/fleet-query-service.js +12 -0
  4. package/dist/application/role-creation-service.js +3 -1
  5. package/dist/application/types.d.ts +11 -0
  6. package/dist/briefing.d.ts +1 -1
  7. package/dist/briefing.js +103 -10
  8. package/dist/build-info.json +5 -5
  9. package/dist/cli.js +41 -11
  10. package/dist/config.d.ts +21 -7
  11. package/dist/config.js +32 -18
  12. package/dist/creation.d.ts +1 -1
  13. package/dist/docs.d.ts +2 -2
  14. package/dist/docs.js +117 -24
  15. package/dist/doctor.js +10 -10
  16. package/dist/duration.js +1 -1
  17. package/dist/fleet-proxy.d.ts +5 -0
  18. package/dist/harness/acp-agent.js +11 -6
  19. package/dist/harness/claude-code.js +201 -8
  20. package/dist/harness/codex.d.ts +4 -1
  21. package/dist/harness/codex.js +72 -13
  22. package/dist/harness/types.d.ts +57 -6
  23. package/dist/isolation/bubblewrap.d.ts +4 -4
  24. package/dist/isolation/bubblewrap.js +4 -4
  25. package/dist/isolation/policy.d.ts +1 -1
  26. package/dist/isolation/policy.js +2 -2
  27. package/dist/isolation/registry.d.ts +3 -3
  28. package/dist/isolation/registry.js +5 -5
  29. package/dist/isolation/resources.d.ts +1 -1
  30. package/dist/isolation/resources.js +1 -1
  31. package/dist/isolation/types.d.ts +3 -3
  32. package/dist/loops/manager.d.ts +30 -1
  33. package/dist/loops/manager.js +69 -6
  34. package/dist/loops/state.d.ts +18 -0
  35. package/dist/loops/state.js +4 -0
  36. package/dist/model-env.d.ts +71 -0
  37. package/dist/model-env.js +106 -0
  38. package/dist/monitor.d.ts +2 -2
  39. package/dist/monitor.js +4 -4
  40. package/dist/ops.d.ts +2 -2
  41. package/dist/ops.js +9 -9
  42. package/dist/owner-channel/channel.d.ts +17 -0
  43. package/dist/owner-channel/channel.js +104 -23
  44. package/dist/owner-channel/commands.d.ts +9 -0
  45. package/dist/owner-channel/commands.js +226 -99
  46. package/dist/owner-channel/notices.d.ts +7 -0
  47. package/dist/owner-channel/notices.js +9 -0
  48. package/dist/rooms-tasks/cli.js +731 -219
  49. package/dist/rooms-tasks/close.d.ts +35 -0
  50. package/dist/rooms-tasks/close.js +182 -0
  51. package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
  52. package/dist/rooms-tasks/cowork-adapter.js +116 -0
  53. package/dist/rooms-tasks/external-worker.d.ts +2 -0
  54. package/dist/rooms-tasks/external-worker.js +40 -0
  55. package/dist/rooms-tasks/index.d.ts +2 -0
  56. package/dist/rooms-tasks/index.js +2 -0
  57. package/dist/rooms-tasks/markdown.d.ts +49 -0
  58. package/dist/rooms-tasks/markdown.js +206 -0
  59. package/dist/rooms-tasks/member-startup.d.ts +51 -0
  60. package/dist/rooms-tasks/member-startup.js +151 -0
  61. package/dist/rooms-tasks/provision.d.ts +8 -0
  62. package/dist/rooms-tasks/provision.js +363 -97
  63. package/dist/rooms-tasks/room-state.d.ts +13 -1
  64. package/dist/rooms-tasks/room-state.js +149 -1
  65. package/dist/rooms-tasks/task-state.d.ts +13 -2
  66. package/dist/rooms-tasks/task-state.js +131 -3
  67. package/dist/rooms-tasks/terminal.d.ts +23 -0
  68. package/dist/rooms-tasks/terminal.js +56 -0
  69. package/dist/rooms-tasks/types.d.ts +108 -2
  70. package/dist/runner.d.ts +48 -0
  71. package/dist/runner.js +252 -100
  72. package/dist/session/acp.d.ts +106 -2
  73. package/dist/session/acp.js +217 -14
  74. package/dist/session/activity.d.ts +31 -0
  75. package/dist/session/activity.js +48 -0
  76. package/dist/session/control.d.ts +1 -1
  77. package/dist/session/conversation-normalizer.d.ts +7 -1
  78. package/dist/session/conversation-normalizer.js +154 -11
  79. package/dist/session/conversation-store.js +1 -1
  80. package/dist/session/conversation-types.d.ts +25 -6
  81. package/dist/session/types.d.ts +35 -0
  82. package/dist/spawn.d.ts +4 -2
  83. package/dist/spawn.js +39 -26
  84. package/dist/supervisor/launchd.d.ts +2 -2
  85. package/dist/supervisor/launchd.js +4 -4
  86. package/dist/supervisor/systemd.js +4 -4
  87. package/dist/supervisor/types.d.ts +1 -1
  88. package/dist/temp-lifecycle.d.ts +14 -2
  89. package/dist/temp-lifecycle.js +90 -0
  90. package/dist/watchdog/alerts.d.ts +2 -2
  91. package/dist/watchdog/alerts.js +2 -2
  92. package/dist/watchdog/briefing.d.ts +5 -5
  93. package/dist/watchdog/briefing.js +13 -6
  94. package/dist/watchdog/config.js +1 -1
  95. package/dist/watchdog/query.d.ts +2 -2
  96. package/dist/watchdog/query.js +2 -2
  97. package/dist/watchdog/report.js +2 -2
  98. package/dist/watchdog/run.d.ts +2 -2
  99. package/dist/watchdog/run.js +6 -6
  100. package/dist/watchdog/scheduler.d.ts +7 -7
  101. package/dist/watchdog/scheduler.js +10 -10
  102. package/dist/watchdog/service.d.ts +4 -4
  103. package/dist/watchdog/service.js +5 -6
  104. package/dist/watchdog/store.d.ts +4 -4
  105. package/dist/watchdog/store.js +4 -4
  106. package/dist/web/runtime.js +1 -1
  107. package/dist/web/server.js +1 -1
  108. package/dist/web/topology-promote.js +1 -1
  109. package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
  110. package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
  111. package/dist/web-app/index.html +1 -1
  112. package/dist/worklog.d.ts +7 -1
  113. package/dist/worklog.js +191 -39
  114. package/package.json +1 -1
package/dist/ops.js CHANGED
@@ -17,7 +17,7 @@ import { daemonIdentityProvisioner, reconcilePermanentRoleIdentities, } from './
17
17
  /** Materialize a role's state dir from config: briefing + markers. Returns the dir. */
18
18
  export function applyRole(role, opts = {}) {
19
19
  const adapter = getAdapter(role.harness);
20
- const errs = adapter.validateOptions(role.harness_options);
20
+ const errs = adapter.validateOptions(role.harness_options, role);
21
21
  if (errs.length)
22
22
  throw new Error(`role '${role.name}': ` + errs.map(e => `${e.path}: ${e.message}`).join('; '));
23
23
  const dir = agentDir(role.name, opts.temp === true);
@@ -62,7 +62,7 @@ export async function up(cfg, names, deps, configPath, identityGuarantee) {
62
62
  // guessing "stopped" from an unanswered probe silently discards a live
63
63
  // conversation.
64
64
  // An explicit operator `up` is the sanctioned way to release a held-down
65
- // role: the still-alive runner polls this file and resumes (3.2).
65
+ // role: the still-alive runner polls this file and resumes.
66
66
  resetRestartLedger(dir);
67
67
  const live = await deps.backend.liveness(role.name)
68
68
  .catch(e => ({ state: 'unknown', detail: e instanceof Error ? e.message : String(e) }));
@@ -71,7 +71,7 @@ export async function up(cfg, names, deps, configPath, identityGuarantee) {
71
71
  else if (live.state === 'unknown')
72
72
  deps.log(` ! ${role.name}: liveness unknown, keeping session context — ${live.detail}`);
73
73
  // Report what each install actually did, so a creation transaction can undo
74
- // only the registrations IT made (6.2). Announced immediately as well as
74
+ // only the registrations IT made. Announced immediately as well as
75
75
  // returned: a later role in this same loop can throw, and the registrations
76
76
  // already made must still be undoable.
77
77
  const outcome = { ...await deps.backend.install(role.name, deps.binPath), role: role.name };
@@ -94,7 +94,7 @@ function stableStringify(value) {
94
94
  return v;
95
95
  });
96
96
  }
97
- /** sha256 over the resolved enabled-watchdog set, stable regardless of config source order (finding #4). */
97
+ /** SHA-256 over the resolved enabled-watchdog set, stable regardless of config source order. */
98
98
  function watchdogFingerprint(enabled) {
99
99
  const sorted = [...enabled].sort((a, b) => a.name.localeCompare(b.name));
100
100
  return createHash('sha256').update(stableStringify(sorted)).digest('hex');
@@ -117,14 +117,14 @@ function writeFingerprint(fingerprint) {
117
117
  }
118
118
  /**
119
119
  * Install/start (or stop) the single supervised watchdog-scheduler process
120
- * (Task 10) to match the config's enabled watchdogs. Runs on every `up`,
120
+ * to match the config's enabled watchdogs. Runs on every `up`,
121
121
  * including a named `up <Role>` — cheap and idempotent, and the alternative
122
122
  * (only reconciling on a whole-fleet `up`) would leave a newly-enabled
123
123
  * watchdog unscheduled until the next bare `up`. Never throws: a scheduler
124
124
  * hiccup must not fail the role installs that already succeeded.
125
125
  *
126
126
  * Restarts ONLY when something the scheduler actually needs to pick up
127
- * changed (finding #4): an unconditional `restart()` on every `up` —
127
+ * changed: an unconditional `restart()` on every `up` —
128
128
  * including a named `up <SomeUnrelatedRole>` — interrupted whatever the
129
129
  * scheduler was mid-run (a 15s stop timeout can kill a long check without a
130
130
  * report). Two independent signals decide: `svc.install()`'s own `changed`
@@ -142,7 +142,7 @@ async function reconcileWatchdogScheduler(cfg, deps, configPath) {
142
142
  return;
143
143
  const enabled = cfg.watchdogs.filter(w => w.enabled);
144
144
  try {
145
- // Check supervised() before any stop (final review #3): on an
145
+ // Check supervised() before any stop: on an
146
146
  // unsupervised platform/config, the scheduler was never installed, so
147
147
  // stopping it is not just a no-op — on Linux, stop() throws for a unit
148
148
  // that was never loaded. A watchdog-less fleet must not see that
@@ -162,7 +162,7 @@ async function reconcileWatchdogScheduler(cfg, deps, configPath) {
162
162
  if (unitChanged || fingerprintChanged) {
163
163
  // restart(), not start(): start() is a no-op on an already-active unit,
164
164
  // so a config change (new/changed watchdogs) would never reach a
165
- // scheduler that's already running (final review #4).
165
+ // scheduler that's already running.
166
166
  await svc.restart();
167
167
  deps.log(`↑ watchdogs scheduler (${enabled.map(w => w.name).join(', ')})`);
168
168
  }
@@ -208,7 +208,7 @@ export async function down(cfg, names, deps) {
208
208
  // Only a whole-fleet `down` (no names) stops the scheduler — stopping one
209
209
  // named role is not a decision to stop watching the others (tolerate
210
210
  // absence/errors: a never-installed scheduler must not fail `down`).
211
- // supervised() gates the call itself (final review #3): on an
211
+ // supervised() gates the call itself: on an
212
212
  // unsupervised platform/config there is nothing installed to stop, and
213
213
  // Linux's stop() throws for a unit that was never loaded.
214
214
  if (names.length === 0 && deps.watchdogService?.supervised()) {
@@ -217,6 +217,13 @@ export declare class OwnerChannel implements OwnerChannelHandle {
217
217
  * CLI is asked to bounce this very process; neither can happen afterwards.
218
218
  */
219
219
  private restartSelf;
220
+ /**
221
+ * The caller may itself be a room member. Persist and acknowledge acceptance
222
+ * before an external worker starts a saga that can retire this process.
223
+ */
224
+ private closeRoomFromOwner;
225
+ /** Carry a task terminal request through a worker that survives this role. */
226
+ private terminalTaskFromOwner;
220
227
  /** Code-point-safe tail of the worklog, or undefined when there is none. */
221
228
  private readWorklogTail;
222
229
  /** Bound harness-command output to a single outbound message. */
@@ -250,6 +257,16 @@ export declare class OwnerChannel implements OwnerChannelHandle {
250
257
  private warnOwnerOfUnauthorizedSender;
251
258
  private effectiveOwners;
252
259
  private authorizationIntegrity;
260
+ /**
261
+ * Report what the session actually did with the prompt, not what the config
262
+ * asked for. `interrupt: true` used to be reported as "your request
263
+ * interrupted the previous task" unconditionally; the session now answers
264
+ * whether anything was cancelled, whether the request is queued behind
265
+ * earlier prompts, or whether it is held until the current task reaches a
266
+ * safe stopping point. Backends that report no delivery state keep the old
267
+ * queuedBehind-based wording.
268
+ */
269
+ private acceptanceNotice;
253
270
  private complete;
254
271
  private commentsState;
255
272
  /** Model-authored commentary only; raw protocol/tool data never reaches here. */
@@ -6,6 +6,7 @@ import { DEFAULT_OWNER_ATTACHMENT_MIME, canonicalCid, } from '../config.js';
6
6
  import { replaceFileAtomically } from '../atomic-file.js';
7
7
  import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, interruptOutcome, } from '../session/types.js';
8
8
  import { VERSION } from '../version.js';
9
+ import { renderMarkdownFailure, renderMarkdownResult, taskStatus, } from '../rooms-tasks/markdown.js';
9
10
  import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
10
11
  import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
11
12
  import { ownerNotices, } from './notices.js';
@@ -195,10 +196,13 @@ export class OwnerChannel {
195
196
  ? ' with interruption'
196
197
  : event.monitor.interrupt === 'after_tool' ? ' with after-tool steering' : '';
197
198
  const monitor = `${event.monitor.mode} monitor${monitorPolicy}`;
199
+ const permission = event.permissionMode
200
+ ? `; permission ${event.permissionMode.fleetMode}, native ${event.permissionMode.nativeMode}`
201
+ : '';
198
202
  const inherited = event.inherited.length
199
203
  ? ` Supervisor inherited omitted defaults: ${event.inherited.join(', ')}.` : '';
200
204
  await this.sendProactiveMessage(`🧑‍💻 ${event.caller} spawned ${event.lifetime} agent ${event.role} `
201
- + `(${event.harness}/${event.session}${model}; ${monitor}).${inherited}`, `fleet-spawn\0${event.creationActionId}`, 0);
205
+ + `(${event.harness}/${event.session}${model}; ${monitor}${permission}).${inherited}`, `fleet-spawn\0${event.creationActionId}`, 0);
202
206
  });
203
207
  this.managementTail = run.then(() => undefined, () => undefined);
204
208
  return run;
@@ -789,17 +793,13 @@ export class OwnerChannel {
789
793
  outbox = this.outboxDir(originWireId);
790
794
  await mkdir(outbox, { recursive: true, mode: 0o700 });
791
795
  const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
792
- const queued = await this.options.session.queuePrompt(this.ownerAttachmentPrompt(sender, originWireId, requestId, outbox, admitted, group.caption), {
796
+ const queued = await this.options.session.queuePrompt(this.ownerAttachmentPrompt(sender, originWireId, requestId, admitted, group.caption), {
793
797
  interrupt: this.options.config.interrupt,
794
798
  ...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
795
799
  origin: { kind: 'owner', requestId,
796
800
  ...(group.caption ? { displayText: String(group.caption.text ?? '') } : {}) },
797
801
  });
798
- const accepted = this.options.config.interrupt
799
- ? ownerNotices.receivedInterrupting()
800
- : queued.queuedBehind > 0
801
- ? ownerNotices.receivedQueued(queued.queuedBehind)
802
- : ownerNotices.receivedStarted();
802
+ const accepted = this.acceptanceNotice(queued);
803
803
  handledWireIds.forEach(wire => this.inFlight.add(wire));
804
804
  const receipt = this.send(sender.id, accepted, originWireId).then(() => undefined).catch(error => {
805
805
  this.logError(`attachment request ${requestId.slice(0, 12)} acceptance notice failed`, error);
@@ -918,7 +918,7 @@ export class OwnerChannel {
918
918
  let queued;
919
919
  const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
920
920
  try {
921
- queued = await this.options.session.queuePrompt(this.ownerPrompt(sender, text, wireId, outbox), {
921
+ queued = await this.options.session.queuePrompt(this.ownerPrompt(sender, text, wireId), {
922
922
  interrupt: this.options.config.interrupt,
923
923
  ...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
924
924
  origin: { kind: 'owner', requestId, displayText: text },
@@ -940,14 +940,7 @@ export class OwnerChannel {
940
940
  this.state.remember(wireId);
941
941
  return true;
942
942
  }
943
- // Interrupting the live turn does not remove prompts which were already
944
- // accepted into the ACP queue. Never claim this request is running while
945
- // the session itself says earlier work remains ahead of it.
946
- const accepted = queued.queuedBehind > 0
947
- ? ownerNotices.receivedQueued(queued.queuedBehind)
948
- : this.options.config.interrupt
949
- ? ownerNotices.receivedInterrupting()
950
- : ownerNotices.receivedStarted();
943
+ const accepted = this.acceptanceNotice(queued);
951
944
  this.inFlight.add(wireId);
952
945
  const receipt = this.send(sender.id, accepted, wireId).then(() => undefined).catch(error => {
953
946
  this.logError(`request ${requestId.slice(0, 12)} acceptance notice failed`, error);
@@ -996,6 +989,8 @@ export class OwnerChannel {
996
989
  return this.commentsState();
997
990
  },
998
991
  fleetList: () => this.fleetOps.list(),
992
+ closeRoom: roomId => this.closeRoomFromOwner(sender, roomId, wireId),
993
+ terminalTask: (taskId, kind, outcome) => this.terminalTaskFromOwner(sender, taskId, kind, outcome, wireId),
999
994
  recentEvents: limit => this.options.session.eventsSince(0).slice(-limit),
1000
995
  readWorklogTail: maxChars => this.readWorklogTail(maxChars),
1001
996
  reply: async (replyText) => { await this.send(sender.id, replyText, wireId); },
@@ -1046,6 +1041,60 @@ export class OwnerChannel {
1046
1041
  this.options.log(`[${this.options.role}] owner requested ${command}`);
1047
1042
  await this.fleetOps.restart(mode);
1048
1043
  }
1044
+ /**
1045
+ * The caller may itself be a room member. Persist and acknowledge acceptance
1046
+ * before an external worker starts a saga that can retire this process.
1047
+ */
1048
+ async closeRoomFromOwner(sender, roomId, wireId) {
1049
+ const { acceptManagedRoomClose, recordManagedRoomCloseError } = await import('../rooms-tasks/close.js');
1050
+ await acceptManagedRoomClose(roomId);
1051
+ await this.send(sender.id, renderMarkdownFailure({
1052
+ kind: 'pending', subject: `/room delete ${roomId} ${roomId}`,
1053
+ detail: 'The deletion request was accepted and is still being settled.',
1054
+ action: `Run /room recover ${roomId} if deletion remains pending.`,
1055
+ }), wireId);
1056
+ this.state.remember(wireId);
1057
+ try {
1058
+ await this.fleetOps.closeRoom(roomId);
1059
+ }
1060
+ catch (error) {
1061
+ await recordManagedRoomCloseError(roomId, error instanceof Error ? error.message : String(error), `External delete worker failed to start. Retry /room delete ${roomId} ${roomId}.`);
1062
+ throw error;
1063
+ }
1064
+ }
1065
+ /** Carry a task terminal request through a worker that survives this role. */
1066
+ async terminalTaskFromOwner(sender, taskId, kind, outcome, wireId) {
1067
+ const { getTask } = await import('../rooms-tasks/task-state.js');
1068
+ const { acceptTaskTerminalIntent, recordTaskTerminalIntentError, } = await import('../rooms-tasks/terminal.js');
1069
+ const task = getTask(taskId);
1070
+ const accepted = await acceptTaskTerminalIntent({
1071
+ taskId, kind, roomId: task.room_id, outcome,
1072
+ });
1073
+ if (accepted.terminal_intent?.status === 'settled') {
1074
+ await this.send(sender.id, renderMarkdownResult({
1075
+ icon: '📋', title: 'Task terminal action complete',
1076
+ fields: [
1077
+ { label: 'ID', value: taskId, kind: 'code' },
1078
+ { label: 'Status', value: taskStatus(accepted.state), kind: 'markdown' },
1079
+ ],
1080
+ }), wireId);
1081
+ this.state.remember(wireId);
1082
+ return;
1083
+ }
1084
+ await this.send(sender.id, renderMarkdownFailure({
1085
+ kind: 'pending', subject: `/task ${kind === 'done' ? 'done' : 'cancel'} ${taskId}`,
1086
+ detail: 'The terminal request was accepted and is still being settled.',
1087
+ action: `Run /task recover ${taskId} if it remains pending.`,
1088
+ }), wireId);
1089
+ this.state.remember(wireId);
1090
+ try {
1091
+ await this.fleetOps.settleTask(taskId);
1092
+ }
1093
+ catch (error) {
1094
+ await recordTaskTerminalIntentError(taskId, error instanceof Error ? error.message : String(error), `External settle worker failed to start. Retry the identical task command or run task recover ${taskId}.`);
1095
+ throw error;
1096
+ }
1097
+ }
1049
1098
  /** Code-point-safe tail of the worklog, or undefined when there is none. */
1050
1099
  async readWorklogTail(maxChars) {
1051
1100
  try {
@@ -1332,6 +1381,32 @@ export class OwnerChannel {
1332
1381
  authorizationIntegrity() {
1333
1382
  return this.options.config.agent ? { ok: true } : this.authorizations.integrity();
1334
1383
  }
1384
+ /**
1385
+ * Report what the session actually did with the prompt, not what the config
1386
+ * asked for. `interrupt: true` used to be reported as "your request
1387
+ * interrupted the previous task" unconditionally; the session now answers
1388
+ * whether anything was cancelled, whether the request is queued behind
1389
+ * earlier prompts, or whether it is held until the current task reaches a
1390
+ * safe stopping point. Backends that report no delivery state keep the old
1391
+ * queuedBehind-based wording.
1392
+ */
1393
+ acceptanceNotice(queued) {
1394
+ switch (queued.delivery) {
1395
+ case 'interrupted': return ownerNotices.receivedInterrupting();
1396
+ case 'deferred': return ownerNotices.receivedDeferred();
1397
+ case 'queued': return ownerNotices.receivedQueued(Math.max(1, queued.queuedBehind));
1398
+ case 'started': return ownerNotices.receivedStarted();
1399
+ default:
1400
+ // Interrupting the live turn does not remove prompts which were already
1401
+ // accepted into the ACP queue. Never claim this request is running while
1402
+ // the session itself says earlier work remains ahead of it.
1403
+ return queued.queuedBehind > 0
1404
+ ? ownerNotices.receivedQueued(queued.queuedBehind)
1405
+ : this.options.config.interrupt
1406
+ ? ownerNotices.receivedInterrupting()
1407
+ : ownerNotices.receivedStarted();
1408
+ }
1409
+ }
1335
1410
  async complete(active, outbox, queued, activityCursor) {
1336
1411
  const progressMs = this.options.config.progress_interval_ms;
1337
1412
  let lastSeq = activityCursor;
@@ -1532,7 +1607,7 @@ export class OwnerChannel {
1532
1607
  throw new Error('commentary appears to contain secret, reasoning, or raw tool content');
1533
1608
  return message;
1534
1609
  }
1535
- ownerAttachmentPrompt(sender, wireId, requestId, outbox, files, caption) {
1610
+ ownerAttachmentPrompt(sender, wireId, requestId, files, caption) {
1536
1611
  const lines = [
1537
1612
  '[fleet-owner]',
1538
1613
  `Authenticated owner ${safeField(sender.name, 160)} (${sender.id}) sent owner-channel attachment request ${wireId}.`,
@@ -1556,10 +1631,13 @@ export class OwnerChannel {
1556
1631
  lines.push(`- voice transcript status: ${transcription?.status ?? 'unavailable'}`, `- voice transcript fallback: audio path above; category ${transcription?.errorCategory ?? 'not_provided'}`);
1557
1632
  }
1558
1633
  }
1559
- lines.push('Answer in your final assistant response; fleet routes it only to the authenticated sender and correlates it to the originating file wire.', `To attach response files, write regular files only to: ${outbox}`);
1634
+ lines.push('Answer in your final assistant response; fleet routes it only to the authenticated sender and correlates it to the originating file wire.',
1635
+ // send_file is the only delivery route an agent is given: a tool call either
1636
+ // delivers or reports an error, where a file written to disk does neither.
1637
+ 'To send the owner a file — now or later in this turn — call ours `send_file`:', `contact: ${this.options.config.identity}`, 'and the path of the finished file. Fleet routes it to the authenticated owner.', 'A file written anywhere else is not delivered and nothing will report that it was not.', 'Use descriptive unique filenames. Send nothing the owner did not request or should not receive.');
1560
1638
  return lines.join('\n');
1561
1639
  }
1562
- ownerPrompt(sender, text, wireId, outbox) {
1640
+ ownerPrompt(sender, text, wireId) {
1563
1641
  return [
1564
1642
  '[fleet-owner]',
1565
1643
  `Authenticated owner ${sender.name} (${sender.id}) sent owner-channel message ${wireId}.`,
@@ -1573,10 +1651,13 @@ export class OwnerChannel {
1573
1651
  ] : [
1574
1652
  'Managed-agent outbound relay is not configured; do not send intermediate or proactive owner-channel messages.',
1575
1653
  ]),
1576
- 'To attach files to your response, copy each finished file directly into this fleet outbox:',
1577
- outbox,
1578
- 'Fleet sends every regular file in that directory to the authenticated owner, correlated to this request.',
1579
- 'Use descriptive unique filenames. Put nothing there that the owner did not request or should not receive.',
1654
+ // send_file is the only delivery route an agent is given: a tool call either
1655
+ // delivers or reports an error, where a file written to disk does neither.
1656
+ 'To send the owner a file now or later in this turn call ours `send_file`:',
1657
+ `contact: ${this.options.config.identity}`,
1658
+ 'and the path of the finished file. Fleet routes it to the authenticated owner.',
1659
+ 'A file written anywhere else is not delivered and nothing will report that it was not.',
1660
+ 'Use descriptive unique filenames. Send nothing the owner did not request or should not receive.',
1580
1661
  '',
1581
1662
  text || '(empty message)',
1582
1663
  ].join('\n');
@@ -1,4 +1,5 @@
1
1
  import type { InterruptOutcome, SessionEvent, SessionSnapshot } from '../session/types.js';
2
+ import type { TaskOutcome, TaskTerminalIntent } from '../rooms-tasks/types.js';
2
3
  import { type OwnerCommentsState } from './notices.js';
3
4
  /**
4
5
  * Fleet-level effects a deterministic owner command may trigger. Production
@@ -10,6 +11,10 @@ export interface OwnerFleetOps {
10
11
  restart(mode: 'keep' | 'fresh'): Promise<void>;
11
12
  /** `ours-fleet ls` output. */
12
13
  list(): Promise<string>;
14
+ /** Start a room-close worker outside the caller role's supervisor lifecycle. */
15
+ closeRoom(roomId: string): Promise<void>;
16
+ /** Resume a task terminal intent outside the caller role's supervisor lifecycle. */
17
+ settleTask(taskId: string): Promise<void>;
13
18
  }
14
19
  /**
15
20
  * The narrow capability surface a command executor sees. Everything here is
@@ -40,6 +45,10 @@ export interface OwnerCommandContext {
40
45
  */
41
46
  setComments(enabled: boolean): OwnerCommentsState;
42
47
  fleetList(): Promise<string>;
48
+ /** Persist acceptance, acknowledge it, then launch the external close worker. */
49
+ closeRoom(roomId: string): Promise<void>;
50
+ /** Persist terminal intent, acknowledge it, then launch the external settle worker. */
51
+ terminalTask(taskId: string, kind: TaskTerminalIntent['kind'], outcome?: TaskOutcome): Promise<void>;
43
52
  recentEvents(limit: number): SessionEvent[];
44
53
  readWorklogTail(maxChars: number): Promise<string | undefined>;
45
54
  reply(text: string): Promise<void>;