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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +204 -55
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.js +18 -6
  5. package/dist/application/fleet-query-service.js +20 -5
  6. package/dist/application/role-command-service.js +9 -0
  7. package/dist/application/role-creation-service.d.ts +6 -25
  8. package/dist/application/role-creation-service.js +34 -72
  9. package/dist/application/role-repository.d.ts +4 -2
  10. package/dist/application/role-repository.js +6 -3
  11. package/dist/application/session-control.d.ts +3 -1
  12. package/dist/application/session-control.js +4 -2
  13. package/dist/application/task-room-service.d.ts +58 -6
  14. package/dist/application/task-room-service.js +435 -79
  15. package/dist/briefing.js +74 -24
  16. package/dist/build-info.json +4 -4
  17. package/dist/canonical-json.d.ts +2 -0
  18. package/dist/canonical-json.js +10 -0
  19. package/dist/cli.js +241 -76
  20. package/dist/config-yaml.d.ts +1 -1
  21. package/dist/config.d.ts +75 -3
  22. package/dist/config.js +274 -34
  23. package/dist/creation.d.ts +1 -1
  24. package/dist/creation.js +3 -2
  25. package/dist/daemon-recovery.d.ts +93 -0
  26. package/dist/daemon-recovery.js +328 -0
  27. package/dist/docs.d.ts +1 -1
  28. package/dist/docs.js +192 -66
  29. package/dist/doctor.d.ts +2 -0
  30. package/dist/doctor.js +54 -4
  31. package/dist/fleet-command-audit.d.ts +165 -0
  32. package/dist/fleet-command-audit.js +555 -0
  33. package/dist/fleet-proxy.d.ts +7 -2
  34. package/dist/fleet-proxy.js +30 -8
  35. package/dist/harness/agent-session.d.ts +5 -0
  36. package/dist/harness/claude-code-session.d.ts +2 -0
  37. package/dist/harness/claude-code-session.js +6 -1
  38. package/dist/harness/claude-code.js +2 -0
  39. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  40. package/dist/harness/codex-app-server-proxy.js +348 -14
  41. package/dist/harness/codex-session.d.ts +11 -2
  42. package/dist/harness/codex-session.js +22 -2
  43. package/dist/harness/codex.d.ts +5 -1
  44. package/dist/harness/codex.js +110 -10
  45. package/dist/harness/types.d.ts +1 -1
  46. package/dist/index.d.ts +5 -4
  47. package/dist/index.js +4 -3
  48. package/dist/init-guidance.d.ts +1 -1
  49. package/dist/init-guidance.js +4 -3
  50. package/dist/init-wizard.d.ts +98 -0
  51. package/dist/init-wizard.js +629 -0
  52. package/dist/lifecycle-summary.d.ts +134 -0
  53. package/dist/lifecycle-summary.js +179 -0
  54. package/dist/loops/config.d.ts +10 -1
  55. package/dist/loops/config.js +38 -5
  56. package/dist/model-env.d.ts +2 -2
  57. package/dist/model-env.js +1 -1
  58. package/dist/model-recovery.d.ts +1 -1
  59. package/dist/ops.js +2 -17
  60. package/dist/owner-channel/channel.d.ts +58 -0
  61. package/dist/owner-channel/channel.js +321 -42
  62. package/dist/owner-channel/commands.d.ts +6 -1
  63. package/dist/owner-channel/commands.js +34 -19
  64. package/dist/owner-channel/state.d.ts +2 -0
  65. package/dist/owner-channel/state.js +8 -2
  66. package/dist/permissions.js +1 -1
  67. package/dist/preset-bootstrap.d.ts +10 -0
  68. package/dist/preset-bootstrap.js +85 -0
  69. package/dist/preset-migration.d.ts +42 -0
  70. package/dist/preset-migration.js +414 -0
  71. package/dist/reports/artifact.d.ts +10 -0
  72. package/dist/reports/artifact.js +32 -0
  73. package/dist/reports/index.d.ts +5 -0
  74. package/dist/reports/index.js +5 -0
  75. package/dist/reports/render.d.ts +6 -0
  76. package/dist/reports/render.js +240 -0
  77. package/dist/reports/service.d.ts +42 -0
  78. package/dist/reports/service.js +57 -0
  79. package/dist/reports/tasks.d.ts +52 -0
  80. package/dist/reports/tasks.js +177 -0
  81. package/dist/reports/types.d.ts +315 -0
  82. package/dist/reports/types.js +6 -0
  83. package/dist/resolved-plan.js +13 -1
  84. package/dist/rooms-tasks/cli.d.ts +2 -0
  85. package/dist/rooms-tasks/cli.js +514 -75
  86. package/dist/rooms-tasks/close.d.ts +9 -1
  87. package/dist/rooms-tasks/close.js +20 -4
  88. package/dist/rooms-tasks/config.js +16 -14
  89. package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
  90. package/dist/rooms-tasks/cowork-adapter.js +6 -0
  91. package/dist/rooms-tasks/deletion.d.ts +51 -0
  92. package/dist/rooms-tasks/deletion.js +217 -0
  93. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  94. package/dist/rooms-tasks/external-worker.js +14 -9
  95. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  96. package/dist/rooms-tasks/launch-snapshot.js +181 -0
  97. package/dist/rooms-tasks/member-overrides.d.ts +30 -0
  98. package/dist/rooms-tasks/member-overrides.js +204 -0
  99. package/dist/rooms-tasks/provision.js +184 -49
  100. package/dist/rooms-tasks/room-state.d.ts +1 -0
  101. package/dist/rooms-tasks/room-state.js +12 -1
  102. package/dist/rooms-tasks/task-state.d.ts +115 -3
  103. package/dist/rooms-tasks/task-state.js +361 -11
  104. package/dist/rooms-tasks/templates.d.ts +4 -2
  105. package/dist/rooms-tasks/templates.js +42 -78
  106. package/dist/rooms-tasks/terminal.d.ts +3 -0
  107. package/dist/rooms-tasks/terminal.js +7 -3
  108. package/dist/rooms-tasks/types.d.ts +96 -17
  109. package/dist/rooms-tasks/types.js +14 -4
  110. package/dist/runner.d.ts +12 -2
  111. package/dist/runner.js +124 -43
  112. package/dist/session/acp.d.ts +9 -1
  113. package/dist/session/acp.js +35 -9
  114. package/dist/session/arbiter.d.ts +2 -1
  115. package/dist/session/arbiter.js +2 -0
  116. package/dist/session/codex-app-server-transport.d.ts +53 -0
  117. package/dist/session/codex-app-server-transport.js +184 -0
  118. package/dist/session/codex-app-server.d.ts +122 -0
  119. package/dist/session/codex-app-server.js +1031 -0
  120. package/dist/session/control.d.ts +14 -1
  121. package/dist/session/control.js +38 -0
  122. package/dist/session/conversation-types.d.ts +4 -4
  123. package/dist/session/types.d.ts +24 -6
  124. package/dist/session/types.js +28 -0
  125. package/dist/spawn.d.ts +15 -31
  126. package/dist/spawn.js +117 -237
  127. package/dist/temp-lifecycle.d.ts +1 -1
  128. package/dist/watchdog/config.d.ts +10 -7
  129. package/dist/watchdog/config.js +29 -19
  130. package/dist/watchdog/run.js +4 -13
  131. package/dist/web/fleet-config-service.d.ts +1 -0
  132. package/dist/web/fleet-config-service.js +27 -8
  133. package/dist/web/runtime.js +11 -6
  134. package/dist/web/server.js +28 -1
  135. package/dist/web/topology-promote.js +8 -1
  136. package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
  137. package/dist/web-app/index.html +1 -1
  138. package/package.json +3 -2
  139. package/presets/brain-catalog.json +16 -0
  140. package/presets/fleet/agent_templates/Critic.yaml +4 -0
  141. package/presets/fleet/agent_templates/Developer.yaml +4 -0
  142. package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
  143. package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
  144. package/presets/fleet/brains/claude-default.yaml +5 -0
  145. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  146. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  147. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  148. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  149. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  150. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  151. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  152. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  153. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  154. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  155. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  156. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  157. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  158. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  159. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  188. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  189. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  190. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  191. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  192. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  193. package/presets/fleet/roles/Coordinator.yaml +30 -0
  194. package/presets/fleet/roles/Critic.yaml +19 -0
  195. package/presets/fleet/roles/Developer.yaml +19 -0
  196. package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
  197. package/presets/fleet/room_templates/pair.yaml +10 -0
  198. package/presets/fleet/room_templates/single.yaml +7 -0
  199. package/presets/fleet/room_templates/team.yaml +12 -0
  200. package/presets/fleet.yaml +7 -0
  201. package/presets/manifest.json +6 -0
  202. package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
@@ -17,6 +17,10 @@ export interface ResolvedBrainSelection {
17
17
  model?: string | null;
18
18
  harnessOptions?: Record<string, unknown>;
19
19
  }
20
+ export interface AgentSessionConfigSelection {
21
+ configId: string;
22
+ value: string;
23
+ }
20
24
  export interface AgentSessionStartOptions {
21
25
  role: ResolvedRole;
22
26
  prep: SessionPrep;
@@ -42,5 +46,6 @@ export interface AgentSessionAdapter {
42
46
  /** Harness-owned environment channel used to pin/recover a model, if any. */
43
47
  modelEnvironmentVariable(): string | undefined;
44
48
  prepareLaunch(role: ResolvedRole, prep: SessionPrep): PreparedAgentSessionLaunch;
49
+ sessionConfigSelections(role: ResolvedRole): AgentSessionConfigSelection[];
45
50
  start(options: AgentSessionStartOptions): Promise<AgentSession>;
46
51
  }
@@ -6,6 +6,7 @@ export interface ClaudeCodeSessionStrategy {
6
6
  resolveBrain: AgentSessionAdapter['resolveBrain'];
7
7
  modelEnvironmentVariable: AgentSessionAdapter['modelEnvironmentVariable'];
8
8
  prepareLaunch(role: ResolvedRole, prep: SessionPrep): PreparedAgentSessionLaunch;
9
+ sessionConfigSelections(role: ResolvedRole): ReturnType<AgentSessionAdapter['sessionConfigSelections']>;
9
10
  permissionModeId(role: ResolvedRole): string | undefined;
10
11
  mcpServers(role: ResolvedRole): AcpMcpServer[] | undefined;
11
12
  sessionMeta(role: ResolvedRole, prep: SessionPrep): Record<string, unknown> | undefined;
@@ -18,5 +19,6 @@ export declare class ClaudeCodeAgentSessionAdapter implements AgentSessionAdapte
18
19
  resolveBrain(brain: Parameters<AgentSessionAdapter['resolveBrain']>[0]): import("./agent-session.js").ResolvedBrainSelection;
19
20
  modelEnvironmentVariable(): string | undefined;
20
21
  prepareLaunch(role: ResolvedRole, prep: SessionPrep): PreparedAgentSessionLaunch;
22
+ sessionConfigSelections(role: ResolvedRole): import("./agent-session.js").AgentSessionConfigSelection[];
21
23
  start(options: AgentSessionStartOptions): Promise<import("../index.js").AgentSession>;
22
24
  }
@@ -15,14 +15,19 @@ export class ClaudeCodeAgentSessionAdapter {
15
15
  prepareLaunch(role, prep) {
16
16
  return this.strategy.prepareLaunch(role, prep);
17
17
  }
18
+ sessionConfigSelections(role) {
19
+ return this.strategy.sessionConfigSelections(role);
20
+ }
18
21
  start(options) {
19
22
  const { role, prep, launch } = options;
20
23
  return this.transport({
21
- name: role.name, argv: launch.argv, cwd: options.cwd, env: launch.env,
24
+ name: role.name, harness: role.harness,
25
+ argv: launch.argv, cwd: options.cwd, env: launch.env,
22
26
  stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
23
27
  modeId: this.strategy.permissionModeId(role),
24
28
  mcpServers: this.strategy.mcpServers(role),
25
29
  sessionMeta: this.strategy.sessionMeta(role, prep),
30
+ configSelections: this.strategy.sessionConfigSelections(role),
26
31
  permissionMode: options.permissionMode,
27
32
  permissionMetadataSource: acpAdapterState(launch.adapterState).permissionMetadataSource,
28
33
  scrubObsoleteOursAutostart: true,
@@ -247,6 +247,8 @@ export function makeClaudeCodeAdapter(exec = realExec, transport) {
247
247
  : bundledAcpAgent('@agentclientprotocol/claude-agent-acp', 'claude-agent-acp', 'claude-agent-acp');
248
248
  return { argv, env: prep.env };
249
249
  },
250
+ sessionConfigSelections: role => typeof role.effort === 'string'
251
+ ? [{ configId: 'effort', value: role.effort }] : [],
250
252
  permissionModeId: role => permissionMode(role),
251
253
  mcpServers: role => acpMcpServersFor(role.harness_options?.mcp_servers),
252
254
  sessionMeta(role, prep) {
@@ -1,4 +1,44 @@
1
1
  #!/usr/bin/env node
2
+ interface TerminalRecoveryOptions {
3
+ sendToCodex(line: string): void;
4
+ emitToClient(line: string): void;
5
+ log(line: string): void;
6
+ quietMs?: number;
7
+ }
8
+ /**
9
+ * Reconcile Codex's missing terminal notification without guessing from a
10
+ * wall-clock timeout. A candidate needs the exact turn's authoritative final
11
+ * assistant item, no live items or server requests, and a quiet period. The
12
+ * proxy then asks app-server for the current thread snapshot. Only a terminal
13
+ * stored turn, or an idle thread containing that exact turn, may synthesize the
14
+ * missing notification.
15
+ *
16
+ * This lives in the authenticated bundled-Codex proxy, not the generic ACP
17
+ * session: only here are App Server thread/turn IDs and item lifecycles visible.
18
+ */
19
+ export declare class CodexTerminalRecovery {
20
+ private readonly options;
21
+ private readonly turns;
22
+ private readonly pendingTurnStarts;
23
+ private readonly serverRequests;
24
+ private readonly reconcileRequests;
25
+ private readonly inferredTurns;
26
+ private reconcileSequence;
27
+ constructor(options: TerminalRecoveryOptions);
28
+ observeClientLine(line: string): void;
29
+ /** Return false when an internal response or late duplicate was consumed. */
30
+ observeServerLine(line: string): boolean;
31
+ close(): void;
32
+ private parse;
33
+ private ensureTurn;
34
+ private candidate;
35
+ private cancelReconciliation;
36
+ private scheduleReconciliation;
37
+ private reconcile;
38
+ private handleReconcileResponse;
39
+ private finishTurn;
40
+ }
2
41
  /** Transform one app-server NDJSON request; malformed input passes through. */
3
42
  export declare function rewriteCodexAppServerRequest(line: string, approval: string, expectedSandbox: string, disableInheritedMcp?: boolean): string;
4
43
  export declare function runCodexAppServerProxy(): void;
44
+ export {};
@@ -9,9 +9,281 @@ const SANDBOX_ENV = 'OURS_FLEET_CODEX_SANDBOX';
9
9
  const REAL_CODEX_ENV = 'OURS_FLEET_REAL_CODEX_PATH';
10
10
  const ACP_MANIFEST_ENV = 'OURS_FLEET_CODEX_ACP_MANIFEST';
11
11
  const DISABLE_INHERITED_MCP_ENV = 'OURS_FLEET_CODEX_DISABLE_INHERITED_MCP';
12
+ /**
13
+ * Codex 0.145 can finish a tool-enabled turn without emitting the documented
14
+ * `turn/completed` notification. Keep this short: the authoritative
15
+ * `thread/read` reconciliation below, rather than elapsed time, is what makes
16
+ * inference safe.
17
+ */
18
+ const TERMINAL_RECONCILE_QUIET_MS = 2_000;
19
+ const MAX_LATE_TERMINAL_DEDUPLICATIONS = 1_024;
12
20
  const APPROVAL_POLICIES = new Set(['untrusted', 'on-request', 'never']);
13
21
  const SANDBOX_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
14
22
  const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
23
+ const jsonRpcId = (value) => typeof value === 'string' || typeof value === 'number' ? value : undefined;
24
+ const stringField = (value, field) => isObject(value) && typeof value[field] === 'string' ? value[field] : undefined;
25
+ const turnKey = (threadId, turnId) => `${threadId}\0${turnId}`;
26
+ /**
27
+ * Reconcile Codex's missing terminal notification without guessing from a
28
+ * wall-clock timeout. A candidate needs the exact turn's authoritative final
29
+ * assistant item, no live items or server requests, and a quiet period. The
30
+ * proxy then asks app-server for the current thread snapshot. Only a terminal
31
+ * stored turn, or an idle thread containing that exact turn, may synthesize the
32
+ * missing notification.
33
+ *
34
+ * This lives in the authenticated bundled-Codex proxy, not the generic ACP
35
+ * session: only here are App Server thread/turn IDs and item lifecycles visible.
36
+ */
37
+ export class CodexTerminalRecovery {
38
+ options;
39
+ turns = new Map();
40
+ pendingTurnStarts = new Map();
41
+ serverRequests = new Map();
42
+ reconcileRequests = new Map();
43
+ inferredTurns = new Set();
44
+ reconcileSequence = 0;
45
+ constructor(options) {
46
+ this.options = options;
47
+ }
48
+ observeClientLine(line) {
49
+ const message = this.parse(line);
50
+ if (!message)
51
+ return;
52
+ const id = jsonRpcId(message.id);
53
+ if (message.method === 'turn/start' && id !== undefined) {
54
+ const threadId = stringField(message.params, 'threadId');
55
+ if (threadId)
56
+ this.pendingTurnStarts.set(id, threadId);
57
+ return;
58
+ }
59
+ if (id === undefined || message.method !== undefined)
60
+ return;
61
+ const key = this.serverRequests.get(id);
62
+ if (!key)
63
+ return;
64
+ this.serverRequests.delete(id);
65
+ const turn = this.turns.get(key);
66
+ turn?.openServerRequests.delete(id);
67
+ if (turn)
68
+ this.scheduleReconciliation(turn);
69
+ }
70
+ /** Return false when an internal response or late duplicate was consumed. */
71
+ observeServerLine(line) {
72
+ const message = this.parse(line);
73
+ if (!message)
74
+ return true;
75
+ const id = jsonRpcId(message.id);
76
+ if (id !== undefined && message.method === undefined) {
77
+ const reconcileKey = this.reconcileRequests.get(id);
78
+ if (reconcileKey) {
79
+ this.reconcileRequests.delete(id);
80
+ this.handleReconcileResponse(reconcileKey, message);
81
+ return false;
82
+ }
83
+ const threadId = this.pendingTurnStarts.get(id);
84
+ if (threadId) {
85
+ this.pendingTurnStarts.delete(id);
86
+ const turn = isObject(message.result) && isObject(message.result.turn)
87
+ ? message.result.turn : undefined;
88
+ const turnId = stringField(turn, 'id');
89
+ if (turn && turnId)
90
+ this.ensureTurn(threadId, turnId, turn);
91
+ }
92
+ return true;
93
+ }
94
+ if (message.method === 'turn/started') {
95
+ const threadId = stringField(message.params, 'threadId');
96
+ const turn = isObject(message.params) && isObject(message.params.turn)
97
+ ? message.params.turn : undefined;
98
+ const turnId = stringField(turn, 'id');
99
+ if (threadId && turn && turnId)
100
+ this.ensureTurn(threadId, turnId, turn);
101
+ return true;
102
+ }
103
+ if (message.method === 'turn/completed') {
104
+ const threadId = stringField(message.params, 'threadId');
105
+ const turn = isObject(message.params) && isObject(message.params.turn)
106
+ ? message.params.turn : undefined;
107
+ const turnId = stringField(turn, 'id');
108
+ if (!threadId || !turnId)
109
+ return true;
110
+ const key = turnKey(threadId, turnId);
111
+ if (this.inferredTurns.delete(key)) {
112
+ this.options.log(`suppressed late turn/completed after inferred terminal (${threadId}/${turnId})`);
113
+ return false;
114
+ }
115
+ this.finishTurn(key);
116
+ return true;
117
+ }
118
+ if (message.method === 'thread/status/changed') {
119
+ const threadId = stringField(message.params, 'threadId');
120
+ const status = isObject(message.params) && isObject(message.params.status)
121
+ ? message.params.status : undefined;
122
+ if (threadId && status?.type === 'idle') {
123
+ for (const turn of this.turns.values()) {
124
+ if (turn.threadId !== threadId)
125
+ continue;
126
+ turn.idleObserved = true;
127
+ this.scheduleReconciliation(turn);
128
+ }
129
+ }
130
+ return true;
131
+ }
132
+ const params = isObject(message.params) ? message.params : undefined;
133
+ const threadId = stringField(params, 'threadId');
134
+ const routedTurnId = stringField(params, 'turnId');
135
+ const routed = threadId && routedTurnId
136
+ ? this.turns.get(turnKey(threadId, routedTurnId)) : undefined;
137
+ if (routed && message.method === 'item/started') {
138
+ const item = isObject(params?.item) ? params.item : undefined;
139
+ const itemId = stringField(item, 'id');
140
+ if (itemId)
141
+ routed.openItems.add(itemId);
142
+ // Work after a final answer invalidates that candidate. A later exact
143
+ // final answer can establish a new one.
144
+ if (item?.type !== 'agentMessage' || item?.phase !== 'final_answer')
145
+ routed.finalAssistantItem = undefined;
146
+ this.cancelReconciliation(routed);
147
+ }
148
+ else if (routed && message.method === 'item/completed') {
149
+ const item = isObject(params?.item) ? params.item : undefined;
150
+ const itemId = stringField(item, 'id');
151
+ if (itemId)
152
+ routed.openItems.delete(itemId);
153
+ if (item?.type === 'agentMessage' && item.phase === 'final_answer'
154
+ && typeof item.text === 'string' && item.text.length > 0)
155
+ routed.finalAssistantItem = item;
156
+ this.scheduleReconciliation(routed);
157
+ }
158
+ // App-server requests (approvals, elicitation, user input) are obligations
159
+ // the client still owes. Never infer while one for this turn is unresolved.
160
+ if (routed && id !== undefined && typeof message.method === 'string') {
161
+ this.serverRequests.set(id, turnKey(routed.threadId, routed.turnId));
162
+ routed.openServerRequests.add(id);
163
+ this.cancelReconciliation(routed);
164
+ }
165
+ return true;
166
+ }
167
+ close() {
168
+ for (const turn of this.turns.values())
169
+ this.cancelReconciliation(turn);
170
+ this.turns.clear();
171
+ this.pendingTurnStarts.clear();
172
+ this.serverRequests.clear();
173
+ this.reconcileRequests.clear();
174
+ this.inferredTurns.clear();
175
+ }
176
+ parse(line) {
177
+ try {
178
+ const parsed = JSON.parse(line);
179
+ return isObject(parsed) ? parsed : undefined;
180
+ }
181
+ catch {
182
+ return undefined;
183
+ }
184
+ }
185
+ ensureTurn(threadId, turnId, turn) {
186
+ const key = turnKey(threadId, turnId);
187
+ const existing = this.turns.get(key);
188
+ if (existing) {
189
+ existing.turn = turn;
190
+ return existing;
191
+ }
192
+ const created = {
193
+ threadId, turnId, turn, openItems: new Set(), openServerRequests: new Set(),
194
+ idleObserved: false,
195
+ };
196
+ this.turns.set(key, created);
197
+ return created;
198
+ }
199
+ candidate(turn) {
200
+ return turn.finalAssistantItem !== undefined
201
+ && turn.openItems.size === 0 && turn.openServerRequests.size === 0
202
+ && turn.reconcileRequestId === undefined;
203
+ }
204
+ cancelReconciliation(turn) {
205
+ if (turn.reconcileTimer)
206
+ clearTimeout(turn.reconcileTimer);
207
+ turn.reconcileTimer = undefined;
208
+ }
209
+ scheduleReconciliation(turn) {
210
+ this.cancelReconciliation(turn);
211
+ if (!this.candidate(turn))
212
+ return;
213
+ turn.reconcileTimer = setTimeout(() => this.reconcile(turn), this.options.quietMs ?? TERMINAL_RECONCILE_QUIET_MS);
214
+ turn.reconcileTimer.unref?.();
215
+ }
216
+ reconcile(turn) {
217
+ turn.reconcileTimer = undefined;
218
+ if (!this.candidate(turn))
219
+ return;
220
+ const id = `ours-fleet-terminal-reconcile-${++this.reconcileSequence}`;
221
+ const key = turnKey(turn.threadId, turn.turnId);
222
+ turn.reconcileRequestId = id;
223
+ this.reconcileRequests.set(id, key);
224
+ this.options.sendToCodex(JSON.stringify({
225
+ id, method: 'thread/read', params: { threadId: turn.threadId, includeTurns: true },
226
+ }));
227
+ }
228
+ handleReconcileResponse(key, message) {
229
+ const tracked = this.turns.get(key);
230
+ if (!tracked)
231
+ return;
232
+ tracked.reconcileRequestId = undefined;
233
+ if (!this.candidate(tracked))
234
+ return;
235
+ const thread = isObject(message.result) && isObject(message.result.thread)
236
+ ? message.result.thread : undefined;
237
+ const turns = thread && Array.isArray(thread.turns) ? thread.turns : [];
238
+ const snapshot = turns.find(candidate => isObject(candidate) && candidate.id === tracked.turnId);
239
+ if (!snapshot) {
240
+ this.scheduleReconciliation(tracked);
241
+ return;
242
+ }
243
+ const status = typeof snapshot.status === 'string' ? snapshot.status : undefined;
244
+ const threadStatus = thread && isObject(thread.status) ? thread.status.type : undefined;
245
+ const storedTerminal = status === 'completed' || status === 'failed' || status === 'interrupted';
246
+ if (!storedTerminal && threadStatus !== 'idle' && !tracked.idleObserved) {
247
+ // A final answer can precede stop hooks or other short-lived runtime
248
+ // effects. Keep reconciling while the exact thread still reports active;
249
+ // never convert elapsed time alone into success.
250
+ this.scheduleReconciliation(tracked);
251
+ return;
252
+ }
253
+ const inferredTurn = storedTerminal ? snapshot : {
254
+ ...snapshot,
255
+ status: 'completed',
256
+ error: null,
257
+ completedAt: snapshot.completedAt ?? new Date().toISOString(),
258
+ };
259
+ this.inferredTurns.add(key);
260
+ if (this.inferredTurns.size > MAX_LATE_TERMINAL_DEDUPLICATIONS) {
261
+ const oldest = this.inferredTurns.values().next().value;
262
+ if (oldest)
263
+ this.inferredTurns.delete(oldest);
264
+ }
265
+ this.options.log(`inferred missing turn/completed after authoritative reconciliation (${tracked.threadId}/${tracked.turnId})`);
266
+ this.options.emitToClient(JSON.stringify({
267
+ method: 'turn/completed',
268
+ params: { threadId: tracked.threadId, turn: inferredTurn },
269
+ _meta: { oursFleet: { terminalSource: 'inferred_missing_notification' } },
270
+ }));
271
+ this.finishTurn(key);
272
+ }
273
+ finishTurn(key) {
274
+ const turn = this.turns.get(key);
275
+ if (!turn)
276
+ return;
277
+ this.cancelReconciliation(turn);
278
+ if (turn.reconcileRequestId !== undefined) {
279
+ this.reconcileRequests.delete(turn.reconcileRequestId);
280
+ turn.reconcileRequestId = undefined;
281
+ }
282
+ for (const id of turn.openServerRequests)
283
+ this.serverRequests.delete(id);
284
+ this.turns.delete(key);
285
+ }
286
+ }
15
287
  function sandboxMode(policy) {
16
288
  if (!isObject(policy))
17
289
  return undefined;
@@ -94,24 +366,83 @@ export function runCodexAppServerProxy() {
94
366
  const child = spawn(launch.command, launch.args, {
95
367
  env, shell: launch.shell, stdio: ['pipe', 'pipe', 'pipe'],
96
368
  });
97
- child.stdout.pipe(process.stdout);
98
- child.stderr.pipe(process.stderr);
369
+ child.stderr.pipe(process.stderr, { end: false });
99
370
  const input = createInterface({ input: process.stdin });
371
+ const output = createInterface({ input: child.stdout });
100
372
  let finished = false;
373
+ let clientBackpressured = false;
374
+ const writeToClient = (line) => {
375
+ if (finished)
376
+ return;
377
+ if (!process.stdout.write(line + '\n')) {
378
+ if (!clientBackpressured) {
379
+ clientBackpressured = true;
380
+ output.pause();
381
+ process.stdout.once('drain', () => {
382
+ clientBackpressured = false;
383
+ if (!finished)
384
+ output.resume();
385
+ });
386
+ }
387
+ }
388
+ };
389
+ let childBackpressured = false;
390
+ const pendingChildLines = [];
391
+ const writeToChild = (line) => {
392
+ if (finished || child.stdin.destroyed)
393
+ return;
394
+ if (childBackpressured) {
395
+ pendingChildLines.push(line);
396
+ return;
397
+ }
398
+ if (!child.stdin.write(line + '\n')) {
399
+ childBackpressured = true;
400
+ input.pause();
401
+ child.stdin.once('drain', () => {
402
+ childBackpressured = false;
403
+ while (!childBackpressured && pendingChildLines.length > 0) {
404
+ const pending = pendingChildLines.shift();
405
+ if (!child.stdin.write(pending + '\n'))
406
+ childBackpressured = true;
407
+ }
408
+ if (!finished && !childBackpressured)
409
+ input.resume();
410
+ });
411
+ }
412
+ };
413
+ const recovery = new CodexTerminalRecovery({
414
+ sendToCodex: writeToChild,
415
+ emitToClient: writeToClient,
416
+ log: line => process.stderr.write(`ours-fleet Codex terminal recovery: ${line}\n`),
417
+ });
101
418
  const finish = (code) => {
102
419
  if (finished)
103
420
  return;
104
421
  finished = true;
105
- process.exitCode = code;
422
+ recovery.close();
106
423
  input.close();
107
424
  process.stdin.destroy();
425
+ // stdout/stderr are often pipes under Fleet and tests. Setting exitCode
426
+ // before their pending writes flush can drop the child's last diagnostic
427
+ // or protocol line. Empty writes provide ordered flush barriers.
428
+ // Let readable `data`/`line` callbacks queued with the child's `close`
429
+ // notification run before placing the barriers.
430
+ setImmediate(() => {
431
+ process.stdout.write('', () => {
432
+ process.stderr.write('', () => { process.exitCode = code; });
433
+ });
434
+ });
108
435
  };
436
+ let spawnFailed = false;
109
437
  child.once('error', error => {
438
+ spawnFailed = true;
110
439
  process.stderr.write(`ours-fleet Codex app-server proxy: ${error.message}\n`);
111
- finish(1);
112
440
  });
113
- child.once('exit', (code, signal) => {
114
- finish(code ?? (signal ? 1 : 0));
441
+ // `exit` can precede the final stdout/stderr chunks. Wait for `close`, which
442
+ // is emitted only after the child's stdio streams have closed, so a fast
443
+ // failure cannot lose its diagnostic or the last protocol envelope.
444
+ child.once('close', (code, signal) => {
445
+ finish(spawnFailed ? 1 : code ?? (signal ? 1 : 0));
115
446
  });
116
447
  // A child can close its input before Node delivers its exit event. Its exit
117
448
  // status is the authoritative failure; do not let the resulting EPIPE race it.
@@ -119,19 +450,22 @@ export function runCodexAppServerProxy() {
119
450
  input.on('line', line => {
120
451
  if (finished)
121
452
  return;
122
- const output = rewriteCodexAppServerRequest(line, approval, expectedSandbox, disableInheritedMcp) + '\n';
123
- if (!child.stdin.write(output)) {
124
- input.pause();
125
- child.stdin.once('drain', () => {
126
- if (!finished)
127
- input.resume();
128
- });
129
- }
453
+ const rewritten = rewriteCodexAppServerRequest(line, approval, expectedSandbox, disableInheritedMcp);
454
+ recovery.observeClientLine(rewritten);
455
+ writeToChild(rewritten);
456
+ });
457
+ output.on('line', line => {
458
+ if (!finished && recovery.observeServerLine(line))
459
+ writeToClient(line);
130
460
  });
131
461
  input.once('close', () => {
132
462
  if (!child.stdin.destroyed)
133
463
  child.stdin.end();
134
464
  });
465
+ process.stdin.once('end', () => {
466
+ if (!child.stdin.destroyed)
467
+ child.stdin.end();
468
+ });
135
469
  for (const signal of ['SIGINT', 'SIGTERM'])
136
470
  process.once(signal, () => child.kill(signal));
137
471
  }
@@ -1,4 +1,5 @@
1
1
  import type { ResolvedRole } from '../config.js';
2
+ import { CodexAppServerSession } from '../session/codex-app-server.js';
2
3
  import type { AcpMcpServer, SessionPrep } from './types.js';
3
4
  import type { AgentSessionAdapter, AgentSessionStartOptions, PreparedAgentSessionLaunch } from './agent-session.js';
4
5
  import { type AcpSessionTransport } from './acp-session-transport.js';
@@ -6,17 +7,25 @@ export interface CodexSessionStrategy {
6
7
  resolveBrain: AgentSessionAdapter['resolveBrain'];
7
8
  modelEnvironmentVariable: AgentSessionAdapter['modelEnvironmentVariable'];
8
9
  prepareLaunch(role: ResolvedRole, prep: SessionPrep): PreparedAgentSessionLaunch;
10
+ sessionConfigSelections(role: ResolvedRole): ReturnType<AgentSessionAdapter['sessionConfigSelections']>;
9
11
  permissionModeId(role: ResolvedRole): string | undefined;
10
12
  mcpServers(role: ResolvedRole): AcpMcpServer[] | undefined;
11
13
  sessionMeta(role: ResolvedRole, prep: SessionPrep): Record<string, unknown> | undefined;
14
+ approvalPolicy(role: ResolvedRole): string;
15
+ sandbox(role: ResolvedRole): string;
16
+ nativeConfig(role: ResolvedRole): Record<string, unknown> | undefined;
17
+ addDirs(role: ResolvedRole): string[] | undefined;
12
18
  }
19
+ export type CodexAppServerSessionTransport = typeof CodexAppServerSession.start;
13
20
  /** Explicit Codex implementation of Fleet's shared agent-session factory. */
14
21
  export declare class CodexAgentSessionAdapter implements AgentSessionAdapter {
15
22
  private readonly strategy;
16
23
  private readonly transport;
17
- constructor(strategy: CodexSessionStrategy, transport?: AcpSessionTransport);
24
+ private readonly nativeTransport;
25
+ constructor(strategy: CodexSessionStrategy, transport?: AcpSessionTransport, nativeTransport?: CodexAppServerSessionTransport);
18
26
  resolveBrain(brain: Parameters<AgentSessionAdapter['resolveBrain']>[0]): import("./agent-session.js").ResolvedBrainSelection;
19
27
  modelEnvironmentVariable(): string | undefined;
20
28
  prepareLaunch(role: ResolvedRole, prep: SessionPrep): PreparedAgentSessionLaunch;
21
- start(options: AgentSessionStartOptions): Promise<import("../index.js").AgentSession>;
29
+ sessionConfigSelections(role: ResolvedRole): import("./agent-session.js").AgentSessionConfigSelection[];
30
+ start(options: AgentSessionStartOptions): Promise<import("../index.js").AgentSession> | Promise<CodexAppServerSession>;
22
31
  }
@@ -1,12 +1,15 @@
1
1
  import { AcpSession } from '../session/acp.js';
2
+ import { CodexAppServerSession } from '../session/codex-app-server.js';
2
3
  import { acpAdapterState, } from './acp-session-transport.js';
3
4
  /** Explicit Codex implementation of Fleet's shared agent-session factory. */
4
5
  export class CodexAgentSessionAdapter {
5
6
  strategy;
6
7
  transport;
7
- constructor(strategy, transport = AcpSession.start) {
8
+ nativeTransport;
9
+ constructor(strategy, transport = AcpSession.start, nativeTransport = CodexAppServerSession.start) {
8
10
  this.strategy = strategy;
9
11
  this.transport = transport;
12
+ this.nativeTransport = nativeTransport;
10
13
  }
11
14
  resolveBrain(brain) {
12
15
  return this.strategy.resolveBrain(brain);
@@ -15,14 +18,31 @@ export class CodexAgentSessionAdapter {
15
18
  prepareLaunch(role, prep) {
16
19
  return this.strategy.prepareLaunch(role, prep);
17
20
  }
21
+ sessionConfigSelections(role) {
22
+ return this.strategy.sessionConfigSelections(role);
23
+ }
18
24
  start(options) {
19
25
  const { role, prep, launch } = options;
26
+ if (role.session === 'codex-app-server')
27
+ return this.nativeTransport({
28
+ name: role.name, argv: launch.argv, cwd: options.cwd, env: launch.env,
29
+ stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
30
+ permissionMode: options.permissionMode,
31
+ model: role.model, effort: role.effort,
32
+ approvalPolicy: this.strategy.approvalPolicy(role),
33
+ sandbox: this.strategy.sandbox(role),
34
+ config: this.strategy.nativeConfig(role),
35
+ addDirs: this.strategy.addDirs(role),
36
+ log: options.log,
37
+ });
20
38
  return this.transport({
21
- name: role.name, argv: launch.argv, cwd: options.cwd, env: launch.env,
39
+ name: role.name, harness: role.harness,
40
+ argv: launch.argv, cwd: options.cwd, env: launch.env,
22
41
  stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
23
42
  modeId: this.strategy.permissionModeId(role),
24
43
  mcpServers: this.strategy.mcpServers(role),
25
44
  sessionMeta: this.strategy.sessionMeta(role, prep),
45
+ configSelections: this.strategy.sessionConfigSelections(role),
26
46
  permissionMode: options.permissionMode,
27
47
  permissionMetadataSource: acpAdapterState(launch.adapterState).permissionMetadataSource,
28
48
  scrubObsoleteOursAutostart: true,
@@ -1,14 +1,18 @@
1
1
  import { type Exec } from '../exec.js';
2
+ import type { ResolvedRole } from '../config.js';
2
3
  import type { AcpLaunch, HarnessAdapter, UnattendedCapability } from './types.js';
3
4
  import { type AcpAgentResolution } from './acp-agent.js';
4
5
  import type { AcpSessionTransport } from './acp-session-transport.js';
6
+ import type { CodexAppServerSessionTransport } from './codex-session.js';
5
7
  /**
6
8
  * What an unattended role can actually do under Codex's native settings.
7
9
  * `on-request` and `untrusted` stop to ask, and with no console attached that
8
10
  * request is refused rather than answered — so the role can only read.
9
11
  */
10
12
  export declare function codexCapabilities(approval: string, sandbox: string): UnattendedCapability[];
13
+ /** Defense in depth for callers which launch a role after validation was bypassed. */
14
+ export declare function nativeCodexConfig(role: ResolvedRole): Record<string, unknown> | undefined;
11
15
  /** Bind launch argv and metadata provenance to one already-completed resolution. */
12
16
  export declare function codexAcpLaunchForResolution(resolution: AcpAgentResolution): Pick<AcpLaunch, 'argv' | 'permissionMetadataSource'>;
13
- export declare function makeCodexAdapter(exec?: Exec, transport?: AcpSessionTransport): HarnessAdapter;
17
+ export declare function makeCodexAdapter(exec?: Exec, transport?: AcpSessionTransport, nativeTransport?: CodexAppServerSessionTransport): HarnessAdapter;
14
18
  export declare const codexAdapter: HarnessAdapter;