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.
Files changed (136) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +227 -23
  4. package/dist/adapters/codex.d.ts +17 -7
  5. package/dist/adapters/codex.js +195 -17
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +85 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/planner.js +1 -4
  18. package/dist/branch/runner.d.ts +2 -1
  19. package/dist/branch/runner.js +10 -7
  20. package/dist/buildAdmission.js +4 -0
  21. package/dist/buildApproval.d.ts +15 -0
  22. package/dist/buildApproval.js +25 -0
  23. package/dist/buildAssignment.d.ts +1 -0
  24. package/dist/buildAssignment.js +82 -23
  25. package/dist/buildInterventions.d.ts +2 -2
  26. package/dist/buildInterventions.js +11 -4
  27. package/dist/buildLaunchGate.js +3 -2
  28. package/dist/builderGuidanceFollowup.js +17 -6
  29. package/dist/cli/manager.js +22 -20
  30. package/dist/cli/start.js +22 -26
  31. package/dist/cli/tickets.d.ts +0 -7
  32. package/dist/cli/tickets.js +41 -40
  33. package/dist/continuity.d.ts +10 -14
  34. package/dist/continuity.js +198 -179
  35. package/dist/continuityOrigin.d.ts +13 -0
  36. package/dist/continuityOrigin.js +91 -0
  37. package/dist/foreman.d.ts +10 -4
  38. package/dist/foreman.js +51 -193
  39. package/dist/graph/boundary.d.ts +1 -1
  40. package/dist/graph/boundary.js +1 -1
  41. package/dist/graph/host.js +8 -19
  42. package/dist/graph/manager.d.ts +1 -1
  43. package/dist/graph/manager.js +1 -1
  44. package/dist/graph/preparation.d.ts +38 -0
  45. package/dist/graph/preparation.js +7 -6
  46. package/dist/graph/semantic.d.ts +1 -0
  47. package/dist/graph/semantic.js +4 -1
  48. package/dist/graph/session.js +7 -2
  49. package/dist/graph/turn.d.ts +2 -3
  50. package/dist/graph/turn.js +28 -40
  51. package/dist/handoffProtocol.js +2 -2
  52. package/dist/handoffs.d.ts +3 -1
  53. package/dist/handoffs.js +90 -59
  54. package/dist/hostGraphApplicability.d.ts +2 -1
  55. package/dist/hostGraphApplicability.js +17 -19
  56. package/dist/hostQaAugmentation.js +21 -4
  57. package/dist/hostQaComparison.d.ts +21 -2
  58. package/dist/hostQaComparison.js +43 -29
  59. package/dist/hostSourcePreservation.d.ts +38 -0
  60. package/dist/hostSourcePreservation.js +72 -0
  61. package/dist/interviews.d.ts +7 -0
  62. package/dist/interviews.js +18 -1
  63. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  64. package/dist/legacyCommunicationDisplay.js +2 -0
  65. package/dist/managerPacket.js +3 -3
  66. package/dist/markers.d.ts +4 -3
  67. package/dist/markers.js +4 -20
  68. package/dist/probeIpc.d.ts +14 -0
  69. package/dist/probeIpc.js +29 -0
  70. package/dist/qaBuildGate.d.ts +21 -0
  71. package/dist/qaBuildGate.js +47 -12
  72. package/dist/qaBuilderCoverage.js +22 -13
  73. package/dist/qaBuilderEvidence.d.ts +16 -1
  74. package/dist/qaBuilderEvidence.js +48 -8
  75. package/dist/qaContractCoverage.d.ts +2 -2
  76. package/dist/qaContractCoverage.js +4 -9
  77. package/dist/qaContractDelivery.js +34 -20
  78. package/dist/qaCoverageAssembly.d.ts +22 -0
  79. package/dist/qaCoverageAssembly.js +16 -0
  80. package/dist/qaDeliveryJournal.d.ts +2 -0
  81. package/dist/qaDeliveryJournal.js +19 -0
  82. package/dist/qaFailureDelivery.js +97 -85
  83. package/dist/qaHostTransformation.d.ts +26 -0
  84. package/dist/qaHostTransformation.js +100 -0
  85. package/dist/qaPreparation.d.ts +8 -20
  86. package/dist/qaPreparation.js +133 -248
  87. package/dist/qaPreparationInputs.js +19 -0
  88. package/dist/qaPrerequisites.d.ts +2 -1
  89. package/dist/qaPrerequisites.js +21 -1
  90. package/dist/qaProtocolV2.d.ts +15 -0
  91. package/dist/qaRecovery.js +21 -4
  92. package/dist/qaReview.js +220 -395
  93. package/dist/qaReviewEvidence.d.ts +7 -3
  94. package/dist/qaReviewEvidence.js +15 -4
  95. package/dist/qaRuntime.js +1 -1
  96. package/dist/qaSnapshot.js +71 -21
  97. package/dist/qaStructuredResponse.d.ts +10 -0
  98. package/dist/qaStructuredResponse.js +57 -0
  99. package/dist/qaTimeline.js +5 -3
  100. package/dist/qaVerificationContract.d.ts +1 -2
  101. package/dist/qaVerificationContract.js +24 -35
  102. package/dist/readinessGate.js +5 -4
  103. package/dist/runtimeReadiness.js +23 -15
  104. package/dist/statusReporter.d.ts +2 -0
  105. package/dist/statusReporter.js +2 -0
  106. package/dist/structuredCutover.d.ts +31 -0
  107. package/dist/structuredCutover.js +510 -0
  108. package/dist/structuredCutoverFiles.d.ts +10 -0
  109. package/dist/structuredCutoverFiles.js +181 -0
  110. package/dist/structuredDispatchAuthority.d.ts +22 -0
  111. package/dist/structuredDispatchAuthority.js +110 -0
  112. package/dist/structuredDispatchRecord.d.ts +29 -0
  113. package/dist/structuredDispatchRecord.js +36 -0
  114. package/dist/structuredLiveBudget.d.ts +2 -0
  115. package/dist/structuredLiveBudget.js +23 -0
  116. package/dist/structuredOperation.d.ts +18 -0
  117. package/dist/structuredOperation.js +257 -0
  118. package/dist/supervisedStart.js +35 -21
  119. package/dist/ticketPlanning.d.ts +1 -373
  120. package/dist/ticketPlanning.js +2 -26
  121. package/dist/ticketPopulation.d.ts +0 -3
  122. package/dist/ticketPopulation.js +4 -21
  123. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  124. package/dist/ticketPopulationPageAuthority.js +30 -0
  125. package/dist/ticketPopulationPaging.d.ts +27 -0
  126. package/dist/ticketPopulationPaging.js +105 -0
  127. package/dist/tickets/renderMarkdown.d.ts +14 -0
  128. package/dist/tickets/renderMarkdown.js +39 -5
  129. package/dist/tickets/ticketLoader.js +10 -0
  130. package/dist/tickets/ticketSchema.d.ts +107 -0
  131. package/dist/tickets/ticketSchema.js +4 -1
  132. package/dist/workflowDb.d.ts +100 -2
  133. package/dist/workflowDb.js +763 -39
  134. package/dist/workflowReader.d.ts +1 -0
  135. package/dist/workflowReader.js +9 -0
  136. package/package.json +15 -7
@@ -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;
@@ -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 completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId, this.providerIdleTimeoutMs(), true);
239
- void completion.catch(() => { });
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: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" || this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa"
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
- const params = await new OperationDeadline("Codex active turn", this.opts.turnDeadlineMs ?? 3_600_000).run(() => completion);
265
- const turn = params.turn;
266
- const responseOnlyViolation = Boolean(policy?.responseOnly && this.observedToolCalls !== toolsBefore);
267
- const failed = turn?.status === "failed" || responseOnlyViolation;
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
- const text = this.activeText.join("\n");
270
- if (failed && !responseOnlyViolation && this.fastTier && !this.turnSawTool && !text && isFastModeUnavailable(String(error?.message ?? ""))) {
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
- const result = {
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 setup = await this.sendTurnInternal("Rafi internal initialization only. Do not call tools or modify files. Reply briefly that the context is ready.", { purpose: "initialization", responseOnly: true });
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
- if (message.method === "item/started") {
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
- if (policy?.handback || !this.opts.enabled || !result.isError || !result.failure)
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
+ }