ai-foreman 1.7.18 → 1.7.19

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 (83) hide show
  1. package/README.md +18 -7
  2. package/dist/activity.d.ts +11 -1
  3. package/dist/activity.js +70 -15
  4. package/dist/adapters/claude.d.ts +11 -1
  5. package/dist/adapters/claude.js +122 -18
  6. package/dist/adapters/codex.d.ts +13 -1
  7. package/dist/adapters/codex.js +148 -31
  8. package/dist/adapters/recovering.d.ts +3 -1
  9. package/dist/adapters/recovering.js +12 -4
  10. package/dist/adapters/types.d.ts +27 -1
  11. package/dist/agentRun.js +2 -0
  12. package/dist/branch/currentGuard.d.ts +3 -1
  13. package/dist/branch/currentGuard.js +11 -2
  14. package/dist/branch/resume.js +1 -1
  15. package/dist/branch/runner.d.ts +2 -0
  16. package/dist/branch/runner.js +90 -11
  17. package/dist/buildApproval.d.ts +9 -0
  18. package/dist/buildApproval.js +126 -0
  19. package/dist/buildRuns.js +70 -32
  20. package/dist/cli/recovery.js +9 -13
  21. package/dist/cli/runtimeAuthPrompt.d.ts +7 -0
  22. package/dist/cli/runtimeAuthPrompt.js +40 -5
  23. package/dist/cli/start.d.ts +16 -1
  24. package/dist/cli/start.js +159 -79
  25. package/dist/cli/tickets.d.ts +11 -0
  26. package/dist/cli/tickets.js +382 -145
  27. package/dist/continuity.d.ts +7 -1
  28. package/dist/continuity.js +86 -7
  29. package/dist/diagnostics.js +35 -1
  30. package/dist/foreman.d.ts +8 -1
  31. package/dist/foreman.js +144 -29
  32. package/dist/handoffs.js +121 -79
  33. package/dist/humanDecision.d.ts +18 -0
  34. package/dist/humanDecision.js +43 -0
  35. package/dist/index.js +2 -1
  36. package/dist/log.d.ts +1 -1
  37. package/dist/managerPacket.js +1 -0
  38. package/dist/observability.js +2 -1
  39. package/dist/providerQuestions.d.ts +5 -0
  40. package/dist/providerQuestions.js +24 -3
  41. package/dist/qaDeliveryJournal.d.ts +70 -0
  42. package/dist/qaDeliveryJournal.js +1 -0
  43. package/dist/qaFailureDelivery.d.ts +26 -10
  44. package/dist/qaFailureDelivery.js +399 -412
  45. package/dist/qaHandbackHistory.d.ts +4 -0
  46. package/dist/qaHandbackHistory.js +30 -0
  47. package/dist/qaHandbackMigration.d.ts +9 -0
  48. package/dist/qaHandbackMigration.js +147 -0
  49. package/dist/qaPrerequisites.d.ts +26 -0
  50. package/dist/qaPrerequisites.js +61 -0
  51. package/dist/qaProtocolV2.d.ts +8 -0
  52. package/dist/qaProtocolV2.js +3 -3
  53. package/dist/qaReview.d.ts +8 -0
  54. package/dist/qaReview.js +54 -10
  55. package/dist/questionTrace.d.ts +31 -0
  56. package/dist/questionTrace.js +85 -0
  57. package/dist/readOnlyProposal.d.ts +8 -0
  58. package/dist/readOnlyProposal.js +41 -0
  59. package/dist/recoveryPolicy.js +14 -3
  60. package/dist/runtimeAuth.d.ts +2 -1
  61. package/dist/runtimeAuth.js +2 -2
  62. package/dist/runtimeReadiness.d.ts +13 -1
  63. package/dist/runtimeReadiness.js +69 -11
  64. package/dist/sessionLifecycle.d.ts +1 -0
  65. package/dist/sessionLifecycle.js +53 -3
  66. package/dist/sources/sourceRegistry.d.ts +7 -0
  67. package/dist/sources/sourceRegistry.js +14 -0
  68. package/dist/stateTransfer.js +2 -0
  69. package/dist/statusReporter.d.ts +3 -1
  70. package/dist/statusReporter.js +8 -2
  71. package/dist/supervisedStart.d.ts +12 -0
  72. package/dist/supervisedStart.js +276 -0
  73. package/dist/supervisor.js +44 -22
  74. package/dist/ticketPopulation.js +1 -0
  75. package/dist/tickets/setupConfig.d.ts +8 -0
  76. package/dist/tickets/setupConfig.js +38 -4
  77. package/dist/util/deadline.d.ts +13 -0
  78. package/dist/util/deadline.js +60 -0
  79. package/dist/workflowDb.d.ts +55 -11
  80. package/dist/workflowDb.js +288 -63
  81. package/dist/workflowReader.d.ts +10 -2
  82. package/dist/workflowReader.js +35 -1
  83. package/package.json +3 -3
@@ -1,3 +1,4 @@
1
+ import { OperationDeadline } from "../util/deadline.js";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { dirname, join } from "node:path";
3
4
  /** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
@@ -11,8 +12,9 @@ export async function requireClaudeSDK() {
11
12
  "Or use Codex for this run with --agent codex.");
12
13
  }
13
14
  }
15
+ import { QuestionRoundTripTrace } from "../questionTrace.js";
14
16
  import { AsyncQueue } from "../util/asyncQueue.js";
15
- import { BuilderEventQueue, withActivityPhase } from "../activity.js";
17
+ import { BuilderEventQueue, currentActivity, withActivityPhase } from "../activity.js";
16
18
  import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
17
19
  import { classifyClaudeSdkFailure, resolveExecutablePath, sanitizeDiagnostics, } from "../runtimeReadiness.js";
18
20
  import { createProviderSessionRef, validateProviderSessionScope, canonicalSessionPath } from "../sessionIdentity.js";
@@ -178,6 +180,8 @@ export class ClaudeAdapter {
178
180
  structuredError;
179
181
  apiErrorStatus;
180
182
  compactResult;
183
+ manualCompactionDeadline;
184
+ compactionPromise;
181
185
  autoCompactionPrepared = false;
182
186
  preparedAutoCompactThreshold;
183
187
  preparedAutoCompactionPolicy;
@@ -190,6 +194,8 @@ export class ClaudeAdapter {
190
194
  activeProviderTurnSpanId;
191
195
  toolSpans = new Map();
192
196
  pending;
197
+ questionTrace;
198
+ responseOnlyTurn = false;
193
199
  terminalResult;
194
200
  streamEnded = false;
195
201
  closed = false;
@@ -240,6 +246,10 @@ export class ClaudeAdapter {
240
246
  this.rejectSessionIdentity = reject;
241
247
  });
242
248
  void this.sessionIdentityReady.catch(() => { });
249
+ this.questionTrace = new QuestionRoundTripTrace(opts.runtimePhase ?? "builder", opts.onQuestionTrace, opts.observer ? {
250
+ start: (attemptId) => opts.observer.store.startSpan(this.observationContext(), { kind: "provider_wait", name: "Waiting for Claude after question answer", attributes: { attemptId } }),
251
+ finish: (spanId, outcome) => opts.observer.store.finishSpan(spanId, { outcome }),
252
+ } : undefined);
243
253
  this.query = query({
244
254
  prompt: this.inbox,
245
255
  options: {
@@ -259,7 +269,10 @@ export class ClaudeAdapter {
259
269
  }] }] },
260
270
  stderr: (data) => this.captureStderr(data),
261
271
  canUseTool: async (toolName, input, requestOptions = {}) => {
272
+ if (this.responseOnlyTurn)
273
+ return { behavior: "deny", message: "QA response correction forbids all tools, including read-only tools", interrupt: true };
262
274
  const isProviderQuestion = toolName === "AskUserQuestion";
275
+ const questionAttempt = isProviderQuestion ? this.questionTrace?.begin() : undefined;
263
276
  if (isProviderQuestion)
264
277
  this.beginProviderQuestionWait();
265
278
  try {
@@ -274,11 +287,16 @@ export class ClaudeAdapter {
274
287
  blockedPath: requestOptions.blockedPath,
275
288
  toolUseID: requestOptions.toolUseID,
276
289
  });
290
+ const result = permissionDecisionToClaudeResult(decision, requestOptions.toolUseID);
291
+ if (questionAttempt)
292
+ this.questionTrace?.returned(questionAttempt, decision.behavior === "allow" ? "allowed" : "denied");
277
293
  if (isProviderQuestion)
278
294
  this.endProviderQuestionWait(decision.behavior === "allow");
279
- return permissionDecisionToClaudeResult(decision, requestOptions.toolUseID);
295
+ return result;
280
296
  }
281
297
  catch (error) {
298
+ if (questionAttempt)
299
+ this.questionTrace?.returned(questionAttempt, "callback-error");
282
300
  if (isProviderQuestion)
283
301
  this.endProviderQuestionWait(false);
284
302
  throw error;
@@ -296,6 +314,7 @@ export class ClaudeAdapter {
296
314
  }
297
315
  }
298
316
  catch (err) {
317
+ this.questionTrace?.finish(this.closed ? "closed" : "stream-error");
299
318
  // Suppress the AbortError that fires when close() aborts the stream.
300
319
  const isShutdownAbort = this.closed &&
301
320
  (err instanceof Error &&
@@ -311,6 +330,7 @@ export class ClaudeAdapter {
311
330
  }
312
331
  }
313
332
  finally {
333
+ this.questionTrace?.finish(this.closed ? "closed" : "stream-ended");
314
334
  this.streamEnded = true;
315
335
  if (!this.closed && this.pending) {
316
336
  const result = this.streamFailureResult("Claude stream ended without a result", Boolean(this.opts.resumeSessionRef ?? this.opts.resumeSessionId));
@@ -326,6 +346,15 @@ export class ClaudeAdapter {
326
346
  }
327
347
  }
328
348
  handle(msg) {
349
+ const questionStreamOutcome = msg.type === "system" && msg.subtype === "api_retry" ? "provider-retry"
350
+ : msg.type === "auth_status" && msg.error ? "provider-auth-error"
351
+ : msg.type === "assistant" && (msg.error === "authentication_failed" || msg.error === "oauth_org_not_allowed") ? "provider-auth-error"
352
+ : msg.type === "assistant" && msg.error ? "provider-error" : "stream-message";
353
+ this.questionTrace?.message(questionStreamOutcome);
354
+ if (this.pending?.providerQuestionAnswered && !this.pending.providerQuestionWaits) {
355
+ this.pending.providerQuestionAnswered = false;
356
+ currentActivity()?.update("Claude stream resumed");
357
+ }
329
358
  this.touchPendingTurn();
330
359
  if ("session_id" in msg && typeof msg.session_id === "string") {
331
360
  this.observeSession(msg.session_id, "cwd" in msg && typeof msg.cwd === "string" ? msg.cwd : undefined);
@@ -370,8 +399,11 @@ export class ClaudeAdapter {
370
399
  this.turnSignals.push(...msg.output.map((line) => `auth: ${line}`));
371
400
  }
372
401
  else if (msg.type === "system" && msg.subtype === "status") {
373
- if (msg.status === "compacting")
402
+ if (msg.status === "compacting") {
403
+ if (this.manualCompactionInFlight && msg.session_id === this._sessionId)
404
+ this.manualCompactionDeadline?.extendForCorrelatedProgress();
374
405
  this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
406
+ }
375
407
  if (msg.compact_result === "success") {
376
408
  this.compactResult = { ok: true };
377
409
  this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
@@ -476,7 +508,10 @@ export class ClaudeAdapter {
476
508
  this.stderrChunks.length = 0;
477
509
  }
478
510
  }
479
- async sendTurn(text) {
511
+ async sendTurn(text, policy) {
512
+ if (this.pending)
513
+ throw new Error("a turn is already in progress");
514
+ this.responseOnlyTurn = Boolean(policy?.responseOnly);
480
515
  const turnId = randomUUID();
481
516
  this.activeProviderTurnId = turnId;
482
517
  const observer = this.opts.observer;
@@ -486,6 +521,7 @@ export class ClaudeAdapter {
486
521
  }
487
522
  finally {
488
523
  this.activeProviderTurnId = undefined;
524
+ this.responseOnlyTurn = false;
489
525
  }
490
526
  }
491
527
  const context = this.observationContext();
@@ -512,6 +548,7 @@ export class ClaudeAdapter {
512
548
  }
513
549
  this.activeProviderTurnId = undefined;
514
550
  this.activeProviderTurnSpanId = undefined;
551
+ this.responseOnlyTurn = false;
515
552
  }
516
553
  }
517
554
  sendTurnInternal(text) {
@@ -577,7 +614,7 @@ export class ClaudeAdapter {
577
614
  return this._sessionId;
578
615
  }
579
616
  sessionRef() { return this._sessionRef; }
580
- async prepareSession(timeoutMs = 30_000) {
617
+ async prepareSession(timeoutMs = this.opts.preparationTimeoutMs ?? 120_000) {
581
618
  if (this.closed || this.streamEnded || this.terminalResult)
582
619
  throw new Error(this.terminalResult?.text ?? "Claude session is closed");
583
620
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
@@ -592,7 +629,24 @@ export class ClaudeAdapter {
592
629
  aborted();
593
630
  timer = setTimeout(() => reject(new Error(`Claude did not expose a scoped session identity within ${timeoutMs}ms`)), timeoutMs);
594
631
  });
595
- const [, ref] = await Promise.race([Promise.all([this.query.initializationResult(), this.sessionIdentityReady]), interrupted]);
632
+ const prepare = async () => {
633
+ await this.query.initializationResult();
634
+ if (!this._sessionRef?.validatedAt && typeof this.query.supportedCommands === "function") {
635
+ const commands = await this.query.supportedCommands();
636
+ // Some SDK/CLI pairs expose init identity only after their first input.
637
+ // Require an advertised built-in local command; never substitute a
638
+ // model-generated warm-up for QA's pre-dispatch identity validation.
639
+ if (commands.some(command => command.name === "context" && "builtin" in command && command.builtin === true)) {
640
+ this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-start", at: new Date().toISOString() });
641
+ const result = await this.sendTurn("/context", { responseOnly: true });
642
+ this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-complete", at: new Date().toISOString() });
643
+ if (result.isError || result.failure || result.costUsd > 0)
644
+ throw new Error("Claude local context initialization did not complete without model work");
645
+ }
646
+ }
647
+ return this.sessionIdentityReady;
648
+ };
649
+ const ref = await Promise.race([prepare(), interrupted]);
596
650
  if (!ref.validatedAt)
597
651
  throw new Error("Claude session identity was not provider-validated during initialization");
598
652
  return ref;
@@ -627,19 +681,38 @@ export class ClaudeAdapter {
627
681
  });
628
682
  }
629
683
  async compact() {
630
- return this.opts.observer
684
+ if (this.compactionPromise)
685
+ return this.compactionPromise;
686
+ const operation = this.opts.observer
631
687
  ? this.opts.observer.span("compaction", "Claude context compaction", () => this.compactInternal())
632
688
  : this.compactInternal();
689
+ this.compactionPromise = operation;
690
+ try {
691
+ return await operation;
692
+ }
693
+ finally {
694
+ if (this.compactionPromise === operation)
695
+ this.compactionPromise = undefined;
696
+ }
633
697
  }
634
698
  async compactInternal() {
635
699
  this.compactResult = undefined;
636
700
  this.manualCompactionInFlight = true;
701
+ const normalMs = Math.min(180_000, Math.max(1, this.opts.compactionTimeoutMs ?? 120_000));
702
+ const deadline = new OperationDeadline("Claude compaction", normalMs, Math.min(180_000, normalMs * 1.5));
703
+ this.manualCompactionDeadline = deadline;
637
704
  let result;
638
705
  try {
639
- result = await this.sendTurn("/compact");
706
+ result = await deadline.run(() => this.sendTurn("/compact"), () => this.abort.abort());
707
+ }
708
+ catch (error) {
709
+ const failure = new SessionUnavailableError({ runtime: "claude", phase: "turn", dispatchState: "unknown", executable: this.opts.runtimeExecutable ?? "claude", cwd: this.opts.cwd, diagnostics: String(error) });
710
+ await this.close();
711
+ return { ok: false, error: failure.message, failure: failure.failure };
640
712
  }
641
713
  finally {
642
714
  this.manualCompactionInFlight = false;
715
+ this.manualCompactionDeadline = undefined;
643
716
  }
644
717
  if (result.failure?.category === "session-unavailable") {
645
718
  return { ok: false, error: result.text || result.failure.diagnostics, failure: result.failure };
@@ -649,15 +722,23 @@ export class ClaudeAdapter {
649
722
  return { ok: false, error: sanitizeDiagnostics(result.text || "Claude did not emit an explicit compact status") };
650
723
  }
651
724
  async prepareAutoCompaction(thresholdPercent = this.opts.autoCompactThresholdPercent) {
725
+ return new OperationDeadline("Claude auto-compaction preparation", this.opts.preparationTimeoutMs ?? 120_000).run(() => this.prepareAutoCompactionInternal(thresholdPercent), () => this.abort.abort());
726
+ }
727
+ async prepareAutoCompactionInternal(thresholdPercent = this.opts.autoCompactThresholdPercent) {
652
728
  if (thresholdPercent === undefined)
653
729
  return;
654
730
  const threshold = validThreshold(thresholdPercent);
655
731
  if (this.autoCompactionPrepared && this.preparedAutoCompactThreshold === threshold)
656
732
  return this.preparedAutoCompactionPolicy;
657
733
  this.opts.autoCompactThresholdPercent = threshold;
658
- await this.query.initializationResult();
659
- await this.query.applyFlagSettings({ autoCompactEnabled: true });
660
- const baseline = await this.query.getContextUsage();
734
+ await new OperationDeadline("Claude initialization", this.opts.preparationTimeoutMs ?? 120_000).run(() => this.query.initializationResult());
735
+ // The SDK can acknowledge initialization while its CLI is still waiting
736
+ // for first input. Sending getContextUsage then deadlocks the control
737
+ // queue, including a later local command. Establish identity first.
738
+ if (!this._sessionRef?.validatedAt && typeof this.query.supportedCommands === "function")
739
+ await this.prepareSession();
740
+ await new OperationDeadline("Claude settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ autoCompactEnabled: true }));
741
+ const baseline = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
661
742
  if (!Number.isFinite(baseline.maxTokens) || baseline.maxTokens <= 0
662
743
  || !Number.isFinite(baseline.autoCompactThreshold) || baseline.autoCompactThreshold === undefined
663
744
  || !baseline.isAutoCompactEnabled) {
@@ -670,8 +751,8 @@ export class ClaudeAdapter {
670
751
  if (reserve < 0)
671
752
  throw new Error("Claude reported an invalid automatic-compaction reserve");
672
753
  const requestedWindow = tokenLimit(baseline.maxTokens, threshold) + reserve;
673
- await this.query.applyFlagSettings({ autoCompactEnabled: true, autoCompactWindow: requestedWindow });
674
- const installed = await this.query.getContextUsage();
754
+ await new OperationDeadline("Claude settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ autoCompactEnabled: true, autoCompactWindow: requestedWindow }));
755
+ const installed = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
675
756
  if (!installed.isAutoCompactEnabled || !Number.isFinite(installed.maxTokens) || installed.maxTokens <= 0
676
757
  || !Number.isFinite(installed.autoCompactThreshold) || installed.autoCompactThreshold === undefined) {
677
758
  throw new Error("Claude did not expose an enabled native automatic-compaction threshold after configuration");
@@ -700,7 +781,7 @@ export class ClaudeAdapter {
700
781
  restoreNativeCompactions(compactions) { this.nativeCompactions.unshift(...compactions); }
701
782
  async contextUsage() {
702
783
  try {
703
- const usage = await this.query.getContextUsage();
784
+ const usage = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
704
785
  const result = { used: usage.totalTokens, maximum: usage.maxTokens, percentage: usage.percentage, observedAt: new Date().toISOString(), source: "provider-query" };
705
786
  this.eventQueue.push({ kind: "context-usage", ...result });
706
787
  return result;
@@ -717,12 +798,13 @@ export class ClaudeAdapter {
717
798
  return { ok: false, error: "Claude SDK cannot change reasoning/fast controls on an existing transport" };
718
799
  if (settings.model === this.opts.model)
719
800
  return { ok: true };
720
- const result = await this.sendTurn(`/model ${settings.model ?? "default"}`);
801
+ const result = await new OperationDeadline("Claude model settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.sendTurn(`/model ${settings.model ?? "default"}`), () => this.abort.abort());
721
802
  if (result.isError)
722
803
  return { ok: false, error: result.text, ...(result.failure ? { failure: result.failure } : {}) };
723
804
  this.opts.model = settings.model;
724
805
  return { ok: true };
725
806
  }
807
+ observeEvents(listener) { return this.eventQueue.observe(listener); }
726
808
  events() {
727
809
  return this.eventQueue;
728
810
  }
@@ -730,16 +812,18 @@ export class ClaudeAdapter {
730
812
  if (this.closed)
731
813
  return;
732
814
  this.closed = true;
815
+ this.questionTrace?.finish("closed");
733
816
  this.rejectSessionIdentity(new Error("Claude session closed during initialization"));
734
817
  this.abort.abort();
735
818
  this.inbox.close();
736
819
  try {
737
- await this.query.interrupt();
820
+ await new OperationDeadline("Claude interrupt", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.query.interrupt());
738
821
  }
739
822
  catch {
740
823
  // interrupt is best-effort — ignore if no turn is active
741
824
  }
742
- await this.pumpDone.catch(() => { });
825
+ this.query.close?.();
826
+ await new OperationDeadline("Claude shutdown", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.pumpDone).catch(() => { });
743
827
  }
744
828
  failSessionIdentity(error) {
745
829
  this.rejectSessionIdentity(error);
@@ -784,12 +868,15 @@ export class ClaudeAdapter {
784
868
  this.resolveSessionIdentity(this._sessionRef);
785
869
  }
786
870
  settlePending(result, emit = true) {
871
+ this.questionTrace?.finish(result.isError ? "result-error" : "result");
787
872
  const pending = this.pending;
788
873
  if (!pending)
789
874
  return;
790
875
  this.pending = undefined;
791
876
  if (pending.idleTimer)
792
877
  clearTimeout(pending.idleTimer);
878
+ if (pending.hardTimer)
879
+ clearTimeout(pending.hardTimer);
793
880
  if (!result.turnId) {
794
881
  result.turnId = pending.turnId;
795
882
  result.hostInstruction = pending.instruction;
@@ -814,6 +901,11 @@ export class ClaudeAdapter {
814
901
  if (pending.idleTimer)
815
902
  clearTimeout(pending.idleTimer);
816
903
  pending.idleTimer = undefined;
904
+ if (pending.hardTimer) {
905
+ clearTimeout(pending.hardTimer);
906
+ pending.hardTimer = undefined;
907
+ pending.hardRemainingMs = Math.max(0, (pending.hardRemainingMs ?? this.opts.turnDeadlineMs ?? 3_600_000) - (performance.now() - (pending.hardArmedAt ?? performance.now())));
908
+ }
817
909
  }
818
910
  endProviderQuestionWait(answered) {
819
911
  const pending = this.pending;
@@ -829,11 +921,23 @@ export class ClaudeAdapter {
829
921
  clearTimeout(pending.idleTimer);
830
922
  if ((pending.providerQuestionWaits ?? 0) > 0)
831
923
  return;
924
+ if (!pending.hardTimer) {
925
+ pending.hardRemainingMs ??= this.opts.turnDeadlineMs ?? 3_600_000;
926
+ pending.hardArmedAt = performance.now();
927
+ pending.hardTimer = setTimeout(() => {
928
+ if (this.pending !== pending)
929
+ return;
930
+ this.settlePending(this.streamFailureResult("Claude active turn deadline exceeded; dispatched work requires reconciliation", false));
931
+ void this.close();
932
+ }, pending.hardRemainingMs);
933
+ pending.hardTimer.unref();
934
+ }
832
935
  const timeoutMs = this.providerIdleTimeoutMs();
833
936
  pending.idleTimer = setTimeout(() => {
834
937
  if (this.pending !== pending || this.closed)
835
938
  return;
836
- const context = pending.providerQuestionAnswered ? " after your answer was sent" : "";
939
+ this.questionTrace?.finish("idle-timeout");
940
+ const context = pending.providerQuestionAnswered ? " after your answer was sent; Claude has not resumed its stream yet" : "";
837
941
  const message = `Claude provider was silent for ${Math.round(timeoutMs / 60_000)} minutes${context}; the turn may have been dispatched and will not be retried automatically`;
838
942
  const result = this.streamFailureResult(message, false);
839
943
  this.eventQueue.push({ kind: "error", message });
@@ -25,6 +25,7 @@ export declare class CodexAdapter implements BuilderAdapter {
25
25
  private usage?;
26
26
  private usageRevision;
27
27
  private providerSessionUsage?;
28
+ private turnUsageBaseline?;
28
29
  private lastTurnTokens?;
29
30
  private stderr;
30
31
  private nativeAutoCompactTokenLimit?;
@@ -34,6 +35,9 @@ export declare class CodexAdapter implements BuilderAdapter {
34
35
  private nativeCompactions;
35
36
  private nativeCompactionSequence;
36
37
  private manualCompactionInFlight;
38
+ private compactionPromise?;
39
+ private compactionActive;
40
+ private compactionUncertain;
37
41
  private observedToolCalls;
38
42
  private activeProviderTurnId?;
39
43
  private activeProviderTurnSpanId?;
@@ -43,11 +47,16 @@ export declare class CodexAdapter implements BuilderAdapter {
43
47
  buildAppServerArgs(): string[];
44
48
  /** Deprecated fixture helper. Runtime execution uses only app-server. */
45
49
  buildArgs(instruction: string): string[];
46
- sendTurn(instruction: string): Promise<TurnResult>;
50
+ sendTurn(instruction: string, policy?: {
51
+ handback?: boolean;
52
+ responseOnly?: boolean;
53
+ logicalActionId?: string;
54
+ }): Promise<TurnResult>;
47
55
  private sendTurnInternal;
48
56
  compact(): Promise<CompactResult>;
49
57
  private compactInternal;
50
58
  prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<NativeAutoCompactionPolicy | void>;
59
+ private prepareAutoCompactionInternal;
51
60
  requiresAutoCompactionSetupTurn(): boolean;
52
61
  autoCompactionPolicy(): NativeAutoCompactionPolicy | undefined;
53
62
  drainNativeCompactions(): NativeCompaction[];
@@ -61,9 +70,11 @@ export declare class CodexAdapter implements BuilderAdapter {
61
70
  prepareSession(): Promise<ProviderSessionRefV1>;
62
71
  adoptSessionRef(ref: ProviderSessionRefV1): void;
63
72
  validateSession(): Promise<SessionAvailabilityV1>;
73
+ observeEvents(listener: (event: BuilderEvent) => void): () => void;
64
74
  events(): AsyncIterable<BuilderEvent>;
65
75
  close(): Promise<void>;
66
76
  private ensureThread;
77
+ private ensureThreadInternal;
67
78
  private threadAttached;
68
79
  private ensureConnection;
69
80
  private request;
@@ -71,6 +82,7 @@ export declare class CodexAdapter implements BuilderAdapter {
71
82
  private handle;
72
83
  private observationContext;
73
84
  private waitFor;
85
+ private traceLifecycle;
74
86
  private touchWaiters;
75
87
  private providerIdleTimeoutMs;
76
88
  private disconnect;