@duckcodeailabs/dql-agent 1.14.2 → 1.14.3-rc.1

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 (145) hide show
  1. package/dist/agent-run-engine.d.ts +63 -2
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +864 -29
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-planner.d.ts +6 -0
  6. package/dist/agent-run-planner.d.ts.map +1 -1
  7. package/dist/agent-run-planner.js +2 -0
  8. package/dist/agent-run-planner.js.map +1 -1
  9. package/dist/agent-run-store.d.ts.map +1 -1
  10. package/dist/agent-run-store.js +48 -3
  11. package/dist/agent-run-store.js.map +1 -1
  12. package/dist/agentic/analyst-loop.d.ts +7 -1
  13. package/dist/agentic/analyst-loop.d.ts.map +1 -1
  14. package/dist/agentic/analyst-loop.js +163 -0
  15. package/dist/agentic/analyst-loop.js.map +1 -1
  16. package/dist/agentic/assumptions.d.ts +1 -1
  17. package/dist/agentic/assumptions.d.ts.map +1 -1
  18. package/dist/agentic/assumptions.js.map +1 -1
  19. package/dist/agentic/research-agent.d.ts.map +1 -1
  20. package/dist/agentic/research-agent.js +10 -1
  21. package/dist/agentic/research-agent.js.map +1 -1
  22. package/dist/agentic/sql-authorization.d.ts +8 -0
  23. package/dist/agentic/sql-authorization.d.ts.map +1 -1
  24. package/dist/agentic/sql-authorization.js +3 -0
  25. package/dist/agentic/sql-authorization.js.map +1 -1
  26. package/dist/analytical-compatibility.d.ts.map +1 -1
  27. package/dist/analytical-compatibility.js +8 -1
  28. package/dist/analytical-compatibility.js.map +1 -1
  29. package/dist/analytical-failure-repair.d.ts.map +1 -1
  30. package/dist/analytical-failure-repair.js +6 -2
  31. package/dist/analytical-failure-repair.js.map +1 -1
  32. package/dist/analytical-frame.d.ts +23 -1
  33. package/dist/analytical-frame.d.ts.map +1 -1
  34. package/dist/analytical-frame.js +322 -16
  35. package/dist/analytical-frame.js.map +1 -1
  36. package/dist/analytical-orchestration.d.ts +314 -3
  37. package/dist/analytical-orchestration.d.ts.map +1 -1
  38. package/dist/analytical-orchestration.js +486 -43
  39. package/dist/analytical-orchestration.js.map +1 -1
  40. package/dist/answer-loop.d.ts +64 -8
  41. package/dist/answer-loop.d.ts.map +1 -1
  42. package/dist/answer-loop.js +528 -67
  43. package/dist/answer-loop.js.map +1 -1
  44. package/dist/ask-observability/index.d.ts +11 -0
  45. package/dist/ask-observability/index.d.ts.map +1 -0
  46. package/dist/ask-observability/index.js +7 -0
  47. package/dist/ask-observability/index.js.map +1 -0
  48. package/dist/ask-observability/instrumentation.d.ts +35 -0
  49. package/dist/ask-observability/instrumentation.d.ts.map +1 -0
  50. package/dist/ask-observability/instrumentation.js +472 -0
  51. package/dist/ask-observability/instrumentation.js.map +1 -0
  52. package/dist/ask-observability/observer.d.ts +62 -0
  53. package/dist/ask-observability/observer.d.ts.map +1 -0
  54. package/dist/ask-observability/observer.js +537 -0
  55. package/dist/ask-observability/observer.js.map +1 -0
  56. package/dist/ask-observability/portable.d.ts +98 -0
  57. package/dist/ask-observability/portable.d.ts.map +1 -0
  58. package/dist/ask-observability/portable.js +527 -0
  59. package/dist/ask-observability/portable.js.map +1 -0
  60. package/dist/ask-observability/store.d.ts +130 -0
  61. package/dist/ask-observability/store.d.ts.map +1 -0
  62. package/dist/ask-observability/store.js +1114 -0
  63. package/dist/ask-observability/store.js.map +1 -0
  64. package/dist/ask-observability/types.d.ts +357 -0
  65. package/dist/ask-observability/types.d.ts.map +1 -0
  66. package/dist/ask-observability/types.js +2 -0
  67. package/dist/ask-observability/types.js.map +1 -0
  68. package/dist/ask-observability/utils.d.ts +21 -0
  69. package/dist/ask-observability/utils.d.ts.map +1 -0
  70. package/dist/ask-observability/utils.js +102 -0
  71. package/dist/ask-observability/utils.js.map +1 -0
  72. package/dist/generated-analytical-proposal.d.ts +37 -1
  73. package/dist/generated-analytical-proposal.d.ts.map +1 -1
  74. package/dist/generated-analytical-proposal.js +211 -3
  75. package/dist/generated-analytical-proposal.js.map +1 -1
  76. package/dist/governed-relational-compiler.d.ts +15 -0
  77. package/dist/governed-relational-compiler.d.ts.map +1 -1
  78. package/dist/governed-relational-compiler.js +99 -1
  79. package/dist/governed-relational-compiler.js.map +1 -1
  80. package/dist/index.d.ts +9 -7
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +6 -4
  83. package/dist/index.js.map +1 -1
  84. package/dist/intent-controller.d.ts +10 -0
  85. package/dist/intent-controller.d.ts.map +1 -1
  86. package/dist/intent-controller.js +1 -1
  87. package/dist/intent-controller.js.map +1 -1
  88. package/dist/meaning-resolution.d.ts +78 -6
  89. package/dist/meaning-resolution.d.ts.map +1 -1
  90. package/dist/meaning-resolution.js +331 -38
  91. package/dist/meaning-resolution.js.map +1 -1
  92. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  93. package/dist/metadata/analysis-planner.js +21 -1
  94. package/dist/metadata/analysis-planner.js.map +1 -1
  95. package/dist/metadata/catalog.d.ts.map +1 -1
  96. package/dist/metadata/catalog.js +1 -1
  97. package/dist/metadata/catalog.js.map +1 -1
  98. package/dist/metadata/meaning-evidence.d.ts +9 -2
  99. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  100. package/dist/metadata/meaning-evidence.js +261 -11
  101. package/dist/metadata/meaning-evidence.js.map +1 -1
  102. package/dist/plan-execution-adapter.d.ts +9 -1
  103. package/dist/plan-execution-adapter.d.ts.map +1 -1
  104. package/dist/plan-execution-adapter.js +93 -13
  105. package/dist/plan-execution-adapter.js.map +1 -1
  106. package/dist/provider-egress.d.ts +24 -10
  107. package/dist/provider-egress.d.ts.map +1 -1
  108. package/dist/provider-egress.js +22 -17
  109. package/dist/provider-egress.js.map +1 -1
  110. package/dist/providers/claude.d.ts.map +1 -1
  111. package/dist/providers/claude.js +10 -9
  112. package/dist/providers/claude.js.map +1 -1
  113. package/dist/providers/dispatch.d.ts +27 -1
  114. package/dist/providers/dispatch.d.ts.map +1 -1
  115. package/dist/providers/dispatch.js +63 -1
  116. package/dist/providers/dispatch.js.map +1 -1
  117. package/dist/providers/gemini.js +10 -10
  118. package/dist/providers/gemini.js.map +1 -1
  119. package/dist/providers/index.d.ts +2 -2
  120. package/dist/providers/index.d.ts.map +1 -1
  121. package/dist/providers/index.js +1 -1
  122. package/dist/providers/index.js.map +1 -1
  123. package/dist/providers/ollama.js +15 -15
  124. package/dist/providers/ollama.js.map +1 -1
  125. package/dist/providers/openai.js +22 -22
  126. package/dist/providers/openai.js.map +1 -1
  127. package/dist/providers/types.d.ts +52 -0
  128. package/dist/providers/types.d.ts.map +1 -1
  129. package/dist/providers/types.js.map +1 -1
  130. package/dist/relationship-proof.d.ts +81 -0
  131. package/dist/relationship-proof.d.ts.map +1 -1
  132. package/dist/relationship-proof.js +228 -4
  133. package/dist/relationship-proof.js.map +1 -1
  134. package/dist/resolved-analytical-plan.d.ts +18 -4
  135. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  136. package/dist/resolved-analytical-plan.js +764 -47
  137. package/dist/resolved-analytical-plan.js.map +1 -1
  138. package/dist/router.d.ts +10 -1
  139. package/dist/router.d.ts.map +1 -1
  140. package/dist/router.js +1785 -213
  141. package/dist/router.js.map +1 -1
  142. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  143. package/dist/semantic-bridge/compose.js +23 -4
  144. package/dist/semantic-bridge/compose.js.map +1 -1
  145. package/package.json +4 -4
@@ -9,6 +9,8 @@ import { buildAnalysisQuestionPlan } from "./metadata/analysis-planner.js";
9
9
  import { conversationHistoryFromContext, isLikelyClarificationReply, } from "./conversation/snapshot.js";
10
10
  import { buildCoverageGap, classifyProviderFailure, } from './analytical-orchestration.js';
11
11
  import { evaluateAnalyticalRequestPolicy } from './analytical-request-policy.js';
12
+ import { frozenRequiredOutputBindingProofsForPlan } from './generated-analytical-proposal.js';
13
+ import { attachAskTraceObserverV1, askTraceObserverForV1, finalizeAgentRunTraceV1, noOpAskTraceObserverV1, recordAuthoritativeRouterDecisionV1, recordAuthoritativePlanFreezeV1, recordEngineTraceEventV1, recordExecutionAttemptSummaryV1, } from './ask-observability/index.js';
12
14
  /** Routes a stakeholder may never land on (analyst authoring lives in the Notebook). */
13
15
  const ANALYST_ONLY_ROUTES = new Set(["sql_cell", "dql_block_draft", "modeling_draft", "skill_draft"]);
14
16
  /**
@@ -199,21 +201,30 @@ export function resolveClarificationContinuation(request) {
199
201
  if (!reply || (!structuredSelection && !isLikelyClarificationReply(reply)))
200
202
  return undefined;
201
203
  const fromServer = latestClarificationFromConversationContext(request.conversationContext);
204
+ const serverIssuedStructuredSelection = structuredSelection
205
+ ? serverIssuedStructuredClarification(request)
206
+ : undefined;
202
207
  const fromHistory = latestClarificationFromHistory(request.history);
203
208
  // A UI selection is bound to the exact run that rendered the options. Carry
204
209
  // that run's source question explicitly so the continuation still works when
205
210
  // the optional conversation store is unavailable, after a reload, or when
206
211
  // the user selects an option on an older visible answer. Server history still
207
212
  // supplies the original clarifying prose when it is available.
208
- const pending = explicitSourceQuestion
209
- ? {
210
- sourceQuestion: explicitSourceQuestion,
211
- clarifyingQuestion: fromServer?.clarifyingQuestion
212
- ?? fromHistory?.clarifyingQuestion
213
- ?? 'Which governed meaning should be used?',
214
- }
215
- : fromServer ?? fromHistory;
216
- if (!pending || pending.sourceQuestion.trim().toLowerCase() === reply.toLowerCase())
213
+ const pending = serverIssuedStructuredSelection
214
+ ?? (explicitSourceQuestion
215
+ ? {
216
+ sourceQuestion: explicitSourceQuestion,
217
+ clarifyingQuestion: fromServer?.clarifyingQuestion
218
+ ?? fromHistory?.clarifyingQuestion
219
+ ?? 'Which governed meaning should be used?',
220
+ }
221
+ : fromServer ?? fromHistory);
222
+ // A structured option intentionally submits the original question together
223
+ // with a stable, server-issued identifier. Treating that exact text as a
224
+ // fresh question drops the persisted typed frame and makes the router ask
225
+ // the same clarification again. The equality guard remains important for
226
+ // free-text replies, where an unchanged question carries no new meaning.
227
+ if (!pending || (!structuredSelection && pending.sourceQuestion.trim().toLowerCase() === reply.toLowerCase()))
217
228
  return undefined;
218
229
  return {
219
230
  ...pending,
@@ -226,6 +237,44 @@ export function resolveClarificationContinuation(request) {
226
237
  ].join('\n\n'),
227
238
  };
228
239
  }
240
+ /**
241
+ * Use the original server snapshot for an identifier-bound continuation when
242
+ * it carries the host-only authority record that local runtime reconstructs
243
+ * from its persisted thread. This prevents a browser-provided label (or a
244
+ * stale client source question) from replacing the typed analytical frame
245
+ * before router validation. The router still performs the final snapshot and
246
+ * option-ID validation before any plan can freeze.
247
+ */
248
+ function serverIssuedStructuredClarification(request) {
249
+ if (!request.threadId)
250
+ return undefined;
251
+ const context = clarificationRecord(request.conversationContext);
252
+ const authority = clarificationRecord(context?.serverIssuedClarificationSelection);
253
+ if (authority?.version !== 1
254
+ || clarificationString(authority.threadId) !== request.threadId)
255
+ return undefined;
256
+ const authoritySourceTurnId = clarificationString(authority.sourceTurnId);
257
+ const authoritySnapshotId = clarificationString(authority.snapshotId);
258
+ if (!authoritySourceTurnId || !authoritySnapshotId)
259
+ return undefined;
260
+ for (const source of [
261
+ clarificationRecord(context?.conversationEnvelope),
262
+ clarificationRecord(context?.serverSnapshot),
263
+ ]) {
264
+ if (clarificationString(source?.threadId) !== request.threadId)
265
+ continue;
266
+ const pending = clarificationRecord(source?.pendingClarification);
267
+ const selection = clarificationRecord(pending?.selection);
268
+ if (clarificationString(pending?.sourceTurnId) !== authoritySourceTurnId
269
+ || clarificationString(selection?.snapshotId) !== authoritySnapshotId)
270
+ continue;
271
+ const sourceQuestion = clarificationString(pending?.sourceQuestion);
272
+ const clarifyingQuestion = clarificationString(pending?.question);
273
+ if (sourceQuestion && clarifyingQuestion)
274
+ return { sourceQuestion, clarifyingQuestion };
275
+ }
276
+ return undefined;
277
+ }
229
278
  function latestClarificationFromHistory(history) {
230
279
  if (!history?.length)
231
280
  return undefined;
@@ -467,6 +516,7 @@ export class AgentRunEngine {
467
516
  budgetModel;
468
517
  maxSteps;
469
518
  routeTimeoutSignal;
519
+ traceObserverFactory;
470
520
  constructor(options = {}) {
471
521
  this.executors = options.executors ?? {};
472
522
  this.gates = options.gates ?? {};
@@ -485,6 +535,7 @@ export class AgentRunEngine {
485
535
  };
486
536
  this.maxSteps = Math.max(1, options.maxSteps ?? DEFAULT_MAX_STEPS);
487
537
  this.routeTimeoutSignal = options.routeTimeoutSignal ?? ((durationMs) => AbortSignal.timeout(durationMs));
538
+ this.traceObserverFactory = options.traceObserverFactory;
488
539
  }
489
540
  /**
490
541
  * Decide the high-level action. A forced `requestedMode` bypasses routing.
@@ -542,6 +593,59 @@ export class AgentRunEngine {
542
593
  const startedAt = this.timestamp();
543
594
  const runStartedAtMs = Date.parse(startedAt);
544
595
  const requestedMode = request.requestedMode ?? "auto";
596
+ // OBS-001/OBS-002: valid engine requests receive a server-owned trace after
597
+ // their run ID is known. The observer is explicitly non-authoritative and
598
+ // a factory/store failure becomes a no-op, never an Ask failure.
599
+ let traceObserver = noOpAskTraceObserverV1;
600
+ try {
601
+ traceObserver = this.traceObserverFactory?.({ runId, request, startedAt, requestedMode })
602
+ ?? noOpAskTraceObserverV1;
603
+ }
604
+ catch {
605
+ traceObserver = noOpAskTraceObserverV1;
606
+ }
607
+ request = attachAskTraceObserverV1({ ...request }, traceObserver);
608
+ // Continuity is relationship evidence, not new routing input. Keep only
609
+ // stable run IDs and one-way fingerprints so a trace can explain why this
610
+ // turn reused a clarification/result/derived plan without persisting chat
611
+ // text, values, SQL, or an invented parent trace.
612
+ if (clarificationContinuation) {
613
+ traceObserver.recordLink({
614
+ kind: 'clarification_continuation',
615
+ choiceFingerprint: traceLinkFingerprint(request.selectedEvidenceId ?? clarificationContinuation.sourceQuestion),
616
+ });
617
+ }
618
+ if (request.selectedResultBinding) {
619
+ traceObserver.recordLink({
620
+ kind: 'prior_result',
621
+ targetRunId: request.selectedResultBinding.sourceRunId,
622
+ choiceFingerprint: traceLinkFingerprint([
623
+ request.selectedResultBinding.sourceArtifactId,
624
+ request.selectedResultBinding.canonicalColumn,
625
+ request.selectedResultBinding.rowFingerprint,
626
+ request.selectedResultBinding.resultFingerprint,
627
+ ].join('\u0000')),
628
+ });
629
+ }
630
+ const derivedSourceRunId = traceDerivedSourceRunId(request.workspaceContext);
631
+ if (derivedSourceRunId) {
632
+ traceObserver.recordLink({
633
+ kind: 'derived_repair',
634
+ targetRunId: derivedSourceRunId,
635
+ choiceFingerprint: traceLinkFingerprint(derivedSourceRunId),
636
+ });
637
+ }
638
+ const conversationBinding = traceConversationBinding(request, clarificationContinuation);
639
+ const conversationTrace = traceObserver.startSpan({
640
+ name: 'conversation.hydrate',
641
+ stage: 'conversation',
642
+ payload: {
643
+ kind: 'conversation',
644
+ continuation: conversationBinding !== 'none',
645
+ binding: conversationBinding,
646
+ },
647
+ });
648
+ traceObserver.finishSpan(conversationTrace, { outcome: 'ok', reasonCode: 'completed' });
545
649
  const runBudget = request.runBudget ?? createAgentRunBudget({
546
650
  requestedMode,
547
651
  startedAtMs: runStartedAtMs,
@@ -549,7 +653,11 @@ export class AgentRunEngine {
549
653
  timeoutSignal: this.routeTimeoutSignal,
550
654
  nowMs: () => this.now().getTime(),
551
655
  });
552
- request = { ...request, runBudget, signal: runBudget.hardSignal };
656
+ // Preserve the non-enumerable observer across the immutable request update.
657
+ // A plain spread drops symbol properties, which previously made the router
658
+ // lose candidate/cascade/freeze evidence even though the engine still
659
+ // emitted its own outer spans.
660
+ request = attachAskTraceObserverV1({ ...request, runBudget, signal: runBudget.hardSignal }, traceObserver);
553
661
  const events = [];
554
662
  let plan;
555
663
  const executedSteps = [];
@@ -573,6 +681,7 @@ export class AgentRunEngine {
573
681
  startedAt,
574
682
  updatedAt: startedAt,
575
683
  },
684
+ ...(traceObserver.reference() ? { traceReference: traceObserver.reference() } : {}),
576
685
  };
577
686
  let checkpointQueue = Promise.resolve();
578
687
  const persistProgress = () => {
@@ -632,6 +741,12 @@ export class AgentRunEngine {
632
741
  }
633
742
  if (full.type === "step.completed")
634
743
  progress.steps = [...executedSteps];
744
+ // Event payloads are intentionally not copied: they may contain raw
745
+ // answer/tool data. The typed mapping records only stage identity.
746
+ recordEngineTraceEventV1(traceObserver, full);
747
+ const traceReference = traceObserver.reference();
748
+ if (traceReference)
749
+ progress.traceReference = traceReference;
635
750
  persistProgress();
636
751
  onEvent?.(full);
637
752
  };
@@ -728,6 +843,7 @@ export class AgentRunEngine {
728
843
  id: runId,
729
844
  question: submittedQuestion,
730
845
  requestedMode,
846
+ conversationBinding: request.conversationBinding ?? traceConversationBinding(request, clarificationContinuation),
731
847
  route: 'blocked',
732
848
  status: 'blocked',
733
849
  trustState: 'blocked',
@@ -754,7 +870,12 @@ export class AgentRunEngine {
754
870
  run.diagnosticReceipt = diagnosticReceiptForRun(run);
755
871
  run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
756
872
  run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
757
- run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3);
873
+ run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
874
+ run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4);
875
+ // Observability is deliberately finalized only after the authoritative
876
+ // receipt exists, and before the ordinary run store persists its compact
877
+ // reference. A local trace write failure never changes this outcome.
878
+ finalizeAgentRunTraceV1(traceObserver, run);
758
879
  await checkpointQueue;
759
880
  await this.store?.save(run);
760
881
  return run;
@@ -765,6 +886,11 @@ export class AgentRunEngine {
765
886
  // pre-try await escaped the engine and left active UI runs looking endless.
766
887
  let routeDecision = buildIntentDecision(request);
767
888
  try {
889
+ const classifySpan = traceObserver.startSpan({
890
+ name: 'request.classify',
891
+ stage: 'request',
892
+ payload: { kind: 'stage', requestedMode },
893
+ });
768
894
  routeDecision = clarificationContinuation && !request.selectedEvidenceId
769
895
  ? {
770
896
  action: "answer",
@@ -775,6 +901,11 @@ export class AgentRunEngine {
775
901
  }
776
902
  : await awaitWithAbort(this.decideRoute(request), request.signal);
777
903
  routeDecision = enforceOrdinaryAnalyticalPlanBoundary(request, routeDecision);
904
+ traceObserver.finishSpan(classifySpan, { outcome: 'ok', reasonCode: 'route_selected' });
905
+ // Router/cascade evidence is captured after its authoritative decision
906
+ // is sealed. The trace adapter only projects IDs, counters, and typed
907
+ // receipts; it never participates in route selection.
908
+ recordAuthoritativeRouterDecisionV1(traceObserver, routeDecision);
778
909
  const defaultRoute = answerAnywayRoute(constrainRouteForAudience(selectRoute(request, routeDecision), audience), request, audience, routeDecision);
779
910
  const authoritativeAsk = routeDecision.resolvedAnalyticalPlan?.mode === 'authoritative'
780
911
  && requestedMode !== 'research';
@@ -859,6 +990,10 @@ export class AgentRunEngine {
859
990
  : `Re-running ${route.replaceAll("_", " ")} executor (repair attempt ${attempt}).`,
860
991
  route,
861
992
  });
993
+ // Provider readiness belongs at the provider boundary. The engine
994
+ // cannot infer it from an executor return value: a deterministic
995
+ // route may be provider-free and a provider route can fail during
996
+ // preflight before any executor result exists.
862
997
  result = await this.executeRoute({
863
998
  runId,
864
999
  request,
@@ -873,12 +1008,21 @@ export class AgentRunEngine {
873
1008
  emit,
874
1009
  emitAnswerDelta: onAnswerDelta,
875
1010
  });
1011
+ recordExecutionAttemptSummaryV1(traceObserver, result);
876
1012
  // The router owns a frozen analytical tier. An executor may report a
877
1013
  // same-tier execution failure, but it cannot turn a certified or
878
1014
  // semantic plan into generated work (or vice versa) after execution
879
1015
  // has started. Keep this guard in the engine as well as host adapters
880
1016
  // so an injected/legacy executor cannot redefine durable provenance.
1017
+ const planWasFrozen = routeDecision.analyticalCascadeDecision?.planFrozen === true;
881
1018
  routeDecision = applyExploratoryExecutionFreeze(routeDecision, result.analyticalExecutionFreeze);
1019
+ routeDecision = applyExploratoryExecutionFreeze(routeDecision, result.analyticalExecutionRepairFreeze);
1020
+ // The router froze the exploratory plan before SQL generation. The
1021
+ // host receipt below only authorizes this exact SQL/target against
1022
+ // that immutable plan; it never creates a second freeze transition.
1023
+ if (!planWasFrozen && routeDecision.analyticalCascadeDecision?.planFrozen) {
1024
+ recordAuthoritativePlanFreezeV1(traceObserver, routeDecision.analyticalCascadeDecision);
1025
+ }
882
1026
  result = preserveFrozenAnalyticalRoute(route, routeDecision, result);
883
1027
  result = consumeRepeatedClarificationSelection(request, routeDecision, result);
884
1028
  if (result.analyticalTurnPlan)
@@ -1114,7 +1258,9 @@ export class AgentRunEngine {
1114
1258
  run.diagnosticReceipt = diagnosticReceiptForRun(run);
1115
1259
  run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
1116
1260
  run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
1117
- run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3);
1261
+ run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
1262
+ run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4);
1263
+ finalizeAgentRunTraceV1(traceObserver, run);
1118
1264
  await checkpointQueue;
1119
1265
  await this.store?.save(run);
1120
1266
  return run;
@@ -1166,6 +1312,7 @@ export class AgentRunEngine {
1166
1312
  id: runId,
1167
1313
  question: submittedQuestion,
1168
1314
  requestedMode,
1315
+ conversationBinding: request.conversationBinding ?? traceConversationBinding(request, clarificationContinuation),
1169
1316
  route: cancelledRoute,
1170
1317
  status: "cancelled",
1171
1318
  trustState: "not_applicable",
@@ -1201,6 +1348,8 @@ export class AgentRunEngine {
1201
1348
  };
1202
1349
  run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
1203
1350
  run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
1351
+ run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
1352
+ finalizeAgentRunTraceV1(traceObserver, run);
1204
1353
  await checkpointQueue;
1205
1354
  await this.store?.save(run);
1206
1355
  return run;
@@ -1208,7 +1357,9 @@ export class AgentRunEngine {
1208
1357
  const message = isOrchestrationBudgetExhausted(err)
1209
1358
  ? 'Ask could not complete within its bounded orchestration. Nothing was executed; narrow the metric or dimension and retry.'
1210
1359
  : err instanceof Error && err.name === "TimeoutError"
1211
- ? "This analytical run reached its time limit before it finished. A timeout alone does not prove a cross-model join or semantic-modeling problem. Open Trust & Steps to see the last recorded phase; retry the same bounded question or use Research for a longer budget. No result was accepted."
1360
+ ? requestedMode === 'research'
1361
+ ? 'This Research run reached its bounded deadline before finalization. Review the recorded branch receipts and trace, then narrow the investigation and retry. No result was accepted.'
1362
+ : "This analytical run reached its time limit before it finished. A timeout alone does not prove a cross-model join or semantic-modeling problem. Open Trust & Steps to see the last recorded phase; retry the same bounded question or use Research for a longer budget. No result was accepted."
1212
1363
  : err instanceof Error ? err.message : String(err);
1213
1364
  const failedRoute = progress.route;
1214
1365
  const failedPhase = progress.lifecycle.phase;
@@ -1220,7 +1371,7 @@ export class AgentRunEngine {
1220
1371
  trustState: "blocked",
1221
1372
  });
1222
1373
  const completedAt = this.timestamp();
1223
- const failure = diagnosticFailureFromError(err, failedPhase);
1374
+ const failure = diagnosticFailureFromError(err, failedPhase, requestedMode);
1224
1375
  const evaluations = [
1225
1376
  ...progress.evaluations,
1226
1377
  {
@@ -1248,6 +1399,7 @@ export class AgentRunEngine {
1248
1399
  id: runId,
1249
1400
  question: submittedQuestion,
1250
1401
  requestedMode,
1402
+ conversationBinding: request.conversationBinding ?? traceConversationBinding(request, clarificationContinuation),
1251
1403
  route: "blocked",
1252
1404
  status: "blocked",
1253
1405
  trustState: "blocked",
@@ -1285,7 +1437,9 @@ export class AgentRunEngine {
1285
1437
  };
1286
1438
  run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
1287
1439
  run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
1288
- run.artifacts = attachDiagnosticReceipt(retainedArtifacts, receipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3);
1440
+ run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
1441
+ run.artifacts = attachDiagnosticReceipt(retainedArtifacts, receipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4);
1442
+ finalizeAgentRunTraceV1(traceObserver, run);
1289
1443
  await checkpointQueue;
1290
1444
  await this.store?.save(run);
1291
1445
  return run;
@@ -1302,6 +1456,7 @@ export class AgentRunEngine {
1302
1456
  id: input.runId,
1303
1457
  question: input.request.question,
1304
1458
  requestedMode: input.requestedMode,
1459
+ conversationBinding: input.request.conversationBinding ?? traceConversationBinding(input.request, undefined),
1305
1460
  route: "blocked",
1306
1461
  status: "blocked",
1307
1462
  trustState: "blocked",
@@ -1348,6 +1503,7 @@ export class AgentRunEngine {
1348
1503
  id: input.runId,
1349
1504
  question: input.request.question,
1350
1505
  requestedMode: input.requestedMode,
1506
+ conversationBinding: input.request.conversationBinding ?? traceConversationBinding(input.request, undefined),
1351
1507
  route,
1352
1508
  status: finalOutcome.status,
1353
1509
  trustState: finalOutcome.trustState,
@@ -1411,9 +1567,13 @@ export class AgentRunEngine {
1411
1567
  const signal = context.request.runBudget?.hardSignal ?? context.request.signal;
1412
1568
  if (signal?.aborted)
1413
1569
  throw signal.reason ?? routeTimeoutError();
1570
+ // Route executors own physical provider/tool/SQL boundaries. Preserve the
1571
+ // non-enumerable observer when adding the run signal; a normal object
1572
+ // spread would otherwise leave canonical routing evidence intact while
1573
+ // silently dropping every physical execution span.
1414
1574
  const execution = Promise.resolve(executor({
1415
1575
  ...context,
1416
- request: { ...context.request, ...(signal ? { signal } : {}) },
1576
+ request: attachAskTraceObserverV1({ ...context.request, ...(signal ? { signal } : {}) }, askTraceObserverForV1(context.request)),
1417
1577
  }));
1418
1578
  return awaitWithAbort(execution, signal);
1419
1579
  }
@@ -1431,6 +1591,44 @@ export class AgentRunEngine {
1431
1591
  return this.now().toISOString();
1432
1592
  }
1433
1593
  }
1594
+ function traceLinkFingerprint(value) {
1595
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
1596
+ }
1597
+ /** Only a host-produced repair derivation is linkable; ordinary authoring context is not. */
1598
+ function traceDerivedSourceRunId(workspaceContext) {
1599
+ if (!workspaceContext || typeof workspaceContext !== 'object' || Array.isArray(workspaceContext))
1600
+ return undefined;
1601
+ const record = workspaceContext;
1602
+ const derivation = record.traceDerivation;
1603
+ if (derivation !== 'analytical_repair' && derivation !== 'derived_repair')
1604
+ return undefined;
1605
+ return typeof record.sourceRunId === 'string' && record.sourceRunId.trim()
1606
+ ? record.sourceRunId
1607
+ : undefined;
1608
+ }
1609
+ /**
1610
+ * Record only why the conversation boundary was available, never the member,
1611
+ * question, row, or free-text selection that it carried. The local runtime
1612
+ * resolves its typed follow-up before retrieval; this trace label lets an
1613
+ * office reproduction distinguish a missing binding from a later retrieval or
1614
+ * execution failure without turning traces into chat persistence.
1615
+ */
1616
+ function traceConversationBinding(request, clarification) {
1617
+ if (clarification || request.selectedEvidenceId)
1618
+ return 'structured_clarification';
1619
+ if (request.conversationBinding)
1620
+ return request.conversationBinding;
1621
+ if (request.selectedResultBinding)
1622
+ return 'prior_result';
1623
+ const context = request.conversationContext;
1624
+ if (!context || Object.keys(context).length === 0)
1625
+ return 'none';
1626
+ if ('analyticalTaskDependencyBinding' in context)
1627
+ return 'task_dependency';
1628
+ // A snapshot merely makes a prior binding *available*. It is not evidence
1629
+ // that this self-contained question selected it.
1630
+ return 'none';
1631
+ }
1434
1632
  /**
1435
1633
  * Ordinary analytical Ask cannot delegate meaning to the legacy answer
1436
1634
  * generator. Retrieval/meaning must first produce the immutable RAP consumed by
@@ -1595,14 +1793,19 @@ function applyExploratoryExecutionFreeze(decision, freeze) {
1595
1793
  return decision;
1596
1794
  const cascade = decision.analyticalCascadeDecision;
1597
1795
  const attempt = cascade?.attempts.find((candidate) => candidate.tier === 'exploratory_sql');
1796
+ const selectedPlan = decision.resolvedAnalyticalPlan;
1797
+ const existing = cascade?.exploratoryExecutionFreeze;
1798
+ const existingRepair = cascade?.exploratoryRepairExecutionFreeze;
1799
+ const authorizationAttempt = normalizedExploratoryAuthorizationAttempt(freeze);
1598
1800
  const sameCandidates = Boolean(attempt
1599
1801
  && attempt.candidateIds.length === freeze.candidateIds.length
1600
1802
  && attempt.candidateIds.every((candidate, index) => candidate === freeze.candidateIds[index]));
1601
1803
  const retrievalSnapshotId = decision.retrievalEvidence?.snapshotId;
1602
- const valid = Boolean(cascade
1804
+ const validBaseReceipt = Boolean(cascade
1603
1805
  && cascade.selectedTier === 'exploratory_sql'
1604
- && cascade.planFrozen === false
1806
+ && cascade.planFrozen === true
1605
1807
  && attempt?.outcome === 'executable'
1808
+ && attempt.planFrozen === true
1606
1809
  && sameCandidates
1607
1810
  && freeze.version === 1
1608
1811
  && freeze.selectedTier === 'exploratory_sql'
@@ -1612,28 +1815,132 @@ function applyExploratoryExecutionFreeze(decision, freeze) {
1612
1815
  && freeze.snapshotId.trim()
1613
1816
  && freeze.targetFingerprint.trim()
1614
1817
  && freeze.sqlFingerprint.trim()
1818
+ && selectedPlan?.capability === 'bounded_exploration'
1819
+ && selectedPlan.planId === freeze.planId
1820
+ && selectedPlan.fingerprint === freeze.planFingerprint
1821
+ && selectedPlan.snapshotId === freeze.snapshotId
1822
+ && freezeCarriesRequiredOutputBindings(selectedPlan, freeze)
1615
1823
  && (!retrievalSnapshotId || retrievalSnapshotId === freeze.snapshotId));
1616
- if (!valid) {
1617
- throw Object.assign(new Error('The exploratory execution receipt did not match the router-selected candidate set and was not accepted.'), {
1618
- code: 'EXPLORATORY_FREEZE_RECEIPT_MISMATCH',
1619
- });
1824
+ if (!validBaseReceipt) {
1825
+ throw exploratoryAuthorizationStateMismatch();
1826
+ }
1827
+ // A replay of one exact host handoff is harmless. A repair is a fresh,
1828
+ // separately-minted capability, but its receipt must name the initial SQL
1829
+ // authorization and keep every immutable plan binding identical.
1830
+ if (authorizationAttempt.index === 0) {
1831
+ if (existing) {
1832
+ if (sameExploratoryAuthorizationReceipt(existing, freeze))
1833
+ return decision;
1834
+ throw exploratoryAuthorizationStateMismatch();
1835
+ }
1836
+ if (existingRepair)
1837
+ throw exploratoryAuthorizationStateMismatch();
1838
+ return withExploratoryAuthorizationReceipt(decision, freeze, 'initial');
1620
1839
  }
1840
+ if (authorizationAttempt.index !== 1
1841
+ || !authorizationAttempt.parentSqlFingerprint
1842
+ || !existing
1843
+ || existingRepair
1844
+ || authorizationAttempt.parentSqlFingerprint !== existing.sqlFingerprint
1845
+ || !sameExploratoryPlanBindings(existing, freeze)) {
1846
+ throw exploratoryAuthorizationStateMismatch();
1847
+ }
1848
+ return withExploratoryAuthorizationReceipt(decision, freeze, 'repair');
1849
+ }
1850
+ function withExploratoryAuthorizationReceipt(decision, freeze, kind) {
1851
+ const cascade = decision.analyticalCascadeDecision;
1621
1852
  return {
1622
1853
  ...decision,
1623
1854
  analyticalCascadeDecision: {
1624
1855
  ...cascade,
1625
- planFrozen: true,
1626
- exploratoryExecutionFreeze: freeze,
1856
+ ...(kind === 'initial'
1857
+ ? { exploratoryExecutionFreeze: freeze }
1858
+ : { exploratoryRepairExecutionFreeze: freeze }),
1627
1859
  attempts: cascade.attempts.map((candidate) => candidate.tier === 'exploratory_sql'
1628
1860
  ? {
1629
1861
  ...candidate,
1630
- planFrozen: true,
1631
- reason: `${candidate.reason} Host-authorized immutable exploratory execution plan ${freeze.planId}.`,
1862
+ // The router froze the plan before SQL generation. The host only
1863
+ // binds exact SQL/target bytes to that immutable plan. A repair
1864
+ // cannot choose another tier or mutate the analytical frame.
1865
+ reason: kind === 'repair'
1866
+ ? `${candidate.reason} Host authorized one same-plan SQL repair against frozen plan ${freeze.planId}.`
1867
+ : `${candidate.reason} Host authorized SQL execution against frozen plan ${freeze.planId}.`,
1632
1868
  }
1633
1869
  : candidate),
1634
1870
  },
1635
1871
  };
1636
1872
  }
1873
+ function normalizedExploratoryAuthorizationAttempt(freeze) {
1874
+ const attempt = freeze.authorizationAttempt;
1875
+ // V1/V3 persisted receipts predate explicit authorization-attempt evidence.
1876
+ // They are compatible only as the original handoff, never as a repair.
1877
+ if (!attempt)
1878
+ return { index: 0 };
1879
+ if (attempt.version !== 1 || (attempt.index !== 0 && attempt.index !== 1)) {
1880
+ throw exploratoryAuthorizationStateMismatch();
1881
+ }
1882
+ if (attempt.index === 0) {
1883
+ if ('parentSqlFingerprint' in attempt && attempt.parentSqlFingerprint) {
1884
+ throw exploratoryAuthorizationStateMismatch();
1885
+ }
1886
+ return { index: 0 };
1887
+ }
1888
+ if (!attempt.parentSqlFingerprint?.trim())
1889
+ throw exploratoryAuthorizationStateMismatch();
1890
+ return { index: 1, parentSqlFingerprint: attempt.parentSqlFingerprint };
1891
+ }
1892
+ function sameExploratoryPlanBindings(left, right) {
1893
+ return left.version === right.version
1894
+ && left.selectedTier === right.selectedTier
1895
+ && left.planId === right.planId
1896
+ && left.planFingerprint === right.planFingerprint
1897
+ && left.snapshotId === right.snapshotId
1898
+ && left.targetFingerprint === right.targetFingerprint
1899
+ && left.authorization === right.authorization
1900
+ && sameFrozenRequiredOutputBindings(left.requiredOutputBindings, right.requiredOutputBindings)
1901
+ && left.candidateIds.length === right.candidateIds.length
1902
+ && left.candidateIds.every((candidate, index) => candidate === right.candidateIds[index]);
1903
+ }
1904
+ function freezeCarriesRequiredOutputBindings(plan, freeze) {
1905
+ if (!plan)
1906
+ return false;
1907
+ // Pre-V4 persisted plans did not carry an output contract. They remain
1908
+ // readable, but newly frozen plans with explicit outputs must carry the
1909
+ // exact physical binding proofs below.
1910
+ const required = plan.outputContract?.requiredOutputs ?? [];
1911
+ if (required.length === 0)
1912
+ return true;
1913
+ const expected = frozenRequiredOutputBindingProofsForPlan(plan);
1914
+ const actual = freeze.requiredOutputBindings;
1915
+ return expected.length === required.length
1916
+ && Array.isArray(actual)
1917
+ && sameFrozenRequiredOutputBindings(actual, expected);
1918
+ }
1919
+ function sameFrozenRequiredOutputBindings(left, right) {
1920
+ const normalize = (bindings) => (bindings ?? []).map((binding) => [
1921
+ binding.version,
1922
+ binding.outputName.toLowerCase().replace(/["`\[\]]/g, ''),
1923
+ binding.qualifiedId,
1924
+ binding.relation.toLowerCase().replace(/["`\[\]]/g, '').replace(/\s*\.\s*/g, '.'),
1925
+ binding.column.toLowerCase().replace(/["`\[\]]/g, ''),
1926
+ ].join('|')).sort();
1927
+ const leftBindings = normalize(left);
1928
+ const rightBindings = normalize(right);
1929
+ return leftBindings.length === rightBindings.length
1930
+ && leftBindings.every((binding, index) => binding === rightBindings[index]);
1931
+ }
1932
+ function sameExploratoryAuthorizationReceipt(left, right) {
1933
+ const leftAttempt = normalizedExploratoryAuthorizationAttempt(left);
1934
+ const rightAttempt = normalizedExploratoryAuthorizationAttempt(right);
1935
+ return sameExploratoryPlanBindings(left, right)
1936
+ && left.sqlFingerprint === right.sqlFingerprint
1937
+ && left.authorization === right.authorization
1938
+ && leftAttempt.index === rightAttempt.index
1939
+ && leftAttempt.parentSqlFingerprint === rightAttempt.parentSqlFingerprint;
1940
+ }
1941
+ function exploratoryAuthorizationStateMismatch() {
1942
+ return Object.assign(new Error('The exploratory SQL authorization receipt did not match the already-frozen analytical plan. Execution was not attempted.'), { code: 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH' });
1943
+ }
1637
1944
  /**
1638
1945
  * Has this exact clarification already been asked in this thread?
1639
1946
  *
@@ -1946,10 +2253,21 @@ function terminalLifecycle(prior, phase, completedAt, eventCursor) {
1946
2253
  completedAt,
1947
2254
  };
1948
2255
  }
1949
- function diagnosticFailureFromError(error, phase) {
2256
+ function diagnosticFailureFromError(error, phase, requestedMode) {
1950
2257
  const name = error instanceof Error ? error.name : "";
1951
2258
  const message = error instanceof Error ? error.message : String(error);
1952
2259
  const lower = `${name} ${message}`.toLowerCase();
2260
+ if (error
2261
+ && typeof error === 'object'
2262
+ && error.code === 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH') {
2263
+ return {
2264
+ code: 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH',
2265
+ phase: 'sql.authorize',
2266
+ message: 'The frozen exploratory plan did not match the SQL authorization receipt. Execution was not attempted.',
2267
+ recoverable: false,
2268
+ safeActions: ['export_redacted_trace'],
2269
+ };
2270
+ }
1953
2271
  if (isOrchestrationBudgetExhausted(error)) {
1954
2272
  return {
1955
2273
  code: 'orchestration_budget_exhausted',
@@ -1960,6 +2278,15 @@ function diagnosticFailureFromError(error, phase) {
1960
2278
  };
1961
2279
  }
1962
2280
  if (name === "TimeoutError" || lower.includes("time limit") || lower.includes("timeout")) {
2281
+ if (requestedMode === 'research') {
2282
+ return {
2283
+ code: 'RESEARCH_RUN_DEADLINE',
2284
+ phase: 'research.run',
2285
+ message: 'Research reached its bounded run deadline before finalization.',
2286
+ recoverable: true,
2287
+ safeActions: ['inspect_failure'],
2288
+ };
2289
+ }
1963
2290
  return {
1964
2291
  code: "TIMEOUT",
1965
2292
  phase,
@@ -2058,6 +2385,11 @@ function diagnosticReceiptV3ForRun(run) {
2058
2385
  // route names or identifier text here: that erased stale/error lane states
2059
2386
  // and falsely reported governed-relational success for pure exploration.
2060
2387
  const cascade = run.routeDecision?.analyticalCascadeDecision;
2388
+ // The router may retain a broader terminal witness for presentation, while
2389
+ // the cascade carries the only persistable, enumerated relationship-proof
2390
+ // receipt. Prefer that immutable cascade value and do not infer a gap from a
2391
+ // failure message or route label here.
2392
+ const terminalGap = cascade?.terminalGap;
2061
2393
  const sourceCoverage = cascade?.sourceCoverage ?? [];
2062
2394
  const planFrozen = cascade?.planFrozen ?? false;
2063
2395
  const artifactProviderDiagnostic = run.artifacts
@@ -2079,11 +2411,513 @@ function diagnosticReceiptV3ForRun(run) {
2079
2411
  runId: run.id,
2080
2412
  sourceCoverage,
2081
2413
  ...(cascade ? { cascade } : {}),
2414
+ ...(terminalGap ? { terminalGap } : {}),
2082
2415
  planFrozen,
2083
2416
  ...(provider ? { provider } : {}),
2084
2417
  finalStopReason: run.stopReason,
2085
2418
  };
2086
2419
  }
2420
+ /**
2421
+ * Build the one canonical, content-safe Ask story. This is produced from the
2422
+ * authoritative run receipt once, then joined by both the inspector and the
2423
+ * full local trace. Neither surface is allowed to reconstruct an incident from
2424
+ * spans or a generic error string.
2425
+ */
2426
+ function diagnosticReceiptV4ForRun(run) {
2427
+ const cascade = run.routeDecision?.analyticalCascadeDecision;
2428
+ const requirements = cascade?.requirements;
2429
+ const candidates = run.routeDecision?.retrievalEvidence?.candidateTraceMetadata ?? [];
2430
+ const roleCounts = new Map();
2431
+ for (const candidate of candidates) {
2432
+ roleCounts.set(candidate.role, (roleCounts.get(candidate.role) ?? 0) + 1);
2433
+ }
2434
+ const researchBranchObservability = researchBranchObservabilityForRun(run);
2435
+ for (const evidence of researchBranchObservability.evidenceByRole) {
2436
+ roleCounts.set(evidence.role, (roleCounts.get(evidence.role) ?? 0) + evidence.candidateCount);
2437
+ }
2438
+ const terminalIncident = terminalIncidentForRun(run, cascade?.stopReason);
2439
+ const summaryInput = {
2440
+ version: 1,
2441
+ understoodRequest: {
2442
+ measures: requirements?.measures.length ?? 0,
2443
+ dimensions: requirements?.dimensions.length ?? 0,
2444
+ entityRequested: Boolean((requirements?.entityTerms.length ?? 0) || (requirements?.entityDisplayTerms.length ?? 0)),
2445
+ outputCount: requirements?.outputTerms?.length ?? 0,
2446
+ ...(requirements?.ranking
2447
+ ? { ranking: { ...requirements.ranking } }
2448
+ : {}),
2449
+ // This comes from the server-owned request admission, not a generic
2450
+ // `followsUp` heuristic. A complete question with thread history is
2451
+ // still `none` unless it explicitly selected a valid binding.
2452
+ conversationBinding: run.conversationBinding ?? 'none',
2453
+ },
2454
+ evidenceByRole: [...roleCounts.entries()]
2455
+ .map(([role, candidateCount]) => ({ role, candidateCount }))
2456
+ .sort((left, right) => left.role.localeCompare(right.role)),
2457
+ tierDecisions: (cascade?.attempts ?? []).map((attempt) => ({
2458
+ tier: attempt.tier,
2459
+ outcome: attempt.outcome,
2460
+ planFrozen: attempt.planFrozen,
2461
+ })),
2462
+ ...(cascade?.selectedTier
2463
+ ? {
2464
+ selectedPlan: {
2465
+ tier: cascade.selectedTier,
2466
+ planFrozen: cascade.planFrozen,
2467
+ reviewRequired: cascade.selectedTier === 'exploratory_sql',
2468
+ },
2469
+ }
2470
+ : {}),
2471
+ ...(terminalIncident ? { terminalIncident } : {}),
2472
+ ...(researchBranchObservability.summary ? { researchBranchSummary: researchBranchObservability.summary } : {}),
2473
+ safeNextAction: terminalIncident?.safeAction
2474
+ ?? (researchBranchObservability.summary?.partialSuccess
2475
+ ? researchBranchObservability.summary.safeAction
2476
+ : 'none'),
2477
+ };
2478
+ const summary = {
2479
+ ...summaryInput,
2480
+ summaryFingerprint: receiptFingerprint(summaryInput),
2481
+ };
2482
+ return {
2483
+ version: 4,
2484
+ runId: run.id,
2485
+ summary,
2486
+ ...(terminalIncident ? { terminalIncident } : {}),
2487
+ finalStopReason: run.stopReason,
2488
+ };
2489
+ }
2490
+ const ASK_SUMMARY_EVIDENCE_ROLES = [
2491
+ 'metric',
2492
+ 'entity_key',
2493
+ 'entity_label',
2494
+ 'categorical_dimension',
2495
+ 'time_dimension',
2496
+ 'member',
2497
+ 'relationship',
2498
+ 'context',
2499
+ ];
2500
+ const ASK_RESEARCH_BRANCH_FAILURE_CODES = [
2501
+ 'execution_failed',
2502
+ 'research_branch_timeout',
2503
+ 'budget_exhausted',
2504
+ 'run_deadline',
2505
+ 'cancelled',
2506
+ ];
2507
+ const ASK_RESEARCH_CHILD_TIERS = [
2508
+ 'certified',
2509
+ 'semantic',
2510
+ 'governed_relational',
2511
+ 'exploratory_sql',
2512
+ ];
2513
+ /**
2514
+ * Project only persisted, typed Research child evidence into V4. The root
2515
+ * result remains authoritative: this helper never promotes a failed branch
2516
+ * into a root incident or infers a missing plan from spans.
2517
+ */
2518
+ function researchBranchObservabilityForRun(run) {
2519
+ // A persisted V4 receipt can be reprojected after request normalization by
2520
+ // a host. The root route is therefore the durable authority as well as the
2521
+ // original requested mode: an explicit Research run must not lose its
2522
+ // child-story merely because an older host omitted `requestedMode` while
2523
+ // preserving the authoritative `research` route and research artifact.
2524
+ if (run.requestedMode !== 'research' && run.route !== 'research') {
2525
+ return { evidenceByRole: [] };
2526
+ }
2527
+ const payload = persistedResearchArtifactPayloadForRun(run);
2528
+ if (!payload)
2529
+ return { evidenceByRole: [] };
2530
+ const rawReceipts = Array.isArray(payload.researchBranchReceipts)
2531
+ ? payload.researchBranchReceipts
2532
+ : [];
2533
+ const receipts = new Map();
2534
+ for (const value of rawReceipts) {
2535
+ const receipt = clarificationRecord(value);
2536
+ const childRunId = clarificationString(receipt?.childRunId);
2537
+ const branchId = clarificationString(receipt?.branchId);
2538
+ const state = clarificationString(receipt?.state);
2539
+ const stopReason = clarificationString(receipt?.stopReason);
2540
+ // This field is producer-owned. A malformed imported receipt must not
2541
+ // become an apparently successful Research story.
2542
+ if (!receipt || !childRunId || !branchId || !state || !stopReason)
2543
+ continue;
2544
+ const key = `${childRunId}:${branchId}`;
2545
+ if (!receipts.has(key))
2546
+ receipts.set(key, receipt);
2547
+ }
2548
+ if (receipts.size === 0)
2549
+ return { evidenceByRole: [] };
2550
+ const childRuns = persistedResearchChildRuns(payload);
2551
+ const evidenceByRole = persistedResearchChildEvidenceByRole(childRuns);
2552
+ const receiptBackedChildIds = persistedReceiptBackedResearchChildIds(payload);
2553
+ let completedBranches = 0;
2554
+ let failedBranches = 0;
2555
+ let timedOutBranches = 0;
2556
+ let skippedBranches = 0;
2557
+ const failureReasons = new Map();
2558
+ const linkedChildRunIds = new Set();
2559
+ for (const receipt of receipts.values()) {
2560
+ const childRunId = clarificationString(receipt.childRunId);
2561
+ linkedChildRunIds.add(childRunId);
2562
+ const state = clarificationString(receipt.state);
2563
+ const stopReason = clarificationString(receipt.stopReason);
2564
+ if (state === 'completed' && stopReason === 'completed') {
2565
+ completedBranches += 1;
2566
+ continue;
2567
+ }
2568
+ if (state === 'timed_out')
2569
+ timedOutBranches += 1;
2570
+ else if (state === 'skipped')
2571
+ skippedBranches += 1;
2572
+ else
2573
+ failedBranches += 1;
2574
+ if (isAskResearchBranchFailureCode(stopReason)) {
2575
+ failureReasons.set(stopReason, (failureReasons.get(stopReason) ?? 0) + 1);
2576
+ }
2577
+ }
2578
+ const receiptBackedBranches = [...receipts.values()]
2579
+ .filter((receipt) => clarificationString(receipt.state) === 'completed'
2580
+ && clarificationString(receipt.stopReason) === 'completed'
2581
+ && receiptBackedChildIds.has(clarificationString(receipt.childRunId)))
2582
+ .length;
2583
+ const incompleteBranches = failedBranches + timedOutBranches + skippedBranches;
2584
+ const summary = {
2585
+ version: 1,
2586
+ totalBranches: receipts.size,
2587
+ completedBranches,
2588
+ receiptBackedBranches,
2589
+ failedBranches,
2590
+ timedOutBranches,
2591
+ skippedBranches,
2592
+ partialSuccess: receiptBackedBranches > 0 && incompleteBranches > 0,
2593
+ failureReasons: [...failureReasons.entries()]
2594
+ .map(([code, branchCount]) => ({ code, branchCount }))
2595
+ .sort((left, right) => left.code.localeCompare(right.code)),
2596
+ availableChildPlans: persistedResearchChildPlans(childRuns),
2597
+ linkedChildRunCount: linkedChildRunIds.size,
2598
+ safeAction: 'inspect_research_failures',
2599
+ };
2600
+ return { summary, evidenceByRole };
2601
+ }
2602
+ /** Use only the durable root research artifact with branch receipts. */
2603
+ function persistedResearchArtifactPayloadForRun(run) {
2604
+ let selected;
2605
+ let selectedCount = -1;
2606
+ for (const artifact of run.artifacts) {
2607
+ if (artifact.kind !== 'research_run')
2608
+ continue;
2609
+ const payload = clarificationRecord(artifact.payload);
2610
+ const count = Array.isArray(payload?.researchBranchReceipts) ? payload.researchBranchReceipts.length : 0;
2611
+ if (payload && count > selectedCount) {
2612
+ selected = payload;
2613
+ selectedCount = count;
2614
+ }
2615
+ }
2616
+ return selected;
2617
+ }
2618
+ function persistedResearchChildRuns(payload) {
2619
+ const candidates = [
2620
+ ...(Array.isArray(payload.researchRuns) ? payload.researchRuns : []),
2621
+ payload.researchRun,
2622
+ ];
2623
+ const byId = new Map();
2624
+ for (const value of candidates) {
2625
+ const child = clarificationRecord(value);
2626
+ const id = clarificationString(child?.id);
2627
+ if (child && id && !byId.has(id))
2628
+ byId.set(id, child);
2629
+ }
2630
+ return [...byId.values()];
2631
+ }
2632
+ function persistedResearchChildEvidenceByRole(childRuns) {
2633
+ const counts = new Map();
2634
+ for (const child of childRuns) {
2635
+ const routeDecision = clarificationRecord(child.routeDecision);
2636
+ const retrieval = clarificationRecord(routeDecision?.retrievalEvidence);
2637
+ const candidates = Array.isArray(retrieval?.candidateTraceMetadata)
2638
+ ? retrieval.candidateTraceMetadata
2639
+ : [];
2640
+ for (const value of candidates) {
2641
+ const candidate = clarificationRecord(value);
2642
+ const role = clarificationString(candidate?.role);
2643
+ if (!isAskSummaryEvidenceRole(role))
2644
+ continue;
2645
+ counts.set(role, (counts.get(role) ?? 0) + 1);
2646
+ }
2647
+ }
2648
+ return [...counts.entries()]
2649
+ .map(([role, candidateCount]) => ({ role, candidateCount }))
2650
+ .sort((left, right) => left.role.localeCompare(right.role));
2651
+ }
2652
+ function persistedReceiptBackedResearchChildIds(payload) {
2653
+ const ledger = clarificationRecord(payload.researchLedgerV2);
2654
+ const entries = Array.isArray(ledger?.entries) ? ledger.entries : [];
2655
+ const ids = new Set();
2656
+ for (const value of entries) {
2657
+ const entry = clarificationRecord(value);
2658
+ const id = clarificationString(entry?.id);
2659
+ const receipts = Array.isArray(entry?.receipts) ? entry.receipts : [];
2660
+ if (entry?.status === 'observed' && id && receipts.some((receipt) => clarificationString(receipt))) {
2661
+ ids.add(id);
2662
+ }
2663
+ }
2664
+ return ids;
2665
+ }
2666
+ function persistedResearchChildPlans(childRuns) {
2667
+ const plansByTier = new Map();
2668
+ for (const child of childRuns) {
2669
+ const childRunId = clarificationString(child.id);
2670
+ const context = clarificationRecord(child.context);
2671
+ const authority = clarificationRecord(context?.branchAuthority);
2672
+ const tier = clarificationString(authority?.selectedTier);
2673
+ const planId = clarificationString(authority?.planId);
2674
+ const planFingerprint = clarificationString(authority?.planFingerprint);
2675
+ if (!childRunId || authority?.planFrozen !== true || !isAskResearchChildTier(tier) || !planId || !planFingerprint)
2676
+ continue;
2677
+ const entry = plansByTier.get(tier) ?? { planKeys: new Set(), childRunIds: new Set() };
2678
+ entry.planKeys.add(`${planId}:${planFingerprint}`);
2679
+ entry.childRunIds.add(childRunId);
2680
+ plansByTier.set(tier, entry);
2681
+ }
2682
+ return [...plansByTier.entries()]
2683
+ .map(([tier, value]) => ({
2684
+ tier,
2685
+ frozenPlanCount: value.planKeys.size,
2686
+ branchCount: value.childRunIds.size,
2687
+ reviewRequired: tier === 'exploratory_sql',
2688
+ }))
2689
+ .sort((left, right) => left.tier.localeCompare(right.tier));
2690
+ }
2691
+ function isAskSummaryEvidenceRole(value) {
2692
+ return Boolean(value) && ASK_SUMMARY_EVIDENCE_ROLES.includes(value);
2693
+ }
2694
+ function isAskResearchBranchFailureCode(value) {
2695
+ return Boolean(value) && ASK_RESEARCH_BRANCH_FAILURE_CODES.includes(value);
2696
+ }
2697
+ function isAskResearchChildTier(value) {
2698
+ return Boolean(value) && ASK_RESEARCH_CHILD_TIERS.includes(value);
2699
+ }
2700
+ function terminalIncidentForRun(run, cascadeStopReason) {
2701
+ const executionSetupFailure = terminalConnectionSetupFailureForRun(run);
2702
+ if (executionSetupFailure) {
2703
+ return {
2704
+ version: 1,
2705
+ code: 'CONNECTION_NOT_CONFIGURED',
2706
+ boundary: 'sql.execute',
2707
+ origin: 'governance_gate',
2708
+ impact: 'execution_not_attempted',
2709
+ safeAction: 'configure_connection',
2710
+ };
2711
+ }
2712
+ const failureCode = run.diagnosticReceipt?.failure?.code;
2713
+ if (failureCode === 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH') {
2714
+ return {
2715
+ version: 1,
2716
+ code: 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH',
2717
+ boundary: 'sql.authorize',
2718
+ origin: 'internal_invariant',
2719
+ impact: 'execution_not_attempted',
2720
+ safeAction: 'export_redacted_trace',
2721
+ };
2722
+ }
2723
+ if (failureCode === 'RESEARCH_RUN_DEADLINE') {
2724
+ return {
2725
+ version: 1,
2726
+ code: 'RESEARCH_RUN_DEADLINE',
2727
+ boundary: 'run',
2728
+ origin: 'governance_gate',
2729
+ impact: 'answer_not_produced',
2730
+ safeAction: 'inspect_failure',
2731
+ };
2732
+ }
2733
+ // A completed root can still be materially limited when every admitted
2734
+ // Research child exhausted its bounded window. This is producer-owned
2735
+ // receipt evidence, not an incident reconstructed from trace timing. It
2736
+ // must be visible in the same V4 summary used by the inspector and full
2737
+ // trace so the user is never told there was no incident after a zero-finding
2738
+ // investigation.
2739
+ if (terminalResearchBranchTimeoutForRun(run)) {
2740
+ return {
2741
+ version: 1,
2742
+ code: 'RESEARCH_BRANCH_TIMEOUT',
2743
+ boundary: 'run',
2744
+ origin: 'governance_gate',
2745
+ impact: 'answer_not_produced',
2746
+ safeAction: 'inspect_research_failures',
2747
+ };
2748
+ }
2749
+ if (failureCode === 'RUN_CANCELLED' || run.status === 'cancelled') {
2750
+ return { version: 1, code: 'CANCELLED', boundary: 'run', origin: 'unknown', impact: 'run_cancelled', safeAction: 'none' };
2751
+ }
2752
+ if (failureCode === 'CONNECTION_NOT_CONFIGURED') {
2753
+ return { version: 1, code: 'CONNECTION_NOT_CONFIGURED', boundary: 'sql.execute', origin: 'governance_gate', impact: 'execution_not_attempted', safeAction: 'configure_connection' };
2754
+ }
2755
+ // A frozen semantic/analytical plan may fail while the compiler is resolving
2756
+ // its already-proven identifiers. That is categorically different from a
2757
+ // warehouse failure: no statement was authorized or executed. Preserve the
2758
+ // producer's typed `COMPILATION_FAILED` cause before consulting connector
2759
+ // evidence so both Ask surfaces tell the same pre-SQL story.
2760
+ const compilationFailure = terminalCompilationFailureForRun(run);
2761
+ if (compilationFailure) {
2762
+ const semantic = isSemanticCompilationForRun(run);
2763
+ return {
2764
+ version: 1,
2765
+ code: 'COMPILATION_FAILED',
2766
+ boundary: semantic ? 'semantic.compile' : 'plan.compile',
2767
+ origin: semantic ? 'semantic_compiler' : 'plan_compiler',
2768
+ impact: 'execution_not_attempted',
2769
+ safeAction: compilationFailure.safeAction,
2770
+ };
2771
+ }
2772
+ const warehouseFailure = terminalWarehouseFailureForRun(run);
2773
+ if (warehouseFailure) {
2774
+ return {
2775
+ version: 1,
2776
+ code: 'ANALYTICAL_EXECUTION_FAILED',
2777
+ boundary: 'sql.execute',
2778
+ origin: 'warehouse',
2779
+ impact: 'execution_failed',
2780
+ // A typed missing relation after a frozen plan reached the connector is
2781
+ // not a generic retry. The target may be an empty local database or a
2782
+ // different approved warehouse, so direct the operator to that target.
2783
+ safeAction: warehouseFailure.category === 'unknown_relation'
2784
+ ? 'change_authorized_connection'
2785
+ : 'inspect_failure',
2786
+ };
2787
+ }
2788
+ if (run.diagnosticReceiptV3?.provider) {
2789
+ return { version: 1, code: 'PROVIDER_FAILURE', boundary: 'provider', origin: 'provider', impact: 'answer_not_produced', safeAction: 'inspect_failure' };
2790
+ }
2791
+ if (cascadeStopReason === 'coverage_gap' || cascadeStopReason === 'ambiguous' || cascadeStopReason === 'denied') {
2792
+ return { version: 1, code: 'ANALYTICAL_COVERAGE_GAP', boundary: 'cascade', origin: 'governance_gate', impact: 'answer_not_produced', safeAction: 'inspect_failure' };
2793
+ }
2794
+ if (run.status === 'blocked') {
2795
+ return { version: 1, code: 'ANALYTICAL_EXECUTION_FAILED', boundary: 'sql.execute', origin: 'unknown', impact: 'execution_failed', safeAction: 'inspect_failure' };
2796
+ }
2797
+ return undefined;
2798
+ }
2799
+ /**
2800
+ * Read only the narrow host setup receipt emitted before a connector receives
2801
+ * SQL. This must win over the broad analytical failure payload because a
2802
+ * semantic graph can catch the host error after its compiler work completed.
2803
+ */
2804
+ function terminalConnectionSetupFailureForRun(run) {
2805
+ return run.artifacts.some((artifact) => {
2806
+ const payload = artifact.payload;
2807
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
2808
+ return false;
2809
+ const setup = payload.observabilityExecutionFailure;
2810
+ if (!setup || typeof setup !== 'object' || Array.isArray(setup))
2811
+ return false;
2812
+ const record = setup;
2813
+ return record.version === 1
2814
+ && record.phase === 'execution'
2815
+ && record.cause === 'connection_not_configured'
2816
+ && record.safeAction === 'configure_connection';
2817
+ });
2818
+ }
2819
+ /**
2820
+ * Read only a producer-owned analytical failure. A compiler failure may have
2821
+ * prepared SQL text, but it is still pre-execution until the durable telemetry
2822
+ * records a SQL call. This guard keeps a real warehouse failure from being
2823
+ * relabeled as semantic/planning just because a legacy adapter reused a broad
2824
+ * failure code in a later stage.
2825
+ */
2826
+ function terminalCompilationFailureForRun(run) {
2827
+ if ((run.telemetry?.sqlExecutions ?? 0) > 0)
2828
+ return undefined;
2829
+ for (const artifact of run.artifacts) {
2830
+ const payload = artifact.payload;
2831
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
2832
+ continue;
2833
+ const failure = payload.analyticalFailure;
2834
+ if (!failure || typeof failure !== 'object' || Array.isArray(failure))
2835
+ continue;
2836
+ const record = failure;
2837
+ if (record.code !== 'COMPILATION_FAILED' || record.phase !== 'compilation')
2838
+ continue;
2839
+ return {
2840
+ safeAction: terminalIncidentSafeAction(record.safeActions) ?? 'inspect_failure',
2841
+ };
2842
+ }
2843
+ return undefined;
2844
+ }
2845
+ function isSemanticCompilationForRun(run) {
2846
+ // Only the router-owned cascade may identify a semantic execution tier.
2847
+ // Direct/legacy semantic callers can still carry an immutable plan, but
2848
+ // without that authority their failure is accurately a generic plan compile
2849
+ // incident rather than a reconstructed semantic route.
2850
+ return run.routeDecision?.analyticalCascadeDecision?.selectedTier === 'semantic';
2851
+ }
2852
+ /** Keep V4's recovery action in the same compact vocabulary as trace spans. */
2853
+ function terminalIncidentSafeAction(value) {
2854
+ if (!Array.isArray(value))
2855
+ return undefined;
2856
+ const actions = [
2857
+ 'export_redacted_trace',
2858
+ 'configure_connection',
2859
+ 'change_authorized_connection',
2860
+ 'inspect_failure',
2861
+ 'retry_same_plan',
2862
+ 'refresh_snapshot',
2863
+ 'edit_dql',
2864
+ 'open_sql_notebook',
2865
+ 'request_access',
2866
+ 'reapply_semantic_runtime',
2867
+ 'review_analytical_failure',
2868
+ 'inspect_research_failures',
2869
+ 'none',
2870
+ ];
2871
+ return value.find((action) => typeof action === 'string' && actions.includes(action));
2872
+ }
2873
+ /**
2874
+ * A Research root is deliberately allowed to complete its receipt-bound
2875
+ * synthesis after child deadlines. Surface a terminal incident only when no
2876
+ * child completed an observation and all admitted children were bounded out;
2877
+ * a partially successful investigation remains a review-required answer with
2878
+ * a limited-scope note rather than a false failure.
2879
+ */
2880
+ function terminalResearchBranchTimeoutForRun(run) {
2881
+ for (const artifact of run.artifacts) {
2882
+ const payload = artifact.payload;
2883
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
2884
+ continue;
2885
+ const receipts = payload.researchBranchReceipts;
2886
+ if (!Array.isArray(receipts) || receipts.length === 0)
2887
+ continue;
2888
+ const stopReasons = receipts
2889
+ .map((receipt) => receipt && typeof receipt === 'object'
2890
+ ? receipt.stopReason
2891
+ : undefined)
2892
+ .filter((reason) => typeof reason === 'string');
2893
+ if (stopReasons.length !== receipts.length)
2894
+ continue;
2895
+ const allBounded = stopReasons.every((reason) => reason === 'research_branch_timeout' || reason === 'budget_exhausted');
2896
+ if (allBounded && stopReasons.some((reason) => reason === 'research_branch_timeout'))
2897
+ return true;
2898
+ }
2899
+ return false;
2900
+ }
2901
+ /**
2902
+ * Read only enum evidence emitted at the real connector boundary. SQL text and
2903
+ * redacted driver diagnostics remain in the artifact inspector; they cannot
2904
+ * become routing or trace-summary authority.
2905
+ */
2906
+ function terminalWarehouseFailureForRun(run) {
2907
+ for (const artifact of run.artifacts) {
2908
+ const payload = artifact.payload;
2909
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
2910
+ continue;
2911
+ const failure = payload.warehouseFailure;
2912
+ if (!failure || typeof failure !== 'object' || Array.isArray(failure))
2913
+ continue;
2914
+ const record = failure;
2915
+ if (record.version === 1 && record.origin === 'warehouse' && typeof record.category === 'string') {
2916
+ return { category: record.category };
2917
+ }
2918
+ }
2919
+ return undefined;
2920
+ }
2087
2921
  function emptyRunTelemetry(total, fallbackReason) {
2088
2922
  return {
2089
2923
  version: 1,
@@ -2109,7 +2943,7 @@ function durationBetweenMs(startedAt, completedAt) {
2109
2943
  function receiptFingerprint(value) {
2110
2944
  return createHash('sha256').update(JSON.stringify(value)).digest('hex');
2111
2945
  }
2112
- function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3) {
2946
+ function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3, receiptV4) {
2113
2947
  if (artifacts.length === 0) {
2114
2948
  if (!receipt.failure)
2115
2949
  return artifacts;
@@ -2118,7 +2952,7 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3) {
2118
2952
  kind: "answer",
2119
2953
  title: "Agent run diagnostics",
2120
2954
  trustState: "blocked",
2121
- payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}), ...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}) },
2955
+ payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}), ...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}), ...(receiptV4 ? { diagnosticReceiptV4: receiptV4 } : {}) },
2122
2956
  }];
2123
2957
  }
2124
2958
  const preferredIndex = Math.max(0, artifacts.findIndex((artifact) => artifact.kind === "answer"));
@@ -2135,6 +2969,7 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3) {
2135
2969
  diagnosticReceipt: receipt,
2136
2970
  ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}),
2137
2971
  ...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}),
2972
+ ...(receiptV4 ? { diagnosticReceiptV4: receiptV4 } : {}),
2138
2973
  },
2139
2974
  };
2140
2975
  });