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

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 (199) hide show
  1. package/README.md +169 -54
  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 +426 -78
  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 +227 -73
  20. package/dist/config-yaml.d.ts +1 -1
  21. package/dist/config.d.ts +67 -2
  22. package/dist/config.js +260 -30
  23. package/dist/daemon-recovery.d.ts +93 -0
  24. package/dist/daemon-recovery.js +328 -0
  25. package/dist/docs.d.ts +1 -1
  26. package/dist/docs.js +171 -66
  27. package/dist/doctor.d.ts +2 -0
  28. package/dist/doctor.js +54 -4
  29. package/dist/fleet-command-audit.d.ts +165 -0
  30. package/dist/fleet-command-audit.js +543 -0
  31. package/dist/fleet-proxy.d.ts +7 -2
  32. package/dist/fleet-proxy.js +30 -8
  33. package/dist/harness/agent-session.d.ts +5 -0
  34. package/dist/harness/claude-code-session.d.ts +2 -0
  35. package/dist/harness/claude-code-session.js +6 -1
  36. package/dist/harness/claude-code.js +2 -0
  37. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  38. package/dist/harness/codex-app-server-proxy.js +348 -14
  39. package/dist/harness/codex-session.d.ts +11 -2
  40. package/dist/harness/codex-session.js +22 -2
  41. package/dist/harness/codex.d.ts +5 -1
  42. package/dist/harness/codex.js +110 -10
  43. package/dist/harness/types.d.ts +1 -1
  44. package/dist/index.d.ts +5 -4
  45. package/dist/index.js +4 -3
  46. package/dist/init-guidance.d.ts +1 -1
  47. package/dist/init-guidance.js +4 -3
  48. package/dist/init-wizard.d.ts +98 -0
  49. package/dist/init-wizard.js +629 -0
  50. package/dist/lifecycle-summary.d.ts +118 -0
  51. package/dist/lifecycle-summary.js +161 -0
  52. package/dist/loops/config.js +3 -2
  53. package/dist/model-env.d.ts +2 -2
  54. package/dist/model-env.js +1 -1
  55. package/dist/model-recovery.d.ts +1 -1
  56. package/dist/ops.js +2 -17
  57. package/dist/owner-channel/channel.d.ts +58 -0
  58. package/dist/owner-channel/channel.js +321 -42
  59. package/dist/owner-channel/commands.d.ts +6 -1
  60. package/dist/owner-channel/commands.js +34 -19
  61. package/dist/owner-channel/state.d.ts +2 -0
  62. package/dist/owner-channel/state.js +8 -2
  63. package/dist/permissions.js +1 -1
  64. package/dist/preset-bootstrap.d.ts +10 -0
  65. package/dist/preset-bootstrap.js +85 -0
  66. package/dist/preset-migration.d.ts +42 -0
  67. package/dist/preset-migration.js +414 -0
  68. package/dist/reports/artifact.d.ts +10 -0
  69. package/dist/reports/artifact.js +32 -0
  70. package/dist/reports/index.d.ts +5 -0
  71. package/dist/reports/index.js +5 -0
  72. package/dist/reports/render.d.ts +6 -0
  73. package/dist/reports/render.js +240 -0
  74. package/dist/reports/service.d.ts +42 -0
  75. package/dist/reports/service.js +57 -0
  76. package/dist/reports/tasks.d.ts +52 -0
  77. package/dist/reports/tasks.js +177 -0
  78. package/dist/reports/types.d.ts +315 -0
  79. package/dist/reports/types.js +6 -0
  80. package/dist/resolved-plan.js +4 -0
  81. package/dist/rooms-tasks/cli.d.ts +2 -0
  82. package/dist/rooms-tasks/cli.js +503 -75
  83. package/dist/rooms-tasks/close.d.ts +9 -1
  84. package/dist/rooms-tasks/close.js +20 -4
  85. package/dist/rooms-tasks/config.js +16 -14
  86. package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
  87. package/dist/rooms-tasks/cowork-adapter.js +6 -0
  88. package/dist/rooms-tasks/deletion.d.ts +51 -0
  89. package/dist/rooms-tasks/deletion.js +217 -0
  90. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  91. package/dist/rooms-tasks/external-worker.js +14 -9
  92. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  93. package/dist/rooms-tasks/launch-snapshot.js +176 -0
  94. package/dist/rooms-tasks/member-overrides.d.ts +27 -0
  95. package/dist/rooms-tasks/member-overrides.js +170 -0
  96. package/dist/rooms-tasks/provision.js +179 -49
  97. package/dist/rooms-tasks/room-state.d.ts +1 -0
  98. package/dist/rooms-tasks/room-state.js +12 -1
  99. package/dist/rooms-tasks/task-state.d.ts +115 -3
  100. package/dist/rooms-tasks/task-state.js +361 -11
  101. package/dist/rooms-tasks/templates.d.ts +4 -2
  102. package/dist/rooms-tasks/templates.js +42 -78
  103. package/dist/rooms-tasks/terminal.d.ts +3 -0
  104. package/dist/rooms-tasks/terminal.js +7 -3
  105. package/dist/rooms-tasks/types.d.ts +94 -17
  106. package/dist/rooms-tasks/types.js +14 -4
  107. package/dist/runner.d.ts +9 -2
  108. package/dist/runner.js +107 -40
  109. package/dist/session/acp.d.ts +9 -1
  110. package/dist/session/acp.js +35 -9
  111. package/dist/session/arbiter.d.ts +2 -1
  112. package/dist/session/arbiter.js +2 -0
  113. package/dist/session/codex-app-server-transport.d.ts +53 -0
  114. package/dist/session/codex-app-server-transport.js +184 -0
  115. package/dist/session/codex-app-server.d.ts +122 -0
  116. package/dist/session/codex-app-server.js +1031 -0
  117. package/dist/session/control.d.ts +14 -1
  118. package/dist/session/control.js +38 -0
  119. package/dist/session/conversation-types.d.ts +4 -4
  120. package/dist/session/types.d.ts +24 -6
  121. package/dist/session/types.js +28 -0
  122. package/dist/spawn.d.ts +6 -31
  123. package/dist/spawn.js +57 -234
  124. package/dist/temp-lifecycle.d.ts +1 -1
  125. package/dist/watchdog/config.d.ts +10 -7
  126. package/dist/watchdog/config.js +29 -19
  127. package/dist/watchdog/run.js +4 -13
  128. package/dist/web/fleet-config-service.d.ts +1 -0
  129. package/dist/web/fleet-config-service.js +27 -8
  130. package/dist/web/runtime.js +11 -6
  131. package/dist/web/server.js +28 -1
  132. package/dist/web/topology-promote.js +8 -1
  133. package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
  134. package/dist/web-app/index.html +1 -1
  135. package/package.json +3 -2
  136. package/presets/brain-catalog.json +16 -0
  137. package/presets/fleet/agent_templates/Critic.yaml +4 -0
  138. package/presets/fleet/agent_templates/Developer.yaml +4 -0
  139. package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
  140. package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
  141. package/presets/fleet/brains/claude-default.yaml +5 -0
  142. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  143. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  144. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  145. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  146. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  147. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  148. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  149. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  150. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  151. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  152. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  153. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  154. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  155. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  156. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  157. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  158. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  159. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  188. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  189. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  190. package/presets/fleet/roles/Coordinator.yaml +30 -0
  191. package/presets/fleet/roles/Critic.yaml +19 -0
  192. package/presets/fleet/roles/Developer.yaml +19 -0
  193. package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
  194. package/presets/fleet/room_templates/pair.yaml +10 -0
  195. package/presets/fleet/room_templates/single.yaml +7 -0
  196. package/presets/fleet/room_templates/team.yaml +12 -0
  197. package/presets/fleet.yaml +7 -0
  198. package/presets/manifest.json +6 -0
  199. package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
@@ -2,10 +2,11 @@ import { join } from 'node:path';
2
2
  import { withFileLock } from '../atomic-file.js';
3
3
  import { stateRoot } from '../paths.js';
4
4
  import { deleteManagedRoom } from './close.js';
5
- import { beginTaskTerminalIntent, finishTaskTerminalIntent, getTask, setTaskTerminalIntentError, } from './task-state.js';
5
+ import { assertNoPendingDeletion, beginTaskTerminalIntent, finishTaskTerminalIntent, getTask, setTaskTerminalIntentError, } from './task-state.js';
6
6
  import { getRoomRecord } from './room-state.js';
7
- const TASK_OPERATION_LOCK_STALE_MS = 10 * 60_000;
8
- function taskOperationLockPath(taskId) {
7
+ export const TASK_OPERATION_LOCK_STALE_MS = 10 * 60_000;
8
+ /** Common per-task operation lock serializing terminal, deletion, and recovery acceptance. */
9
+ export function taskOperationLockPath(taskId) {
9
10
  return join(stateRoot(), 'locks', 'task-terminal', encodeURIComponent(taskId));
10
11
  }
11
12
  function errorText(error) {
@@ -24,6 +25,9 @@ export function acceptTaskTerminalIntent(input) {
24
25
  export function settleTaskTerminalIntent(input) {
25
26
  return withFileLock(taskOperationLockPath(input.taskId), async () => {
26
27
  const current = getTask(input.taskId);
28
+ // Deletion supersedes a pending terminal intent: fail boundedly before any
29
+ // room side effect so the deletion worker owns all remaining cleanup.
30
+ assertNoPendingDeletion(current);
27
31
  const intent = current.terminal_intent;
28
32
  if (!intent)
29
33
  throw new Error(`task ${input.taskId} has no accepted terminal intent`);
@@ -5,6 +5,7 @@
5
5
  * membership, and archive. Messenger Server owns owner presentation.
6
6
  * Fleet stores only IDs, CIDs, orchestration state, and saga cursors.
7
7
  */
8
+ import type { AgentLaunchConfiguration } from '../lifecycle-summary.js';
8
9
  export type TaskState = 'backlog' | 'provisioning' | 'active' | 'review' | 'done' | 'cancelled' | 'failed';
9
10
  export declare const TASK_TERMINAL_STATES: readonly TaskState[];
10
11
  export declare const TASK_CANCELLABLE_STATES: readonly TaskState[];
@@ -44,6 +45,47 @@ export interface TaskTerminalIntent {
44
45
  first_failure?: string;
45
46
  first_recovery_hint?: string;
46
47
  }
48
+ /** Origin evidence for an accepted deletion; retained until the record is unlinked. */
49
+ export type TaskDeletionActor = {
50
+ kind: 'local_control';
51
+ surface: 'cli' | 'web';
52
+ } | {
53
+ kind: 'authenticated_owner';
54
+ surface: 'messenger';
55
+ cid: string;
56
+ };
57
+ export type TaskDeletionMemberPhase = 'pending' | 'stop_requested' | 'liveness_absent' | 'archive_secured' | 'identity_absent';
58
+ /**
59
+ * Durable retirement cursor for a managed member when no room orchestration
60
+ * record exists to carry the equivalent seat cursor.
61
+ */
62
+ export interface TaskDeletionMemberCursor {
63
+ name: string;
64
+ identity_cid: string;
65
+ phase: TaskDeletionMemberPhase;
66
+ launch_id?: string;
67
+ archive_path?: string;
68
+ updated_at: string;
69
+ }
70
+ /**
71
+ * First-wins durable deletion intent. There is no settled status: settlement
72
+ * ends by unlinking the task record, so physical absence is the completion
73
+ * evidence. While pending, the task is hidden from normal operation and every
74
+ * lifecycle mutation or room publication is rejected.
75
+ */
76
+ export interface TaskDeletionIntent {
77
+ status: 'pending';
78
+ accepted_at: string;
79
+ actor: TaskDeletionActor;
80
+ room_id?: string;
81
+ /** Snapshot of managed members at acceptance; the missing-room retirement evidence. */
82
+ members: TaskDeletionMemberCursor[];
83
+ error?: string;
84
+ error_at?: string;
85
+ recovery_hint?: string;
86
+ first_failure?: string;
87
+ first_recovery_hint?: string;
88
+ }
47
89
  export interface TaskRecord {
48
90
  task_id: string;
49
91
  /** Stable organizational list identifier. Missing legacy values mean `default`. */
@@ -56,6 +98,14 @@ export interface TaskRecord {
56
98
  state: TaskState;
57
99
  blocked?: TaskBlocked;
58
100
  template?: TaskTemplateRef;
101
+ execution_plan?: {
102
+ schema_version: 1;
103
+ snapshot: TemplateSnapshot;
104
+ room_policy?: RoomLaunchPolicy;
105
+ overrides: Record<string, unknown>;
106
+ overrides_hash: string;
107
+ plan_hash: string;
108
+ };
59
109
  no_room?: boolean;
60
110
  room_id?: string;
61
111
  room_identity_cid?: string;
@@ -67,6 +117,7 @@ export interface TaskRecord {
67
117
  ended_at?: string;
68
118
  outcome?: TaskOutcome;
69
119
  terminal_intent?: TaskTerminalIntent;
120
+ deletion?: TaskDeletionIntent;
70
121
  }
71
122
  export interface TaskListRecord {
72
123
  list_id: string;
@@ -100,6 +151,16 @@ export interface RoomMemberLaunchState {
100
151
  /** Expected authenticated proxy caller while adopting a post-spawn crash. */
101
152
  caller_role?: string;
102
153
  mission_sha256?: string;
154
+ /** Redacted, deterministic effective Agent launch definition retained for retry inspection. */
155
+ agent_definition?: Record<string, unknown>;
156
+ agent_fingerprint?: string;
157
+ agent_template?: string;
158
+ agent_template_hash?: string;
159
+ /**
160
+ * Operator-facing launch configuration captured from the exact resolved
161
+ * launch state; the single source for later Task/Room lifecycle reports.
162
+ */
163
+ presentation?: AgentLaunchConfiguration;
103
164
  launch_id?: string;
104
165
  updated_at: string;
105
166
  error?: string;
@@ -172,6 +233,8 @@ export interface RoomOrchestrationRecord {
172
233
  goal?: string;
173
234
  task_id?: string;
174
235
  template_snapshot?: TemplateSnapshot;
236
+ /** Resolved once at launch; legacy absence means non-anonymous. */
237
+ room_policy?: RoomLaunchPolicy;
175
238
  saga: SagaCursor;
176
239
  provisioning_detail?: ProvisioningDetail;
177
240
  owner_seat_cid?: string;
@@ -189,35 +252,48 @@ export interface TemplateMemberSlot {
189
252
  slot: string;
190
253
  role: string;
191
254
  count: number;
192
- role_ref: string;
193
- overrides?: TemplateRoleOverrides;
194
- }
195
- export interface TemplateRoleOverrides {
196
- harness?: string;
197
- model?: string;
198
- model_chain?: string[];
199
- permissions?: Record<string, unknown>;
200
- isolation?: Record<string, unknown>;
201
- cwd?: string;
202
- env?: Record<string, string>;
203
- persona?: string;
204
- mission?: string;
255
+ /** Stable reference to an inert Agent Template, never a persistent Agent. */
256
+ agent_template: string;
205
257
  }
206
258
  export interface TemplateRoomConfig {
207
259
  quiet_membership?: boolean;
208
260
  anonymous?: boolean;
209
261
  }
262
+ export interface RoomLaunchPolicy {
263
+ anonymous: boolean;
264
+ }
265
+ export declare const LEGACY_ROOM_LAUNCH_POLICY: Readonly<RoomLaunchPolicy>;
266
+ /** Validate durable policy before it can influence room creation or recovery. */
267
+ export declare function storedRoomLaunchPolicy(value: RoomLaunchPolicy | undefined): RoomLaunchPolicy;
210
268
  export interface TemplateDefinition {
211
269
  name: string;
212
270
  version: number;
213
271
  description: string;
214
- builtin?: boolean;
272
+ /** Authoring provenance only; excluded from snapshots and semantic hashes. */
273
+ sourceFile?: string;
215
274
  room?: TemplateRoomConfig;
216
275
  contract?: string;
217
276
  members: TemplateMemberSlot[];
218
277
  }
219
- export interface TemplateSnapshot extends TemplateDefinition {
278
+ export interface TemplateSnapshotMember extends TemplateMemberSlot {
279
+ /** Secret-safe public projection; launch material lives in the sealed snapshot. */
280
+ agent_projection?: Record<string, unknown>;
281
+ agent_template_hash?: string;
282
+ /** Private sealed-snapshot lookup key; harmless provenance, not an identity. */
283
+ launch_definition_id?: string;
284
+ role_preset?: {
285
+ id: string;
286
+ hash: string;
287
+ };
288
+ brain_preset?: {
289
+ id: string;
290
+ hash: string;
291
+ };
292
+ }
293
+ export interface TemplateSnapshot extends Omit<TemplateDefinition, 'members'> {
294
+ members: TemplateSnapshotMember[];
220
295
  content_hash: string;
296
+ launch_snapshot_hash?: string;
221
297
  }
222
298
  export interface RoomsOwnerConfig {
223
299
  provider: string;
@@ -252,5 +328,6 @@ export declare const ROOMS_COWORK_KEYS: readonly ["config"];
252
328
  export declare const ROOMS_DEFAULTS_KEYS: readonly ["template", "attach_owner", "close_when_task_done"];
253
329
  export declare const TASKS_KEYS: readonly ["default_room_template", "create_mode", "close_room_on_done", "retain_completed_for"];
254
330
  export declare const TEMPLATE_KEYS: readonly ["version", "description", "room", "contract", "members", "override_builtin"];
255
- export declare const TEMPLATE_MEMBER_KEYS: readonly ["slot", "role", "count", "role_ref", "overrides"];
256
- export declare const TEMPLATE_OVERRIDE_KEYS: readonly ["harness", "model", "model_chain", "permissions", "isolation", "cwd", "env", "persona", "mission"];
331
+ export declare const TEMPLATE_ROOM_KEYS: readonly ["quiet_membership", "anonymous"];
332
+ /** `agent` is accepted only so validation can emit its actionable migration error. */
333
+ export declare const TEMPLATE_MEMBER_KEYS: readonly ["slot", "role", "count", "agent_template", "agent"];
@@ -7,6 +7,17 @@
7
7
  */
8
8
  export const TASK_TERMINAL_STATES = ['done', 'cancelled', 'failed'];
9
9
  export const TASK_CANCELLABLE_STATES = ['backlog', 'provisioning', 'active', 'review'];
10
+ export const LEGACY_ROOM_LAUNCH_POLICY = Object.freeze({
11
+ anonymous: false,
12
+ });
13
+ /** Validate durable policy before it can influence room creation or recovery. */
14
+ export function storedRoomLaunchPolicy(value) {
15
+ if (value === undefined)
16
+ return { ...LEGACY_ROOM_LAUNCH_POLICY };
17
+ if (typeof value.anonymous !== 'boolean')
18
+ throw new Error('invalid durable Room launch policy; anonymous must be a boolean');
19
+ return { ...value };
20
+ }
10
21
  // ── Validation keys ─────────────────────────────────────────────────────
11
22
  export const ROOMS_KEYS = ['cowork', 'owner', 'defaults'];
12
23
  export const ROOMS_OWNER_KEYS = ['provider', 'public_invite', 'public_invite_file', 'expected_cid', 'role'];
@@ -14,7 +25,6 @@ export const ROOMS_COWORK_KEYS = ['config'];
14
25
  export const ROOMS_DEFAULTS_KEYS = ['template', 'attach_owner', 'close_when_task_done'];
15
26
  export const TASKS_KEYS = ['default_room_template', 'create_mode', 'close_room_on_done', 'retain_completed_for'];
16
27
  export const TEMPLATE_KEYS = ['version', 'description', 'room', 'contract', 'members', 'override_builtin'];
17
- export const TEMPLATE_MEMBER_KEYS = ['slot', 'role', 'count', 'role_ref', 'overrides'];
18
- export const TEMPLATE_OVERRIDE_KEYS = [
19
- 'harness', 'model', 'model_chain', 'permissions', 'isolation', 'cwd', 'env', 'persona', 'mission',
20
- ];
28
+ export const TEMPLATE_ROOM_KEYS = ['quiet_membership', 'anonymous'];
29
+ /** `agent` is accepted only so validation can emit its actionable migration error. */
30
+ export const TEMPLATE_MEMBER_KEYS = ['slot', 'role', 'count', 'agent_template', 'agent'];
package/dist/runner.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type ResolvedRole } from './config.js';
2
2
  import { type MonitorHandle, type MonitorOpts, type FetchLike } from './monitor.js';
3
+ import { type DaemonGenerationProbe } from './daemon-recovery.js';
3
4
  import { type Exec } from './exec.js';
4
5
  import { RoleControlServer } from './session/control.js';
5
6
  import type { AgentSession, ExitRecord, TurnResult } from './session/types.js';
@@ -15,6 +16,7 @@ export interface RunnerDeps {
15
16
  log(line: string): void;
16
17
  /** HTTP transport for the monitor's daemon long-poll (injectable for tests). */
17
18
  fetch: FetchLike;
19
+ probeGeneration(env: NodeJS.ProcessEnv): Promise<DaemonGenerationProbe>;
18
20
  /** Construct the supervisor mail monitor (injectable so tests stub it out). */
19
21
  createMonitor(opts: MonitorOpts): MonitorHandle;
20
22
  /** Construct trusted owner ingress (injectable for lifecycle tests). */
@@ -23,7 +25,7 @@ export interface RunnerDeps {
23
25
  /** Neutral construction seam for deterministic runner lifecycle tests. */
24
26
  startAgentSession(adapter: AgentSessionAdapter, options: AgentSessionStartOptions): Promise<AgentSession>;
25
27
  /** Construct the authenticated role control route (injectable where sockets are unavailable). */
26
- createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
28
+ createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setFleetAuditor' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
27
29
  /** Acquire the cross-process owner-channel binder lease before replacing the control socket. */
28
30
  acquireOwnerBinder(stateDir: string, role: string, identity: string): Promise<OwnerBinderLease>;
29
31
  /** Ask the still-authenticated predecessor to emit the fixed recovery notice. */
@@ -31,6 +33,11 @@ export interface RunnerDeps {
31
33
  /** Lets a test (or a shutdown path) end the supervised restart loop. */
32
34
  shouldStop?(): boolean;
33
35
  }
36
+ export declare const SUPERVISOR_RECYCLE_REQUIRED = "OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED";
37
+ export declare class SupervisorRecycleRequiredError extends Error {
38
+ readonly code = "OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED";
39
+ constructor();
40
+ }
34
41
  /** Environment injected only into the managed harness process. */
35
42
  export declare function managedFleetProxyEnv(role: ResolvedRole, stateDir: string): Record<string, string>;
36
43
  /**
@@ -176,4 +183,4 @@ export declare function runSupervised(name: string, opts?: {
176
183
  allowResumeRotation?: boolean;
177
184
  }, d: Partial<RunnerDeps>) => Promise<AttemptResult>): Promise<RestartLedger>;
178
185
  /** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */
179
- export declare function runTemp(name: string, deps?: Partial<RunnerDeps>): Promise<void>;
186
+ export declare function runTemp(name: string, deps?: Partial<RunnerDeps>, attempt?: typeof runOnce): Promise<void>;
package/dist/runner.js CHANGED
@@ -6,13 +6,15 @@ import { agentDir, stateRoot } from './paths.js';
6
6
  import { loadConfig, findRole, isolationContextFor, resolveMonitorConfig, resolvePermissions, } from './config.js';
7
7
  import { getAdapter } from './harness/registry.js';
8
8
  import { createMonitor, probeIdentityPresence, } from './monitor.js';
9
+ import { DaemonGenerationObserver, RoleRecoveryController, probeDaemonGeneration, } from './daemon-recovery.js';
10
+ import { recoverAgentIdentity } from './agent-recovery-gate.js';
9
11
  import { realExec } from './exec.js';
10
12
  import { resolveIsolation } from './isolation/policy.js';
11
13
  import { selectIsolationBackend } from './isolation/registry.js';
12
14
  import { resourceArgs, cpuControllerDelegated } from './isolation/resources.js';
13
15
  import { resolveLaunchRuntime } from './isolation/runtime.js';
14
16
  import { controlRequest, RoleControlServer } from './session/control.js';
15
- import { ACP_CANCEL_DEADLINE_EXCEEDED, classifyShellStatus } from './session/types.js';
17
+ import { ACP_CANCEL_DEADLINE_EXCEEDED, CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED, classifyShellStatus, } from './session/types.js';
16
18
  import { effectiveModelForRole, modelRecoveryHeld, reconcileModelRecovery, recordModelFailure, classifyFailureText, } from './model-recovery.js';
17
19
  import { rotateWorklog } from './worklog.js';
18
20
  import { reconcilePermanentRoleIdentities } from './creation.js';
@@ -24,6 +26,14 @@ import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-prox
24
26
  import { effectivePermissionMode } from './permissions.js';
25
27
  import { assertModelPinReachesChild, effectiveRoleModel, repinModelEnv } from './model-env.js';
26
28
  import { archiveTempState, markTempSupervisorActive, requestedTempStopReason, } from './temp-lifecycle.js';
29
+ export const SUPERVISOR_RECYCLE_REQUIRED = 'OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED';
30
+ export class SupervisorRecycleRequiredError extends Error {
31
+ code = SUPERVISOR_RECYCLE_REQUIRED;
32
+ constructor() {
33
+ super('owner channel requires a fresh supervisor process after unproven client quiescence');
34
+ this.name = 'SupervisorRecycleRequiredError';
35
+ }
36
+ }
27
37
  const defaultDeps = () => ({
28
38
  exec: realExec,
29
39
  cpuDelegated: () => cpuControllerDelegated(),
@@ -38,6 +48,7 @@ const defaultDeps = () => ({
38
48
  now: () => Date.now(),
39
49
  log: line => process.stderr.write(line + '\n'),
40
50
  fetch: (url, init) => globalThis.fetch(url, init),
51
+ probeGeneration: env => probeDaemonGeneration((url, init) => globalThis.fetch(url, init), env),
41
52
  createMonitor: opts => createMonitor(opts),
42
53
  createOwnerChannel: opts => new OwnerChannel(opts),
43
54
  startAgentSession: (adapter, options) => adapter.start(options),
@@ -453,6 +464,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
453
464
  const runCwd = role.cwd && existsSync(role.cwd) ? role.cwd : dir;
454
465
  const prep = await adapter.prepareSession(role, { stateDir: dir, runCwd });
455
466
  const sessionBackend = role.session ?? 'acp';
467
+ const sessionLabel = sessionBackend === 'acp' ? 'ACP' : 'Codex app-server';
456
468
  let launch = adapter.agentSession.prepareLaunch(role, prep);
457
469
  // Isolation is additive: only roles that declare `isolation:` are wrapped.
458
470
  let wrappedArgv = launch.argv;
@@ -532,6 +544,9 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
532
544
  name, identity: role.identity, agentDir: dir, cfg: role.monitor,
533
545
  deps: resolvedMonitorDeps,
534
546
  }) : null;
547
+ const daemonObserver = new DaemonGenerationObserver();
548
+ const initialDaemon = daemonObserver.observe(await deps.probeGeneration(resolvedMonitorDeps.env));
549
+ let sessionStartedWithoutDaemonBaseline = initialDaemon.kind !== 'baseline';
535
550
  if (monitor)
536
551
  await monitor.prime({ resetCursor: resetMonitorCursor });
537
552
  rmSync(exitFile, { force: true });
@@ -540,13 +555,13 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
540
555
  let control;
541
556
  let unsubscribeRecovery;
542
557
  let monitorLoop;
543
- let acpStartupComplete = false;
558
+ let sessionStartupComplete = false;
544
559
  let sessionClosed = false;
545
560
  let ownerChannel;
546
561
  let ownerBinder;
547
- const pendingFleetSpawnNotices = [];
548
562
  let loopManager;
549
563
  let arbiter;
564
+ let recoveryController;
550
565
  let reloadLoopConfig;
551
566
  let loopGeneration = JSON.stringify((role.loops ?? []).map(loop => [
552
567
  loop.name, loop.definitionHash, loop.promptHash,
@@ -571,7 +586,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
571
586
  unsubscribeRecovery = agentSession.subscribe(event => {
572
587
  if (event.kind !== 'error' || !event.text)
573
588
  return;
574
- const evidence = classifyFailureText(event.text, 'acp', new Date(deps.now()).toISOString());
589
+ const evidence = classifyFailureText(event.text, sessionBackend, new Date(deps.now()).toISOString());
575
590
  if (evidence)
576
591
  resolvedMonitorDeps.onFailureEvidence?.(evidence);
577
592
  });
@@ -606,23 +621,22 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
606
621
  unsubscribeRecovery?.();
607
622
  throw error;
608
623
  }
609
- control.setFleetSpawner(async (requested) => {
610
- const event = await executeManagedSpawn(role, configPath, requested, deps.log);
611
- if (!role.owner_channel)
612
- return event;
613
- if (ownerChannel?.notifyFleetSpawn) {
614
- try {
615
- await ownerChannel.notifyFleetSpawn(event);
616
- }
617
- catch (error) {
618
- deps.log(`[${name}] spawned-agent owner notice failed: `
619
- + `${error?.message ?? String(error)}`);
624
+ if (!role.roomMemberStartup)
625
+ control.setFleetSpawner(async (requested) => {
626
+ const event = await executeManagedSpawn(role, configPath, requested, deps.log);
627
+ // Room-member launches are collected with their Task/Room transaction so
628
+ // causal delivery is Task → Room → Agents → Room active → Task active.
629
+ if (!requested.roomMemberStartup && role.owner_channel && ownerChannel?.notifyFleetSpawn) {
630
+ try {
631
+ await ownerChannel.notifyFleetSpawn(event);
632
+ }
633
+ catch (error) {
634
+ deps.log(`[${name}] Agent lifecycle notice delivery failed: `
635
+ + `${error?.message ?? String(error)}`);
636
+ }
620
637
  }
621
- }
622
- else
623
- pendingFleetSpawnNotices.push(event);
624
- return event;
625
- });
638
+ return event;
639
+ });
626
640
  resolvedMonitorDeps.delivery = {
627
641
  // A wake is only delivered when its turn TERMINATES successfully. A
628
642
  // refusal or a cancellation reached the agent and was not acted on, so
@@ -633,7 +647,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
633
647
  // steer into the live turn instead; after it completes, honor the
634
648
  // configured interrupt policy normally.
635
649
  const policy = options?.interrupt;
636
- const interrupt = policy === true && acpStartupComplete;
650
+ const interrupt = policy === true && sessionStartupComplete;
637
651
  const promptOptions = {
638
652
  interrupt, steer: true,
639
653
  ...(interrupt ? { interruptSource: 'fleet-monitor' } : {}),
@@ -641,7 +655,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
641
655
  };
642
656
  // Startup is already a protected boundary: as with immediate mode,
643
657
  // steer rather than waiting on/cancelling the runner-owned first turn.
644
- const result = policy === 'after_tool' && acpStartupComplete
658
+ const result = policy === 'after_tool' && sessionStartupComplete
645
659
  ? await arbiter.submitPromptAfterTool(text, promptOptions)
646
660
  : await arbiter.submitPrompt(text, promptOptions);
647
661
  const steered = result.accepted
@@ -702,20 +716,20 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
702
716
  exit: {
703
717
  version: 1,
704
718
  class: 'program-exit',
705
- detail: `ACP startup triggered model recovery (${modelRecovery})`,
719
+ detail: `${sessionLabel} startup triggered model recovery (${modelRecovery})`,
706
720
  },
707
721
  rotated: false,
708
722
  mode,
709
723
  modelRecovery,
710
724
  };
711
725
  }
712
- throw new Error(`[${name}] ACP startup prompt ${started.outcome}` +
726
+ throw new Error(`[${name}] ${sessionLabel} startup prompt ${started.outcome}` +
713
727
  `${started.detail ? `: ${started.detail}` : ''}`);
714
728
  }
715
729
  if (interruptedForWake)
716
- deps.log(`[${name}] ACP startup prompt cancelled by ${started.cancellationSource}; `
730
+ deps.log(`[${name}] ${sessionLabel} startup prompt cancelled by ${started.cancellationSource}; `
717
731
  + 'keeping temporary supervisor alive');
718
- acpStartupComplete = true;
732
+ sessionStartupComplete = true;
719
733
  if (role.owner_channel) {
720
734
  ownerChannel = deps.createOwnerChannel({
721
735
  role: name,
@@ -743,18 +757,14 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
743
757
  throw new Error(`[${name}] owner channel failed to start: `
744
758
  + `${error?.message ?? String(error)}`);
745
759
  }
746
- for (const event of pendingFleetSpawnNotices.splice(0)) {
747
- try {
748
- await ownerChannel.notifyFleetSpawn?.(event);
749
- }
750
- catch (error) {
751
- deps.log(`[${name}] deferred spawned-agent owner notice failed: `
752
- + `${error?.message ?? String(error)}`);
753
- }
754
- }
755
760
  }
756
- if (ownerChannel)
761
+ if (ownerChannel) {
757
762
  control.setOwnerChannel(ownerChannel);
763
+ control.setFleetAuditor({
764
+ begin: (requestId, argv) => ownerChannel.beginFleetCommandAudit(requestId, argv),
765
+ finish: input => ownerChannel.finishFleetCommandAudit(input),
766
+ });
767
+ }
758
768
  reloadLoopConfig = async () => {
759
769
  const nextRole = findRole(loadConfig(configPath), name);
760
770
  const definitions = nextRole.loops ?? [];
@@ -801,16 +811,49 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
801
811
  // The monitor loop lives exactly as long as the session: it starts once the
802
812
  // pane pid is known and is stopped when that pid dies (task dies with runner).
803
813
  monitorLoop ??= monitor?.run(pid);
814
+ recoveryController = new RoleRecoveryController({
815
+ role: name, identity: role.identity, stateDir: dir, now: deps.now, sleep: deps.sleep,
816
+ log: deps.log,
817
+ recoverAgent: async () => {
818
+ const evidence = await recoverAgentIdentity(arbiter, role.identity);
819
+ return evidence.ok ? { ok: true } : { ok: false, reason: evidence.reason };
820
+ },
821
+ recoverOwner: async (epoch) => {
822
+ if (!ownerChannel?.recover)
823
+ return { ok: true };
824
+ try {
825
+ await ownerChannel.recover(epoch);
826
+ return { ok: true };
827
+ }
828
+ catch (error) {
829
+ return { ok: false, reason: error instanceof Error
830
+ ? `OWNER_${error.name.toUpperCase()}` : 'OWNER_UNKNOWN_ERROR' };
831
+ }
832
+ },
833
+ });
804
834
  const start = deps.now();
805
835
  let nextLoopReloadAt = deps.now() + 30_000;
806
836
  let nextIdentityPollAt = deps.now();
837
+ let nextDaemonProbeAt = deps.now();
807
838
  let lastReloadError = '';
808
839
  let identityObserved = false;
809
840
  let identityAbsentSince;
810
841
  let retirementReason;
842
+ let supervisorRecycleRequired = false;
811
843
  while (sessionHandle.isAlive()) {
812
844
  await deps.sleep(temp ? 500 : 2000);
813
845
  const now = deps.now();
846
+ if (now >= nextDaemonProbeAt) {
847
+ nextDaemonProbeAt = now + 2_000;
848
+ const observation = daemonObserver.observe(await deps.probeGeneration(resolvedMonitorDeps.env));
849
+ if (observation.kind === 'lost')
850
+ recoveryController.noteLoss(observation.reason);
851
+ if (observation.kind === 'changed' || observation.kind === 'available'
852
+ || (observation.kind === 'baseline' && sessionStartedWithoutDaemonBaseline)) {
853
+ sessionStartedWithoutDaemonBaseline = false;
854
+ void recoveryController.recover(observation.generation).catch(error => deps.log(`[${name}] daemon recovery controller failed: ${error?.name ?? 'Error'}`));
855
+ }
856
+ }
814
857
  if (temp && deps.shouldStop?.()) {
815
858
  retirementReason = requestedTempStopReason(dir) ?? 'supervisor-signal';
816
859
  deps.log(`[${name}] temporary supervisor retirement requested (${retirementReason})`);
@@ -861,6 +904,11 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
861
904
  control?.setLoopManager(undefined);
862
905
  await loopManager.stop();
863
906
  }
907
+ // Let already-settled path operations publish their aggregate result before
908
+ // the shutdown fence invalidates the epoch. This never waits on I/O.
909
+ await Promise.resolve();
910
+ await Promise.resolve();
911
+ recoveryController?.cancel();
864
912
  control?.setConfigReloader(undefined);
865
913
  // Close the authenticated control route before releasing the binder lease;
866
914
  // otherwise the predecessor can unlink the replacement's new socket.
@@ -871,7 +919,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
871
919
  }
872
920
  if (ownerChannel)
873
921
  await ownerChannel.close();
874
- ownerBinder?.release();
922
+ if (ownerChannel?.binderReleaseSafe?.() === false) {
923
+ deps.log(`[${name}] owner binder retained because client quiescence was not proven at shutdown`);
924
+ supervisorRecycleRequired = true;
925
+ }
926
+ else
927
+ ownerBinder?.release();
875
928
  if (monitor) {
876
929
  monitor.stop();
877
930
  await monitorLoop;
@@ -895,7 +948,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
895
948
  rotated = true;
896
949
  deps.log(`[${name}] ${why} -> rotated session-id; next start is FRESH`);
897
950
  };
898
- if (exitRecord.detail.includes(ACP_CANCEL_DEADLINE_EXCEEDED))
951
+ if (exitRecord.detail.includes(ACP_CANCEL_DEADLINE_EXCEEDED)
952
+ || exitRecord.detail.includes(CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED))
899
953
  // This is a deliberate adapter reclamation, not evidence that resume state
900
954
  // is poisoned. Preserve the context even when the resumed generation hits
901
955
  // the same bound immediately; runSupervised still counts the fast exit and
@@ -920,6 +974,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
920
974
  }
921
975
  else
922
976
  deps.log(`[${name}] ${exitRecord.detail} (${elapsed.toFixed(0)}s) -> next start RESUMES context`);
977
+ if (supervisorRecycleRequired)
978
+ throw new SupervisorRecycleRequiredError();
923
979
  return {
924
980
  elapsedSecs: elapsed, exit: exitRecord, rotated, mode, modelRecovery,
925
981
  ...(retirementReason ? { retirementReason } : {}),
@@ -996,6 +1052,16 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
996
1052
  result = await attempt(name, { configPath: opts.configPath, allowResumeRotation: !ledger.resumeDiscarded }, deps);
997
1053
  }
998
1054
  catch (e) {
1055
+ if (e instanceof SupervisorRecycleRequiredError) {
1056
+ writeRestartLedger(dir, {
1057
+ ...ledger,
1058
+ lastReason: SUPERVISOR_RECYCLE_REQUIRED,
1059
+ nextDelayMs: 0,
1060
+ updatedAt: stamp(),
1061
+ });
1062
+ deps.log(`[${name}] supervisor recycle required: ${SUPERVISOR_RECYCLE_REQUIRED}`);
1063
+ throw e;
1064
+ }
999
1065
  // A session that could not even start is an immediate failure like any
1000
1066
  // other; it must count, or an unstartable role loops forever.
1001
1067
  result = {
@@ -1089,7 +1155,7 @@ function fastFailSecsFor(name, configPath) {
1089
1155
  }
1090
1156
  }
1091
1157
  /** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */
1092
- export async function runTemp(name, deps = {}) {
1158
+ export async function runTemp(name, deps = {}, attempt = runOnce) {
1093
1159
  const dir = agentDir(name, true);
1094
1160
  await markTempSupervisorActive(dir);
1095
1161
  let signal;
@@ -1100,7 +1166,7 @@ export async function runTemp(name, deps = {}) {
1100
1166
  let result;
1101
1167
  let failure;
1102
1168
  try {
1103
- result = await runOnce(name, { temp: true }, {
1169
+ result = await attempt(name, { temp: true }, {
1104
1170
  ...deps,
1105
1171
  shouldStop: () => Boolean(signal) || (deps.shouldStop?.() ?? false),
1106
1172
  });
@@ -1114,6 +1180,7 @@ export async function runTemp(name, deps = {}) {
1114
1180
  const requested = requestedTempStopReason(dir);
1115
1181
  const reason = requested
1116
1182
  ?? result?.retirementReason
1183
+ ?? (failure instanceof SupervisorRecycleRequiredError ? 'supervisor-recycle' : undefined)
1117
1184
  ?? (signal ? 'supervisor-signal' : failure ? 'startup-failure' : 'session-ended');
1118
1185
  // A service-manager stop can make the child connection close before the
1119
1186
  // runner reaches its normal loop. That is still a successful requested
@@ -28,6 +28,8 @@ export declare function promptContentBlocks(text: string, origin?: PromptOrigin)
28
28
  export declare function runtimeSelector(options: acp.SessionConfigOption[] | null | undefined, category: string): RuntimeSelectorMetadata | undefined;
29
29
  export interface AcpSessionOptions {
30
30
  name: string;
31
+ /** Harness identity used only for honest optional capability reporting. */
32
+ harness?: string;
31
33
  argv: string[];
32
34
  cwd: string;
33
35
  env: Record<string, string>;
@@ -36,6 +38,11 @@ export interface AcpSessionOptions {
36
38
  permissions: CommonPermissions;
37
39
  /** Native permission-mode id to request via session/set_mode; undefined keeps the agent default. */
38
40
  modeId?: string;
41
+ /** Ordered explicit Brain choices that must be applied before readiness. */
42
+ configSelections?: Array<{
43
+ configId: string;
44
+ value: string;
45
+ }>;
39
46
  /** Adapter-resolved live permission policy; separate from ACP agent-specific session modes. */
40
47
  permissionMode?: NonNullable<SessionSnapshot['permissionMode']>;
41
48
  /** Adapter-authenticated request-metadata vocabulary; never inferred from ACP `_meta`. */
@@ -84,6 +91,7 @@ export declare function classifyStopReason(stopReason: string | undefined): Turn
84
91
  export declare class AcpSession implements AgentSession {
85
92
  private readonly options;
86
93
  readonly backend: "acp";
94
+ readonly capabilities: import("./types.js").AgentSessionCapabilities;
87
95
  readonly pid: number;
88
96
  private readonly child;
89
97
  private readonly events;
@@ -110,7 +118,7 @@ export declare class AcpSession implements AgentSession {
110
118
  private queueDepth;
111
119
  private exit;
112
120
  private steeringSupported;
113
- private capabilities?;
121
+ private agentCapabilities?;
114
122
  private runtimeModel?;
115
123
  private reasoningEffort?;
116
124
  private controllerCount;