@ours.network/fleet 1.1.0-nightly.9 → 1.1.1

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 +285 -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 +8 -1
  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 +562 -289
  13. package/dist/briefing.js +79 -22
  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 +129 -47
  18. package/dist/config.d.ts +42 -3
  19. package/dist/config.js +202 -29
  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 +195 -41
  24. package/dist/doctor.d.ts +2 -0
  25. package/dist/doctor.js +40 -4
  26. package/dist/fleet-command-audit.d.ts +22 -1
  27. package/dist/fleet-command-audit.js +355 -50
  28. package/dist/fleet-proxy.d.ts +5 -2
  29. package/dist/harness/claude-code-session.js +5 -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 +21 -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 +19 -3
  50. package/dist/owner-channel/channel.js +301 -120
  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/permissions.js +1 -1
  58. package/dist/preset-migration.d.ts +42 -0
  59. package/dist/preset-migration.js +450 -0
  60. package/dist/reports/artifact.d.ts +10 -0
  61. package/dist/reports/artifact.js +32 -0
  62. package/dist/reports/index.d.ts +5 -0
  63. package/dist/reports/index.js +5 -0
  64. package/dist/reports/render.d.ts +6 -0
  65. package/dist/reports/render.js +240 -0
  66. package/dist/reports/service.d.ts +42 -0
  67. package/dist/reports/service.js +57 -0
  68. package/dist/reports/tasks.d.ts +52 -0
  69. package/dist/reports/tasks.js +177 -0
  70. package/dist/reports/types.d.ts +315 -0
  71. package/dist/reports/types.js +6 -0
  72. package/dist/resolved-plan.js +13 -1
  73. package/dist/rooms-tasks/cli.d.ts +2 -0
  74. package/dist/rooms-tasks/cli.js +361 -285
  75. package/dist/rooms-tasks/close.d.ts +9 -1
  76. package/dist/rooms-tasks/close.js +23 -7
  77. package/dist/rooms-tasks/config.js +16 -25
  78. package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
  79. package/dist/rooms-tasks/cowork-adapter.js +13 -0
  80. package/dist/rooms-tasks/deletion.d.ts +51 -0
  81. package/dist/rooms-tasks/deletion.js +217 -0
  82. package/dist/rooms-tasks/external-worker.d.ts +8 -0
  83. package/dist/rooms-tasks/external-worker.js +38 -0
  84. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  85. package/dist/rooms-tasks/launch-snapshot.js +181 -0
  86. package/dist/rooms-tasks/member-overrides.d.ts +30 -0
  87. package/dist/rooms-tasks/member-overrides.js +204 -0
  88. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  89. package/dist/rooms-tasks/member-startup.js +9 -2
  90. package/dist/rooms-tasks/provision.js +146 -83
  91. package/dist/rooms-tasks/room-state.d.ts +1 -0
  92. package/dist/rooms-tasks/room-state.js +12 -1
  93. package/dist/rooms-tasks/task-room-name.d.ts +12 -0
  94. package/dist/rooms-tasks/task-room-name.js +61 -0
  95. package/dist/rooms-tasks/task-state.d.ts +115 -3
  96. package/dist/rooms-tasks/task-state.js +361 -11
  97. package/dist/rooms-tasks/templates.d.ts +4 -1
  98. package/dist/rooms-tasks/templates.js +34 -3
  99. package/dist/rooms-tasks/terminal.d.ts +3 -0
  100. package/dist/rooms-tasks/terminal.js +10 -4
  101. package/dist/rooms-tasks/types.d.ts +92 -7
  102. package/dist/rooms-tasks/types.js +14 -1
  103. package/dist/runner.d.ts +3 -0
  104. package/dist/runner.js +97 -47
  105. package/dist/session/acp.d.ts +27 -1
  106. package/dist/session/acp.js +307 -17
  107. package/dist/session/arbiter.d.ts +2 -1
  108. package/dist/session/arbiter.js +2 -0
  109. package/dist/session/codex-app-server-transport.d.ts +53 -0
  110. package/dist/session/codex-app-server-transport.js +184 -0
  111. package/dist/session/codex-app-server.d.ts +122 -0
  112. package/dist/session/codex-app-server.js +1031 -0
  113. package/dist/session/control.d.ts +1 -1
  114. package/dist/session/control.js +17 -1
  115. package/dist/session/conversation-types.d.ts +4 -4
  116. package/dist/session/stall-watchdog.d.ts +60 -0
  117. package/dist/session/stall-watchdog.js +210 -0
  118. package/dist/session/types.d.ts +29 -8
  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
@@ -9,7 +9,7 @@ export interface ControlRequest {
9
9
  version: 1 | 2 | 3;
10
10
  id: string;
11
11
  token: string;
12
- command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'interrupt_v2' | 'respond_permission_v2' | 'fleet_spawn' | 'fleet_audit_begin' | 'fleet_audit_finish';
12
+ command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'interrupt_v2' | 'respond_permission_v2' | 'fleet_spawn' | 'fleet_audit_begin' | 'fleet_audit_present' | 'fleet_audit_finish';
13
13
  text?: string;
14
14
  permissionId?: string;
15
15
  optionId?: string;
@@ -3,7 +3,7 @@ import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node
3
3
  import { createConnection, createServer } from 'node:net';
4
4
  import { join } from 'node:path';
5
5
  import { SessionControlError } from './types.js';
6
- import { validateFleetAuditBegin, validateFleetAuditFinish, } from '../fleet-command-audit.js';
6
+ import { validateFleetAuditBegin, validateFleetAuditFinish, validateFleetAuditPresentations, } from '../fleet-command-audit.js';
7
7
  import { interruptSession, queueSessionPrompt, respondSessionPermission, respondSessionPermissionV2, } from '../application/session-mutations.js';
8
8
  const MAX_LINE_BYTES = 64 * 1024;
9
9
  /** Commands that require protocol version 3. */
@@ -236,6 +236,9 @@ export class RoleControlServer {
236
236
  features: [
237
237
  'events_since', 'observer_follow', 'retained_range',
238
238
  ...(this.session.conversationPage ? ['conversation_v3'] : []),
239
+ ...(this.session.capabilities?.steering ? ['steering'] : []),
240
+ ...(this.session.capabilities?.permissions ? ['permissions'] : []),
241
+ ...(this.session.capabilities?.messagePhases ? ['message_phases'] : []),
239
242
  ],
240
243
  },
241
244
  });
@@ -346,6 +349,19 @@ export class RoleControlServer {
346
349
  this.write(socket, { version: 1, id: request.id, ok: true, result });
347
350
  return;
348
351
  }
352
+ case 'fleet_audit_present': {
353
+ if (!this.fleetAuditor)
354
+ throw new SessionControlError('rejected', 'fleet auditing is unavailable');
355
+ try {
356
+ validateFleetAuditPresentations(request.audit?.presentations);
357
+ }
358
+ catch {
359
+ throw new SessionControlError('rejected', 'valid Fleet lifecycle presentations are required');
360
+ }
361
+ await this.fleetAuditor.present(request.audit.presentations);
362
+ this.write(socket, { version: 1, id: request.id, ok: true, result: { delivered: true } });
363
+ return;
364
+ }
349
365
  case 'owner_channel_manage': {
350
366
  if (!request.ownerChannel || typeof request.ownerChannel.action !== 'string')
351
367
  throw new SessionControlError('rejected', 'owner-channel management action is required');
@@ -1,14 +1,14 @@
1
1
  import type { PromptOrigin, TurnCancellationSource, TurnOutcome } from './types.js';
2
2
  /**
3
- * Durable conversation domain schema for the ACP web console.
3
+ * Durable conversation domain schema for managed-session consoles.
4
4
  *
5
5
  * `SessionEvent` in ./types.js remains the compact diagnostic projection; the
6
6
  * types here describe the durable per-role conversation ledger. Nothing in this
7
- * file touches the wire: ACP updates are reduced into these shapes by the
8
- * normalizer, and the store assigns `seq`/`eventId`/timestamps.
7
+ * file touches the wire: transport updates are reduced into these shapes by a
8
+ * session adapter, and the store assigns `seq`/`eventId`/timestamps.
9
9
  */
10
10
  export type ConversationEventKind = 'prompt.admitted' | 'prompt.started' | 'prompt.interrupt_requested' | 'message.chunk' | 'message.replace' | 'thought.chunk' | 'thought.replace' | 'plan.replace' | 'tool.upsert' | 'tool.content_chunk' | 'permission.requested' | 'permission.resolved' | 'monitor.delivery' | 'usage.updated' | 'turn.state' | 'turn.completed' | 'session.state' | 'session.info' | 'capabilities.updated' | 'error'
11
- /** A well-formed ACP update this version cannot represent. Bounded, never a crash. */
11
+ /** A well-formed transport update this version cannot represent. Bounded, never a crash. */
12
12
  | 'unsupported';
13
13
  /** Where a conversation record came from. Typed provenance, never prompt text. */
14
14
  export type ConversationSource = 'owner_admin_console' | 'owner_channel' | 'fleet_monitor' | 'scheduled_loop' | 'startup' | 'local_console' | 'agent' | 'agent_replay';
@@ -0,0 +1,60 @@
1
+ export declare const DEFAULT_STALL_TIMEOUT_MS: number;
2
+ export declare const STALL_RECOVERY_PROMPT: string;
3
+ export type StallStatus = 'interrupt_requested' | 'recovery_started' | 'progress_resumed' | 'recovery_completed' | 'blocked_cancel' | 'blocked_recovery' | 'blocked_restall' | 'blocked_persistence' | 'blocked_previous_attempt' | 'blocked_evidence' | 'superseded';
4
+ export interface StallDiagnostic {
5
+ version: 1;
6
+ kind: 'stall_recovery';
7
+ eventId: string;
8
+ session: string;
9
+ turn: string;
10
+ status: StallStatus;
11
+ evidence: 'adapter_transport' | 'no_progress';
12
+ idleMs: number;
13
+ }
14
+ export interface StallObservation {
15
+ sessionId: string;
16
+ generation: string;
17
+ turnId: string;
18
+ startedAt: number;
19
+ lastProgressAt: number;
20
+ progressCount: number;
21
+ transportFailures: number;
22
+ safe: boolean;
23
+ boundaryEvidenceAvailable?: boolean;
24
+ }
25
+ /** Presence, including an incomplete claim, restores conservative mail policy. */
26
+ export declare function hasStallRecoveryClaim(stateDir: string, sessionId: string): boolean;
27
+ /** ACP has no turn IDs on tool updates. Reuse across turns is ambiguous. */
28
+ export declare class StallToolHistory {
29
+ private readonly turns;
30
+ private healthy;
31
+ private readonly directory;
32
+ private readonly path;
33
+ private readonly session;
34
+ constructor(stateDir: string, sessionId: string, resume: boolean);
35
+ available(): boolean;
36
+ /** Record before relying on a tool event. False means cancellation is unsafe. */
37
+ observe(toolId: string, turnId: string): boolean;
38
+ private persist;
39
+ }
40
+ /**
41
+ * One durable attempt per ACP session, deliberately stricter than one per turn.
42
+ * A restarted supervisor never guesses whether cancellation or recovery ran.
43
+ * Claim files are never reclaimed automatically, including malformed/empty ones.
44
+ */
45
+ export declare class StallWatchdog {
46
+ private readonly options;
47
+ private checking;
48
+ private disabled;
49
+ private unavailableTurn?;
50
+ constructor(options: {
51
+ stateDir: string;
52
+ timeoutMs: number;
53
+ previouslyClaimed?: boolean;
54
+ now(): number;
55
+ observe(): StallObservation | undefined;
56
+ recover(observed: StallObservation, report: (status: StallStatus) => void): Promise<void>;
57
+ diagnostic(event: StallDiagnostic): void;
58
+ });
59
+ tick(): Promise<void>;
60
+ }
@@ -0,0 +1,210 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { replaceFileAtomically } from '../atomic-file.js';
5
+ export const DEFAULT_STALL_TIMEOUT_MS = 15 * 60_000;
6
+ export const STALL_RECOVERY_PROMPT = 'This is a diagnostic interruption. The previous turn showed no progress. '
7
+ + 'Inspect recorded terminal events and re-check completed actions before continuing. '
8
+ + 'Never assume an issued side effect failed: do not replay ambiguous or already-completed mutations. '
9
+ + 'Continue the previous task if safe; otherwise report an actionable blocker.';
10
+ const digest = (text) => createHash('sha256').update(text).digest('hex');
11
+ /** Presence, including an incomplete claim, restores conservative mail policy. */
12
+ export function hasStallRecoveryClaim(stateDir, sessionId) {
13
+ try {
14
+ lstatSync(join(stateDir, '.stall-recovery', `${digest(sessionId)}.claim`));
15
+ return true;
16
+ }
17
+ catch (error) {
18
+ return error.code !== 'ENOENT';
19
+ }
20
+ }
21
+ /** ACP has no turn IDs on tool updates. Reuse across turns is ambiguous. */
22
+ export class StallToolHistory {
23
+ turns = new Map();
24
+ healthy = true;
25
+ directory;
26
+ path;
27
+ session;
28
+ constructor(stateDir, sessionId, resume) {
29
+ this.session = digest(sessionId);
30
+ this.directory = join(stateDir, '.stall-recovery');
31
+ this.path = join(this.directory, `${this.session}.tools.json`);
32
+ try {
33
+ const stored = JSON.parse(readFileSync(this.path, 'utf8'));
34
+ if (stored.version !== 1 || stored.session !== this.session || !Array.isArray(stored.tools)
35
+ || stored.tools.length > 4096 || !stored.tools.every((id) => typeof id === 'string' && /^[a-f0-9]{64}$/.test(id)))
36
+ throw new Error('invalid history');
37
+ for (const id of stored.tools)
38
+ this.turns.set(id, 'previous-generation');
39
+ }
40
+ catch (error) {
41
+ if (error.code !== 'ENOENT' || resume)
42
+ this.healthy = false;
43
+ else {
44
+ try {
45
+ mkdirSync(this.directory, { recursive: true, mode: 0o700 });
46
+ this.persist();
47
+ const fd = openSync(stateDir, 'r');
48
+ try {
49
+ fsyncSync(fd);
50
+ }
51
+ finally {
52
+ closeSync(fd);
53
+ }
54
+ }
55
+ catch {
56
+ this.healthy = false;
57
+ }
58
+ }
59
+ }
60
+ }
61
+ available() { return this.healthy; }
62
+ /** Record before relying on a tool event. False means cancellation is unsafe. */
63
+ observe(toolId, turnId) {
64
+ if (!this.healthy || !toolId)
65
+ return false;
66
+ const id = digest(toolId);
67
+ const previous = this.turns.get(id);
68
+ if (previous !== undefined)
69
+ return previous === turnId;
70
+ if (this.turns.size >= 4096) {
71
+ this.healthy = false;
72
+ return false;
73
+ }
74
+ this.turns.set(id, turnId);
75
+ try {
76
+ this.persist();
77
+ }
78
+ catch {
79
+ this.healthy = false;
80
+ }
81
+ return this.healthy;
82
+ }
83
+ persist() {
84
+ replaceFileAtomically(this.path, JSON.stringify({ version: 1, session: this.session,
85
+ tools: [...this.turns.keys()] }) + '\n');
86
+ const fd = openSync(this.directory, 'r');
87
+ try {
88
+ fsyncSync(fd);
89
+ }
90
+ finally {
91
+ closeSync(fd);
92
+ }
93
+ }
94
+ }
95
+ /**
96
+ * One durable attempt per ACP session, deliberately stricter than one per turn.
97
+ * A restarted supervisor never guesses whether cancellation or recovery ran.
98
+ * Claim files are never reclaimed automatically, including malformed/empty ones.
99
+ */
100
+ export class StallWatchdog {
101
+ options;
102
+ checking = false;
103
+ disabled = false;
104
+ unavailableTurn;
105
+ constructor(options) {
106
+ this.options = options;
107
+ }
108
+ async tick() {
109
+ if (this.checking || this.disabled)
110
+ return;
111
+ const observed = this.options.observe();
112
+ if (!observed)
113
+ return;
114
+ const missingBoundary = observed.boundaryEvidenceAvailable === false;
115
+ if (!observed.safe && !missingBoundary && !this.options.previouslyClaimed)
116
+ return;
117
+ const missingEvidence = observed.progressCount === 0 || missingBoundary;
118
+ // Turn age can only produce an informational blocker, never cancellation.
119
+ const idleMs = this.options.now() - (observed.progressCount === 0 ? observed.startedAt : observed.lastProgressAt);
120
+ // Generic silence needs two full windows. Authenticated repeated transport
121
+ // failures strengthen evidence, but never bypass protected-operation checks.
122
+ const threshold = this.options.timeoutMs * (!missingEvidence && observed.transportFailures >= 2 ? 1 : 2);
123
+ if (!Number.isFinite(idleMs) || (idleMs < threshold && !this.options.previouslyClaimed))
124
+ return;
125
+ this.checking = true;
126
+ const session = digest(observed.sessionId);
127
+ const turn = digest(observed.generation + '\0' + observed.turnId);
128
+ const directory = join(this.options.stateDir, '.stall-recovery');
129
+ const report = (status) => {
130
+ const event = {
131
+ version: 1, kind: 'stall_recovery', eventId: `${turn}:${status}`,
132
+ session, turn, status, idleMs: Math.floor(idleMs),
133
+ evidence: observed.transportFailures >= 2 ? 'adapter_transport' : 'no_progress',
134
+ };
135
+ const fd = openSync(join(directory, 'audit.jsonl'), 'a', 0o600);
136
+ try {
137
+ writeFileSync(fd, JSON.stringify(event) + '\n');
138
+ fsyncSync(fd);
139
+ }
140
+ finally {
141
+ closeSync(fd);
142
+ }
143
+ this.options.diagnostic(event);
144
+ };
145
+ try {
146
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
147
+ const parentFd = openSync(this.options.stateDir, 'r');
148
+ try {
149
+ fsyncSync(parentFd);
150
+ }
151
+ finally {
152
+ closeSync(parentFd);
153
+ }
154
+ if (this.options.previouslyClaimed) {
155
+ this.disabled = true;
156
+ report('blocked_previous_attempt');
157
+ return;
158
+ }
159
+ if (missingEvidence) {
160
+ if (this.unavailableTurn !== turn) {
161
+ this.unavailableTurn = turn;
162
+ report('blocked_evidence');
163
+ }
164
+ return;
165
+ }
166
+ let fd;
167
+ try {
168
+ fd = openSync(join(directory, `${session}.claim`), 'wx', 0o600);
169
+ }
170
+ catch (error) {
171
+ this.disabled = true;
172
+ if (error.code === 'EEXIST') {
173
+ report('blocked_previous_attempt');
174
+ return;
175
+ }
176
+ throw error;
177
+ }
178
+ // Even a crash before this write leaves a permanent conservative fence.
179
+ try {
180
+ writeFileSync(fd, JSON.stringify({ version: 1, session, turn }) + '\n');
181
+ fsyncSync(fd);
182
+ }
183
+ finally {
184
+ closeSync(fd);
185
+ }
186
+ const dirFd = openSync(directory, 'r');
187
+ try {
188
+ fsyncSync(dirFd);
189
+ }
190
+ finally {
191
+ closeSync(dirFd);
192
+ }
193
+ this.disabled = true;
194
+ report('interrupt_requested');
195
+ // No await between the durable claim and the adapter's final atomic
196
+ // observation check. Recovery itself must re-check before session/cancel.
197
+ await this.options.recover(observed, report);
198
+ }
199
+ catch {
200
+ this.disabled = true;
201
+ // Never include an exception string: it may contain paths or wire data.
202
+ this.options.diagnostic({ version: 1, kind: 'stall_recovery',
203
+ eventId: `${turn}:blocked_persistence`, session, turn,
204
+ status: 'blocked_persistence', evidence: 'no_progress', idleMs: Math.floor(idleMs) });
205
+ }
206
+ finally {
207
+ this.checking = false;
208
+ }
209
+ }
210
+ }
@@ -13,9 +13,11 @@ import type { ConversationEventV1, ConversationSnapshot, PromptReceipt, SubmitPr
13
13
  */
14
14
  export type SessionReadiness = 'starting' | 'idle' | 'running' | 'awaiting_permission' | 'failed';
15
15
  export type TurnOutcome = 'completed' | 'refused' | 'cancelled' | 'failed' | 'inconclusive';
16
- export type TurnCancellationSource = 'owner' | 'local-console' | 'fleet-monitor' | 'scheduled-loop' | 'shutdown';
16
+ export type TurnCancellationSource = 'owner' | 'local-console' | 'fleet-monitor' | 'scheduled-loop' | 'shutdown' | 'stall-watchdog';
17
17
  export type PromptOrigin = {
18
18
  kind: 'startup';
19
+ } | {
20
+ kind: 'stall-watchdog';
19
21
  } | {
20
22
  kind: 'local-console';
21
23
  } | {
@@ -33,7 +35,7 @@ export type PromptOrigin = {
33
35
  commandId: string;
34
36
  };
35
37
  export interface RuntimeSelectorMetadata {
36
- /** Exact provider/model identifier reported by the live ACP session. */
38
+ /** Exact provider/model identifier reported by the live managed session. */
37
39
  value: string;
38
40
  /** Complete provider-supplied label for that exact value, when available. */
39
41
  label?: string;
@@ -75,6 +77,8 @@ export interface TurnResult {
75
77
  export type ControlFailureKind = 'offline' | 'control-unavailable' | 'timeout' | 'rejected' | 'backend';
76
78
  /** Stable body-free reason shared by ACP recovery and durable ingress. */
77
79
  export declare const ACP_CANCEL_DEADLINE_EXCEEDED = "ACP_CANCEL_DEADLINE_EXCEEDED";
80
+ /** Native Codex equivalent; kept distinct so diagnostics name the failing transport. */
81
+ export declare const CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED = "CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED";
78
82
  export declare class SessionControlError extends Error {
79
83
  readonly kind: ControlFailureKind;
80
84
  /** Stable body-free machine reason for recovery/audit decisions. */
@@ -170,7 +174,7 @@ export interface SubmitPromptOptions {
170
174
  interruptSource?: TurnCancellationSource;
171
175
  /** Typed in-process provenance. Text markers never grant this origin. */
172
176
  origin?: PromptOrigin;
173
- /** Use the ACP steering extension when available; ignored by other backends. */
177
+ /** Inject into active work when the selected backend supports steering. */
174
178
  steer?: boolean;
175
179
  /** Audit-grade actor detail persisted with the conversation admission record. */
176
180
  actor?: {
@@ -200,12 +204,26 @@ export interface SessionSnapshot {
200
204
  activity?: SessionActivity;
201
205
  }
202
206
  export interface SessionActivity {
203
- /** ACP tool calls currently reserved (lifecycle open or permission pending). */
207
+ /** Tool calls currently reserved (lifecycle open or permission pending). */
204
208
  activeToolCalls: number;
205
209
  /** When the agent last sent ANY session update, replay excluded. */
206
210
  lastUpdateAt?: string;
207
211
  }
208
- export type SessionEventKind = 'state' | 'agent_text' | 'thought' | 'tool_call' | 'tool_update' | 'permission' | 'monitor_delivery' | 'turn_stop' | 'error';
212
+ /** Provider-neutral behavior exposed by a session transport. */
213
+ export interface AgentSessionCapabilities {
214
+ streaming: boolean;
215
+ durableConversation: boolean;
216
+ promptInput: boolean;
217
+ interrupt: boolean;
218
+ steering: boolean;
219
+ permissions: boolean;
220
+ toolBoundaryDelivery: boolean;
221
+ messagePhases: boolean;
222
+ resume: boolean;
223
+ }
224
+ /** Conservative static capabilities used before a live session is reachable. */
225
+ export declare function sessionBackendCapabilities(backend: SessionBackendId, harness?: string): AgentSessionCapabilities;
226
+ export type SessionEventKind = 'state' | 'agent_text' | 'thought' | 'tool_call' | 'tool_update' | 'permission' | 'stall_recovery' | 'monitor_delivery' | 'turn_stop' | 'error';
209
227
  /** What a settled permission request resolved to. */
210
228
  export type PermissionDecision = 'allowed' | 'denied' | 'cancelled';
211
229
  export interface SessionEvent {
@@ -219,7 +237,7 @@ export interface SessionEvent {
219
237
  text?: string;
220
238
  /**
221
239
  * Adapter-authenticated presentation phase for assistant text. Only the
222
- * exact Codex ACP marker is promoted; absence/unknown values stay unset.
240
+ * exact Codex phase marker is promoted; absence/unknown values stay unset.
223
241
  */
224
242
  messagePhase?: 'commentary' | 'final_answer';
225
243
  /** Stable adapter message/item id when supplied. */
@@ -247,6 +265,7 @@ export interface SessionEvent {
247
265
  /** The option actually selected, when one was. */
248
266
  optionId?: string;
249
267
  /** Body-free evidence for monitor safe-boundary delivery. */
268
+ stallDiagnostic?: import('./stall-watchdog.js').StallDiagnostic;
250
269
  monitorPolicy?: 'after_tool';
251
270
  activeToolCount?: number;
252
271
  waitedMs?: number;
@@ -261,11 +280,13 @@ export interface ConversationHandlePage {
261
280
  /**
262
281
  * The live, harness-neutral contract between Fleet and one agent session.
263
282
  * Harness adapters construct this handle; orchestration code must not depend
264
- * on the ACP transport which currently implements it.
283
+ * on any concrete transport which implements it.
265
284
  */
266
285
  export interface AgentSession {
267
286
  readonly backend: SessionBackendId;
268
287
  readonly pid: number;
288
+ /** Live capabilities; optional only for source compatibility with external adapters. */
289
+ readonly capabilities?: AgentSessionCapabilities;
269
290
  isAlive(): boolean;
270
291
  snapshot(): SessionSnapshot;
271
292
  conversationPage?(request: {
@@ -283,7 +304,7 @@ export interface AgentSession {
283
304
  queuePrompt(text: string, options?: SubmitPromptOptions): Promise<QueuedPrompt>;
284
305
  /** Queue a prompt and wait for its terminal result. */
285
306
  submitPrompt(text: string, options?: SubmitPromptOptions): Promise<TurnResult>;
286
- /** Monitor-only ACP safe-boundary delivery. Never implies human/control cancellation. */
307
+ /** Monitor-only safe-boundary delivery. Never implies human/control cancellation. */
287
308
  submitPromptAfterTool?(text: string, options?: SubmitPromptOptions): Promise<TurnResult>;
288
309
  /**
289
310
  * Cancel the active turn. Resolves when the cancellation has taken effect —
@@ -1,5 +1,7 @@
1
1
  /** Stable body-free reason shared by ACP recovery and durable ingress. */
2
2
  export const ACP_CANCEL_DEADLINE_EXCEEDED = 'ACP_CANCEL_DEADLINE_EXCEEDED';
3
+ /** Native Codex equivalent; kept distinct so diagnostics name the failing transport. */
4
+ export const CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED = 'CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED';
3
5
  export class SessionControlError extends Error {
4
6
  kind;
5
7
  reasonCode;
@@ -53,3 +55,29 @@ export function turnResult(accepted, outcome, detail, output, cancellationSource
53
55
  ...(outcome === 'cancelled' && cancellationSource ? { cancellationSource } : {}),
54
56
  };
55
57
  }
58
+ /** Conservative static capabilities used before a live session is reachable. */
59
+ export function sessionBackendCapabilities(backend, harness) {
60
+ if (backend === 'codex-app-server')
61
+ return {
62
+ streaming: true,
63
+ durableConversation: true,
64
+ promptInput: true,
65
+ interrupt: true,
66
+ steering: true,
67
+ permissions: true,
68
+ toolBoundaryDelivery: false,
69
+ messagePhases: true,
70
+ resume: true,
71
+ };
72
+ return {
73
+ streaming: true,
74
+ durableConversation: true,
75
+ promptInput: true,
76
+ interrupt: true,
77
+ steering: false,
78
+ permissions: true,
79
+ toolBoundaryDelivery: true,
80
+ messagePhases: harness === 'codex',
81
+ resume: true,
82
+ };
83
+ }
package/dist/spawn.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { IsolationConfig } from './isolation/types.js';
2
+ import type { AgentLoopsConfig } from './loops/config.js';
2
3
  import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type MonitorConfig, type UnattendedMode, type RoomMemberStartup, type AgentDefinition, type AgentSelection } from './config.js';
3
4
  import { type OpsDeps } from './ops.js';
4
5
  import { type CreationDeps, type CreationProvenance } from './creation.js';
@@ -40,6 +41,12 @@ export interface SpawnOpts {
40
41
  * input in this release.
41
42
  */
42
43
  isolationFile?: string;
44
+ /** Trusted YAML containing exactly a top-level `loops:` map. Temporary launches only. */
45
+ loopsFile?: string;
46
+ /** Explicitly override any selected Agent Template loops with none. */
47
+ noLoops?: boolean;
48
+ /** Internal source label retained after room-member plan resolution. */
49
+ loopSource?: 'agent-template' | 'cli' | 'omitted';
43
50
  overseeInterval?: string;
44
51
  configPath?: string;
45
52
  dryRun?: boolean;
@@ -56,6 +63,8 @@ export declare function agentDefinitionFromSpawn(o: SpawnOpts): AgentDefinition;
56
63
  * must fail before any artifact exists.
57
64
  */
58
65
  export declare function readIsolationFile(path: string): IsolationConfig;
66
+ /** Read a private canonical temporary-loop override before any creation side effect. */
67
+ export declare function readLoopsFile(path: string): AgentLoopsConfig;
59
68
  export declare function validateSpawnOpts(o: SpawnOpts): void;
60
69
  /** The ours identity a spawn will bind: explicit, else the role name. */
61
70
  export declare const effectiveIdentity: (o: SpawnOpts) => string;