ai-foreman 1.7.17 → 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 (87) hide show
  1. package/README.md +18 -7
  2. package/dist/activity.d.ts +11 -1
  3. package/dist/activity.js +84 -16
  4. package/dist/adapters/claude.d.ts +16 -1
  5. package/dist/adapters/claude.js +196 -29
  6. package/dist/adapters/codex.d.ts +15 -1
  7. package/dist/adapters/codex.js +172 -38
  8. package/dist/adapters/recovering.d.ts +3 -1
  9. package/dist/adapters/recovering.js +12 -4
  10. package/dist/adapters/types.d.ts +34 -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 +74 -55
  20. package/dist/cli/events.js +21 -1
  21. package/dist/cli/recovery.js +9 -13
  22. package/dist/cli/runtimeAuthPrompt.d.ts +7 -0
  23. package/dist/cli/runtimeAuthPrompt.js +40 -5
  24. package/dist/cli/start.d.ts +16 -1
  25. package/dist/cli/start.js +161 -81
  26. package/dist/cli/tickets.d.ts +11 -0
  27. package/dist/cli/tickets.js +382 -145
  28. package/dist/continuity.d.ts +7 -1
  29. package/dist/continuity.js +86 -7
  30. package/dist/diagnostics.js +37 -8
  31. package/dist/foreman.d.ts +8 -1
  32. package/dist/foreman.js +144 -29
  33. package/dist/handoffs.js +121 -79
  34. package/dist/humanDecision.d.ts +18 -0
  35. package/dist/humanDecision.js +43 -0
  36. package/dist/index.js +2 -1
  37. package/dist/log.d.ts +1 -1
  38. package/dist/managerPacket.js +1 -0
  39. package/dist/observability.js +3 -7
  40. package/dist/processIdentity.d.ts +8 -0
  41. package/dist/processIdentity.js +40 -0
  42. package/dist/projectDiagnostics.js +3 -16
  43. package/dist/providerQuestions.d.ts +5 -0
  44. package/dist/providerQuestions.js +27 -2
  45. package/dist/qaDeliveryJournal.d.ts +70 -0
  46. package/dist/qaDeliveryJournal.js +1 -0
  47. package/dist/qaFailureDelivery.d.ts +26 -10
  48. package/dist/qaFailureDelivery.js +399 -412
  49. package/dist/qaHandbackHistory.d.ts +4 -0
  50. package/dist/qaHandbackHistory.js +30 -0
  51. package/dist/qaHandbackMigration.d.ts +9 -0
  52. package/dist/qaHandbackMigration.js +147 -0
  53. package/dist/qaPrerequisites.d.ts +26 -0
  54. package/dist/qaPrerequisites.js +61 -0
  55. package/dist/qaProtocolV2.d.ts +8 -0
  56. package/dist/qaProtocolV2.js +3 -3
  57. package/dist/qaReview.d.ts +8 -0
  58. package/dist/qaReview.js +54 -10
  59. package/dist/questionTrace.d.ts +31 -0
  60. package/dist/questionTrace.js +85 -0
  61. package/dist/readOnlyProposal.d.ts +8 -0
  62. package/dist/readOnlyProposal.js +41 -0
  63. package/dist/recoveryPolicy.js +14 -3
  64. package/dist/runtimeAuth.d.ts +2 -1
  65. package/dist/runtimeAuth.js +2 -2
  66. package/dist/runtimeReadiness.d.ts +13 -1
  67. package/dist/runtimeReadiness.js +69 -11
  68. package/dist/sessionLifecycle.d.ts +1 -0
  69. package/dist/sessionLifecycle.js +53 -3
  70. package/dist/sources/sourceRegistry.d.ts +7 -0
  71. package/dist/sources/sourceRegistry.js +14 -0
  72. package/dist/stateTransfer.js +2 -0
  73. package/dist/statusReporter.d.ts +3 -1
  74. package/dist/statusReporter.js +8 -2
  75. package/dist/supervisedStart.d.ts +12 -0
  76. package/dist/supervisedStart.js +276 -0
  77. package/dist/supervisor.js +44 -22
  78. package/dist/ticketPopulation.js +1 -0
  79. package/dist/tickets/setupConfig.d.ts +8 -0
  80. package/dist/tickets/setupConfig.js +38 -4
  81. package/dist/util/deadline.d.ts +13 -0
  82. package/dist/util/deadline.js +60 -0
  83. package/dist/workflowDb.d.ts +55 -11
  84. package/dist/workflowDb.js +284 -70
  85. package/dist/workflowReader.d.ts +10 -2
  86. package/dist/workflowReader.js +35 -1
  87. 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,12 +12,14 @@ 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";
19
21
  import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
22
+ const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
20
23
  /**
21
24
  * Pure function: build the `options` object passed to `query()`.
22
25
  * Extracted so tests can assert on the shape without making a live SDK call.
@@ -177,6 +180,8 @@ export class ClaudeAdapter {
177
180
  structuredError;
178
181
  apiErrorStatus;
179
182
  compactResult;
183
+ manualCompactionDeadline;
184
+ compactionPromise;
180
185
  autoCompactionPrepared = false;
181
186
  preparedAutoCompactThreshold;
182
187
  preparedAutoCompactionPolicy;
@@ -189,6 +194,8 @@ export class ClaudeAdapter {
189
194
  activeProviderTurnSpanId;
190
195
  toolSpans = new Map();
191
196
  pending;
197
+ questionTrace;
198
+ responseOnlyTurn = false;
192
199
  terminalResult;
193
200
  streamEnded = false;
194
201
  closed = false;
@@ -239,6 +246,10 @@ export class ClaudeAdapter {
239
246
  this.rejectSessionIdentity = reject;
240
247
  });
241
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);
242
253
  this.query = query({
243
254
  prompt: this.inbox,
244
255
  options: {
@@ -258,18 +269,38 @@ export class ClaudeAdapter {
258
269
  }] }] },
259
270
  stderr: (data) => this.captureStderr(data),
260
271
  canUseTool: async (toolName, input, requestOptions = {}) => {
261
- const decision = await opts.permission({
262
- toolName,
263
- input,
264
- signal: requestOptions.signal,
265
- title: requestOptions.title,
266
- displayName: requestOptions.displayName,
267
- description: requestOptions.description,
268
- decisionReason: requestOptions.decisionReason,
269
- blockedPath: requestOptions.blockedPath,
270
- toolUseID: requestOptions.toolUseID,
271
- });
272
- return permissionDecisionToClaudeResult(decision, requestOptions.toolUseID);
272
+ if (this.responseOnlyTurn)
273
+ return { behavior: "deny", message: "QA response correction forbids all tools, including read-only tools", interrupt: true };
274
+ const isProviderQuestion = toolName === "AskUserQuestion";
275
+ const questionAttempt = isProviderQuestion ? this.questionTrace?.begin() : undefined;
276
+ if (isProviderQuestion)
277
+ this.beginProviderQuestionWait();
278
+ try {
279
+ const decision = await opts.permission({
280
+ toolName,
281
+ input,
282
+ signal: requestOptions.signal,
283
+ title: requestOptions.title,
284
+ displayName: requestOptions.displayName,
285
+ description: requestOptions.description,
286
+ decisionReason: requestOptions.decisionReason,
287
+ blockedPath: requestOptions.blockedPath,
288
+ toolUseID: requestOptions.toolUseID,
289
+ });
290
+ const result = permissionDecisionToClaudeResult(decision, requestOptions.toolUseID);
291
+ if (questionAttempt)
292
+ this.questionTrace?.returned(questionAttempt, decision.behavior === "allow" ? "allowed" : "denied");
293
+ if (isProviderQuestion)
294
+ this.endProviderQuestionWait(decision.behavior === "allow");
295
+ return result;
296
+ }
297
+ catch (error) {
298
+ if (questionAttempt)
299
+ this.questionTrace?.returned(questionAttempt, "callback-error");
300
+ if (isProviderQuestion)
301
+ this.endProviderQuestionWait(false);
302
+ throw error;
303
+ }
273
304
  },
274
305
  },
275
306
  });
@@ -283,6 +314,7 @@ export class ClaudeAdapter {
283
314
  }
284
315
  }
285
316
  catch (err) {
317
+ this.questionTrace?.finish(this.closed ? "closed" : "stream-error");
286
318
  // Suppress the AbortError that fires when close() aborts the stream.
287
319
  const isShutdownAbort = this.closed &&
288
320
  (err instanceof Error &&
@@ -298,6 +330,7 @@ export class ClaudeAdapter {
298
330
  }
299
331
  }
300
332
  finally {
333
+ this.questionTrace?.finish(this.closed ? "closed" : "stream-ended");
301
334
  this.streamEnded = true;
302
335
  if (!this.closed && this.pending) {
303
336
  const result = this.streamFailureResult("Claude stream ended without a result", Boolean(this.opts.resumeSessionRef ?? this.opts.resumeSessionId));
@@ -313,6 +346,16 @@ export class ClaudeAdapter {
313
346
  }
314
347
  }
315
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
+ }
358
+ this.touchPendingTurn();
316
359
  if ("session_id" in msg && typeof msg.session_id === "string") {
317
360
  this.observeSession(msg.session_id, "cwd" in msg && typeof msg.cwd === "string" ? msg.cwd : undefined);
318
361
  }
@@ -356,8 +399,11 @@ export class ClaudeAdapter {
356
399
  this.turnSignals.push(...msg.output.map((line) => `auth: ${line}`));
357
400
  }
358
401
  else if (msg.type === "system" && msg.subtype === "status") {
359
- if (msg.status === "compacting")
402
+ if (msg.status === "compacting") {
403
+ if (this.manualCompactionInFlight && msg.session_id === this._sessionId)
404
+ this.manualCompactionDeadline?.extendForCorrelatedProgress();
360
405
  this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
406
+ }
361
407
  if (msg.compact_result === "success") {
362
408
  this.compactResult = { ok: true };
363
409
  this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
@@ -462,7 +508,10 @@ export class ClaudeAdapter {
462
508
  this.stderrChunks.length = 0;
463
509
  }
464
510
  }
465
- 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);
466
515
  const turnId = randomUUID();
467
516
  this.activeProviderTurnId = turnId;
468
517
  const observer = this.opts.observer;
@@ -472,6 +521,7 @@ export class ClaudeAdapter {
472
521
  }
473
522
  finally {
474
523
  this.activeProviderTurnId = undefined;
524
+ this.responseOnlyTurn = false;
475
525
  }
476
526
  }
477
527
  const context = this.observationContext();
@@ -498,6 +548,7 @@ export class ClaudeAdapter {
498
548
  }
499
549
  this.activeProviderTurnId = undefined;
500
550
  this.activeProviderTurnSpanId = undefined;
551
+ this.responseOnlyTurn = false;
501
552
  }
502
553
  }
503
554
  sendTurnInternal(text) {
@@ -513,7 +564,8 @@ export class ClaudeAdapter {
513
564
  this.turnSignals = [];
514
565
  this.structuredError = undefined;
515
566
  this.apiErrorStatus = undefined;
516
- this.pending = { resolve, reject, instruction: text, turnId: this.activeProviderTurnId ?? randomUUID() };
567
+ const pending = this.pending = { resolve, reject, instruction: text, turnId: this.activeProviderTurnId ?? randomUUID() };
568
+ this.armPendingTurn(pending);
517
569
  this.inbox.push({
518
570
  type: "user",
519
571
  message: { role: "user", content: text },
@@ -562,7 +614,7 @@ export class ClaudeAdapter {
562
614
  return this._sessionId;
563
615
  }
564
616
  sessionRef() { return this._sessionRef; }
565
- async prepareSession(timeoutMs = 30_000) {
617
+ async prepareSession(timeoutMs = this.opts.preparationTimeoutMs ?? 120_000) {
566
618
  if (this.closed || this.streamEnded || this.terminalResult)
567
619
  throw new Error(this.terminalResult?.text ?? "Claude session is closed");
568
620
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
@@ -577,7 +629,24 @@ export class ClaudeAdapter {
577
629
  aborted();
578
630
  timer = setTimeout(() => reject(new Error(`Claude did not expose a scoped session identity within ${timeoutMs}ms`)), timeoutMs);
579
631
  });
580
- 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]);
581
650
  if (!ref.validatedAt)
582
651
  throw new Error("Claude session identity was not provider-validated during initialization");
583
652
  return ref;
@@ -612,19 +681,38 @@ export class ClaudeAdapter {
612
681
  });
613
682
  }
614
683
  async compact() {
615
- return this.opts.observer
684
+ if (this.compactionPromise)
685
+ return this.compactionPromise;
686
+ const operation = this.opts.observer
616
687
  ? this.opts.observer.span("compaction", "Claude context compaction", () => this.compactInternal())
617
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
+ }
618
697
  }
619
698
  async compactInternal() {
620
699
  this.compactResult = undefined;
621
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;
622
704
  let result;
623
705
  try {
624
- 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 };
625
712
  }
626
713
  finally {
627
714
  this.manualCompactionInFlight = false;
715
+ this.manualCompactionDeadline = undefined;
628
716
  }
629
717
  if (result.failure?.category === "session-unavailable") {
630
718
  return { ok: false, error: result.text || result.failure.diagnostics, failure: result.failure };
@@ -634,15 +722,23 @@ export class ClaudeAdapter {
634
722
  return { ok: false, error: sanitizeDiagnostics(result.text || "Claude did not emit an explicit compact status") };
635
723
  }
636
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) {
637
728
  if (thresholdPercent === undefined)
638
729
  return;
639
730
  const threshold = validThreshold(thresholdPercent);
640
731
  if (this.autoCompactionPrepared && this.preparedAutoCompactThreshold === threshold)
641
732
  return this.preparedAutoCompactionPolicy;
642
733
  this.opts.autoCompactThresholdPercent = threshold;
643
- await this.query.initializationResult();
644
- await this.query.applyFlagSettings({ autoCompactEnabled: true });
645
- 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());
646
742
  if (!Number.isFinite(baseline.maxTokens) || baseline.maxTokens <= 0
647
743
  || !Number.isFinite(baseline.autoCompactThreshold) || baseline.autoCompactThreshold === undefined
648
744
  || !baseline.isAutoCompactEnabled) {
@@ -655,8 +751,8 @@ export class ClaudeAdapter {
655
751
  if (reserve < 0)
656
752
  throw new Error("Claude reported an invalid automatic-compaction reserve");
657
753
  const requestedWindow = tokenLimit(baseline.maxTokens, threshold) + reserve;
658
- await this.query.applyFlagSettings({ autoCompactEnabled: true, autoCompactWindow: requestedWindow });
659
- 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());
660
756
  if (!installed.isAutoCompactEnabled || !Number.isFinite(installed.maxTokens) || installed.maxTokens <= 0
661
757
  || !Number.isFinite(installed.autoCompactThreshold) || installed.autoCompactThreshold === undefined) {
662
758
  throw new Error("Claude did not expose an enabled native automatic-compaction threshold after configuration");
@@ -685,7 +781,7 @@ export class ClaudeAdapter {
685
781
  restoreNativeCompactions(compactions) { this.nativeCompactions.unshift(...compactions); }
686
782
  async contextUsage() {
687
783
  try {
688
- const usage = await this.query.getContextUsage();
784
+ const usage = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
689
785
  const result = { used: usage.totalTokens, maximum: usage.maxTokens, percentage: usage.percentage, observedAt: new Date().toISOString(), source: "provider-query" };
690
786
  this.eventQueue.push({ kind: "context-usage", ...result });
691
787
  return result;
@@ -702,12 +798,13 @@ export class ClaudeAdapter {
702
798
  return { ok: false, error: "Claude SDK cannot change reasoning/fast controls on an existing transport" };
703
799
  if (settings.model === this.opts.model)
704
800
  return { ok: true };
705
- 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());
706
802
  if (result.isError)
707
803
  return { ok: false, error: result.text, ...(result.failure ? { failure: result.failure } : {}) };
708
804
  this.opts.model = settings.model;
709
805
  return { ok: true };
710
806
  }
807
+ observeEvents(listener) { return this.eventQueue.observe(listener); }
711
808
  events() {
712
809
  return this.eventQueue;
713
810
  }
@@ -715,16 +812,18 @@ export class ClaudeAdapter {
715
812
  if (this.closed)
716
813
  return;
717
814
  this.closed = true;
815
+ this.questionTrace?.finish("closed");
718
816
  this.rejectSessionIdentity(new Error("Claude session closed during initialization"));
719
817
  this.abort.abort();
720
818
  this.inbox.close();
721
819
  try {
722
- await this.query.interrupt();
820
+ await new OperationDeadline("Claude interrupt", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.query.interrupt());
723
821
  }
724
822
  catch {
725
823
  // interrupt is best-effort — ignore if no turn is active
726
824
  }
727
- await this.pumpDone.catch(() => { });
825
+ this.query.close?.();
826
+ await new OperationDeadline("Claude shutdown", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.pumpDone).catch(() => { });
728
827
  }
729
828
  failSessionIdentity(error) {
730
829
  this.rejectSessionIdentity(error);
@@ -769,10 +868,15 @@ export class ClaudeAdapter {
769
868
  this.resolveSessionIdentity(this._sessionRef);
770
869
  }
771
870
  settlePending(result, emit = true) {
871
+ this.questionTrace?.finish(result.isError ? "result-error" : "result");
772
872
  const pending = this.pending;
773
873
  if (!pending)
774
874
  return;
775
875
  this.pending = undefined;
876
+ if (pending.idleTimer)
877
+ clearTimeout(pending.idleTimer);
878
+ if (pending.hardTimer)
879
+ clearTimeout(pending.hardTimer);
776
880
  if (!result.turnId) {
777
881
  result.turnId = pending.turnId;
778
882
  result.hostInstruction = pending.instruction;
@@ -785,6 +889,69 @@ export class ClaudeAdapter {
785
889
  this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
786
890
  pending.resolve(result);
787
891
  }
892
+ touchPendingTurn() {
893
+ if (this.pending)
894
+ this.armPendingTurn(this.pending);
895
+ }
896
+ beginProviderQuestionWait() {
897
+ const pending = this.pending;
898
+ if (!pending)
899
+ return;
900
+ pending.providerQuestionWaits = (pending.providerQuestionWaits ?? 0) + 1;
901
+ if (pending.idleTimer)
902
+ clearTimeout(pending.idleTimer);
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
+ }
909
+ }
910
+ endProviderQuestionWait(answered) {
911
+ const pending = this.pending;
912
+ if (!pending)
913
+ return;
914
+ pending.providerQuestionWaits = Math.max(0, (pending.providerQuestionWaits ?? 1) - 1);
915
+ pending.providerQuestionAnswered ||= answered;
916
+ if (pending.providerQuestionWaits === 0)
917
+ this.armPendingTurn(pending);
918
+ }
919
+ armPendingTurn(pending) {
920
+ if (pending.idleTimer)
921
+ clearTimeout(pending.idleTimer);
922
+ if ((pending.providerQuestionWaits ?? 0) > 0)
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
+ }
935
+ const timeoutMs = this.providerIdleTimeoutMs();
936
+ pending.idleTimer = setTimeout(() => {
937
+ if (this.pending !== pending || this.closed)
938
+ return;
939
+ this.questionTrace?.finish("idle-timeout");
940
+ const context = pending.providerQuestionAnswered ? " after your answer was sent; Claude has not resumed its stream yet" : "";
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`;
942
+ const result = this.streamFailureResult(message, false);
943
+ this.eventQueue.push({ kind: "error", message });
944
+ this.settlePending(result);
945
+ // Do not leave a hidden stream running after the host has declared its
946
+ // result uncertain. close() is intentionally not awaited here.
947
+ void this.close();
948
+ }, timeoutMs);
949
+ pending.idleTimer.unref();
950
+ }
951
+ providerIdleTimeoutMs() {
952
+ const configured = this.opts.providerIdleTimeoutMs;
953
+ return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PROVIDER_IDLE_TIMEOUT_MS;
954
+ }
788
955
  streamFailureResult(message, sessionUnavailable) {
789
956
  if (sessionUnavailable) {
790
957
  const availability = {
@@ -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,9 @@ export declare class CodexAdapter implements BuilderAdapter {
71
82
  private handle;
72
83
  private observationContext;
73
84
  private waitFor;
85
+ private traceLifecycle;
86
+ private touchWaiters;
87
+ private providerIdleTimeoutMs;
74
88
  private disconnect;
75
89
  private restartForAutoCompaction;
76
90
  private buildSkillsAppendix;