ai-foreman 1.7.8 → 1.7.9

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.
@@ -1,4 +1,4 @@
1
- import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextManagementPolicy, ContextUsage, InterruptResult, PreparedContextManagement, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
1
+ import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
2
2
  import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
3
3
  export interface CodexLineResult {
4
4
  events: BuilderEvent[];
@@ -24,21 +24,16 @@ export declare class CodexAdapter implements BuilderAdapter {
24
24
  private activeText;
25
25
  private usage?;
26
26
  private usageRevision;
27
- private contextSequence;
28
27
  private providerSessionUsage?;
29
28
  private lastTurnTokens?;
30
29
  private stderr;
31
- private contextPolicy?;
32
- private nativeTokenLimit?;
33
- private spawnedNativeTokenLimit?;
34
- private contextPrepared;
35
- private setupTurnActive;
36
- private setupToolAttempted;
37
- private activeTurnId?;
38
- private boundaryInterrupt?;
39
- private compactionSequence;
40
- private activeCompaction?;
41
- private manualCompactionPending;
30
+ private nativeAutoCompactTokenLimit?;
31
+ private autoCompactionPrepared;
32
+ private preparedAutoCompactThreshold?;
33
+ private nativeCompactions;
34
+ private nativeCompactionSequence;
35
+ private manualCompactionInFlight;
36
+ private observedToolCalls;
42
37
  constructor(opts: BuilderAdapterOptions);
43
38
  buildInstruction(instruction: string): string;
44
39
  buildAppServerArgs(): string[];
@@ -46,11 +41,12 @@ export declare class CodexAdapter implements BuilderAdapter {
46
41
  buildArgs(instruction: string): string[];
47
42
  sendTurn(instruction: string): Promise<TurnResult>;
48
43
  private sendTurnInternal;
49
- prepareContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
50
- updateContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
51
- interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<InterruptResult>;
52
44
  compact(): Promise<CompactResult>;
45
+ prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<void>;
46
+ drainNativeCompactions(): NativeCompaction[];
47
+ restoreNativeCompactions(compactions: NativeCompaction[]): void;
53
48
  contextUsage(): Promise<ContextUsage | undefined>;
49
+ contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
54
50
  sessionUsage(): Promise<ProviderSessionUsage | undefined>;
55
51
  switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
56
52
  sessionId(): string | undefined;
@@ -67,7 +63,6 @@ export declare class CodexAdapter implements BuilderAdapter {
67
63
  private handle;
68
64
  private waitFor;
69
65
  private disconnect;
70
- /** Restart only app-server; the scoped thread remains the continuity boundary. */
71
- private restartTransport;
66
+ private restartForAutoCompaction;
72
67
  private buildSkillsAppendix;
73
68
  }
@@ -23,7 +23,6 @@ export function parseCodexLine(raw) {
23
23
  return { events: [{ kind: "error", message: String(raw.error?.message ?? JSON.stringify(raw)) }] };
24
24
  return { events: [] };
25
25
  }
26
- const CODEX_COMPACTION_TIMEOUT_MS = 120_000;
27
26
  /** Persistent JSON-RPC controller for one live Codex thread. */
28
27
  export class CodexAdapter {
29
28
  opts;
@@ -41,21 +40,16 @@ export class CodexAdapter {
41
40
  activeText = [];
42
41
  usage;
43
42
  usageRevision = 0;
44
- contextSequence = 0;
45
43
  providerSessionUsage;
46
44
  lastTurnTokens;
47
45
  stderr = "";
48
- contextPolicy;
49
- nativeTokenLimit;
50
- spawnedNativeTokenLimit;
51
- contextPrepared = false;
52
- setupTurnActive = false;
53
- setupToolAttempted = false;
54
- activeTurnId;
55
- boundaryInterrupt;
56
- compactionSequence = 0;
57
- activeCompaction;
58
- manualCompactionPending = false;
46
+ nativeAutoCompactTokenLimit;
47
+ autoCompactionPrepared = false;
48
+ preparedAutoCompactThreshold;
49
+ nativeCompactions = [];
50
+ nativeCompactionSequence = 0;
51
+ manualCompactionInFlight = false;
52
+ observedToolCalls = 0;
59
53
  constructor(opts) {
60
54
  this.opts = opts;
61
55
  this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
@@ -67,8 +61,8 @@ export class CodexAdapter {
67
61
  }
68
62
  buildAppServerArgs() {
69
63
  const args = ["app-server", "--listen", "stdio://"];
70
- if (this.nativeTokenLimit !== undefined) {
71
- args.push("-c", `model_auto_compact_token_limit=${this.nativeTokenLimit}`, "-c", 'model_auto_compact_token_limit_scope="total"');
64
+ if (this.nativeAutoCompactTokenLimit !== undefined) {
65
+ args.push("-c", `model_auto_compact_token_limit=${this.nativeAutoCompactTokenLimit}`, "-c", 'model_auto_compact_token_limit_scope="total"');
72
66
  }
73
67
  return args;
74
68
  }
@@ -87,11 +81,9 @@ export class CodexAdapter {
87
81
  return args;
88
82
  }
89
83
  async sendTurn(instruction) {
90
- if (this.opts.contextManagementPolicy && !this.contextPrepared)
91
- await this.prepareContextManagement(this.opts.contextManagementPolicy);
92
84
  return withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction));
93
85
  }
94
- async sendTurnInternal(instruction, setup = false) {
86
+ async sendTurnInternal(instruction) {
95
87
  if (this.closed)
96
88
  throw new Error("builder is closed");
97
89
  this.eventQueue.push({ kind: "activity", state: "starting Codex turn", provider: "codex", model: this.opts.model });
@@ -100,49 +92,29 @@ export class CodexAdapter {
100
92
  await this.ensureThread();
101
93
  this.activeText = [];
102
94
  this.lastTurnTokens = undefined;
103
- this.setupTurnActive = setup;
104
- this.setupToolAttempted = false;
105
95
  const completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId);
106
96
  turnStartDispatched = true;
107
- const started = await this.request("turn/start", {
97
+ await this.request("turn/start", {
108
98
  threadId: this._sessionId,
109
- input: [{ type: "text", text: setup ? instruction : this.buildInstruction(instruction), text_elements: [] }],
99
+ input: [{ type: "text", text: this.buildInstruction(instruction), text_elements: [] }],
110
100
  cwd: this.opts.cwd,
111
101
  model: this.opts.model ?? null,
112
102
  effort: this.opts.effort ?? (this.opts.fast ? "low" : null),
113
103
  });
114
- const startedTurn = started.turn;
115
- this.activeTurnId = typeof startedTurn?.id === "string" ? startedTurn.id : undefined;
116
104
  const params = await completion;
117
105
  const turn = params.turn;
118
106
  const failed = turn?.status === "failed";
119
- const interrupted = turn?.status === "interrupted" || Boolean(this.boundaryInterrupt);
120
107
  const error = turn?.error;
121
108
  const text = this.activeText.join("\n");
122
109
  const result = {
123
110
  text: failed ? normalizeRuntimeErrorText("codex", String(error?.message ?? text), null, "app-server turn") : text,
124
111
  isError: failed, numTurns: 1, costUsd: 0, costAuthoritative: false,
125
112
  ...(this.lastTurnTokens ?? {}),
126
- ...(interrupted ? { interrupted: { reason: "compaction-boundary", ...(this.boundaryInterrupt?.providerEventId ? { providerEventId: this.boundaryInterrupt.providerEventId } : {}) } } : {}),
127
113
  };
128
- if (setup && this.setupToolAttempted)
129
- throw new Error("Codex context setup attempted to invoke a tool; native context capability was not established");
130
114
  this.eventQueue.push({ kind: "turn-complete", result });
131
115
  return result;
132
116
  }
133
117
  catch (error) {
134
- if (this.boundaryInterrupt) {
135
- const result = {
136
- text: this.activeText.join("\n"), isError: false, numTurns: 1, costUsd: 0,
137
- costAuthoritative: false, ...(this.lastTurnTokens ?? {}),
138
- interrupted: {
139
- reason: "compaction-boundary",
140
- ...(this.boundaryInterrupt.providerEventId ? { providerEventId: this.boundaryInterrupt.providerEventId } : {}),
141
- },
142
- };
143
- this.eventQueue.push({ kind: "turn-complete", result });
144
- return result;
145
- }
146
118
  if (error instanceof SessionUnavailableError) {
147
119
  const result = sessionUnavailableResult(error);
148
120
  this.eventQueue.push({ kind: "error", message: result.text });
@@ -164,87 +136,10 @@ export class CodexAdapter {
164
136
  this.eventQueue.push({ kind: "turn-complete", result });
165
137
  return result;
166
138
  }
167
- finally {
168
- this.activeTurnId = undefined;
169
- this.setupTurnActive = false;
170
- this.boundaryInterrupt = undefined;
171
- }
172
- }
173
- async prepareContextManagement(policy) {
174
- validateContextPolicy(policy);
175
- this.contextPolicy = { ...policy };
176
- this.compactionSequence = Math.max(this.compactionSequence, policy.providerSequenceStart ?? 0);
177
- this.contextSequence = Math.max(this.contextSequence, policy.providerSequenceStart ?? 0);
178
- if (policy.knownModelContextWindow && policy.knownModelContextWindow > 0) {
179
- this.nativeTokenLimit = tokenCeiling(policy.knownModelContextWindow, policy.configuredThresholdPercent);
180
- }
181
- await this.ensureThread();
182
- if (!this.usage) {
183
- const setup = await this.sendTurnInternal("Rafi context setup only. Do not call tools or change files. Reply with exactly CONTEXT_READY.", true);
184
- if (setup.isError || setup.text.trim() !== "CONTEXT_READY") {
185
- throw new Error(`Codex context setup did not complete safely: ${setup.text.slice(0, 240)}`);
186
- }
187
- }
188
- const maximum = this.usage?.maximum ?? policy.knownModelContextWindow;
189
- if (!maximum || !Number.isFinite(maximum) || maximum <= 0) {
190
- throw new Error("Codex did not expose modelContextWindow during context setup");
191
- }
192
- const configuredTokenLimit = tokenCeiling(maximum, policy.configuredThresholdPercent);
193
- this.nativeTokenLimit = configuredTokenLimit;
194
- if (this.spawnedNativeTokenLimit !== configuredTokenLimit) {
195
- await this.restartTransport();
196
- await this.ensureThread();
197
- }
198
- this.contextPrepared = true;
199
- return {
200
- modelContextWindow: maximum,
201
- configuredTokenLimit,
202
- installedNativeTokenLimit: configuredTokenLimit,
203
- installedNativePercent: configuredTokenLimit / maximum * 100,
204
- ...(this.usage ? { sample: { ...this.usage } } : {}),
205
- };
206
- }
207
- async updateContextManagement(policy) {
208
- validateContextPolicy(policy);
209
- if (this.activeTurnId)
210
- throw new Error("Codex native context control can be reconfigured only after the active turn settles");
211
- const maximum = this.usage?.maximum ?? policy.knownModelContextWindow;
212
- if (!maximum || maximum <= 0)
213
- return this.prepareContextManagement(policy);
214
- this.contextPolicy = { ...policy };
215
- this.compactionSequence = Math.max(this.compactionSequence, policy.providerSequenceStart ?? 0);
216
- this.contextSequence = Math.max(this.contextSequence, policy.providerSequenceStart ?? 0);
217
- const configuredTokenLimit = tokenCeiling(maximum, policy.configuredThresholdPercent);
218
- this.nativeTokenLimit = configuredTokenLimit;
219
- if (this.spawnedNativeTokenLimit !== configuredTokenLimit) {
220
- await this.restartTransport();
221
- await this.ensureThread();
222
- }
223
- return {
224
- modelContextWindow: maximum,
225
- configuredTokenLimit,
226
- installedNativeTokenLimit: configuredTokenLimit,
227
- installedNativePercent: configuredTokenLimit / maximum * 100,
228
- ...(this.usage ? { sample: { ...this.usage } } : {}),
229
- };
230
- }
231
- async interruptTurnAtCompactionBoundary(providerEventId) {
232
- if (!this.activeTurnId || !this._sessionId)
233
- return { ok: false, error: "no active Codex turn exists at the compaction boundary", providerEventId };
234
- try {
235
- this.boundaryInterrupt = { providerEventId };
236
- await this.request("turn/interrupt", { threadId: this._sessionId, turnId: this.activeTurnId });
237
- return { ok: true, providerEventId };
238
- }
239
- catch (error) {
240
- this.boundaryInterrupt = undefined;
241
- return { ok: false, error: error instanceof Error ? error.message : String(error), providerEventId };
242
- }
243
139
  }
244
140
  async compact() {
245
141
  try {
246
142
  await this.ensureThread();
247
- this.manualCompactionPending = true;
248
143
  this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
249
144
  const usageRevision = this.usageRevision;
250
145
  // A pre-compaction observation must never be mistaken for proof that the
@@ -255,17 +150,23 @@ export class CodexAdapter {
255
150
  const done = this.waitFor("item/completed", (params) => {
256
151
  const item = params.item;
257
152
  return params.threadId === this._sessionId && item?.type === "contextCompaction";
258
- }, CODEX_COMPACTION_TIMEOUT_MS);
153
+ }, 30_000);
259
154
  const postCompactUsage = this.waitFor("thread/tokenUsage/updated", (params) => {
260
155
  return (params.threadId === undefined || params.threadId === this._sessionId)
261
156
  && this.usageRevision > usageRevision
262
157
  && this.usage !== undefined;
263
- }, CODEX_COMPACTION_TIMEOUT_MS);
264
- await Promise.all([
265
- this.request("thread/compact/start", { threadId: this._sessionId }),
266
- done,
267
- postCompactUsage,
268
- ]);
158
+ }, 30_000);
159
+ this.manualCompactionInFlight = true;
160
+ try {
161
+ await Promise.all([
162
+ this.request("thread/compact/start", { threadId: this._sessionId }),
163
+ done,
164
+ postCompactUsage,
165
+ ]);
166
+ }
167
+ finally {
168
+ this.manualCompactionInFlight = false;
169
+ }
269
170
  this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
270
171
  return { ok: true };
271
172
  }
@@ -274,11 +175,55 @@ export class CodexAdapter {
274
175
  return { ok: false, error: error.message, failure: error.failure };
275
176
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
276
177
  }
277
- finally {
278
- this.manualCompactionPending = false;
178
+ }
179
+ async prepareAutoCompaction(thresholdPercent = this.opts.autoCompactThresholdPercent) {
180
+ if (thresholdPercent === undefined)
181
+ return;
182
+ const threshold = validThreshold(thresholdPercent);
183
+ if (this.autoCompactionPrepared && this.preparedAutoCompactThreshold === threshold)
184
+ return;
185
+ this.opts.autoCompactThresholdPercent = threshold;
186
+ await this.ensureThread();
187
+ // Codex reports its model context window in token-usage notifications, not
188
+ // in thread/start. Establish the otherwise idle thread with a constrained
189
+ // setup turn, then restart the app server with the provider-native ceiling
190
+ // before any Builder or QA work is sent.
191
+ if (!this.usage?.maximum) {
192
+ const toolsBefore = this.observedToolCalls;
193
+ const setup = await this.sendTurnInternal("Rafi internal initialization only. Do not call tools or modify files. Reply briefly that the context is ready.");
194
+ if (setup.isError)
195
+ throw new Error(`Codex automatic-compaction setup failed: ${setup.text.slice(0, 240)}`);
196
+ if (this.observedToolCalls !== toolsBefore)
197
+ throw new Error("Codex automatic-compaction setup unexpectedly called a tool");
279
198
  }
199
+ const maximum = this.usage?.maximum;
200
+ if (!maximum || !Number.isFinite(maximum) || maximum <= 0) {
201
+ throw new Error("Codex did not report a model context window during automatic-compaction setup");
202
+ }
203
+ this.nativeAutoCompactTokenLimit = tokenLimit(maximum, threshold);
204
+ await this.restartForAutoCompaction();
205
+ await this.ensureThread();
206
+ this.autoCompactionPrepared = true;
207
+ this.preparedAutoCompactThreshold = threshold;
280
208
  }
209
+ drainNativeCompactions() {
210
+ const pending = this.nativeCompactions;
211
+ this.nativeCompactions = [];
212
+ return pending;
213
+ }
214
+ restoreNativeCompactions(compactions) { this.nativeCompactions.unshift(...compactions); }
281
215
  async contextUsage() { return this.usage; }
216
+ async contextUsageAfterNativeCompaction(compaction) {
217
+ if (compaction.usageRevision === undefined || this.usageRevision > compaction.usageRevision)
218
+ return this.usage;
219
+ try {
220
+ await this.waitFor("thread/tokenUsage/updated", () => this.usageRevision > compaction.usageRevision, 5_000);
221
+ return this.usage;
222
+ }
223
+ catch {
224
+ return undefined;
225
+ }
226
+ }
282
227
  async sessionUsage() { return this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined; }
283
228
  async switchSettings(settings) {
284
229
  this.opts.model = settings.model;
@@ -413,7 +358,6 @@ export class CodexAdapter {
413
358
  this.stderr = "";
414
359
  const executable = this.opts.runtimeExecutable ?? "codex";
415
360
  const child = spawn(executable, this.buildAppServerArgs(), { cwd: this.opts.cwd, stdio: ["pipe", "pipe", "pipe"] });
416
- this.spawnedNativeTokenLimit = this.nativeTokenLimit;
417
361
  this.process = child;
418
362
  // A short-lived server may close stdin before its stderr/exit event. The
419
363
  // close handler owns rejection so diagnostics include the complete stderr.
@@ -463,29 +407,10 @@ export class CodexAdapter {
463
407
  if (!message.method)
464
408
  return;
465
409
  const params = message.params ?? {};
466
- if (message.method === "turn/started") {
467
- const turn = params.turn;
468
- if (typeof turn?.id === "string")
469
- this.activeTurnId = turn.id;
470
- }
471
- else if (message.method === "item/started") {
410
+ if (message.method === "item/started") {
472
411
  const item = params.item;
473
- if (this.setupTurnActive && item && isToolItem(item.type)) {
474
- this.setupToolAttempted = true;
475
- if (this.activeTurnId && this._sessionId)
476
- void this.request("turn/interrupt", { threadId: this._sessionId, turnId: this.activeTurnId }).catch(() => { });
477
- }
478
- if (item?.type === "contextCompaction") {
479
- const providerSequence = ++this.compactionSequence;
480
- const providerEventId = String(item.id ?? `codex-compaction-${providerSequence}`);
481
- const origin = this.manualCompactionPending ? "rafi-manual" : "provider-auto";
482
- this.activeCompaction = { providerEventId, providerSequence, origin, usageRevision: this.usageRevision };
483
- this.eventQueue.push({
484
- kind: "context-compaction", phase: "started", origin, providerEventId, providerSequence,
485
- provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef,
486
- observedAt: new Date().toISOString(),
487
- });
488
- }
412
+ if (isCodexToolItem(item))
413
+ this.observedToolCalls += 1;
489
414
  const activity = codexItemActivity(item);
490
415
  if (activity)
491
416
  this.eventQueue.push({ kind: "activity", provider: "codex", ...activity });
@@ -496,24 +421,22 @@ export class CodexAdapter {
496
421
  this.activeText.push(item.text);
497
422
  this.eventQueue.push({ kind: "text", text: item.text });
498
423
  }
499
- else if (item?.type === "commandExecution")
424
+ else if (item?.type === "commandExecution") {
500
425
  this.eventQueue.push({ kind: "tool", name: "command_execution", input: { command: item.command } });
426
+ }
427
+ else if (item?.type === "contextCompaction" && !this.manualCompactionInFlight) {
428
+ // Completion is the provider's authoritative confirmation. Token-usage
429
+ // notifications may arrive before or after it, so do not make durable
430
+ // compaction accounting depend on their delivery order.
431
+ this.nativeCompactions.push({
432
+ id: `codex:${this._sessionId ?? "unknown"}:${Date.now()}:${++this.nativeCompactionSequence}`,
433
+ occurredAt: new Date().toISOString(),
434
+ provider: "codex",
435
+ usageRevision: this.usageRevision,
436
+ });
437
+ this.eventQueue.push({ kind: "session-transition", transition: "compacted", detail: "provider-native automatic compaction" });
438
+ }
501
439
  else {
502
- if (item?.type === "contextCompaction") {
503
- const active = this.activeCompaction;
504
- const providerSequence = active?.providerSequence ?? ++this.compactionSequence;
505
- const providerEventId = String(item.id ?? active?.providerEventId ?? `codex-compaction-${providerSequence}`);
506
- const origin = active?.origin ?? (this.manualCompactionPending ? "rafi-manual" : "provider-auto");
507
- const failed = item.status === "failed" || item.status === "error" || item.error !== undefined;
508
- this.eventQueue.push({
509
- kind: "context-compaction", phase: failed ? "failed" : "succeeded", origin, providerEventId, providerSequence,
510
- provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef,
511
- observedAt: new Date().toISOString(),
512
- ...(failed ? { reason: String(item.error?.message ?? item.error ?? "Codex context compaction failed") } : {}),
513
- ...(!failed && this.usage && (!active || this.usageRevision > active.usageRevision) ? { postCompactSample: { ...this.usage, source: "post-compact" } } : {}),
514
- });
515
- this.activeCompaction = undefined;
516
- }
517
440
  const activity = codexItemActivity(item, true);
518
441
  if (activity)
519
442
  this.eventQueue.push({ kind: "activity", provider: "codex", ...activity });
@@ -531,8 +454,7 @@ export class CodexAdapter {
531
454
  const tokenUsage = params.tokenUsage;
532
455
  const total = tokenUsage?.total;
533
456
  const last = tokenUsage?.last;
534
- // `last` is active context occupancy. `total` is lifetime session usage.
535
- const used = Number(last?.totalTokens);
457
+ const used = Number(total?.totalTokens);
536
458
  const maximum = Number(tokenUsage?.modelContextWindow);
537
459
  const observedAt = new Date().toISOString();
538
460
  const totalInput = optionalNonNegative(total?.inputTokens);
@@ -546,11 +468,7 @@ export class CodexAdapter {
546
468
  outputTokens: optionalNonNegative(last?.outputTokens),
547
469
  };
548
470
  if (Number.isFinite(used) && used >= 0 && Number.isFinite(maximum) && maximum > 0) {
549
- const sequence = ++this.contextSequence;
550
- this.usage = {
551
- used, maximum, percentage: used / maximum * 100, observedAt, source: "provider-event", sequence,
552
- ...(this._sessionId ? { sessionId: this._sessionId } : {}), ...(this.opts.model ? { model: this.opts.model } : {}),
553
- };
471
+ this.usage = { used, maximum, percentage: used / maximum * 100, observedAt, source: "provider-event" };
554
472
  this.usageRevision += 1;
555
473
  this.eventQueue.push({ kind: "context-usage", ...this.usage });
556
474
  }
@@ -606,23 +524,15 @@ export class CodexAdapter {
606
524
  waiter.reject(error);
607
525
  this.notificationWaiters.clear();
608
526
  }
609
- /** Restart only app-server; the scoped thread remains the continuity boundary. */
610
- async restartTransport() {
611
- if (this.activeTurnId)
612
- throw new Error("cannot restart Codex app-server while a turn is active");
527
+ async restartForAutoCompaction() {
613
528
  const child = this.process;
529
+ if (!child)
530
+ return;
614
531
  this.process = undefined;
615
532
  this.initialized = false;
616
533
  this.threadAttached = false;
617
- if (child && child.exitCode === null)
534
+ if (child.exitCode === null)
618
535
  child.kill("SIGTERM");
619
- for (const pending of this.pending.values())
620
- pending.reject(new Error("Codex app-server restarted for context policy"));
621
- this.pending.clear();
622
- for (const waiters of this.notificationWaiters.values())
623
- for (const waiter of waiters)
624
- waiter.reject(new Error("Codex app-server restarted for context policy"));
625
- this.notificationWaiters.clear();
626
536
  }
627
537
  buildSkillsAppendix() {
628
538
  if (!this.opts.skills?.length)
@@ -635,18 +545,13 @@ function optionalNonNegative(value) {
635
545
  const number = Number(value);
636
546
  return Number.isFinite(number) && number >= 0 ? number : undefined;
637
547
  }
638
- function tokenCeiling(maximum, percentage) {
639
- return Math.max(1, Math.floor(maximum * percentage / 100));
548
+ function validThreshold(value) {
549
+ if (!Number.isInteger(value) || value < 1 || value > 99)
550
+ throw new Error("automatic compaction threshold must be an integer from 1 to 99");
551
+ return value;
640
552
  }
641
- function validateContextPolicy(policy) {
642
- if (!Number.isInteger(policy.configuredThresholdPercent) || policy.configuredThresholdPercent < 1 || policy.configuredThresholdPercent > 99) {
643
- throw new Error("configured context threshold must be an integer from 1 to 99");
644
- }
645
- if (!Number.isSafeInteger(policy.compactMaximum) || policy.compactMaximum < 1)
646
- throw new Error("compact maximum must be a positive safe integer");
647
- }
648
- function isToolItem(type) {
649
- return type === "commandExecution" || type === "fileChange" || type === "mcpToolCall" || type === "dynamicToolCall" || type === "webSearch";
553
+ function tokenLimit(maximum, percentage) {
554
+ return Math.max(1, Math.floor(maximum * percentage / 100));
650
555
  }
651
556
  function activityPhase(phase) {
652
557
  if (phase === "planning")
@@ -679,6 +584,9 @@ function codexItemActivity(item, completed = false) {
679
584
  return { state: completed ? "received response" : "writing response" };
680
585
  return undefined;
681
586
  }
587
+ function isCodexToolItem(item) {
588
+ return item?.type === "commandExecution" || item?.type === "mcpToolCall" || item?.type === "dynamicToolCall" || item?.type === "webSearch" || item?.type === "fileChange";
589
+ }
682
590
  function loadSkillMarkdown(cwd, skill) {
683
591
  const projectPath = join(cwd, ".agents", "skills", skill, "SKILL.md");
684
592
  if (existsSync(projectPath))
@@ -1,5 +1,5 @@
1
1
  import type { AgentRuntime } from "../runtimeAuth.js";
2
- import type { BuilderAdapter, BuilderEvent, CompactResult, ContextManagementPolicy, ContextUsage, InterruptResult, ManagedTurnDispatcher, PreparedContextManagement, ProviderSessionUsage, TurnResult } from "./types.js";
2
+ import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, NativeCompaction, ProviderSessionUsage, TurnResult } from "./types.js";
3
3
  import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
4
4
  export type TurnRecoveryChoice = "retry" | "switch" | "cancel";
5
5
  export interface RecoveringAdapterOptions {
@@ -21,7 +21,6 @@ export declare class RecoveringAdapter implements BuilderAdapter {
21
21
  private readonly eventQueue;
22
22
  private eventPump;
23
23
  private closed;
24
- private turnDispatcher?;
25
24
  constructor(opts: RecoveringAdapterOptions);
26
25
  get agent(): "claude" | "codex";
27
26
  sendTurn(text: string): Promise<TurnResult>;
@@ -30,10 +29,10 @@ export declare class RecoveringAdapter implements BuilderAdapter {
30
29
  adoptSessionRef(ref: ProviderSessionRefV1): void;
31
30
  validateSession(): Promise<SessionAvailabilityV1>;
32
31
  compact(): Promise<CompactResult>;
33
- prepareContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
34
- updateContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
35
- interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<InterruptResult>;
36
- installManagedTurnDispatcher(dispatcher: ManagedTurnDispatcher): void;
32
+ prepareAutoCompaction(thresholdPercent?: number): Promise<void>;
33
+ drainNativeCompactions(): import("./types.js").NativeCompaction[];
34
+ restoreNativeCompactions(compactions: NativeCompaction[]): void;
35
+ contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
37
36
  contextUsage(): Promise<ContextUsage | undefined>;
38
37
  sessionUsage(): Promise<ProviderSessionUsage | undefined>;
39
38
  events(): AsyncIterable<BuilderEvent>;
@@ -9,7 +9,6 @@ export class RecoveringAdapter {
9
9
  eventQueue = new AsyncQueue();
10
10
  eventPump;
11
11
  closed = false;
12
- turnDispatcher;
13
12
  constructor(opts) {
14
13
  this.opts = opts;
15
14
  this.adapter = opts.initial;
@@ -21,8 +20,7 @@ export class RecoveringAdapter {
21
20
  }
22
21
  async sendTurn(text) {
23
22
  while (true) {
24
- const invoke = () => this.adapter.sendTurn(text);
25
- const result = this.turnDispatcher ? await this.turnDispatcher(text, invoke) : await invoke();
23
+ const result = await this.adapter.sendTurn(text);
26
24
  const sessionRef = this.adapter.sessionRef?.();
27
25
  if (sessionRef && this.opts.onSessionRef) {
28
26
  try {
@@ -69,12 +67,10 @@ export class RecoveringAdapter {
69
67
  return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped", detail: "wrapped adapter does not expose scoped session validation" });
70
68
  }
71
69
  compact() { return this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" }); }
72
- prepareContextManagement(policy) { if (!this.adapter.prepareContextManagement)
73
- return Promise.reject(new Error("native context management unavailable")); return this.adapter.prepareContextManagement(policy); }
74
- updateContextManagement(policy) { if (!this.adapter.updateContextManagement)
75
- return Promise.reject(new Error("native context reconfiguration unavailable")); return this.adapter.updateContextManagement(policy); }
76
- interruptTurnAtCompactionBoundary(providerEventId) { return this.adapter.interruptTurnAtCompactionBoundary?.(providerEventId) ?? Promise.resolve({ ok: false, error: "compaction-boundary interruption unavailable", providerEventId }); }
77
- installManagedTurnDispatcher(dispatcher) { this.turnDispatcher = dispatcher; }
70
+ prepareAutoCompaction(thresholdPercent) { return this.adapter.prepareAutoCompaction?.(thresholdPercent) ?? Promise.resolve(); }
71
+ drainNativeCompactions() { return this.adapter.drainNativeCompactions?.() ?? []; }
72
+ restoreNativeCompactions(compactions) { this.adapter.restoreNativeCompactions?.(compactions); }
73
+ contextUsageAfterNativeCompaction(compaction) { return this.adapter.contextUsageAfterNativeCompaction?.(compaction) ?? Promise.resolve(undefined); }
78
74
  contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
79
75
  sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
80
76
  events() {