@gajae-code/coding-agent 0.15.3 → 0.15.4

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.
@@ -72,6 +72,7 @@ export interface HookSelectorOptions {
72
72
  customInput?: {
73
73
  optionLabel: string;
74
74
  onSubmit: (text: string) => void;
75
+ allowEmpty?: boolean;
75
76
  };
76
77
  clarificationInput?: {
77
78
  optionLabel: string;
@@ -390,7 +391,7 @@ export class HookSelectorComponent extends Container {
390
391
  #outline: boolean;
391
392
  #scrollTitleRows: number | undefined;
392
393
  #inlineEditorMaxHeight: number | undefined;
393
- #customInput: { optionLabel: string; onSubmit: (text: string) => void } | undefined;
394
+ #customInput: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
394
395
  #clarificationInput: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
395
396
  #activeInput: { onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
396
397
  #inputArea: Container;
@@ -1154,6 +1154,7 @@ export class ExtensionUiController {
1154
1154
  customInput: dialogOptions?.customInput
1155
1155
  ? {
1156
1156
  optionLabel: dialogOptions.customInput.optionLabel,
1157
+ allowEmpty: (dialogOptions.customInput as { allowEmpty?: boolean }).allowEmpty,
1157
1158
  onSubmit: text => {
1158
1159
  const optionLabel = dialogOptions.customInput?.optionLabel;
1159
1160
  this.hideHookSelector();
@@ -20,6 +20,7 @@ import * as path from "node:path";
20
20
  import { scheduler } from "node:timers/promises";
21
21
  import * as util from "node:util";
22
22
  import {
23
+ AdaptiveCompactionTracker,
23
24
  type AfterToolCallContext,
24
25
  type AfterToolCallResult,
25
26
  type Agent,
@@ -2430,6 +2431,8 @@ export class AgentSession {
2430
2431
  // required to re-trigger. A content signature (not object identity) is used so
2431
2432
  // it survives the message-array rebuild that compaction/prune perform.
2432
2433
  #lastMidRunMaintenanceAnchorSignature: string | undefined = undefined;
2434
+ #adaptiveCompactionTracker = new AdaptiveCompactionTracker();
2435
+ #adaptiveCompactionRecordedMessageKey: string | undefined;
2433
2436
  #resourceSampler: () => EmergencyCompactionSample = () => this.#defaultResourceSample();
2434
2437
  #retainedMemorySampler: (() => RetainedMemorySample) | undefined;
2435
2438
 
@@ -7702,6 +7705,7 @@ export class AgentSession {
7702
7705
  * such event.
7703
7706
  */
7704
7707
  #syncAgentSessionId(sessionId?: string): void {
7708
+ this.#resetAdaptiveCompactionState();
7705
7709
  this.#reasoningControlContextGeneration++;
7706
7710
  const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
7707
7711
  this.agent.sessionId = sid;
@@ -15857,6 +15861,7 @@ export class AgentSession {
15857
15861
  fromExtension,
15858
15862
  preserveData,
15859
15863
  );
15864
+ this.#recordAdaptiveCompactionReset(tokensBefore);
15860
15865
  await this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
15861
15866
 
15862
15867
  const compactionResult: CompactionResult = {
@@ -16380,7 +16385,12 @@ export class AgentSession {
16380
16385
  // which breaks the provider prompt-cache prefix mid-epoch. Only prune at a
16381
16386
  // sanctioned maintenance boundary, i.e. when the un-pruned context already
16382
16387
  // crosses the compaction threshold. Pruning may then avert full compaction.
16383
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens))
16388
+ const adaptiveCompactionSettings = this.#recordAdaptiveCompactionCall(
16389
+ contextTokens,
16390
+ compactionSettings,
16391
+ assistantMessage,
16392
+ );
16393
+ if (!shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens))
16384
16394
  return true;
16385
16395
  const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
16386
16396
  relaxedMinimum: 0,
@@ -16391,7 +16401,7 @@ export class AgentSession {
16391
16401
  !shouldCompact(
16392
16402
  Math.max(0, contextTokens - pruneEstimate.tokensSaved),
16393
16403
  contextWindow,
16394
- compactionSettings,
16404
+ adaptiveCompactionSettings,
16395
16405
  autoCompactionOutputReserveTokens,
16396
16406
  )
16397
16407
  ) {
@@ -16399,7 +16409,8 @@ export class AgentSession {
16399
16409
  if (ownershipSignal?.aborted) return false;
16400
16410
  if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
16401
16411
  }
16402
- if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
16412
+ const prunedCompactionSettings = this.#compactionSettingsWithAdaptiveState(compactionSettings);
16413
+ if (shouldCompact(contextTokens, contextWindow, prunedCompactionSettings, autoCompactionOutputReserveTokens)) {
16403
16414
  // Try promotion first — if a larger model is available, switch instead of compacting
16404
16415
  const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
16405
16416
  if (ownershipSignal?.aborted) return false;
@@ -16453,7 +16464,7 @@ export class AgentSession {
16453
16464
  }
16454
16465
  if (isAborted()) return result("aborted");
16455
16466
 
16456
- // In-place context-full maintenance only. "off" defers entirely; "handoff"
16467
+ // context-full maintenance. "off" defers entirely; "handoff"
16457
16468
  // keeps its existing agent_end / pre-prompt boundaries (a mid-tool-loop
16458
16469
  // session swap would be far more disruptive than the overflow it avoids).
16459
16470
  const compactionSettings = this.settings.getGroup("compaction");
@@ -16469,7 +16480,12 @@ export class AgentSession {
16469
16480
  const autoCompactionOutputReserveTokens = 0;
16470
16481
  const anchor = this.#findMidRunUsageAnchor(context.messages);
16471
16482
  let contextTokens = this.#estimateMidRunContextTokens(context.messages);
16472
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
16483
+ const adaptiveCompactionSettings = anchor
16484
+ ? this.#recordAdaptiveCompactionCall(contextTokens, compactionSettings, anchor.message)
16485
+ : this.#compactionSettingsWithAdaptiveState(compactionSettings);
16486
+ if (
16487
+ !shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens)
16488
+ ) {
16473
16489
  return result("not-needed");
16474
16490
  }
16475
16491
  // Anti-loop (#1662): a given provider response anchors at most one
@@ -16503,7 +16519,7 @@ export class AgentSession {
16503
16519
  !shouldCompact(
16504
16520
  Math.max(0, contextTokens - pruneEstimate.tokensSaved),
16505
16521
  contextWindow,
16506
- compactionSettings,
16522
+ adaptiveCompactionSettings,
16507
16523
  autoCompactionOutputReserveTokens,
16508
16524
  )
16509
16525
  ) {
@@ -16512,7 +16528,9 @@ export class AgentSession {
16512
16528
  if (pruneResult?.failure === "artifact_persistence") return result("failed");
16513
16529
  if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
16514
16530
  }
16515
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
16531
+ if (
16532
+ !shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens)
16533
+ ) {
16516
16534
  return pruneResult?.committed ? result("pruned", true) : result("not-needed");
16517
16535
  }
16518
16536
 
@@ -16675,7 +16693,8 @@ export class AgentSession {
16675
16693
  // Model maxTokens is a capability ceiling, not a per-turn reservation.
16676
16694
  // Auto maintenance should track actual context fullness.
16677
16695
  const autoCompactionOutputReserveTokens = 0;
16678
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
16696
+ const adaptiveCompactionSettings = this.#compactionSettingsWithAdaptiveState(compactionSettings);
16697
+ if (!shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens)) {
16679
16698
  // Below the compaction threshold: optionally run evidence-gated maintenance
16680
16699
  // pruning (opt-in, high savings + cache-epoch payback required).
16681
16700
  await this.#maybeRunBelowThresholdMaintenancePrune();
@@ -16691,14 +16710,15 @@ export class AgentSession {
16691
16710
  !shouldCompact(
16692
16711
  Math.max(0, contextTokens - pruneEstimate.tokensSaved),
16693
16712
  contextWindow,
16694
- compactionSettings,
16713
+ adaptiveCompactionSettings,
16695
16714
  autoCompactionOutputReserveTokens,
16696
16715
  )
16697
16716
  ) {
16698
16717
  const pruneResult = await this.#pruneToolOutputs(undefined, true);
16699
16718
  if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
16700
16719
  }
16701
- if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
16720
+ const prunedCompactionSettings = this.#compactionSettingsWithAdaptiveState(compactionSettings);
16721
+ if (shouldCompact(contextTokens, contextWindow, prunedCompactionSettings, autoCompactionOutputReserveTokens)) {
16702
16722
  await this.#runAutoCompaction("threshold", false, false, {
16703
16723
  continueAfterMaintenance: false,
16704
16724
  deferHandoffMaintenance: false,
@@ -16706,6 +16726,45 @@ export class AgentSession {
16706
16726
  }
16707
16727
  }
16708
16728
 
16729
+ #recordAdaptiveCompactionCall<T extends { adaptive?: { enabled: boolean; turnWindow: number } }>(
16730
+ contextTokens: number,
16731
+ settings: T,
16732
+ assistantMessage: AssistantMessage,
16733
+ ): T {
16734
+ if (!settings.adaptive?.enabled) return settings;
16735
+ this.#adaptiveCompactionTracker.setWindowMs(settings.adaptive.turnWindow * 60_000);
16736
+ const messageKey = this.#adaptiveCompactionMessageKey(assistantMessage);
16737
+ if (this.#adaptiveCompactionRecordedMessageKey === messageKey) {
16738
+ return this.#compactionSettingsWithAdaptiveState(settings);
16739
+ }
16740
+ this.#adaptiveCompactionRecordedMessageKey = messageKey;
16741
+ this.#adaptiveCompactionTracker.recordCall(contextTokens);
16742
+ return this.#compactionSettingsWithAdaptiveState(settings);
16743
+ }
16744
+
16745
+ #adaptiveCompactionMessageKey(message: AssistantMessage): string {
16746
+ return `${message.provider}\u0000${message.model}\u0000${message.timestamp}\u0000${message.usage?.totalTokens ?? ""}`;
16747
+ }
16748
+
16749
+ #compactionSettingsWithAdaptiveState<T extends { adaptive?: { enabled: boolean } }>(settings: T): T {
16750
+ if (!settings.adaptive?.enabled) return settings;
16751
+ return {
16752
+ ...settings,
16753
+ adaptiveState: this.#adaptiveCompactionTracker.decisionState(),
16754
+ };
16755
+ }
16756
+
16757
+ #resetAdaptiveCompactionState(): void {
16758
+ this.#adaptiveCompactionTracker.reset();
16759
+ this.#adaptiveCompactionRecordedMessageKey = undefined;
16760
+ }
16761
+
16762
+ #recordAdaptiveCompactionReset(contextTokens: number): void {
16763
+ this.#adaptiveCompactionTracker.reset();
16764
+ this.#adaptiveCompactionTracker.recordCompact(contextTokens);
16765
+ this.#adaptiveCompactionRecordedMessageKey = undefined;
16766
+ }
16767
+
16709
16768
  #assistantEndedWithSuccessfulYield(assistantMessage: AssistantMessage): boolean {
16710
16769
  const toolCallId = this.#lastSuccessfulYieldToolCallId;
16711
16770
  if (!toolCallId) return false;
@@ -18156,6 +18215,7 @@ export class AgentSession {
18156
18215
  fromExtension,
18157
18216
  preserveData,
18158
18217
  );
18218
+ this.#recordAdaptiveCompactionReset(tokensBefore);
18159
18219
  await this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
18160
18220
  if (autoCompactionSignal.aborted) return await emitAborted();
18161
18221
 
@@ -7,6 +7,11 @@ import { INTENT_FIELD } from "@gajae-code/agent-core";
7
7
  import type { RawArgumentValidationResult } from "@gajae-code/ai/types";
8
8
  import * as z from "zod/v4";
9
9
  import { deepInterviewCharacterCount } from "../gjc-runtime/deep-interview-state";
10
+ import {
11
+ isLegacyDeepInterviewPlaceholder,
12
+ isWorkflowPlaceholderText,
13
+ WORKFLOW_PLACEHOLDER_CORRECTION,
14
+ } from "../gjc-runtime/workflow-placeholder";
10
15
 
11
16
  function deepInterviewBoundedString(maximum: number) {
12
17
  return z.string().superRefine((value, context) => {
@@ -146,6 +151,13 @@ function createQuestionItemSchema(deepInterviewSchema: z.ZodType<DeepInterviewMe
146
151
  workflowGate: WorkflowGateMeta.describe("optional workflow gate stage/kind override").optional(),
147
152
  })
148
153
  .superRefine((value, context) => {
154
+ if (value.deepInterview !== undefined && isWorkflowPlaceholderText(value.question)) {
155
+ context.addIssue({
156
+ code: "custom",
157
+ message: `deep-interview question body must ${WORKFLOW_PLACEHOLDER_CORRECTION}`,
158
+ path: ["question"],
159
+ });
160
+ }
149
161
  const labels = new Set(value.options.map(option => option.label));
150
162
  const contract = intentContract(value.deepInterview);
151
163
  const review = intentReview(value.deepInterview);
@@ -459,19 +471,58 @@ function knownIntentRejection(arguments_: Record<string, unknown>): RawArgumentV
459
471
  return { outcome: "reject", code: "ask-intent-contract-requires-non-empty-authority" };
460
472
  return undefined;
461
473
  }
474
+
475
+ function deepInterviewPlaceholderRejection(
476
+ arguments_: Record<string, unknown>,
477
+ ): RawArgumentValidationResult | undefined {
478
+ const root = parseEncodedContainer(arguments_);
479
+ if (!isPlainRecord(root)) return undefined;
480
+ const questions = parseEncodedContainer(root.questions);
481
+ if (!Array.isArray(questions)) return undefined;
482
+ for (const [index, rawValue] of questions.entries()) {
483
+ const rawQuestion = parseEncodedContainer(rawValue);
484
+ if (!isPlainRecord(rawQuestion) || !Object.hasOwn(rawQuestion, "deepInterview")) continue;
485
+ const metadata = parseEncodedContainer(rawQuestion.deepInterview);
486
+ if (metadata !== null && metadata !== undefined && isWorkflowPlaceholderText(rawQuestion.question)) {
487
+ return {
488
+ outcome: "reject",
489
+ code: "ask-deep-interview-question-body-required",
490
+ detail: {
491
+ rejectedKeys: [`questions[${index}].question`],
492
+ hint: WORKFLOW_PLACEHOLDER_CORRECTION,
493
+ },
494
+ };
495
+ }
496
+ }
497
+ for (const [index, rawValue] of questions.entries()) {
498
+ const question = parseEncodedContainer(rawValue);
499
+ if (isPlainRecord(question) && isLegacyDeepInterviewPlaceholder(question.question)) {
500
+ return {
501
+ outcome: "reject",
502
+ code: "ask-deep-interview-question-body-required",
503
+ detail: { rejectedKeys: [`questions[${index}].question`], hint: WORKFLOW_PLACEHOLDER_CORRECTION },
504
+ };
505
+ }
506
+ }
507
+ return undefined;
508
+ }
509
+
462
510
  export function recoverRoundZeroIntentContract(
463
511
  arguments_: Record<string, unknown>,
464
512
  stage?: "topology" | "post-topology",
465
513
  ): RawArgumentValidationResult {
514
+ const normalizedInput = normalizeRoundZeroOptionalNulls(arguments_);
515
+ const placeholderRejection = deepInterviewPlaceholderRejection(normalizedInput);
516
+ if (placeholderRejection) return placeholderRejection;
466
517
  // #4649: an incomplete Round-0 topology object (deepInterview present,
467
518
  // required fields omitted) is NOT a retired-pair recovery candidate, so it
468
519
  // would passthrough into generic Zod validation whose error names no contract
469
520
  // and no correction — the repeat-invalid-bisect loop. Non-candidates get the
470
521
  // targeted correction here; candidates get it only after the stricter known
471
522
  // intent rejections below, so every previously-covered verdict is unchanged.
472
- if (!isRoundZeroRecoveryCandidate(arguments_))
473
- return roundZeroMetadataRejection(arguments_, stage) ?? { outcome: "passthrough" };
474
- const normalizedArguments = normalizeRoundZeroOptionalNulls(arguments_);
523
+ if (!isRoundZeroRecoveryCandidate(normalizedInput))
524
+ return roundZeroMetadataRejection(normalizedInput, stage) ?? { outcome: "passthrough" };
525
+ const normalizedArguments = normalizedInput;
475
526
  const knownRejection = knownIntentRejection(normalizedArguments);
476
527
  if (knownRejection) return knownRejection;
477
528
  const missingTopologyFields = roundZeroMetadataRejection(normalizedArguments, stage);
package/src/tools/ask.ts CHANGED
@@ -114,11 +114,17 @@ const HEADLESS_CHECKBOX_CHECKED = "[x]";
114
114
  const HEADLESS_CHECKBOX_UNCHECKED = "[ ]";
115
115
  const DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS = Number.MAX_SAFE_INTEGER;
116
116
  const DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS = 250;
117
+ export const MAX_EMPTY_CUSTOM_ATTEMPTS = 3;
118
+ const EMPTY_CUSTOM_RETRY_MESSAGE = "Custom input cannot be empty. Enter text or cancel the ask.";
117
119
 
118
120
  function errorMessage(error: unknown): string {
119
121
  return error instanceof Error ? error.message : String(error);
120
122
  }
121
123
 
124
+ function nonEmptyCustomInput(value: string | undefined): string | undefined {
125
+ return value !== undefined && value.trim().length > 0 ? value : undefined;
126
+ }
127
+
122
128
  function isAskTimeoutError(error: unknown): boolean {
123
129
  return typeof error === "object" && error !== null && (error as { code?: unknown }).code === GJC_ASK_TIMEOUT_CODE;
124
130
  }
@@ -324,7 +330,7 @@ interface UIContext {
324
330
  onLeft?: () => void;
325
331
  onRight?: () => void;
326
332
  helpText?: string;
327
- customInput?: { optionLabel: string; onSubmit: (text: string) => void };
333
+ customInput?: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean };
328
334
  clarificationInput?: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean };
329
335
  },
330
336
  ): Promise<string | undefined>;
@@ -400,6 +406,7 @@ async function askSingleQuestion(
400
406
  helpText,
401
407
  customInput: {
402
408
  optionLabel: otherOptionLabel,
409
+ allowEmpty: false,
403
410
  onSubmit: (text: string) => {
404
411
  inlineInput = text;
405
412
  },
@@ -541,7 +548,9 @@ async function askSingleQuestion(
541
548
  timedOut = true;
542
549
  break;
543
550
  }
544
- const input = inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input;
551
+ const input = nonEmptyCustomInput(
552
+ inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input,
553
+ );
545
554
  if (input === undefined) {
546
555
  break;
547
556
  }
@@ -642,7 +651,9 @@ async function askSingleQuestion(
642
651
  }
643
652
  } else if (choice === otherOptionLabel) {
644
653
  if (!selectTimedOut) {
645
- const input = inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input;
654
+ const input = nonEmptyCustomInput(
655
+ inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input,
656
+ );
646
657
  if (input !== undefined) {
647
658
  customInput = input;
648
659
  selectedOptions = [];
@@ -1100,10 +1111,15 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1100
1111
 
1101
1112
  const askQuestion = async (
1102
1113
  q: AskParams["questions"][number],
1103
- options?: { previous?: QuestionResult; navigation?: NavigationControls },
1114
+ options?: {
1115
+ previous?: QuestionResult;
1116
+ navigation?: NavigationControls;
1117
+ emptyCustomAttempt?: number;
1118
+ },
1104
1119
  ) => {
1105
1120
  const rawOptionLabels = q.options.map(o => o.label);
1106
1121
  const questionIndex = params.questions.indexOf(q);
1122
+ const emptyCustomAttempt = options?.emptyCustomAttempt ?? 0;
1107
1123
  // Route headless asks through the SDK workflow-gate emitter; a connected
1108
1124
  // SDK responder supplies the durable answer instead of an interactive UI.
1109
1125
  if (gateEmitter && canUseWorkflowGate) {
@@ -1133,7 +1149,11 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1133
1149
  try {
1134
1150
  const deepInterviewPrompt = formatDeepInterviewSelectorPrompt(q.question);
1135
1151
  const isDeepInterviewQuestion = deepInterviewPrompt !== null || q.deepInterview !== undefined;
1136
- const displayQuestion = deepInterviewPrompt ?? q.question;
1152
+ const baseDisplayQuestion = deepInterviewPrompt ?? q.question;
1153
+ const displayQuestion =
1154
+ emptyCustomAttempt > 0
1155
+ ? `${baseDisplayQuestion}\n\n${EMPTY_CUSTOM_RETRY_MESSAGE} (attempt ${emptyCustomAttempt + 1} of ${MAX_EMPTY_CUSTOM_ATTEMPTS})`
1156
+ : baseDisplayQuestion;
1137
1157
  const shouldNumberOptions = isDeepInterviewQuestion || isDeepInterviewAskQuestion(q.question);
1138
1158
  const optionLabels = shouldNumberOptions ? numberOptionLabels(rawOptionLabels) : rawOptionLabels;
1139
1159
  const clarificationOptionLabel = shouldNumberOptions
@@ -1256,7 +1276,27 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1256
1276
  : { kind: "resolve_without_commit", reason: "cancelled" };
1257
1277
  await settleActiveRemote(settlement);
1258
1278
  activeRemoteRequest = undefined;
1259
- if (settlement.kind === "invalid") return askQuestion(q, options);
1279
+ if (settlement.kind === "invalid") {
1280
+ if (emptyCustomAttempt + 1 >= MAX_EMPTY_CUSTOM_ATTEMPTS) {
1281
+ return {
1282
+ optionLabels: rawOptionLabels,
1283
+ selectedOptions: [] as string[],
1284
+ customInput: undefined,
1285
+ clarificationQuestion: undefined,
1286
+ navigation: undefined,
1287
+ cancelled: true,
1288
+ timedOut: false,
1289
+ };
1290
+ }
1291
+ // A remote source may resolve synchronously. Yield to a timer queue
1292
+ // between retries so repeated invalid answers cannot starve the
1293
+ // event loop indefinitely.
1294
+ await Bun.sleep(1);
1295
+ return askQuestion(q, {
1296
+ ...options,
1297
+ emptyCustomAttempt: emptyCustomAttempt + 1,
1298
+ });
1299
+ }
1260
1300
  }
1261
1301
  activeRemoteRequest = undefined;
1262
1302
  return {