@ours.network/fleet 1.1.0-nightly.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -46
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +7 -4
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +4 -4
- package/dist/application/role-creation-service.js +8 -1
- package/dist/application/role-repository.d.ts +4 -2
- package/dist/application/role-repository.js +6 -3
- package/dist/application/session-control.d.ts +3 -1
- package/dist/application/session-control.js +4 -2
- package/dist/application/task-room-service.d.ts +92 -39
- package/dist/application/task-room-service.js +562 -289
- package/dist/briefing.js +79 -22
- package/dist/build-info.json +5 -5
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +129 -47
- package/dist/config.d.ts +38 -3
- package/dist/config.js +193 -28
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +181 -41
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +40 -4
- package/dist/fleet-command-audit.d.ts +22 -1
- package/dist/fleet-command-audit.js +355 -50
- package/dist/fleet-proxy.d.ts +5 -2
- package/dist/harness/claude-code-session.js +2 -1
- package/dist/harness/codex-app-server-proxy.d.ts +40 -0
- package/dist/harness/codex-app-server-proxy.js +348 -14
- package/dist/harness/codex-session.d.ts +9 -2
- package/dist/harness/codex-session.js +18 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +104 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +1 -1
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +631 -0
- package/dist/lifecycle-summary.d.ts +134 -0
- package/dist/lifecycle-summary.js +179 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +38 -5
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +19 -3
- package/dist/owner-channel/channel.js +301 -120
- package/dist/owner-channel/commands.d.ts +28 -6
- package/dist/owner-channel/commands.js +109 -27
- package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
- package/dist/owner-channel/lifecycle-outbox.js +87 -0
- package/dist/owner-channel/ours-client.d.ts +19 -0
- package/dist/owner-channel/ours-client.js +3 -0
- package/dist/permissions.js +1 -1
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +450 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.js +13 -1
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +361 -285
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +23 -7
- package/dist/rooms-tasks/config.js +16 -25
- package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
- package/dist/rooms-tasks/cowork-adapter.js +13 -0
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/external-worker.d.ts +8 -0
- package/dist/rooms-tasks/external-worker.js +38 -0
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +181 -0
- package/dist/rooms-tasks/member-overrides.d.ts +30 -0
- package/dist/rooms-tasks/member-overrides.js +204 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +9 -2
- package/dist/rooms-tasks/provision.js +146 -83
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-room-name.d.ts +12 -0
- package/dist/rooms-tasks/task-room-name.js +61 -0
- package/dist/rooms-tasks/task-state.d.ts +115 -3
- package/dist/rooms-tasks/task-state.js +361 -11
- package/dist/rooms-tasks/templates.d.ts +4 -1
- package/dist/rooms-tasks/templates.js +34 -3
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +10 -4
- package/dist/rooms-tasks/types.d.ts +92 -7
- package/dist/rooms-tasks/types.js +14 -1
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +91 -44
- package/dist/session/acp.d.ts +4 -1
- package/dist/session/acp.js +29 -13
- package/dist/session/arbiter.d.ts +2 -1
- package/dist/session/arbiter.js +2 -0
- package/dist/session/codex-app-server-transport.d.ts +53 -0
- package/dist/session/codex-app-server-transport.js +184 -0
- package/dist/session/codex-app-server.d.ts +122 -0
- package/dist/session/codex-app-server.js +1031 -0
- package/dist/session/control.d.ts +1 -1
- package/dist/session/control.js +17 -1
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/types.d.ts +24 -6
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +9 -0
- package/dist/spawn.js +77 -9
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +25 -7
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
- package/dist/web-app/index.html +1 -1
- package/package.json +4 -4
- package/presets/brain-catalog.json +17 -0
- package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
- package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
- package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +37 -0
- package/presets/fleet/roles/Critic.yaml +23 -1
- package/presets/fleet/roles/Developer.yaml +27 -5
- package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
- package/presets/fleet/room_templates/pair.yaml +9 -7
- package/presets/fleet/room_templates/single.yaml +5 -4
- package/presets/fleet/room_templates/team.yaml +12 -8
- package/presets/manifest.json +2 -1
- package/presets/fleet/agents/Architect.yaml +0 -3
- package/presets/fleet/agents/Secretary.yaml +0 -3
- package/presets/fleet/agents/Tester.yaml +0 -3
- package/presets/fleet/roles/Agent.yaml +0 -6
- package/presets/fleet/roles/Architect.yaml +0 -6
- package/presets/fleet/roles/Secretary.yaml +0 -6
- package/presets/fleet/roles/Tester.yaml +0 -6
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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';
|
|
@@ -10,15 +11,21 @@ export interface CodexSessionStrategy {
|
|
|
10
11
|
permissionModeId(role: ResolvedRole): string | undefined;
|
|
11
12
|
mcpServers(role: ResolvedRole): AcpMcpServer[] | undefined;
|
|
12
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;
|
|
13
18
|
}
|
|
19
|
+
export type CodexAppServerSessionTransport = typeof CodexAppServerSession.start;
|
|
14
20
|
/** Explicit Codex implementation of Fleet's shared agent-session factory. */
|
|
15
21
|
export declare class CodexAgentSessionAdapter implements AgentSessionAdapter {
|
|
16
22
|
private readonly strategy;
|
|
17
23
|
private readonly transport;
|
|
18
|
-
|
|
24
|
+
private readonly nativeTransport;
|
|
25
|
+
constructor(strategy: CodexSessionStrategy, transport?: AcpSessionTransport, nativeTransport?: CodexAppServerSessionTransport);
|
|
19
26
|
resolveBrain(brain: Parameters<AgentSessionAdapter['resolveBrain']>[0]): import("./agent-session.js").ResolvedBrainSelection;
|
|
20
27
|
modelEnvironmentVariable(): string | undefined;
|
|
21
28
|
prepareLaunch(role: ResolvedRole, prep: SessionPrep): PreparedAgentSessionLaunch;
|
|
22
29
|
sessionConfigSelections(role: ResolvedRole): import("./agent-session.js").AgentSessionConfigSelection[];
|
|
23
|
-
start(options: AgentSessionStartOptions): Promise<import("../index.js").AgentSession>;
|
|
30
|
+
start(options: AgentSessionStartOptions): Promise<import("../index.js").AgentSession> | Promise<CodexAppServerSession>;
|
|
24
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
|
-
|
|
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);
|
|
@@ -20,8 +23,21 @@ export class CodexAgentSessionAdapter {
|
|
|
20
23
|
}
|
|
21
24
|
start(options) {
|
|
22
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
|
+
});
|
|
23
38
|
return this.transport({
|
|
24
|
-
name: role.name,
|
|
39
|
+
name: role.name, harness: role.harness,
|
|
40
|
+
argv: launch.argv, cwd: options.cwd, env: launch.env,
|
|
25
41
|
stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
|
|
26
42
|
modeId: this.strategy.permissionModeId(role),
|
|
27
43
|
mcpServers: this.strategy.mcpServers(role),
|
package/dist/harness/codex.d.ts
CHANGED
|
@@ -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;
|