ai-foreman 1.7.29 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/claude.d.ts +23 -12
- package/dist/adapters/claude.js +227 -23
- package/dist/adapters/codex.d.ts +17 -7
- package/dist/adapters/codex.js +195 -17
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +5 -1
- package/dist/adapters/structured.d.ts +71 -0
- package/dist/adapters/structured.js +213 -0
- package/dist/adapters/types.d.ts +85 -9
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +2 -1
- package/dist/boundedTurnEvidence.d.ts +18 -0
- package/dist/boundedTurnEvidence.js +76 -0
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +2 -0
- package/dist/branch/planner.js +1 -4
- package/dist/branch/runner.d.ts +2 -1
- package/dist/branch/runner.js +10 -7
- package/dist/buildAdmission.js +4 -0
- package/dist/buildApproval.d.ts +15 -0
- package/dist/buildApproval.js +25 -0
- package/dist/buildAssignment.d.ts +1 -0
- package/dist/buildAssignment.js +82 -23
- package/dist/buildInterventions.d.ts +2 -2
- package/dist/buildInterventions.js +11 -4
- package/dist/buildLaunchGate.js +3 -2
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +22 -26
- package/dist/cli/tickets.d.ts +0 -7
- package/dist/cli/tickets.js +41 -40
- package/dist/continuity.d.ts +10 -14
- package/dist/continuity.js +198 -179
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/foreman.d.ts +10 -4
- package/dist/foreman.js +51 -193
- package/dist/graph/boundary.d.ts +1 -1
- package/dist/graph/boundary.js +1 -1
- package/dist/graph/host.js +8 -19
- package/dist/graph/manager.d.ts +1 -1
- package/dist/graph/manager.js +1 -1
- package/dist/graph/preparation.d.ts +38 -0
- package/dist/graph/preparation.js +7 -6
- package/dist/graph/semantic.d.ts +1 -0
- package/dist/graph/semantic.js +4 -1
- package/dist/graph/session.js +7 -2
- package/dist/graph/turn.d.ts +2 -3
- package/dist/graph/turn.js +28 -40
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +3 -1
- package/dist/handoffs.js +90 -59
- package/dist/hostGraphApplicability.d.ts +2 -1
- package/dist/hostGraphApplicability.js +17 -19
- package/dist/hostQaAugmentation.js +21 -4
- package/dist/hostQaComparison.d.ts +21 -2
- package/dist/hostQaComparison.js +43 -29
- package/dist/hostSourcePreservation.d.ts +38 -0
- package/dist/hostSourcePreservation.js +72 -0
- package/dist/interviews.d.ts +7 -0
- package/dist/interviews.js +18 -1
- package/dist/legacyCommunicationDisplay.d.ts +2 -0
- package/dist/legacyCommunicationDisplay.js +2 -0
- package/dist/managerPacket.js +3 -3
- package/dist/markers.d.ts +4 -3
- package/dist/markers.js +4 -20
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +47 -12
- package/dist/qaBuilderCoverage.js +22 -13
- package/dist/qaBuilderEvidence.d.ts +16 -1
- package/dist/qaBuilderEvidence.js +48 -8
- package/dist/qaContractCoverage.d.ts +2 -2
- package/dist/qaContractCoverage.js +4 -9
- package/dist/qaContractDelivery.js +34 -20
- package/dist/qaCoverageAssembly.d.ts +22 -0
- package/dist/qaCoverageAssembly.js +16 -0
- package/dist/qaDeliveryJournal.d.ts +2 -0
- package/dist/qaDeliveryJournal.js +19 -0
- package/dist/qaFailureDelivery.js +97 -85
- package/dist/qaHostTransformation.d.ts +26 -0
- package/dist/qaHostTransformation.js +100 -0
- package/dist/qaPreparation.d.ts +8 -20
- package/dist/qaPreparation.js +133 -248
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.js +21 -4
- package/dist/qaReview.js +220 -395
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +71 -21
- package/dist/qaStructuredResponse.d.ts +10 -0
- package/dist/qaStructuredResponse.js +57 -0
- package/dist/qaTimeline.js +5 -3
- package/dist/qaVerificationContract.d.ts +1 -2
- package/dist/qaVerificationContract.js +24 -35
- package/dist/readinessGate.js +5 -4
- package/dist/runtimeReadiness.js +23 -15
- package/dist/statusReporter.d.ts +2 -0
- package/dist/statusReporter.js +2 -0
- package/dist/structuredCutover.d.ts +31 -0
- package/dist/structuredCutover.js +510 -0
- package/dist/structuredCutoverFiles.d.ts +10 -0
- package/dist/structuredCutoverFiles.js +181 -0
- package/dist/structuredDispatchAuthority.d.ts +22 -0
- package/dist/structuredDispatchAuthority.js +110 -0
- package/dist/structuredDispatchRecord.d.ts +29 -0
- package/dist/structuredDispatchRecord.js +36 -0
- package/dist/structuredLiveBudget.d.ts +2 -0
- package/dist/structuredLiveBudget.js +23 -0
- package/dist/structuredOperation.d.ts +18 -0
- package/dist/structuredOperation.js +257 -0
- package/dist/supervisedStart.js +35 -21
- package/dist/ticketPlanning.d.ts +1 -373
- package/dist/ticketPlanning.js +2 -26
- package/dist/ticketPopulation.d.ts +0 -3
- package/dist/ticketPopulation.js +4 -21
- package/dist/ticketPopulationPageAuthority.d.ts +13 -0
- package/dist/ticketPopulationPageAuthority.js +30 -0
- package/dist/ticketPopulationPaging.d.ts +27 -0
- package/dist/ticketPopulationPaging.js +105 -0
- package/dist/tickets/renderMarkdown.d.ts +14 -0
- package/dist/tickets/renderMarkdown.js +39 -5
- package/dist/tickets/ticketLoader.js +10 -0
- package/dist/tickets/ticketSchema.d.ts +107 -0
- package/dist/tickets/ticketSchema.js +4 -1
- package/dist/workflowDb.d.ts +100 -2
- package/dist/workflowDb.js +763 -39
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- package/package.json +15 -7
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ProviderTurnPurpose } from "../providerPhase.js";
|
|
2
1
|
import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, NativeAutoCompactionPolicy, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
|
|
3
2
|
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
3
|
+
import type { ProviderTurnPolicy } from "./types.js";
|
|
4
4
|
export interface CodexLineResult {
|
|
5
5
|
events: BuilderEvent[];
|
|
6
6
|
sessionId?: string;
|
|
@@ -30,6 +30,12 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
30
30
|
private closed;
|
|
31
31
|
private closePromise?;
|
|
32
32
|
private activeText;
|
|
33
|
+
private structuredCollector?;
|
|
34
|
+
private turnStreamedBytes;
|
|
35
|
+
private streamStartedAt;
|
|
36
|
+
private streamActivityAt;
|
|
37
|
+
private streamDiagnosticAt;
|
|
38
|
+
private turnInFlight;
|
|
33
39
|
private usage?;
|
|
34
40
|
private usageRevision;
|
|
35
41
|
private providerSessionUsage?;
|
|
@@ -82,12 +88,7 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
82
88
|
buildAppServerArgs(): string[];
|
|
83
89
|
/** Deprecated fixture helper. Runtime execution uses only app-server. */
|
|
84
90
|
buildArgs(instruction: string): string[];
|
|
85
|
-
sendTurn(instruction: string, policy?:
|
|
86
|
-
handback?: boolean;
|
|
87
|
-
responseOnly?: boolean;
|
|
88
|
-
logicalActionId?: string;
|
|
89
|
-
purpose?: ProviderTurnPurpose;
|
|
90
|
-
}): Promise<TurnResult>;
|
|
91
|
+
sendTurn(instruction: string, policy?: ProviderTurnPolicy): Promise<TurnResult>;
|
|
91
92
|
private sendTurnInternal;
|
|
92
93
|
compact(): Promise<CompactResult>;
|
|
93
94
|
private compactInternal;
|
|
@@ -119,6 +120,15 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
119
120
|
private handle;
|
|
120
121
|
private observationContext;
|
|
121
122
|
private waitFor;
|
|
123
|
+
/** Ask the app-server to stop a native turn; true only once its terminal is observed. */
|
|
124
|
+
private interruptNativeTurn;
|
|
125
|
+
/** Streamed output is visible progress; it is never treated as a result. */
|
|
126
|
+
private observeAgentMessageDelta;
|
|
127
|
+
/**
|
|
128
|
+
* Bounded interruption evidence for structured turns under .foreman (Rafi's
|
|
129
|
+
* git-excluded log directory). Diagnostic only: never read back as output.
|
|
130
|
+
*/
|
|
131
|
+
private writeStreamDiagnostic;
|
|
122
132
|
private traceLifecycle;
|
|
123
133
|
private touchWaiters;
|
|
124
134
|
private providerIdleTimeoutMs;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { inheritedStructuredLiveBudget } from "../structuredLiveBudget.js";
|
|
1
2
|
import { OperationDeadline } from "../util/deadline.js";
|
|
2
3
|
import { spawn } from "node:child_process";
|
|
3
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { createInterface } from "node:readline";
|
|
7
8
|
import { loadSkill } from "special-agents";
|
|
@@ -11,6 +12,9 @@ import { canonicalSessionPath, createProviderSessionRef, validateProviderSession
|
|
|
11
12
|
import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
|
|
12
13
|
import { isFastModeUnavailable, reportFastModeUnavailable } from "./fastMode.js";
|
|
13
14
|
import { handoffSystemInstructions } from "../handoffProtocol.js";
|
|
15
|
+
import { providerControlOperationV2, providerWireContract, renderProviderWireInstruction, initializationOutput, validateStructuredCandidate, selectedDispatchTupleV2, structuredDigest } from "rafi-spec";
|
|
16
|
+
import { assertActiveProviderDispatch, withEphemeralInitializationDispatch } from "../structuredDispatchAuthority.js";
|
|
17
|
+
import { CodexRunawayOutputError, CodexTurnCollector, structuredReceipt } from "./structured.js";
|
|
14
18
|
/** Compatibility parser for recorded pre-app-server JSONL fixtures. */
|
|
15
19
|
export function parseCodexLine(raw) {
|
|
16
20
|
const type = raw.type;
|
|
@@ -46,6 +50,12 @@ export class CodexAdapter {
|
|
|
46
50
|
closed = false;
|
|
47
51
|
closePromise;
|
|
48
52
|
activeText = [];
|
|
53
|
+
structuredCollector;
|
|
54
|
+
turnStreamedBytes = 0;
|
|
55
|
+
streamStartedAt = 0;
|
|
56
|
+
streamActivityAt = 0;
|
|
57
|
+
streamDiagnosticAt = 0;
|
|
58
|
+
turnInFlight = false;
|
|
49
59
|
usage;
|
|
50
60
|
usageRevision = 0;
|
|
51
61
|
providerSessionUsage;
|
|
@@ -117,6 +127,7 @@ export class CodexAdapter {
|
|
|
117
127
|
}
|
|
118
128
|
constructor(opts) {
|
|
119
129
|
this.opts = opts;
|
|
130
|
+
inheritedStructuredLiveBudget("codex");
|
|
120
131
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
121
132
|
this._sessionRef = opts.resumeSessionRef;
|
|
122
133
|
}
|
|
@@ -163,9 +174,23 @@ export class CodexAdapter {
|
|
|
163
174
|
return args;
|
|
164
175
|
}
|
|
165
176
|
async sendTurn(instruction, policy) {
|
|
177
|
+
if (this.turnInFlight)
|
|
178
|
+
throw new Error("a turn is already in progress");
|
|
179
|
+
if (policy?.output)
|
|
180
|
+
assertActiveProviderDispatch(policy, instruction, "codex", this.sessionRef());
|
|
181
|
+
if (policy?.output) {
|
|
182
|
+
const selected = policy.wireContract ?? providerWireContract(policy.output, "codex", "default", { requireRegistered: true });
|
|
183
|
+
if (selected.provider !== "codex" || selected.canonical.schemaDigest !== policy.output.schemaDigest)
|
|
184
|
+
throw new Error("Codex received a mismatched provider wire contract");
|
|
185
|
+
if (policy.renderedWireContractDigest && policy.renderedWireContractDigest !== selected.descriptorDigest)
|
|
186
|
+
throw new Error("Codex received mismatched rendered wire guidance");
|
|
187
|
+
instruction = renderProviderWireInstruction(instruction, selected);
|
|
188
|
+
policy = { ...policy, wireContract: selected, renderedWireContractDigest: selected.descriptorDigest };
|
|
189
|
+
}
|
|
190
|
+
this.turnInFlight = true;
|
|
166
191
|
const prepared = this.instructionWithPendingQaReminder(instruction);
|
|
167
192
|
instruction = prepared.instruction;
|
|
168
|
-
const turnId = randomUUID();
|
|
193
|
+
const turnId = policy?.dispatchId ?? randomUUID();
|
|
169
194
|
this.activeProviderTurnId = turnId;
|
|
170
195
|
const observer = this.opts.observer;
|
|
171
196
|
if (!observer) {
|
|
@@ -187,6 +212,8 @@ export class CodexAdapter {
|
|
|
187
212
|
finally {
|
|
188
213
|
this.activeProviderTurnId = undefined;
|
|
189
214
|
this.activeNativeTurnId = undefined;
|
|
215
|
+
this.structuredCollector = undefined;
|
|
216
|
+
this.turnInFlight = false;
|
|
190
217
|
}
|
|
191
218
|
}
|
|
192
219
|
const context = this.observationContext();
|
|
@@ -218,9 +245,11 @@ export class CodexAdapter {
|
|
|
218
245
|
this.activeProviderTurnId = undefined;
|
|
219
246
|
this.activeNativeTurnId = undefined;
|
|
220
247
|
this.activeProviderTurnSpanId = undefined;
|
|
248
|
+
this.structuredCollector = undefined;
|
|
249
|
+
this.turnInFlight = false;
|
|
221
250
|
}
|
|
222
251
|
}
|
|
223
|
-
async sendTurnInternal(instruction, policy) {
|
|
252
|
+
async sendTurnInternal(instruction, policy, runawayRetries = 0) {
|
|
224
253
|
if (this.closed)
|
|
225
254
|
throw new Error("builder is closed");
|
|
226
255
|
this.eventQueue.push({ kind: "activity", state: "starting Codex turn", provider: "codex", model: this.opts.model });
|
|
@@ -233,12 +262,29 @@ export class CodexAdapter {
|
|
|
233
262
|
const toolsBefore = this.observedToolCalls;
|
|
234
263
|
this.turnSawTool = false;
|
|
235
264
|
this.activeText = [];
|
|
265
|
+
this.turnStreamedBytes = 0;
|
|
266
|
+
this.streamStartedAt = 0;
|
|
267
|
+
this.streamActivityAt = 0;
|
|
268
|
+
this.streamDiagnosticAt = 0;
|
|
236
269
|
this.lastTurnTokens = undefined;
|
|
237
270
|
this.turnUsageBaseline = this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined;
|
|
238
|
-
const
|
|
239
|
-
|
|
271
|
+
const deadline = new OperationDeadline("Codex active turn", this.opts.turnDeadlineMs ?? 3_600_000);
|
|
272
|
+
let idleTimer;
|
|
273
|
+
const clearIdle = () => { if (idleTimer)
|
|
274
|
+
clearTimeout(idleTimer); idleTimer = undefined; };
|
|
275
|
+
const resetIdle = () => {
|
|
276
|
+
clearIdle();
|
|
277
|
+
idleTimer = setTimeout(() => collector?.fail(new Error("Codex native turn idle deadline expired; execution outcome requires reconciliation")), this.providerIdleTimeoutMs());
|
|
278
|
+
idleTimer.unref();
|
|
279
|
+
};
|
|
280
|
+
const wire = policy?.output ? policy.wireContract ?? providerWireContract(policy.output, "codex", "default", { requireRegistered: true }) : undefined;
|
|
281
|
+
const collector = policy?.output ? this.structuredCollector = new CodexTurnCollector(this._sessionId, true, resetIdle, wire.maximumResponseBytes) : undefined;
|
|
282
|
+
const completion = collector ? collector.completion : this.waitFor("turn/completed", (params) => params.threadId === this._sessionId, this.providerIdleTimeoutMs(), true);
|
|
283
|
+
void completion.then(clearIdle, clearIdle);
|
|
240
284
|
turnStartDispatched = true;
|
|
285
|
+
const readOnlyTurn = Boolean(policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" || this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa");
|
|
241
286
|
const startParams = {
|
|
287
|
+
...(wire ? { outputSchema: wire.wireSchema } : {}),
|
|
242
288
|
threadId: this._sessionId,
|
|
243
289
|
input: [{ type: "text", text: providerInstruction, text_elements: [] }],
|
|
244
290
|
cwd: this.opts.cwd,
|
|
@@ -246,41 +292,82 @@ export class CodexAdapter {
|
|
|
246
292
|
effort: this.opts.effort ?? null,
|
|
247
293
|
...(this.opts.fast ? { serviceTier: this.fastTier ?? "default" } : {}),
|
|
248
294
|
approvalPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" ? "never" : this.effectiveApprovalPolicy(),
|
|
249
|
-
sandboxPolicy:
|
|
295
|
+
sandboxPolicy: readOnlyTurn
|
|
250
296
|
? { type: "readOnly", networkAccess: false }
|
|
251
297
|
: { type: "workspaceWrite", writableRoots: [this.opts.cwd], networkAccess: this.effectiveNetworkAccess() },
|
|
252
298
|
};
|
|
253
299
|
let started;
|
|
254
300
|
try {
|
|
255
|
-
started = await this.request("turn/start", startParams);
|
|
301
|
+
started = await deadline.run(() => this.request("turn/start", startParams));
|
|
256
302
|
}
|
|
257
303
|
catch (error) {
|
|
258
|
-
if (!this.fastTier || !isFastModeUnavailable(String(error)) || this.turnSawTool || this.activeText.length)
|
|
304
|
+
if (policy?.output || !this.fastTier || !isFastModeUnavailable(String(error)) || this.turnSawTool || this.activeText.length)
|
|
259
305
|
throw error;
|
|
260
306
|
this.disableFastMode();
|
|
261
307
|
started = await this.request("turn/start", { ...startParams, serviceTier: "default" });
|
|
262
308
|
}
|
|
263
309
|
this.activeNativeTurnId = String(started.turn?.id ?? "") || undefined;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
310
|
+
collector?.bind(this.activeNativeTurnId ?? "");
|
|
311
|
+
let params;
|
|
312
|
+
try {
|
|
313
|
+
params = await deadline.run(() => completion);
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
// Runaway output is a definite provider rejection, not an unknown
|
|
317
|
+
// outcome. A read-only turn cannot change the workspace, so after the
|
|
318
|
+
// native turn is confirmed stopped it may be asked once more, inside
|
|
319
|
+
// the same sealed host dispatch. Writable turns are never retried.
|
|
320
|
+
const nativeTurnId = this.activeNativeTurnId;
|
|
321
|
+
if (error instanceof CodexRunawayOutputError && runawayRetries < 1 && readOnlyTurn && nativeTurnId && await this.interruptNativeTurn(nativeTurnId)) {
|
|
322
|
+
this.writeStreamDiagnostic("interrupted", `${error.message}; retrying once`);
|
|
323
|
+
this.traceLifecycle("runaway-output-retry", { elapsedMs: 0 });
|
|
324
|
+
this.eventQueue.push({ kind: "retry", provider: "codex", reason: error.message, attempt: 1, maximum: 1, managedBy: "rafi", retryId: nativeTurnId });
|
|
325
|
+
return this.sendTurnInternal(instruction, policy, runawayRetries + 1);
|
|
326
|
+
}
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
const turn = collector ? params : params.turn;
|
|
330
|
+
const responseOnlyViolation = Boolean(policy?.responseOnly && (collector ? collector.tools : this.observedToolCalls !== toolsBefore));
|
|
331
|
+
const failed = turn?.status !== "completed" || responseOnlyViolation;
|
|
268
332
|
const error = turn?.error;
|
|
269
|
-
|
|
270
|
-
|
|
333
|
+
let final;
|
|
334
|
+
let invalidFinal;
|
|
335
|
+
try {
|
|
336
|
+
final = collector?.final();
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
invalidFinal = String(error);
|
|
340
|
+
}
|
|
341
|
+
if (this.streamDiagnosticAt)
|
|
342
|
+
this.writeStreamDiagnostic(failed || invalidFinal ? "interrupted" : "completed", failed ? `Codex terminal ${String(turn?.status)}` : invalidFinal);
|
|
343
|
+
const text = final?.text ?? this.activeText.join("\n");
|
|
344
|
+
if (!policy?.output && failed && !responseOnlyViolation && this.fastTier && !this.turnSawTool && !text && isFastModeUnavailable(String(error?.message ?? ""))) {
|
|
271
345
|
this.disableFastMode();
|
|
272
346
|
return this.sendTurnInternal(instruction, policy);
|
|
273
347
|
}
|
|
274
|
-
|
|
348
|
+
let result = {
|
|
275
349
|
text: failed ? normalizeRuntimeErrorText("codex", String(responseOnlyViolation ? "Response-only correction used tools; its result cannot authorize completion" : error?.message ?? text), null, "app-server turn") : text,
|
|
276
350
|
isError: failed, numTurns: 1, costUsd: 0, costAuthoritative: false,
|
|
277
351
|
turnId: this.activeProviderTurnId, hostInstruction: instruction, providerInstruction,
|
|
278
352
|
rawResponse: text, cleanedResponse: failed ? normalizeRuntimeErrorText("codex", String(responseOnlyViolation ? "Response-only correction used tools; its result cannot authorize completion" : error?.message ?? text), null, "app-server turn") : text,
|
|
279
353
|
providerMetadata: { provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef },
|
|
354
|
+
...(policy?.selectedDispatch ? { selectedDispatch: policy.selectedDispatch } : {}),
|
|
280
355
|
...(this.lastTurnTokens ?? {}),
|
|
281
356
|
};
|
|
282
357
|
result.usage = { scope: "turn-delta", inputTokens: result.inputTokens, outputTokens: result.outputTokens,
|
|
283
358
|
...(result.inputTokens !== undefined || result.outputTokens !== undefined ? { totalTokens: (result.inputTokens ?? 0) + (result.outputTokens ?? 0) } : {}) };
|
|
359
|
+
if (policy?.output) {
|
|
360
|
+
const selectedDispatch = policy.selectedDispatch;
|
|
361
|
+
if (!selectedDispatch || !this._sessionId)
|
|
362
|
+
throw new Error("Structured terminal lost its sealed dispatch tuple or session");
|
|
363
|
+
result = { ...result, selectedDispatch, structured: failed ? { state: "rejected", reason: responseOnlyViolation ? "tool-policy-violation" : "execution-not-completed", detail: `Codex terminal ${String(turn?.status)}`, dispatchId: result.turnId }
|
|
364
|
+
: invalidFinal ? { state: "rejected", reason: "ambiguous-result", detail: invalidFinal, dispatchId: result.turnId }
|
|
365
|
+
: !wire || !policy.selectedDispatch
|
|
366
|
+
? { state: "rejected", reason: "invalid-contract", detail: "Codex structured turn has no sealed descriptor or tuple", dispatchId: result.turnId }
|
|
367
|
+
: structuredReceipt(policy.output, "codex", final?.itemId ? text : undefined, { dispatchId: result.turnId, sessionId: this._sessionId, nativeTurnId: this.activeNativeTurnId, nativeItemId: final?.itemId }, "json-text", wire, policy.selectedDispatch) };
|
|
368
|
+
if (result.structured.state === "rejected")
|
|
369
|
+
result.isError = true;
|
|
370
|
+
}
|
|
284
371
|
this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
|
|
285
372
|
return result;
|
|
286
373
|
}
|
|
@@ -298,6 +385,8 @@ export class CodexAdapter {
|
|
|
298
385
|
return result;
|
|
299
386
|
}
|
|
300
387
|
const detail = [error instanceof Error ? error.message : String(error), this.stderr].filter(Boolean).join("\n");
|
|
388
|
+
if (this.turnStreamedBytes)
|
|
389
|
+
this.writeStreamDiagnostic("interrupted", error instanceof Error ? error.message : String(error));
|
|
301
390
|
const text = normalizeRuntimeErrorText("codex", detail, this.process?.exitCode ?? null, "builder turn");
|
|
302
391
|
this.disconnect(new Error(text));
|
|
303
392
|
const resumed = Boolean(this.opts.resumeSessionRef);
|
|
@@ -379,6 +468,7 @@ export class CodexAdapter {
|
|
|
379
468
|
}, hardTimeoutMs, false, controller.signal);
|
|
380
469
|
this.manualCompactionInFlight = true;
|
|
381
470
|
try {
|
|
471
|
+
providerControlOperationV2("codex", "thread/compact/start");
|
|
382
472
|
const acknowledgementFailure = this.request("thread/compact/start", { threadId: this._sessionId }).then(() => { this.traceLifecycle("compaction-acknowledged"); return new Promise(() => { }); }, error => {
|
|
383
473
|
// A missing acknowledgement is not failure evidence. Explicit
|
|
384
474
|
// completion plus fresh occupancy is sufficient to reconcile it.
|
|
@@ -391,6 +481,13 @@ export class CodexAdapter {
|
|
|
391
481
|
finally {
|
|
392
482
|
this.manualCompactionInFlight = false;
|
|
393
483
|
}
|
|
484
|
+
// Manual completion must invalidate delivery receipts just like automatic
|
|
485
|
+
// compaction. Continuity accounts for this requested compaction separately.
|
|
486
|
+
this.nativeCompactionSequence++;
|
|
487
|
+
if (this.qaContractReminder) {
|
|
488
|
+
this.qaContractReminderPending = true;
|
|
489
|
+
this.qaReminderEvent("compaction-observed");
|
|
490
|
+
}
|
|
394
491
|
this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
|
|
395
492
|
this.traceLifecycle("compaction-completed", { elapsedMs: performance.now() - began });
|
|
396
493
|
return { ok: true };
|
|
@@ -437,11 +534,28 @@ export class CodexAdapter {
|
|
|
437
534
|
if (this.opts.allowAutoCompactionSetupTurn === false)
|
|
438
535
|
return;
|
|
439
536
|
const toolsBefore = this.observedToolCalls;
|
|
440
|
-
const
|
|
537
|
+
const descriptor = providerWireContract(initializationOutput, "codex");
|
|
538
|
+
const instruction = renderProviderWireInstruction("Rafi internal initialization only. Do not call tools or modify files. Submit the native session-initialization result with completed payload {ready:true} and null continuity.", descriptor);
|
|
539
|
+
const session = this.sessionRef();
|
|
540
|
+
if (!session)
|
|
541
|
+
throw new Error("Ephemeral Codex initialization requires a prepared session");
|
|
542
|
+
const id = randomUUID();
|
|
543
|
+
const tuple = selectedDispatchTupleV2({ operation: initializationOutput.operation, schemaVersion: initializationOutput.schemaVersion, schemaDigest: initializationOutput.schemaDigest,
|
|
544
|
+
provider: "codex", profile: descriptor.profile, variant: descriptor.variant, descriptorDigest: descriptor.descriptorDigest,
|
|
545
|
+
wireSchemaDigest: descriptor.wireSchemaDigest, guidanceDigest: descriptor.guidanceDigest, normalizerId: descriptor.normalizerId,
|
|
546
|
+
normalizerVersion: descriptor.normalizerVersion, maximumSchemaBytes: descriptor.maximumSchemaBytes, maximumResponseBytes: descriptor.maximumResponseBytes,
|
|
547
|
+
maximumPageBytes: descriptor.maximumPageBytes ?? null, instructionDigest: structuredDigest(instruction), actionId: id, dispatchId: id, sessionId: session.sessionId, generation: session.generation });
|
|
548
|
+
const setup = await withEphemeralInitializationDispatch(this.opts.configRoot ?? this.opts.cwd, tuple, dispatchCapability => this.sendTurn(instruction, { purpose: "initialization", responseOnly: true, output: initializationOutput, dispatchId: id, logicalActionId: id, wireContract: descriptor,
|
|
549
|
+
renderedWireContractDigest: descriptor.descriptorDigest, selectedDispatch: tuple, dispatchCapability }), session);
|
|
441
550
|
if (setup.isError)
|
|
442
551
|
throw new Error(`Codex automatic-compaction setup failed: ${setup.text.slice(0, 240)}`);
|
|
443
552
|
if (this.observedToolCalls !== toolsBefore)
|
|
444
553
|
throw new Error("Codex automatic-compaction setup unexpectedly called a tool");
|
|
554
|
+
if (!setup.selectedDispatch)
|
|
555
|
+
throw new Error("Codex initialization returned no sealed dispatch tuple");
|
|
556
|
+
const accepted = validateStructuredCandidate(initializationOutput, setup.structured, providerWireContract(initializationOutput, "codex"), setup.selectedDispatch);
|
|
557
|
+
if (accepted.result.kind !== "completed" || !accepted.result.payload.ready)
|
|
558
|
+
throw new Error("Codex initialization did not confirm readiness");
|
|
445
559
|
}
|
|
446
560
|
const maximum = this.usage?.maximum;
|
|
447
561
|
if (!maximum || !Number.isFinite(maximum) || maximum <= 0) {
|
|
@@ -527,6 +641,8 @@ export class CodexAdapter {
|
|
|
527
641
|
if (this.closed)
|
|
528
642
|
return;
|
|
529
643
|
this.closed = true;
|
|
644
|
+
if (this.turnInFlight && this.turnStreamedBytes)
|
|
645
|
+
this.writeStreamDiagnostic("interrupted", "adapter closed during an active turn");
|
|
530
646
|
const child = this.process;
|
|
531
647
|
let onExit;
|
|
532
648
|
const exited = child && child.exitCode === null ? new Promise(resolve => { onExit = resolve; child.once("exit", resolve); }) : Promise.resolve();
|
|
@@ -716,7 +832,17 @@ export class CodexAdapter {
|
|
|
716
832
|
if (!message.method)
|
|
717
833
|
return;
|
|
718
834
|
const params = message.params ?? {};
|
|
719
|
-
|
|
835
|
+
this.structuredCollector?.ingest(message.method, params);
|
|
836
|
+
// Native receipts and tool observations must belong to the same dispatch.
|
|
837
|
+
// Foreign notifications remain unable to affect completion or evidence.
|
|
838
|
+
if (this.structuredCollector && (params.threadId && params.threadId !== this._sessionId || this.activeNativeTurnId && params.turnId && params.turnId !== this.activeNativeTurnId))
|
|
839
|
+
return;
|
|
840
|
+
if (message.method === "item/agentMessage/delta") {
|
|
841
|
+
if (!this.turnInFlight || params.threadId && this._sessionId && params.threadId !== this._sessionId)
|
|
842
|
+
return;
|
|
843
|
+
this.observeAgentMessageDelta(params);
|
|
844
|
+
}
|
|
845
|
+
else if (message.method === "item/started") {
|
|
720
846
|
const item = params.item;
|
|
721
847
|
if (item)
|
|
722
848
|
this.eventQueue.push({ kind: "provider-item", provider: "codex", lifecycle: "started", itemType: String(item.type ?? "unknown"), payload: exactProviderItem(item), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
@@ -904,6 +1030,54 @@ export class CodexAdapter {
|
|
|
904
1030
|
}
|
|
905
1031
|
});
|
|
906
1032
|
}
|
|
1033
|
+
/** Ask the app-server to stop a native turn; true only once its terminal is observed. */
|
|
1034
|
+
async interruptNativeTurn(nativeTurnId) {
|
|
1035
|
+
const stopped = this.waitFor("turn/completed", params => params.threadId === this._sessionId && params.turn?.id === nativeTurnId, 15_000)
|
|
1036
|
+
.then(() => true, () => false);
|
|
1037
|
+
try {
|
|
1038
|
+
await new OperationDeadline("Codex turn interrupt", 10_000).run(() => this.request("turn/interrupt", { threadId: this._sessionId, turnId: nativeTurnId }));
|
|
1039
|
+
}
|
|
1040
|
+
catch (error) {
|
|
1041
|
+
this.traceLifecycle("turn-interrupt-failed", { method: error instanceof Error ? error.message.slice(0, 200) : "unknown" });
|
|
1042
|
+
}
|
|
1043
|
+
return stopped;
|
|
1044
|
+
}
|
|
1045
|
+
/** Streamed output is visible progress; it is never treated as a result. */
|
|
1046
|
+
observeAgentMessageDelta(params) {
|
|
1047
|
+
if (typeof params.delta !== "string")
|
|
1048
|
+
return;
|
|
1049
|
+
this.turnStreamedBytes += Buffer.byteLength(params.delta);
|
|
1050
|
+
const now = Date.now();
|
|
1051
|
+
if (!this.streamStartedAt)
|
|
1052
|
+
this.streamStartedAt = now;
|
|
1053
|
+
if (now - this.streamActivityAt >= 1_000) {
|
|
1054
|
+
this.streamActivityAt = now;
|
|
1055
|
+
this.eventQueue.push({ kind: "activity", provider: "codex", state: "writing response", detail: `${formatStreamBytes(this.turnStreamedBytes)} streamed`, transient: true });
|
|
1056
|
+
}
|
|
1057
|
+
if (now - (this.streamDiagnosticAt || this.streamStartedAt) >= 2_000)
|
|
1058
|
+
this.writeStreamDiagnostic("streaming");
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Bounded interruption evidence for structured turns under .foreman (Rafi's
|
|
1062
|
+
* git-excluded log directory). Diagnostic only: never read back as output.
|
|
1063
|
+
*/
|
|
1064
|
+
writeStreamDiagnostic(state, detail) {
|
|
1065
|
+
const collector = this.structuredCollector, nativeTurnId = this.activeNativeTurnId, threadId = this._sessionId;
|
|
1066
|
+
if (!collector || !nativeTurnId || !threadId)
|
|
1067
|
+
return;
|
|
1068
|
+
this.streamDiagnosticAt = Date.now();
|
|
1069
|
+
try {
|
|
1070
|
+
const safe = (value) => value.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 120);
|
|
1071
|
+
const dir = join(this.opts.configRoot ?? this.opts.cwd, ".foreman", "provider-streams");
|
|
1072
|
+
mkdirSync(dir, { recursive: true });
|
|
1073
|
+
const path = join(dir, `codex-${safe(threadId)}-${safe(nativeTurnId)}.json`);
|
|
1074
|
+
const body = { version: 1, provider: "codex", state, threadId, nativeTurnId, dispatchId: this.activeProviderTurnId, updatedAt: new Date().toISOString(),
|
|
1075
|
+
...(detail ? { detail: Buffer.from(detail).subarray(0, 2_000).toString() } : {}), ...collector.streamDiagnostic() };
|
|
1076
|
+
writeFileSync(`${path}.tmp`, JSON.stringify(body, null, 2));
|
|
1077
|
+
renameSync(`${path}.tmp`, path);
|
|
1078
|
+
}
|
|
1079
|
+
catch { /* diagnostics cannot change execution */ }
|
|
1080
|
+
}
|
|
907
1081
|
traceLifecycle(phase, data = {}) {
|
|
908
1082
|
try {
|
|
909
1083
|
this.opts.onLifecycleTrace?.({ phase, at: new Date().toISOString(), sessionId: this._sessionId, ...data });
|
|
@@ -973,6 +1147,7 @@ export class CodexAdapter {
|
|
|
973
1147
|
}
|
|
974
1148
|
}
|
|
975
1149
|
disconnect(error) {
|
|
1150
|
+
this.structuredCollector?.fail(error);
|
|
976
1151
|
this.connectionGeneration++;
|
|
977
1152
|
for (const controller of this.approvalRequests.values())
|
|
978
1153
|
controller.abort();
|
|
@@ -1088,3 +1263,6 @@ function loadSkillMarkdown(cwd, skill) {
|
|
|
1088
1263
|
function cumulativeDelta(before, after) {
|
|
1089
1264
|
return before !== undefined && after !== undefined && after >= before ? after - before : undefined;
|
|
1090
1265
|
}
|
|
1266
|
+
function formatStreamBytes(bytes) {
|
|
1267
|
+
return bytes < 1024 ? `${bytes} B` : bytes < 1_048_576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1_048_576).toFixed(1)} MB`;
|
|
1268
|
+
}
|
|
@@ -18,6 +18,8 @@ export interface RecoveringAdapterOptions {
|
|
|
18
18
|
/** Adds interactive recovery around structured runtime turn failures. */
|
|
19
19
|
export declare class RecoveringAdapter implements BuilderAdapter {
|
|
20
20
|
private readonly opts;
|
|
21
|
+
postConfirmedDispatch(reference: import("../adapters/types.js").ConfirmedDispatchReference): Promise<TurnResult | void>;
|
|
22
|
+
structuredDispatchRequirement(): import("../adapters/types.js").StructuredDispatchRequirement | undefined;
|
|
21
23
|
private adapter;
|
|
22
24
|
private runtime;
|
|
23
25
|
private readonly eventQueue;
|
|
@@ -5,6 +5,8 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
/** Adds interactive recovery around structured runtime turn failures. */
|
|
6
6
|
export class RecoveringAdapter {
|
|
7
7
|
opts;
|
|
8
|
+
postConfirmedDispatch(reference) { return this.adapter.postConfirmedDispatch?.(reference) ?? Promise.resolve(); }
|
|
9
|
+
structuredDispatchRequirement() { return this.adapter.structuredDispatchRequirement?.(); }
|
|
8
10
|
adapter;
|
|
9
11
|
runtime;
|
|
10
12
|
eventQueue = new AsyncQueue();
|
|
@@ -45,7 +47,9 @@ export class RecoveringAdapter {
|
|
|
45
47
|
this.eventQueue.push({ kind: "error", message: `failed to persist scoped provider session binding: ${error instanceof Error ? error.message : String(error)}` });
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
|
-
|
|
50
|
+
// Structured operations have one recovery owner above the adapter. Never
|
|
51
|
+
// hide a second actual dispatch behind the same request/receipt identity.
|
|
52
|
+
if (policy?.output || policy?.handback || !this.opts.enabled || !result.isError || !result.failure)
|
|
49
53
|
return result;
|
|
50
54
|
// The host cannot know whether a missing exact session received this
|
|
51
55
|
// instruction. Never replay it or silently switch providers.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { providerWireContract, type OutputContract, type SelectedDispatchTupleV2, type StructuredResponse } from "rafi-spec";
|
|
2
|
+
export declare function structuredReceipt(contract: OutputContract, provider: "claude" | "codex", value: unknown, identity: {
|
|
3
|
+
dispatchId: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
nativeTurnId?: string;
|
|
6
|
+
nativeItemId?: string;
|
|
7
|
+
}, representation: "json-text" | "decoded-json", selected: ReturnType<typeof providerWireContract>, tuple: SelectedDispatchTupleV2): StructuredResponse;
|
|
8
|
+
/** Pretty-printed JSON never needs this much consecutive whitespace. */
|
|
9
|
+
export declare const CODEX_RUNAWAY_WHITESPACE_BYTES = 2048;
|
|
10
|
+
/** The provider is emitting a runaway whitespace sequence; its output can never be accepted. */
|
|
11
|
+
export declare class CodexRunawayOutputError extends Error {
|
|
12
|
+
readonly itemId: string;
|
|
13
|
+
readonly whitespaceBytes: number;
|
|
14
|
+
constructor(itemId: string, whitespaceBytes: number);
|
|
15
|
+
}
|
|
16
|
+
/** Bounded, non-authoritative view of one streamed agent message. */
|
|
17
|
+
export interface CodexStreamItemDiagnostic {
|
|
18
|
+
itemId: string;
|
|
19
|
+
bytes: number;
|
|
20
|
+
deltas: number;
|
|
21
|
+
completed: boolean;
|
|
22
|
+
whitespaceRun: number;
|
|
23
|
+
head: string;
|
|
24
|
+
tail: string;
|
|
25
|
+
textRetained: boolean;
|
|
26
|
+
startedAt: string;
|
|
27
|
+
lastDeltaAt: string;
|
|
28
|
+
}
|
|
29
|
+
export interface CodexStreamDiagnostic {
|
|
30
|
+
authoritative: false;
|
|
31
|
+
streamedBytes: number;
|
|
32
|
+
deltas: number;
|
|
33
|
+
items: CodexStreamItemDiagnostic[];
|
|
34
|
+
}
|
|
35
|
+
/** Holds early notifications until turn/start supplies the exact turn identity. */
|
|
36
|
+
export declare class CodexTurnCollector {
|
|
37
|
+
private readonly threadId;
|
|
38
|
+
private readonly requirePhase;
|
|
39
|
+
private readonly onProgress?;
|
|
40
|
+
private readonly maximumResponseBytes;
|
|
41
|
+
private turnId?;
|
|
42
|
+
private early;
|
|
43
|
+
private bytes;
|
|
44
|
+
private finals;
|
|
45
|
+
private streams;
|
|
46
|
+
private streamedBytes;
|
|
47
|
+
private streamedDeltas;
|
|
48
|
+
private earlyDeltaBytes;
|
|
49
|
+
private terminal?;
|
|
50
|
+
private error?;
|
|
51
|
+
tools: boolean;
|
|
52
|
+
private resolve;
|
|
53
|
+
private reject;
|
|
54
|
+
readonly completion: Promise<Record<string, unknown>>;
|
|
55
|
+
constructor(threadId: string, requirePhase: boolean, onProgress?: (() => void) | undefined, maximumResponseBytes?: number);
|
|
56
|
+
bind(turnId: string): void;
|
|
57
|
+
ingest(method: string, params: Record<string, unknown>): void;
|
|
58
|
+
/**
|
|
59
|
+
* Streamed text is bounded before completion, but never becomes a result:
|
|
60
|
+
* only a completed final item is accepted and charged against the budget.
|
|
61
|
+
*/
|
|
62
|
+
private ingestDelta;
|
|
63
|
+
streamDiagnostic(): CodexStreamDiagnostic;
|
|
64
|
+
fail(error: Error): void;
|
|
65
|
+
private invalidate;
|
|
66
|
+
private settle;
|
|
67
|
+
final(): {
|
|
68
|
+
text: string;
|
|
69
|
+
itemId?: string;
|
|
70
|
+
};
|
|
71
|
+
}
|