@duckcodeailabs/dql-cli 1.13.4 → 1.13.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/assets/dql-notebook/assets/{AgentLogPage-DYmHO6_i.js → AgentLogPage-BzQOKjyV.js} +1 -1
  2. package/dist/assets/dql-notebook/assets/{AiBuildDialog-BfhaOd1h.js → AiBuildDialog-CTxha499.js} +1 -1
  3. package/dist/assets/dql-notebook/assets/{AiBuildResult-DLKFAe6R.js → AiBuildResult--MuD_I6g.js} +1 -1
  4. package/dist/assets/dql-notebook/assets/{AiSidePanel-C6Z3URGr.js → AiSidePanel-DcI4PMJ1.js} +1 -1
  5. package/dist/assets/dql-notebook/assets/{AnalyticsHome-uG3xCP_s.js → AnalyticsHome-Dq54vjNz.js} +1 -1
  6. package/dist/assets/dql-notebook/assets/{AppsView-DfieNjkh.js → AppsView-0SlwWYex.js} +4 -4
  7. package/dist/assets/dql-notebook/assets/{BlockStudio-90J_wvWL.js → BlockStudio-CxxXZD3M.js} +1 -1
  8. package/dist/assets/dql-notebook/assets/{BusinessArtifactView-_zSMae5q.js → BusinessArtifactView-DxdAmeUN.js} +1 -1
  9. package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-DoXiPssu.js → DbtFirstModelingPage-y81gFg_b.js} +1 -1
  10. package/dist/assets/dql-notebook/assets/{GitPage-CUfAhS67.js → GitPage-GQtcwncb.js} +1 -1
  11. package/dist/assets/dql-notebook/assets/{GlobalAiRail-ovtL7nQu.js → GlobalAiRail-DM4wkxR_.js} +1 -1
  12. package/dist/assets/dql-notebook/assets/{GovernedContextPage-CaMiKAzA.js → GovernedContextPage-B8Ft6JES.js} +1 -1
  13. package/dist/assets/dql-notebook/assets/{HelpDocsPage-CEGUx28b.js → HelpDocsPage-DnD4nMuF.js} +1 -1
  14. package/dist/assets/dql-notebook/assets/{HomePage-2v3SRf8V.js → HomePage-Ehb0ITj-.js} +1 -1
  15. package/dist/assets/dql-notebook/assets/{LineageDAG-AaqLEiBN.js → LineageDAG-Lvjc2AQX.js} +1 -1
  16. package/dist/assets/dql-notebook/assets/{LineageDetailView-CFc8pm91.js → LineageDetailView-DX32pfbp.js} +1 -1
  17. package/dist/assets/dql-notebook/assets/{LineageDrawer-DOkFjPq-.js → LineageDrawer-CFq3jPfk.js} +1 -1
  18. package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-BrDwCuMs.js → LineagePathBreadcrumb-CVHXuHls.js} +1 -1
  19. package/dist/assets/dql-notebook/assets/{MiniLineageGraph-CiHtQs3y.js → MiniLineageGraph-BKaeT5Kt.js} +1 -1
  20. package/dist/assets/dql-notebook/assets/{NewBlockModal-DAgJZbws.js → NewBlockModal-DI-JDzFH.js} +1 -1
  21. package/dist/assets/dql-notebook/assets/{NewNotebookModal-xjG7nZ3O.js → NewNotebookModal-Csw6eF74.js} +1 -1
  22. package/dist/assets/dql-notebook/assets/{NotebookEditor-CXZaKfYT.js → NotebookEditor-FEqs3789.js} +1 -1
  23. package/dist/assets/dql-notebook/assets/{ReadinessPage-MuJBFwwg.js → ReadinessPage-Bk0S_MEA.js} +1 -1
  24. package/dist/assets/dql-notebook/assets/{SetupOnboarding-vKFtKtjh.js → SetupOnboarding-DyLaPFGn.js} +1 -1
  25. package/dist/assets/dql-notebook/assets/{SkillsPage-DCFVOV5f.js → SkillsPage-DLHyvhci.js} +1 -1
  26. package/dist/assets/dql-notebook/assets/{TrustBadge-TcwXFFHK.js → TrustBadge-CIDLj2A6.js} +1 -1
  27. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-C0oKTU6G.js +88 -0
  28. package/dist/assets/dql-notebook/assets/{answer-to-notebook-DCFWlHB7.js → answer-to-notebook-CFEJHLvs.js} +1 -1
  29. package/dist/assets/dql-notebook/assets/{arrow-left-BLnlJAtp.js → arrow-left-B-Zdcyvm.js} +1 -1
  30. package/dist/assets/dql-notebook/assets/{arrow-right-BmUAb1aW.js → arrow-right-Cn8TM7cp.js} +1 -1
  31. package/dist/assets/dql-notebook/assets/{book-open-text-bdSChSag.js → book-open-text-Bko2NNUs.js} +1 -1
  32. package/dist/assets/dql-notebook/assets/{circle-x-Cc6dEaf4.js → circle-x-AWJAmUBB.js} +1 -1
  33. package/dist/assets/dql-notebook/assets/{dagre.esm-CPebQKQ9.js → dagre.esm-Cl_ucrRh.js} +1 -1
  34. package/dist/assets/dql-notebook/assets/{external-link-rmXvubf3.js → external-link-DN57tb5f.js} +1 -1
  35. package/dist/assets/dql-notebook/assets/{grip-vertical-Sg4zd5-Q.js → grip-vertical-BOWwFFva.js} +1 -1
  36. package/dist/assets/dql-notebook/assets/{index-mQd_pC-P.js → index-Ck-wqvV2.js} +127 -127
  37. package/dist/assets/dql-notebook/assets/{link-2-BLaPoFxW.js → link-2-xUhbfBs8.js} +1 -1
  38. package/dist/assets/dql-notebook/assets/{list-tree-j0s4asFK.js → list-tree-BDWcBr67.js} +1 -1
  39. package/dist/assets/dql-notebook/assets/{minimize-2-Cl4pef4s.js → minimize-2-CEeNoMTS.js} +1 -1
  40. package/dist/assets/dql-notebook/assets/{panel-right-open-Clz-FtWi.js → panel-right-open-DnSeUvOY.js} +1 -1
  41. package/dist/assets/dql-notebook/assets/{play-DFdazMg7.js → play-EoDj8-fa.js} +1 -1
  42. package/dist/assets/dql-notebook/assets/{rotate-ccw-C3tTaR7s.js → rotate-ccw-BAMxWT9A.js} +1 -1
  43. package/dist/assets/dql-notebook/assets/{semantic-fields-DIAhTlfy.js → semantic-fields-BwLOs1kl.js} +1 -1
  44. package/dist/assets/dql-notebook/assets/{sliders-horizontal-BUiu2tfY.js → sliders-horizontal-BYkm8SWW.js} +1 -1
  45. package/dist/assets/dql-notebook/assets/{star--Yq77aH4.js → star-sVexqNCs.js} +1 -1
  46. package/dist/assets/dql-notebook/assets/{triangle-alert-D5Ng-xZO.js → triangle-alert-D2njv4o4.js} +1 -1
  47. package/dist/assets/dql-notebook/assets/{upload-L8BKQAxP.js → upload-CXTMxIA8.js} +1 -1
  48. package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-DBu97YaM.js → usePersistedAgentThreadId-CPDeaAgL.js} +1 -1
  49. package/dist/assets/dql-notebook/assets/{user-round-BK5N1cJW.js → user-round-zVsI_uud.js} +1 -1
  50. package/dist/assets/dql-notebook/assets/{wand-sparkles-D8XDQD9z.js → wand-sparkles-BMIwIzXT.js} +1 -1
  51. package/dist/assets/dql-notebook/assets/{workflow-Bl9zwG0q.js → workflow-Dj9y1sNk.js} +1 -1
  52. package/dist/assets/dql-notebook/assets/{wrench-C-zLCMbW.js → wrench-DPQi6zrs.js} +1 -1
  53. package/dist/assets/dql-notebook/assets/{x-stiJ-sP7.js → x-XhhbtinL.js} +1 -1
  54. package/dist/assets/dql-notebook/index.html +1 -1
  55. package/dist/llm/answer-loop-tools.d.ts +3 -1
  56. package/dist/llm/answer-loop-tools.d.ts.map +1 -1
  57. package/dist/llm/answer-loop-tools.js +31 -23
  58. package/dist/llm/answer-loop-tools.js.map +1 -1
  59. package/dist/llm/providers/dql-agent-provider.d.ts +3 -1
  60. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
  61. package/dist/llm/providers/dql-agent-provider.js +278 -19
  62. package/dist/llm/providers/dql-agent-provider.js.map +1 -1
  63. package/dist/llm/types.d.ts +29 -2
  64. package/dist/llm/types.d.ts.map +1 -1
  65. package/dist/local-runtime.d.ts +176 -4
  66. package/dist/local-runtime.d.ts.map +1 -1
  67. package/dist/local-runtime.js +1775 -372
  68. package/dist/local-runtime.js.map +1 -1
  69. package/dist/package.json +10 -10
  70. package/dist/providers/oauth/claude-oauth.d.ts +1 -1
  71. package/dist/providers/oauth/claude-oauth.d.ts.map +1 -1
  72. package/dist/providers/oauth/claude-oauth.js +18 -11
  73. package/dist/providers/oauth/claude-oauth.js.map +1 -1
  74. package/dist/providers/oauth/codex-oauth.d.ts +1 -1
  75. package/dist/providers/oauth/codex-oauth.d.ts.map +1 -1
  76. package/dist/providers/oauth/codex-oauth.js +9 -1
  77. package/dist/providers/oauth/codex-oauth.js.map +1 -1
  78. package/dist/providers/subscription-cli.d.ts +1 -1
  79. package/dist/providers/subscription-cli.d.ts.map +1 -1
  80. package/dist/providers/subscription-cli.js +33 -4
  81. package/dist/providers/subscription-cli.js.map +1 -1
  82. package/package.json +10 -10
  83. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-Cu-YgIe4.js +0 -88
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
1
2
  import { execFileSync, execSync } from "node:child_process";
2
3
  import { createHash } from "node:crypto";
3
4
  import { gzip } from "node:zlib";
@@ -15,7 +16,7 @@ import { buildMixedSourceWarehouseFallbackSql, findMentionedNotebookDataset, pla
15
16
  import { resolveNpmInvocation } from './npm-runtime.js';
16
17
  import { ensureDqlGitignore, isGovernedSourceFile, isLegacyBroadDqlIgnore, } from './git-contract.js';
17
18
  import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, hasStandaloneSemanticRef, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
18
- import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, previewModelingChanges, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, normalizeAnalyticalFailureV1, relationshipValidationProofFingerprint, renderSemanticBlockSource, discoverDbtDomains, collectDbtRelationshipTests, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, semanticDimensionReference, modelAreaLocalId, DEFAULT_MODEL_AREA_ID, } from '@duckcodeailabs/dql-core';
19
+ import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, previewModelingChanges, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, normalizeAnalyticalFailureV1, normalizeAnalyticalRepairCapabilityV1, relationshipValidationProofFingerprint, renderSemanticBlockSource, discoverDbtDomains, collectDbtRelationshipTests, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, buildSqlAnalyticalSignature, buildGeneratedAnalyticalSqlSignature, buildSqlOutputExpressionSignature, semanticDimensionReference, semanticExecutionFingerprint, modelAreaLocalId, DEFAULT_MODEL_AREA_ID, } from '@duckcodeailabs/dql-core';
19
20
  import { load as loadYaml } from 'js-yaml';
20
21
  import { listBlockTemplates } from './block-templates.js';
21
22
  import { getRunner as getLLMRunner } from './llm/index.js';
@@ -24,7 +25,7 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
24
25
  import { resolveRetrievalHealthStatus } from './retrieval-health.js';
25
26
  import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
26
27
  import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
27
- import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, previewSkillChange, buildContextAuthoringProposal, contextAuthoringDependencyClosure, FileContextAuthoringProposalStore, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, } from '@duckcodeailabs/dql-agent';
28
+ import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildAggregationSafetyProof, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, previewSkillChange, buildContextAuthoringProposal, contextAuthoringDependencyClosure, FileContextAuthoringProposalStore, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, createAgentRunBudget, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, assertProviderPayloadAllowed, createProviderDispatchEgressReceipt, prepareProviderWireEnvelopeForDispatch, markProviderMetadataArray, createProviderEgressReceipt, redactProviderResultRows, composeVerifiedAnalyticalNarrative, DEFAULT_ASK_ROW_EGRESS_POLICY, ZERO_ROW_EGRESS_POLICY, resolveProviderResultRowEgressPolicy, } from '@duckcodeailabs/dql-agent';
28
29
  import { addSqlResultFilter, dashboardFilterableResultColumns, filterableResultColumns, replaceBlockStudioSql } from './sql-result-filter.js';
29
30
  import { gatherProposeEnrichment } from './propose-enrich.js';
30
31
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization, } from './apps-api.js';
@@ -291,6 +292,12 @@ export function parseAgentRunRequestBody(body) {
291
292
  const workspaceContext = agentRunRecord(record.workspaceContext) ?? agentRunRecord(record.context);
292
293
  const signals = agentRunRecord(record.signals);
293
294
  const requestedMode = parseAgentRunRequestedMode(record.requestedMode) ?? parseAgentRunRequestedMode(record.mode);
295
+ if (record.researchResultRowsOptIn !== undefined && typeof record.researchResultRowsOptIn !== 'boolean') {
296
+ return { error: 'researchResultRowsOptIn must be a boolean.' };
297
+ }
298
+ if (record.researchResultRowsOptIn === true && requestedMode !== 'research') {
299
+ return { error: 'researchResultRowsOptIn is accepted only for an explicitly requested Research run.' };
300
+ }
294
301
  const audience = record.audience === 'stakeholder' || record.audience === 'analyst'
295
302
  ? record.audience
296
303
  : undefined;
@@ -306,29 +313,47 @@ export function parseAgentRunRequestBody(body) {
306
313
  selectedObject,
307
314
  executionTarget,
308
315
  workspaceContext,
309
- conversationContext: agentRunRecord(record.conversationContext),
316
+ conversationContext: sanitizeClientConversationContext(agentRunRecord(record.conversationContext)),
310
317
  history: parseAgentRunHistory(record.history),
311
318
  threadId: agentRunString(record.threadId),
312
319
  runId: agentRunString(record.runId),
313
320
  reasoningEffort: parseAgentRunReasoningEffort(record.reasoningEffort),
314
321
  analysisDepth: parseAgentRunAnalysisDepth(record.analysisDepth) ?? parseAgentRunAnalysisDepth(record.depth),
315
322
  thinkingMode: coerceThinkingMode(record.thinkingMode),
323
+ researchResultRowsOptIn: requestedMode === 'research' && record.researchResultRowsOptIn === true,
316
324
  },
317
325
  };
318
326
  }
327
+ const CLIENT_PLAN_AUTHORITY_KEYS = new Set([
328
+ 'priorResolvedAnalyticalPlan',
329
+ 'resolvedAnalyticalPlan',
330
+ 'analyticalFrame',
331
+ ]);
332
+ /**
333
+ * Browser/embedding context is useful retrieval and history input, but it is
334
+ * not a plan-authority channel. Remove plan-shaped fields recursively at HTTP
335
+ * ingress; server-retained thread context is added afterwards from local state.
336
+ */
337
+ function sanitizeClientConversationContext(context) {
338
+ if (!context)
339
+ return undefined;
340
+ const sanitize = (value) => {
341
+ if (Array.isArray(value))
342
+ return value.map(sanitize);
343
+ const record = agentRunRecord(value);
344
+ if (!record)
345
+ return value;
346
+ return Object.fromEntries(Object.entries(record).flatMap(([key, nested]) => CLIENT_PLAN_AUTHORITY_KEYS.has(key) ? [] : [[key, sanitize(nested)]]));
347
+ };
348
+ return sanitize(context);
349
+ }
319
350
  const AGENT_LOOKUP_DEADLINE_MS = 45_000;
320
351
  const AGENT_RESEARCH_DEADLINE_MS = 120_000;
321
- /** Env-tunable run deadline (e.g. slow subscription-CLI providers), clamped to sane bounds. */
322
- function resolveAgentDeadlineMs(envKey, fallback, env = process.env) {
323
- const configured = Number(env[envKey]);
324
- if (!Number.isFinite(configured) || configured <= 0)
325
- return fallback;
326
- return Math.max(15_000, Math.min(600_000, Math.floor(configured)));
327
- }
328
352
  /**
329
353
  * PERF-002: one wall-clock budget follows the request through routing, provider
330
354
  * calls, repair, and execution. Ordinary Ask never inherits Research's budget
331
- * merely because it spans two tables; explicit/deep investigation does.
355
+ * because of question wording, depth, provider, or relational shape; only an
356
+ * explicit `requestedMode: research` activates the Research budget.
332
357
  */
333
358
  /** Resolve governed display formats for result columns from the semantic layer (best-effort, cached per call). */
334
359
  function agentColumnDisplayFormats(projectRoot, columns) {
@@ -346,91 +371,63 @@ function agentColumnDisplayFormats(projectRoot, columns) {
346
371
  catch { /* formatting is presentation-only; never fail an answer over it */ }
347
372
  return formats;
348
373
  }
349
- /** Subscription CLI providers spawn a cold external process per LLM call (2-15s
350
- * startup each) across a multi-call loop, so the API-sized 45s default made even
351
- * simple questions "reach the bounded execution deadline" on office laptops with
352
- * no env override. The defaults must fit the transport's physics. */
353
- const AGENT_CLI_LOOKUP_DEADLINE_MS = 150_000;
354
- const AGENT_CLI_RESEARCH_DEADLINE_MS = 300_000;
355
- /** High reasoning effort makes every LLM call materially slower, so the same
356
- * multi-step build that fits a budget at medium effort overruns it at high. The
357
- * budget must scale with effort or "High · thorough" reliably times out. */
358
- function reasoningEffortBudgetMultiplier(effort) {
359
- return effort === 'high' ? 1.8 : effort === 'low' ? 0.9 : 1;
374
+ export function agentRunDeadlineMs(request, env = process.env, activeProviderId) {
375
+ void env;
376
+ void activeProviderId;
377
+ return request.requestedMode === 'research'
378
+ ? AGENT_RESEARCH_DEADLINE_MS
379
+ : AGENT_LOOKUP_DEADLINE_MS;
360
380
  }
361
381
  /**
362
- * A ranking/breakdown/filtered question needs LLM SQL generation (the grain
363
- * dimension usually lives on a joined model the semantic layer can't compose),
364
- * and that multi-call build does NOT fit the bare lookup budget on a slow or
365
- * high-effort provider it dead-ends at the deadline. It is not "research", but
366
- * it needs research-sized time to finish. Detected from the requested SHAPE (a
367
- * top-N, a member filter, a breakdown dimension, or a time window) rather than a
368
- * recognized metric word, since office questions like "top bcm customers from
369
- * the south region" name no metric the planner knows yet. A bare scalar
370
- * ("total revenue") has none of these and stays on the short budget.
382
+ * Decide how a settled answer gets its business-facing prose.
383
+ *
384
+ * This deliberately does NOT read `requestedMode`. Gating narration on
385
+ * `requestedMode === 'research'` meant every ordinary Ask which the UI sends
386
+ * as `'auto'` skipped synthesis entirely and shipped the answer loop's
387
+ * deterministic fact-join as the primary answer: the reported `column: value`
388
+ * dump. Narration is owed to any run that actually produced values.
389
+ *
390
+ * Certification, a DQL artifact, and the exploratory candidate are no longer
391
+ * vetoes. EXP-001's grain concern is real, but the answer to "the model might
392
+ * relabel an entity-level measure" is to VERIFY the claims against the fact set
393
+ * (`verified_facts`) and pass the grain statement in as a caveat — not to refuse
394
+ * to write a sentence.
371
395
  */
372
- function isAnalyticalBuildQuestion(plan) {
373
- if (!plan.needsGeneratedSql)
374
- return false;
375
- return plan.requestedShape.dimensions.length > 0
376
- || plan.dimensionTerms.length > 0
377
- || plan.requestedShape.filters.length > 0
378
- || plan.timeTerms.length > 0
379
- || Boolean(plan.requestedShape.topN);
380
- }
381
- export function agentRunDeadlineMs(request, env = process.env, activeProviderId) {
382
- const cliProvider = activeProviderId === 'claude-code' || activeProviderId === 'codex';
383
- const lookupDeadline = resolveAgentDeadlineMs('DQL_AGENT_LOOKUP_DEADLINE_MS', cliProvider ? AGENT_CLI_LOOKUP_DEADLINE_MS : AGENT_LOOKUP_DEADLINE_MS, env);
384
- const researchDeadline = resolveAgentDeadlineMs('DQL_AGENT_RESEARCH_DEADLINE_MS', cliProvider ? AGENT_CLI_RESEARCH_DEADLINE_MS : AGENT_RESEARCH_DEADLINE_MS, env);
385
- // The "Thinking" control sends a thinkingMode; explicit reasoningEffort/
386
- // analysisDepth (CLI flags) take precedence but the mode is the common case.
387
- // The deadline MUST see the resolved values — "High" both slows every call
388
- // AND means deep — or picking High reliably times out at the lookup budget.
389
- const resolvedMode = request.thinkingMode ? resolveThinkingMode(request.thinkingMode) : {};
390
- const effectiveEffort = request.reasoningEffort ?? resolvedMode.reasoningEffort;
391
- const effectiveDepth = request.analysisDepth ?? resolvedMode.analysisDepth;
392
- // The budget is a CEILING, not a target — a fast answer returns immediately;
393
- // the larger tiers only matter when the build genuinely needs the time.
394
- const scale = (ms) => Math.min(600_000, Math.round(ms * reasoningEffortBudgetMultiplier(effectiveEffort)));
395
- if (request.requestedMode === 'research' || effectiveDepth === 'deep') {
396
- return scale(researchDeadline);
397
- }
398
- // A structured clarification chip displays a compact metric/path label, but
399
- // the engine restores and executes the original analytical question. Budget
400
- // from that same original question as well. Otherwise a complex ranking or
401
- // cross-model request can incorrectly inherit the 45s scalar-lookup budget
402
- // merely because the selected label is short, then time out after the user
403
- // already supplied the exact governed meaning.
404
- const deadlineQuestion = request.selectedEvidenceId
405
- ? request.clarificationSourceQuestion?.trim() || request.question
406
- : request.question;
407
- const plan = buildAnalysisQuestionPlan(deadlineQuestion);
408
- if (plan.needsResearchWorkspace || isAnalyticalBuildQuestion(plan)) {
409
- return scale(researchDeadline);
410
- }
411
- return scale(lookupDeadline);
412
- }
413
- export function shouldSynthesizeAgentRunAnswer(governedAnswer) {
396
+ export function planAgentRunNarration(governedAnswer, context) {
397
+ // A refusal is not narrated. It owes the user a reason or a clarifying
398
+ // question, which is the clarification lane's job, not the narrator's.
414
399
  if (governedAnswer.kind === 'no_answer')
415
- return false;
416
- // The executed rows are the presentation boundary: certified, semantic,
417
- // DQL-first, and exploratory routes all deserve a separate stakeholder-facing
418
- // explanation grounded in those values. Trust and query mechanics stay in the
419
- // artifact/inspector instead of leaking into the primary answer.
400
+ return { mode: 'skip', reason: 'no_answer' };
401
+ if (!context.providerAvailable)
402
+ return { mode: 'skip', reason: 'no_provider' };
403
+ const maxRows = Math.max(0, context.rowEgress.maxNarrationRows);
404
+ // The fact set is the strongest grounding available: every sentence can be
405
+ // tied back to a fact id and rejected when it is not.
406
+ if (governedAnswer.analyticalFacts)
407
+ return { mode: 'verified_facts', maxRows };
420
408
  if (governedAnswer.result)
421
- return true;
422
- if (governedAnswer.kind === 'certified' || governedAnswer.certification === 'certified')
423
- return false;
424
- // EXP-001: exploratory prose is part of the trust boundary. A free-form
425
- // synthesis pass can accidentally relabel an entity-level lifetime measure
426
- // as a child-level allocation or total repeated values. Keep the host's
427
- // deterministic grain statement instead.
428
- if (governedAnswer.exploratoryCandidate)
429
- return false;
430
- const finalText = (governedAnswer.answer ?? governedAnswer.text ?? '').trim();
431
- if (governedAnswer.dqlArtifact && finalText)
432
- return false;
433
- return true;
409
+ return { mode: 'preview_grounded', maxRows };
410
+ return { mode: 'skip', reason: 'nothing_to_narrate' };
411
+ }
412
+ /**
413
+ * Boolean view of {@link planAgentRunNarration}, kept for callers and tests that
414
+ * only need "does this run narrate at all".
415
+ */
416
+ export function shouldSynthesizeAgentRunAnswer(governedAnswer, requestedMode = 'ask') {
417
+ return planAgentRunNarration(governedAnswer, {
418
+ requestedMode,
419
+ providerAvailable: true,
420
+ rowEgress: DEFAULT_ASK_ROW_EGRESS_POLICY,
421
+ }).mode !== 'skip';
422
+ }
423
+ /**
424
+ * AGT-010 — the semantic route label is descriptive, while the exact
425
+ * route-specific aggregation proof is authoritative for governed trust.
426
+ * Missing proof remains blocked for legacy or malformed results.
427
+ */
428
+ export function semanticAnswerHasPassedAggregationProof(governedAnswer) {
429
+ return governedAnswer.route?.tier === 'semantic_metric'
430
+ && governedAnswer.aggregationSafetyProof?.status === 'safe';
434
431
  }
435
432
  export function agentAnswerHasExecutionFailure(governedAnswer) {
436
433
  return typeof governedAnswer.executionError === 'string'
@@ -544,6 +541,194 @@ export function warehouseFailureFromAgentRun(run) {
544
541
  }
545
542
  return undefined;
546
543
  }
544
+ const AUTOMATIC_ASK_REPAIR_FAILURE_CODES = new Set([
545
+ 'COLUMN_NOT_FOUND',
546
+ 'RELATION_NOT_FOUND',
547
+ 'AMBIGUOUS_COLUMN',
548
+ 'DIALECT_ERROR',
549
+ ]);
550
+ /** Validate the immutable custom wrapper before any automatic repair is offered. */
551
+ export function validAskRepairDqlWrapper(source) {
552
+ try {
553
+ const program = new Parser(source, '<ask-repair-capability>').parse();
554
+ if (program.statements.length !== 1)
555
+ return false;
556
+ const block = program.statements[0];
557
+ return block.kind === NodeKind.BlockDecl
558
+ && block.blockType === 'custom'
559
+ && Boolean(extractBlockStudioSql(source)?.trim());
560
+ }
561
+ catch {
562
+ return false;
563
+ }
564
+ }
565
+ /** Build retained automatic-repair authority from analytical failure state only. */
566
+ export function analyticalRepairCapabilityForAgentRun(run, resolvedTargetFingerprint) {
567
+ if (run.status !== 'blocked')
568
+ return undefined;
569
+ const failedRun = analyticalFailedRunFromAgentRun(run);
570
+ const failure = failedRun?.failure;
571
+ const dqlArtifact = run.artifacts.flatMap((artifact) => {
572
+ const payload = agentRunRecord(artifact.payload);
573
+ const candidate = normalizeDqlArtifactReference(payload?.dqlArtifact);
574
+ return candidate?.kind === 'sql_block' && candidate.source?.trim() ? [candidate] : [];
575
+ })[0];
576
+ const dqlSource = dqlArtifact?.source?.trim();
577
+ const sql = dqlArtifact?.compiledSql?.trim() || repairableSqlFromAgentRun(run);
578
+ const target = run.executionTarget;
579
+ const planFingerprint = failure?.planFingerprint?.trim();
580
+ const hash = (value) => `sha256:${createHash('sha256').update(stableExecutionValue(value)).digest('hex')}`;
581
+ const sourceArtifact = run.artifacts.find((artifact) => {
582
+ const payload = agentRunRecord(artifact.payload);
583
+ return Boolean(normalizeDqlArtifactReference(payload?.dqlArtifact)?.source?.trim());
584
+ });
585
+ const retainedPlanFingerprint = sourceArtifact
586
+ ? agentRunString(agentRunRecord(agentRunRecord(sourceArtifact.payload)?.resolvedAnalyticalPlan)?.fingerprint)
587
+ : undefined;
588
+ const sourceFingerprint = sourceArtifact && dqlArtifact
589
+ ? hash({
590
+ runId: run.id,
591
+ artifactId: sourceArtifact.id,
592
+ dqlArtifact: {
593
+ kind: dqlArtifact.kind,
594
+ name: dqlArtifact.name,
595
+ source: dqlArtifact.source?.trim(),
596
+ parameterValues: dqlArtifact.parameterValues ?? {},
597
+ trustState: dqlArtifact.trustState,
598
+ persistence: dqlArtifact.persistence,
599
+ },
600
+ })
601
+ : undefined;
602
+ const manualActions = (failure?.safeActions ?? []).flatMap((action) => {
603
+ if (action === 'edit_dql' || action === 'open_sql_notebook' || action === 'refresh_snapshot'
604
+ || action === 'retry_same_plan' || action === 'change_authorized_connection' || action === 'request_access') {
605
+ return [action];
606
+ }
607
+ return [];
608
+ });
609
+ const ineligibilityReason = !failure
610
+ ? 'missing_failure_authority'
611
+ : failure.phase !== 'execution' || !AUTOMATIC_ASK_REPAIR_FAILURE_CODES.has(failure.code)
612
+ ? 'failure_not_eligible'
613
+ : !dqlSource
614
+ ? 'missing_dql_wrapper'
615
+ : !validAskRepairDqlWrapper(dqlSource)
616
+ ? 'invalid_dql_wrapper'
617
+ : !sql
618
+ ? 'missing_compiled_sql'
619
+ : !planFingerprint || !retainedPlanFingerprint || retainedPlanFingerprint !== planFingerprint
620
+ ? 'missing_plan_fingerprint'
621
+ : !sourceFingerprint
622
+ ? 'missing_dql_wrapper'
623
+ : !target || !resolvedTargetFingerprint
624
+ ? 'missing_execution_target'
625
+ : undefined;
626
+ const eligible = ineligibilityReason === undefined;
627
+ return {
628
+ version: 1,
629
+ automatic: {
630
+ eligible,
631
+ action: eligible ? 'repair_embedded_sql' : 'none',
632
+ correctionCode: eligible ? 'SQL_EXECUTION_REPAIR' : 'MANUAL_REVIEW_REQUIRED',
633
+ attemptsRemaining: eligible ? 1 : 0,
634
+ },
635
+ failureFingerprint: hash(failure ?? { unavailable: 'analytical_failure', runId: run.id }),
636
+ sourceFingerprint: sourceFingerprint ?? hash({ unavailable: 'source', runId: run.id }),
637
+ planFingerprint: retainedPlanFingerprint ?? planFingerprint ?? hash({ unavailable: 'plan', runId: run.id }),
638
+ dqlFingerprint: dqlSource ? hash(dqlSource) : hash({ unavailable: 'dql', runId: run.id }),
639
+ sqlFingerprint: sql ? hash(sql) : hash({ unavailable: 'sql', runId: run.id }),
640
+ targetFingerprint: resolvedTargetFingerprint ?? hash({ unavailable: 'target', runId: run.id }),
641
+ routeLocked: true,
642
+ targetLocked: true,
643
+ sourceImmutable: true,
644
+ manualActions: Array.from(new Set(manualActions)),
645
+ ...(ineligibilityReason ? { ineligibilityReason } : {}),
646
+ };
647
+ }
648
+ function attachAnalyticalRepairCapability(run, resolvedTargetFingerprint) {
649
+ const capability = analyticalRepairCapabilityForAgentRun(run, resolvedTargetFingerprint);
650
+ if (!capability)
651
+ return run;
652
+ run.repairCapability = capability;
653
+ if (run.diagnosticReceipt)
654
+ run.diagnosticReceipt = { ...run.diagnosticReceipt, repairCapability: capability };
655
+ run.artifacts = run.artifacts.map((artifact) => {
656
+ const payload = agentRunRecord(artifact.payload);
657
+ if (!payload)
658
+ return artifact;
659
+ const diagnostic = agentRunRecord(payload.diagnosticReceipt);
660
+ return {
661
+ ...artifact,
662
+ payload: {
663
+ ...payload,
664
+ repairCapability: capability,
665
+ ...(diagnostic ? { diagnosticReceipt: { ...diagnostic, repairCapability: capability } } : {}),
666
+ },
667
+ };
668
+ });
669
+ return run;
670
+ }
671
+ export function targetGenerationFingerprint(connection, connectionName) {
672
+ const identity = {
673
+ driver: connection.driver,
674
+ connectionName: connectionName ?? null,
675
+ host: connection.host ?? null,
676
+ port: connection.port ?? null,
677
+ username: connection.username ?? null,
678
+ authMethod: connection.authMethod ?? null,
679
+ authenticator: connection.authenticator ?? null,
680
+ profile: connection.profile ?? null,
681
+ ssl: connection.ssl ?? null,
682
+ account: connection.account ?? null,
683
+ database: connection.database ?? null,
684
+ schema: connection.schema ?? null,
685
+ warehouse: connection.warehouse ?? null,
686
+ role: connection.role ?? null,
687
+ catalog: connection.catalog ?? null,
688
+ httpPath: connection.httpPath ?? null,
689
+ filepath: connection.filepath ?? null,
690
+ projectId: connection.projectId ?? null,
691
+ region: connection.region ?? null,
692
+ workgroup: connection.workgroup ?? null,
693
+ location: connection.location ?? null,
694
+ outputLocation: connection.outputLocation ?? null,
695
+ accessUrl: connection.accessUrl ?? null,
696
+ application: connection.application ?? null,
697
+ queryTag: connection.queryTag ?? null,
698
+ workloadIdentityProvider: connection.workloadIdentityProvider ?? null,
699
+ workloadIdentityAzureClientId: connection.workloadIdentityAzureClientId ?? null,
700
+ workloadIdentityImpersonationPath: connection.workloadIdentityImpersonationPath ?? null,
701
+ oauthAuthorizationUrl: connection.oauthAuthorizationUrl ?? null,
702
+ oauthClientId: connection.oauthClientId ?? null,
703
+ oauthRedirectUri: connection.oauthRedirectUri ?? null,
704
+ oauthScope: connection.oauthScope ?? null,
705
+ oauthTokenRequestUrl: connection.oauthTokenRequestUrl ?? null,
706
+ keyFilename: connection.keyFilename ?? null,
707
+ privateKeyPath: connection.privateKeyPath ?? null,
708
+ accessKeyId: connection.accessKeyId ?? null,
709
+ proxyHost: connection.proxyHost ?? null,
710
+ proxyPort: connection.proxyPort ?? null,
711
+ proxyProtocol: connection.proxyProtocol ?? null,
712
+ proxyUser: connection.proxyUser ?? null,
713
+ noProxy: connection.noProxy ?? null,
714
+ credentialCacheDir: connection.credentialCacheDir ?? null,
715
+ moduleSearchPaths: connection.moduleSearchPaths ?? null,
716
+ clientRequestMFAToken: connection.clientRequestMFAToken ?? null,
717
+ clientStoreTemporaryCredential: connection.clientStoreTemporaryCredential ?? null,
718
+ clientSessionKeepAlive: connection.clientSessionKeepAlive ?? null,
719
+ clientSessionKeepAliveHeartbeatFrequency: connection.clientSessionKeepAliveHeartbeatFrequency ?? null,
720
+ keepAlive: connection.keepAlive ?? null,
721
+ passcodeInPassword: connection.passcodeInPassword ?? null,
722
+ browserActionTimeout: connection.browserActionTimeout ?? null,
723
+ timeout: connection.timeout ?? null,
724
+ waitTimeout: connection.waitTimeout ?? null,
725
+ byteLimit: connection.byteLimit ?? null,
726
+ };
727
+ return executionFingerprint(stableExecutionValue(identity));
728
+ }
729
+ function normalizeSqlForComparison(sql) {
730
+ return sql.trim().replace(/;\s*$/, '').replace(/\s+/g, ' ');
731
+ }
547
732
  /**
548
733
  * Retain planning and retrieval evidence for a successful derived repair while
549
734
  * deliberately dropping the source failure and any result-bound facts. This
@@ -656,7 +841,7 @@ async function conversationContextFromThread(store, threadId, clientContext, que
656
841
  });
657
842
  }
658
843
  return {
659
- ...(clientContext ?? {}),
844
+ ...(sanitizeClientConversationContext(clientContext) ?? {}),
660
845
  conversationStateVersion: 1,
661
846
  threadId,
662
847
  ...(serverSnapshot ? { conversationEnvelope: serverSnapshot } : {}),
@@ -783,7 +968,9 @@ export function conversationTurnInputFromRun(run) {
783
968
  ?? run.artifacts.find((candidate) => candidate.kind === 'research_run')
784
969
  ?? run.artifacts[0];
785
970
  const payload = agentRunRecord(artifact?.payload);
786
- const result = agentRunRecord(payload?.result);
971
+ // A blocked run may retain diagnostic artifacts, but their result-shaped
972
+ // payload is never accepted conversation evidence or prose authority.
973
+ const result = run.status === 'blocked' ? undefined : agentRunRecord(payload?.result);
787
974
  const columns = conversationResultColumns(result?.columns);
788
975
  // The visual preview stays tiny, but member resolution needs a wider bounded
789
976
  // value window. Deriving dimensions from only the eight preview rows caused a
@@ -924,6 +1111,230 @@ function apiErrorCode(error, fallback) {
924
1111
  function apiErrorMessage(error) {
925
1112
  return error instanceof Error ? error.message : String(error);
926
1113
  }
1114
+ /**
1115
+ * Slowest provider dispatch we assume before any have been observed, and the
1116
+ * settle time reserved after the last one. A subscription-CLI provider costs
1117
+ * roughly 10-15s per call, so these keep the first admission decision honest.
1118
+ */
1119
+ const ASSUMED_PROVIDER_DISPATCH_MS = 12_000;
1120
+ const DISPATCH_SETTLE_MARGIN_MS = 4_000;
1121
+ export class RunScopedProviderDispatchEvidence {
1122
+ policy;
1123
+ runBudget;
1124
+ rowEgress;
1125
+ receipts = [];
1126
+ phaseCounts = new Map();
1127
+ currentRoute;
1128
+ /** Wall-clock start of the previous dispatch, used to learn this provider's cost. */
1129
+ lastDispatchStartedAtMs;
1130
+ observedDispatchDurations = [];
1131
+ /**
1132
+ * How long the NEXT dispatch is likely to take, learned from this run.
1133
+ *
1134
+ * Counting dispatches is not a time budget. A provider costing ~13s a call
1135
+ * will burn a 45s deadline in three calls and get killed mid-flight, so the
1136
+ * run ends with nothing — strictly worse than stopping early and answering
1137
+ * from what it already has.
1138
+ */
1139
+ expectedDispatchMs() {
1140
+ if (this.observedDispatchDurations.length === 0)
1141
+ return ASSUMED_PROVIDER_DISPATCH_MS;
1142
+ const sorted = [...this.observedDispatchDurations].sort((left, right) => left - right);
1143
+ // The slowest observed call is the honest predictor: an optimistic median
1144
+ // still admits a dispatch that the deadline then kills.
1145
+ return sorted[sorted.length - 1];
1146
+ }
1147
+ /** True when the remaining wall clock cannot fit another provider call. */
1148
+ cannotFitAnotherDispatch() {
1149
+ if (!this.runBudget)
1150
+ return false;
1151
+ return this.runBudget.remainingMs() < this.expectedDispatchMs() + DISPATCH_SETTLE_MARGIN_MS;
1152
+ }
1153
+ /**
1154
+ * The run budget's own clock when there is one, so dispatch cost is measured
1155
+ * against the same timeline the deadline is enforced on (and stays injectable
1156
+ * for deterministic tests).
1157
+ */
1158
+ nowMs() {
1159
+ return this.runBudget ? this.runBudget.elapsedMs() : Date.now();
1160
+ }
1161
+ recordDispatchStart() {
1162
+ const now = this.nowMs();
1163
+ if (this.lastDispatchStartedAtMs !== undefined) {
1164
+ this.observedDispatchDurations.push(Math.max(0, now - this.lastDispatchStartedAtMs));
1165
+ }
1166
+ this.lastDispatchStartedAtMs = now;
1167
+ }
1168
+ constructor(policy, runBudget, rowEgress = ZERO_ROW_EGRESS_POLICY) {
1169
+ this.policy = policy;
1170
+ this.runBudget = runBudget;
1171
+ this.rowEgress = rowEgress;
1172
+ this.currentRoute = runBudget?.mode === 'research' ? 'research' : 'generated_answer';
1173
+ }
1174
+ setRoute(route) {
1175
+ if (route)
1176
+ this.currentRoute = route;
1177
+ }
1178
+ mayStartToolCall() {
1179
+ return !this.runBudget || this.runBudget.mayStartDiscovery(this.currentRoute);
1180
+ }
1181
+ observe(event, context) {
1182
+ const softRoute = context.dispatchPhase === 'meaning_resolution' ? 'clarify' : this.currentRoute;
1183
+ // Admission control BEFORE the phase targets: starting a call that the hard
1184
+ // deadline will kill mid-flight wastes the remaining budget and ends the run
1185
+ // with nothing. Stopping here lets the caller answer from what it has.
1186
+ // Narration is exempt: it is the step that produces the answer, and it is
1187
+ // separately bounded by its own soft target below.
1188
+ if (context.dispatchPhase !== 'narration' && this.cannotFitAnotherDispatch()) {
1189
+ throw Object.assign(new Error(`Only ${Math.round((this.runBudget?.remainingMs() ?? 0) / 1_000)}s of the run deadline remain, which is not enough for another ~${Math.round(this.expectedDispatchMs() / 1_000)}s provider call. Answering from what has already been gathered.`), { code: 'RUN_DEADLINE_INSUFFICIENT' });
1190
+ }
1191
+ if (context.dispatchPhase === 'narration') {
1192
+ if (this.runBudget && !this.runBudget.mayStartNarration()) {
1193
+ throw Object.assign(new Error(`The ${Math.round(this.runBudget.narrationSoftTargetMs() / 1_000)}-second narration target elapsed before the answer could be written.`), { code: 'RUN_SOFT_TARGET_EXCEEDED' });
1194
+ }
1195
+ }
1196
+ else if (this.runBudget && !this.runBudget.mayStartDiscovery(softRoute)) {
1197
+ throw Object.assign(new Error(`The ${Math.round(this.runBudget.softTargetMs(softRoute) / 1_000)}-second soft target elapsed before this provider dispatch could start.`), { code: 'RUN_SOFT_TARGET_EXCEEDED' });
1198
+ }
1199
+ this.recordDispatchStart();
1200
+ if (this.receipts.length >= this.policy.total) {
1201
+ throw Object.assign(new Error(`Run-wide provider dispatch budget exhausted after ${this.policy.total} physical attempts.`), { code: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED' });
1202
+ }
1203
+ const phaseCount = this.phaseCounts.get(context.dispatchPhase) ?? 0;
1204
+ const phaseLimit = context.dispatchPhase === 'meaning_resolution'
1205
+ ? this.policy.meaningResolution
1206
+ : context.dispatchPhase === 'repair'
1207
+ ? this.policy.repair
1208
+ : context.dispatchPhase === 'narration'
1209
+ ? this.policy.narration
1210
+ : this.policy.generationGroup;
1211
+ // Planning and generation share one ledger because they compete for the
1212
+ // same "work out the query" budget. Narration does not: it is the step that
1213
+ // turns a settled result into an answer, and starving it produces a run
1214
+ // that computed the right numbers and then could not say them.
1215
+ const generationGroupCount = this.receipts.filter((receipt) => receipt.dispatchPhase === 'planning'
1216
+ || receipt.dispatchPhase === 'generation').length;
1217
+ if (phaseCount >= phaseLimit || ((context.dispatchPhase === 'planning' || context.dispatchPhase === 'generation')
1218
+ && generationGroupCount >= this.policy.generationGroup)) {
1219
+ throw Object.assign(new Error(`Provider dispatch budget exhausted for ${context.dispatchPhase} after ${phaseLimit} physical attempts.`), { code: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED' });
1220
+ }
1221
+ const envelope = prepareProviderWireEnvelopeForDispatch(event.provider, event.envelope);
1222
+ const projectedRowCount = context.serializedResultShape?.resultRowCount ?? 0;
1223
+ const projectedCumulativeRowCount = context.cumulativeResultRowCount ?? projectedRowCount;
1224
+ // Ordinary Ask narration reads its ceiling from the resolved project policy;
1225
+ // Research keeps its own explicit opt-in limits.
1226
+ const permittedRowLimit = context.purpose === 'answer_narration'
1227
+ ? this.rowEgress.maxNarrationRows
1228
+ : context.optIn && context.purpose === 'research_narration'
1229
+ ? 20
1230
+ : context.optIn && context.purpose === 'research_tool'
1231
+ ? 200
1232
+ : 0;
1233
+ if (projectedRowCount > permittedRowLimit || projectedCumulativeRowCount > permittedRowLimit) {
1234
+ throw Object.assign(new Error(permittedRowLimit === 0
1235
+ ? 'Provider egress blocked result rows without explicit Research run consent.'
1236
+ : `Provider egress blocked result rows beyond the cumulative Research limit of ${permittedRowLimit}.`), { code: permittedRowLimit === 0 ? 'PROVIDER_RESULT_ROWS_BLOCKED' : 'PROVIDER_RESULT_ROWS_LIMIT_EXCEEDED' });
1237
+ }
1238
+ assertProviderPayloadAllowed(envelope, {
1239
+ allowResultRows: false,
1240
+ maxResultRows: 0,
1241
+ purpose: context.purpose,
1242
+ });
1243
+ const rowCount = projectedRowCount;
1244
+ this.receipts.push(createProviderDispatchEgressReceipt({
1245
+ purpose: context.purpose,
1246
+ dispatchPhase: context.dispatchPhase,
1247
+ provider: event.provider,
1248
+ ...(event.model ? { model: event.model } : {}),
1249
+ operation: event.operation,
1250
+ attemptIndex: event.attemptIndex,
1251
+ options: event.options,
1252
+ permittedCategories: rowCount > 0
1253
+ ? ['instructions', 'question', 'schema_metadata', 'governed_context', 'result_rows']
1254
+ : ['instructions', 'question', 'schema_metadata', 'governed_context'],
1255
+ optIn: context.optIn && rowCount > 0,
1256
+ envelope,
1257
+ ...(context.serializedResultShape ? { serializedResultShape: context.serializedResultShape } : {}),
1258
+ ...(typeof context.cumulativeResultRowCount === 'number'
1259
+ ? { cumulativeResultRowCount: context.cumulativeResultRowCount }
1260
+ : {}),
1261
+ }));
1262
+ this.phaseCounts.set(context.dispatchPhase, phaseCount + 1);
1263
+ return envelope;
1264
+ }
1265
+ snapshot(fallbackReason = 'none') {
1266
+ return {
1267
+ providerEgressReceipts: [...this.receipts],
1268
+ providerRoundTrips: this.receipts.length,
1269
+ toolCalls: 0,
1270
+ sqlExecutions: 0,
1271
+ repairs: 0,
1272
+ fallbackReason,
1273
+ };
1274
+ }
1275
+ }
1276
+ const agentRunProviderEvidenceContext = new AsyncLocalStorage();
1277
+ function mergeRunScopedProviderDispatchEvidence(run, evidence) {
1278
+ const providerEgressReceipts = [...evidence.providerEgressReceipts];
1279
+ const elapsed = Math.max(0, Date.parse(run.completedAt) - Date.parse(run.startedAt));
1280
+ const telemetry = {
1281
+ version: 1,
1282
+ stageDurationsMs: {
1283
+ ...(run.telemetry?.stageDurationsMs ?? {}),
1284
+ total: Number.isFinite(elapsed) ? Math.min(86_400_000, elapsed) : 0,
1285
+ },
1286
+ providerRoundTrips: evidence.providerRoundTrips,
1287
+ toolCalls: run.telemetry?.toolCalls ?? evidence.toolCalls,
1288
+ sqlExecutions: run.telemetry?.sqlExecutions ?? evidence.sqlExecutions,
1289
+ repairs: run.telemetry?.repairs ?? evidence.repairs,
1290
+ egressReceipts: providerEgressReceipts.length,
1291
+ ...(run.telemetry?.warehouseDurationMs !== undefined
1292
+ ? { warehouseDurationMs: run.telemetry.warehouseDurationMs }
1293
+ : {}),
1294
+ ...(run.telemetry?.fallbackReason
1295
+ ? { fallbackReason: run.telemetry.fallbackReason }
1296
+ : evidence.fallbackReason !== 'none'
1297
+ ? { fallbackReason: evidence.fallbackReason }
1298
+ : {}),
1299
+ };
1300
+ const diagnosticReceipt = run.diagnosticReceipt
1301
+ ? { ...run.diagnosticReceipt, providerEgressReceipts }
1302
+ : undefined;
1303
+ const diagnosticReceiptV2 = {
1304
+ version: 2,
1305
+ runId: run.id,
1306
+ route: run.route,
1307
+ status: run.status,
1308
+ telemetry,
1309
+ providerEgressReceiptFingerprints: providerEgressReceipts.map((receipt) => executionFingerprint(stableExecutionValue(receipt))),
1310
+ ...(run.diagnosticReceiptV2?.repairCapabilityFingerprint
1311
+ ? { repairCapabilityFingerprint: run.diagnosticReceiptV2.repairCapabilityFingerprint }
1312
+ : {}),
1313
+ };
1314
+ const artifacts = run.artifacts.map((artifact) => {
1315
+ if (!artifact.payload || typeof artifact.payload !== 'object' || Array.isArray(artifact.payload))
1316
+ return artifact;
1317
+ const payload = artifact.payload;
1318
+ if (!('diagnosticReceipt' in payload) && !('diagnosticReceiptV2' in payload))
1319
+ return artifact;
1320
+ return {
1321
+ ...artifact,
1322
+ payload: {
1323
+ ...payload,
1324
+ ...(diagnosticReceipt ? { diagnosticReceipt } : {}),
1325
+ diagnosticReceiptV2,
1326
+ },
1327
+ };
1328
+ });
1329
+ return {
1330
+ ...run,
1331
+ artifacts,
1332
+ providerEgressReceipts,
1333
+ telemetry,
1334
+ ...(diagnosticReceipt ? { diagnosticReceipt } : {}),
1335
+ diagnosticReceiptV2,
1336
+ };
1337
+ }
927
1338
  export async function startLocalServer(opts) {
928
1339
  const { rootDir, executor, connection: rawConnection, preferredPort, projectRoot = process.cwd() } = opts;
929
1340
  const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
@@ -945,6 +1356,11 @@ export async function startLocalServer(opts) {
945
1356
  if (gitRoot)
946
1357
  ensureLocalRuntimeGitignore(projectRoot);
947
1358
  let projectConfig = loadProjectConfig(projectRoot);
1359
+ const analyticalExecutionService = new ExecutionService({
1360
+ executor,
1361
+ projectRoot,
1362
+ projectConfig: () => projectConfig,
1363
+ });
948
1364
  recordAgentRuntimeVersion(projectRoot, runtimeVersion);
949
1365
  // A clone carries governed Hint Graph files in Git, never the rebuildable
950
1366
  // `.dql/cache` database. Materialize that projection as soon as the Notebook
@@ -1566,23 +1982,81 @@ export async function startLocalServer(opts) {
1566
1982
  };
1567
1983
  // Provider-backed narration for stakeholder stories. Reuses the same provider
1568
1984
  // adapter as the planner; narrateResult always returns (deterministic fallback).
1569
- const narrateForAgentRun = async (input) => narrateResult(input, {
1570
- complete: async ({ system, user, signal }) => {
1571
- const provider = await createBlockStudioAssistProvider(projectRoot);
1572
- if (!provider)
1573
- throw new Error('No AI provider configured for narration.');
1574
- return provider.generate([{ role: 'system', content: system }, { role: 'user', content: user }], { maxTokens: 600, temperature: 0.2, signal });
1575
- },
1576
- });
1985
+ const narrateForAgentRun = async (input, allowProviderResultRows = false) => {
1986
+ // Without caller consent, keep narration deterministic and make no physical
1987
+ // provider dispatch. With consent, serialize only the bounded, redacted
1988
+ // sample the transport receipt will account for — bounded by the project's
1989
+ // own egress policy, so an admin kill-switch is not silently bypassed here.
1990
+ if (!allowProviderResultRows || !input.result)
1991
+ return narrateResult(input);
1992
+ const narrationRowEgress = resolveProviderResultRowEgressPolicy({
1993
+ projectSetting: projectConfig?.agent?.providerResultRowEgress,
1994
+ });
1995
+ if (narrationRowEgress.maxNarrationRows === 0)
1996
+ return narrateResult(input);
1997
+ const safeResult = {
1998
+ ...input.result,
1999
+ rows: redactProviderResultRows(input.result.rows, narrationRowEgress.maxNarrationRows),
2000
+ };
2001
+ const safeInput = {
2002
+ ...input,
2003
+ result: safeResult,
2004
+ };
2005
+ return narrateResult(safeInput, {
2006
+ complete: async ({ system, user, signal }) => {
2007
+ const provider = await createBlockStudioAssistProvider(projectRoot);
2008
+ if (!provider)
2009
+ throw new Error('No AI provider configured for narration.');
2010
+ return provider.generate([{ role: 'system', content: system }, { role: 'user', content: user }], {
2011
+ maxTokens: 600,
2012
+ temperature: 0.2,
2013
+ signal,
2014
+ maxProviderDispatches: 2,
2015
+ ...(agentRunProviderEvidenceContext.getStore() ? {
2016
+ // This lane narrates App/notebook results, not Research. It used to
2017
+ // declare `research_narration` to clear the row check, which made
2018
+ // receipt analytics conflate two different lanes.
2019
+ onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
2020
+ purpose: 'answer_narration',
2021
+ dispatchPhase: 'narration',
2022
+ optIn: true,
2023
+ serializedResultShape: {
2024
+ resultRowCount: safeResult.rows.length,
2025
+ columnCount: safeResult.columns.length,
2026
+ },
2027
+ cumulativeResultRowCount: safeResult.rows.length,
2028
+ }),
2029
+ } : {}),
2030
+ });
2031
+ },
2032
+ });
2033
+ };
1577
2034
  async function runGovernedAgentAnswerForRun(request, repair, route = 'generated_answer', onProgress, routeDecision) {
1578
2035
  const governed = resolveGovernedAnswerRunner(projectRoot);
1579
- const resolvedProvider = governed?.provider ?? null;
1580
- const runner = governed?.runner ?? null;
2036
+ let resolvedProvider = governed?.provider ?? null;
2037
+ let runner = governed?.runner ?? null;
2038
+ const exactProviderFreePlan = routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative'
2039
+ && (routeDecision.resolvedAnalyticalPlan.capability === 'certified_execution'
2040
+ || routeDecision.resolvedAnalyticalPlan.capability === 'semantic_execution');
2041
+ if ((!resolvedProvider || !runner) && exactProviderFreePlan) {
2042
+ const deterministicProvider = {
2043
+ name: 'ollama',
2044
+ available: async () => true,
2045
+ generate: async () => {
2046
+ throw Object.assign(new Error('Exact certified/semantic execution must not dispatch a provider.'), {
2047
+ code: 'EXACT_ROUTE_PROVIDER_DISPATCH_FORBIDDEN',
2048
+ });
2049
+ },
2050
+ };
2051
+ resolvedProvider = 'ollama';
2052
+ runner = createDqlAgentProviderRunner('ollama', deterministicProvider);
2053
+ }
1581
2054
  if (!resolvedProvider || !runner) {
1582
2055
  throw new Error('No AI provider is configured. Configure a subscription (Claude Code / Codex), OpenAI, Gemini, Ollama, or a custom OpenAI-compatible endpoint in Settings.');
1583
2056
  }
1584
2057
  let governedAnswer;
1585
2058
  let providerError;
2059
+ let providerDispatchEvidence;
1586
2060
  const isRepair = (repair?.attempt ?? 0) > 0 && Boolean(repair?.repairHint);
1587
2061
  // The chat composer sends a `thinkingMode` (auto/low/medium/high); resolve it
1588
2062
  // into the effort+depth bundle it stands for. An explicit `reasoningEffort` /
@@ -1642,6 +2116,9 @@ export async function startLocalServer(opts) {
1642
2116
  const semanticTableMapping = semanticLayer && semanticConnection
1643
2117
  ? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer, projectRoot, semanticConnectionName)
1644
2118
  : undefined;
2119
+ const generatedProposalTargetIdentity = semanticConnection
2120
+ ? await observeWarehouseTargetIdentity(executor, semanticConnection)
2121
+ : undefined;
1645
2122
  const requestedDomain = normalizeAgentRunDomain(agentRunWorkspaceValue(request, 'domain'));
1646
2123
  const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
1647
2124
  const requestedModelAreaId = agentRunWorkspaceValue(request, 'modelAreaId');
@@ -1659,6 +2136,9 @@ export async function startLocalServer(opts) {
1659
2136
  : undefined;
1660
2137
  await runner.run({
1661
2138
  provider: resolvedProvider,
2139
+ ...(agentRunProviderEvidenceContext.getStore()
2140
+ ? { providerDispatchEvidenceSink: agentRunProviderEvidenceContext.getStore() }
2141
+ : {}),
1662
2142
  messages: [
1663
2143
  // No-thread fallback path: bound the raw client history so follow-ups
1664
2144
  // cannot inflate every provider call (8 turns × 4k chars).
@@ -1678,6 +2158,8 @@ export async function startLocalServer(opts) {
1678
2158
  },
1679
2159
  reasoningEffort,
1680
2160
  ...(analysisDepth ? { analysisDepth } : {}),
2161
+ allowProviderSemanticMemberSelection: route === 'research',
2162
+ researchResultRowsOptIn: route === 'research' && request.researchResultRowsOptIn === true,
1681
2163
  projectRoot,
1682
2164
  preparedContextPack: preparedAgentContextPacks.get(request),
1683
2165
  domainContext,
@@ -1799,9 +2281,20 @@ export async function startLocalServer(opts) {
1799
2281
  : projectConfig.dbt?.projectDir;
1800
2282
  throw new SemanticRuntimeRequiredError(await explainMissingSemanticRuntime(projectRoot, dbtProjectPath));
1801
2283
  }
1802
- compiledSemanticQueries.set(executionFingerprint(compiled.sql), compiled);
2284
+ // The semantic bridge freezes the compiler output after trimming
2285
+ // transport-only surrounding whitespace. Bind the compiler receipt
2286
+ // and execution lookup to that exact consumed SQL identity.
2287
+ const governedCompiledSql = compiled.sql.trim();
2288
+ compiledSemanticQueries.set(executionFingerprint(governedCompiledSql), compiled);
2289
+ const aggregationCompilerReceipt = buildSemanticAggregationCompilerReceipt({
2290
+ semanticLayer: semanticLayer,
2291
+ metricNames: compiled.effectiveRequest.metrics,
2292
+ compiledSql: governedCompiledSql,
2293
+ plan: routeDecision?.resolvedAnalyticalPlan,
2294
+ targetFingerprint: targetBinding?.executionTarget.identityFingerprint,
2295
+ });
1803
2296
  return {
1804
- sql: compiled.sql,
2297
+ sql: governedCompiledSql,
1805
2298
  engine: compiled.engine,
1806
2299
  selection: {
1807
2300
  ...selection,
@@ -1814,7 +2307,9 @@ export async function startLocalServer(opts) {
1814
2307
  trace: {
1815
2308
  ...compiled.semanticTrace,
1816
2309
  ...(targetBinding ? { targetBinding } : {}),
2310
+ ...(aggregationCompilerReceipt ? { aggregationCompilerReceipt } : {}),
1817
2311
  },
2312
+ ...(aggregationCompilerReceipt ? { aggregationCompilerReceipt } : {}),
1818
2313
  };
1819
2314
  },
1820
2315
  } : {}),
@@ -1827,6 +2322,9 @@ export async function startLocalServer(opts) {
1827
2322
  ...(routeDecision?.resolvedAnalyticalPlan
1828
2323
  ? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
1829
2324
  : {}),
2325
+ ...(generatedProposalTargetIdentity?.identityFingerprint
2326
+ ? { generatedProposalTargetFingerprint: generatedProposalTargetIdentity.identityFingerprint }
2327
+ : {}),
1830
2328
  analyticalReferenceInstant: new Date().toISOString(),
1831
2329
  executeCertifiedBlock: (node, invocation) => executeCertifiedBlockForAgent(node, invocation, semanticConnection, semanticConnectionName),
1832
2330
  executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName),
@@ -1843,10 +2341,11 @@ export async function startLocalServer(opts) {
1843
2341
  }
1844
2342
  if (turn.kind === 'error') {
1845
2343
  providerError = turn.message;
2344
+ providerDispatchEvidence = turn.dispatchEvidence;
1846
2345
  }
1847
2346
  }, runSignal);
1848
2347
  if (!governedAnswer) {
1849
- throw new Error(providerError ?? 'The AI provider did not return a governed answer.');
2348
+ throw Object.assign(new Error(providerError ?? 'The AI provider did not return a governed answer.'), ...(providerDispatchEvidence ? [{ providerDispatchEvidence }] : []));
1850
2349
  }
1851
2350
  return governedAnswer;
1852
2351
  }
@@ -2073,7 +2572,11 @@ export async function startLocalServer(opts) {
2073
2572
  surface: 'ask_ai',
2074
2573
  });
2075
2574
  const certified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
2076
- const semantic = governedAnswer.route?.tier === 'semantic_metric';
2575
+ // A semantic route is governed only when its route-specific aggregation
2576
+ // proof passed. The route label is not authority: treating a blocked or
2577
+ // missing proof as governed caused the CLI to persist a successful badge
2578
+ // even though the answer artifact recorded insufficient context.
2579
+ const semantic = semanticAnswerHasPassedAggregationProof(governedAnswer);
2077
2580
  governedAnswer.dqlArtifact = {
2078
2581
  ...artifact,
2079
2582
  ...(invocation.parameters.length > 0 ? { parameters: invocation.parameters } : {}),
@@ -2082,11 +2585,19 @@ export async function startLocalServer(opts) {
2082
2585
  trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
2083
2586
  ...(governedAnswer.result?.sql ? { compiledSql: governedAnswer.result.sql } : originalSql ? { compiledSql: originalSql } : {}),
2084
2587
  ...(governedAnswer.result?.executionReceipt ? { executionReceipt: governedAnswer.result.executionReceipt } : {}),
2588
+ ...(governedAnswer.result?.executableArtifact ? {
2589
+ executableArtifact: {
2590
+ ...governedAnswer.result.executableArtifact,
2591
+ kind: artifact.kind,
2592
+ trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
2593
+ },
2594
+ } : {}),
2085
2595
  };
2086
2596
  if (governedAnswer.result)
2087
2597
  delete governedAnswer.result.dqlArtifact;
2088
2598
  }
2089
2599
  const answerRunExecutor = async ({ request, route, routeDecision, attempt, repairHint, emit }) => {
2600
+ const runStartedAtMs = Date.now();
2090
2601
  let governedAnswer;
2091
2602
  try {
2092
2603
  governedAnswer = await runGovernedAgentAnswerForRun(request, { attempt, repairHint }, route, (message) => emit({ type: 'executor.started', message, route }), routeDecision);
@@ -2222,39 +2733,67 @@ export async function startLocalServer(opts) {
2222
2733
  // Deadline/cancellation propagates to the engine, which renders the
2223
2734
  // graceful "bounded execution deadline" outcome — never a provider error.
2224
2735
  rethrowIfCancelled(error, request.signal);
2225
- const message = formatAgentRunInfrastructureError(error, 'AI answer provider');
2736
+ const dispatchEvidence = providerDispatchTerminalEvidence(error);
2737
+ const dispatchBudgetExhausted = Boolean((error && typeof error === 'object'
2738
+ && error.code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED')
2739
+ || dispatchEvidence?.fallbackReason === 'orchestration_budget_exhausted');
2740
+ const message = dispatchBudgetExhausted
2741
+ ? 'Ask reached its internal provider-dispatch limit before it froze one executable analytical plan. Nothing was run. Narrow the metric or dimension and retry; this is an orchestration-budget diagnostic, not a provider outage.'
2742
+ : formatAgentRunInfrastructureError(error, 'AI answer provider');
2226
2743
  return {
2227
2744
  summary: message,
2228
2745
  status: 'blocked',
2229
2746
  trustState: 'blocked',
2230
2747
  stopReason: 'blocked',
2231
- artifacts: [agentRunArtifact('answer', 'AI answer provider failed', {
2748
+ artifacts: [agentRunArtifact('answer', dispatchBudgetExhausted ? 'Internal orchestration budget exhausted' : 'AI answer provider failed', {
2232
2749
  kind: 'no_answer',
2233
- refusalCode: 'provider_error',
2750
+ refusalCode: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'provider_error',
2234
2751
  text: message,
2235
2752
  answer: message,
2236
2753
  ...(routeDecision?.resolvedAnalyticalPlan
2237
2754
  ? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
2238
2755
  : {}),
2239
2756
  providerFailure: {
2240
- code: 'AI_PROVIDER_FAILURE',
2757
+ code: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'AI_PROVIDER_FAILURE',
2241
2758
  message,
2242
- recoverable: true,
2759
+ recoverable: !dispatchBudgetExhausted,
2243
2760
  },
2244
2761
  }, undefined, 'blocked')],
2245
2762
  evaluations: [
2246
2763
  agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.'),
2247
- agentRunEvaluation('ai-provider', 'AI provider', false, 'blocking', message, { originalErrorType: error instanceof Error ? error.name : typeof error }),
2764
+ agentRunEvaluation(dispatchBudgetExhausted ? 'orchestration-budget' : 'ai-provider', dispatchBudgetExhausted ? 'Internal orchestration budget' : 'AI provider', false, 'blocking', message, { originalErrorType: error instanceof Error ? error.name : typeof error }),
2248
2765
  ],
2249
2766
  nextActions: [
2250
- { id: 'retry-ask-after-provider', label: 'Retry after provider setup', route: 'generated_answer' },
2251
- { id: 'research-without-answer', label: 'Research with available metadata', route: 'research', artifactKind: 'research_run' },
2767
+ ...(dispatchBudgetExhausted
2768
+ ? [{ id: 'narrow-and-retry-ask', label: 'Narrow the metric or dimension and retry', route: 'generated_answer' }]
2769
+ : [
2770
+ { id: 'retry-ask-after-provider', label: 'Retry after provider setup', route: 'generated_answer' },
2771
+ { id: 'research-without-answer', label: 'Research with available metadata', route: 'research', artifactKind: 'research_run' },
2772
+ ]),
2252
2773
  ],
2774
+ ...(dispatchEvidence?.providerEgressReceipts.length
2775
+ ? { providerEgressReceipts: dispatchEvidence.providerEgressReceipts }
2776
+ : {}),
2777
+ telemetry: {
2778
+ version: 1,
2779
+ stageDurationsMs: { total: Date.now() - runStartedAtMs },
2780
+ providerRoundTrips: dispatchEvidence?.providerRoundTrips ?? 0,
2781
+ toolCalls: dispatchEvidence?.toolCalls ?? 0,
2782
+ sqlExecutions: dispatchEvidence?.sqlExecutions ?? 0,
2783
+ repairs: Math.max(attempt, dispatchEvidence?.repairs ?? 0),
2784
+ egressReceipts: dispatchEvidence?.providerEgressReceipts.length ?? 0,
2785
+ fallbackReason: dispatchBudgetExhausted
2786
+ ? 'provider_dispatch_budget_exhausted'
2787
+ : dispatchEvidence?.fallbackReason ?? 'provider_error',
2788
+ },
2253
2789
  };
2254
2790
  }
2255
2791
  const answeredRoute = resolvedRunRouteFromAnswer(governedAnswer) ?? route;
2256
2792
  const isCertified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
2257
- const isSemantic = governedAnswer.route?.tier === 'semantic_metric';
2793
+ const semanticRouteClaimed = governedAnswer.route?.tier === 'semantic_metric';
2794
+ const semanticAggregationProofPassed = semanticAnswerHasPassedAggregationProof(governedAnswer);
2795
+ const semanticAggregationBlocked = semanticRouteClaimed && !semanticAggregationProofPassed;
2796
+ const isSemantic = semanticRouteClaimed && semanticAggregationProofPassed;
2258
2797
  const requestedNotebookDataset = findMentionedNotebookDataset(request.question, datasetWorkspace.list());
2259
2798
  const governedAssetMissesLocalSource = Boolean(requestedNotebookDataset && (isCertified
2260
2799
  || isSemantic
@@ -2271,7 +2810,8 @@ export async function startLocalServer(opts) {
2271
2810
  // governance boundary, not an ambiguous user question and not a repairable
2272
2811
  // retrieval miss. Keep the precise policy detail visible, but never burn two
2273
2812
  // more provider calls retrying the same incompatible candidate.
2274
- const isPolicyBlocked = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked';
2813
+ const isPolicyBlocked = (governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked')
2814
+ || semanticAggregationBlocked;
2275
2815
  const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure || isPolicyBlocked;
2276
2816
  // The model tried to compose a governed query and declined despite having usable
2277
2817
  // context (e.g. it wasn't confident about a multi-table join). The answer loop
@@ -2290,48 +2830,137 @@ export async function startLocalServer(opts) {
2290
2830
  // reply ran context-free and was clarified again: the reported clarify loop.
2291
2831
  const resolvedRoute = needsClarification ? 'clarify' : answeredRoute;
2292
2832
  const sql = governedAnswer.proposedSql ?? governedAnswer.sql;
2293
- const runnableSql = governedAnswer.kind === 'no_answer' || isExecutionFailure || (isExploratory && !governedAnswer.result)
2833
+ const runnableSql = governedAnswer.kind === 'no_answer'
2834
+ || isExecutionFailure
2835
+ || semanticAggregationBlocked
2836
+ || (isExploratory && !governedAnswer.result)
2294
2837
  ? undefined
2295
2838
  : sql;
2296
- // Render executed rows deterministically for ordinary lookups. A second LLM
2297
- // call is reserved for an explicit research route; certified, semantic, and
2298
- // generated lookup answers must not pay another provider round-trip merely
2299
- // to restate values the host already has.
2839
+ // Narrate any run that actually produced values. This used to be gated on
2840
+ // `requestedMode === 'research'`, and because the UI sends ordinary Ask as
2841
+ // `'auto'`, every normal question skipped synthesis and shipped the answer
2842
+ // loop's deterministic fact-join as the primary answer — the reported
2843
+ // `column: value` dump. A model that never sees the result cannot describe
2844
+ // it, so a bounded, redacted row sample goes with the request unless a
2845
+ // project admin has switched row egress off.
2300
2846
  let synthesizedAnswer;
2301
- if (shouldSynthesizeAgentRunAnswer(governedAnswer)) {
2847
+ const providerEgressReceipts = [
2848
+ ...(governedAnswer.providerEgressReceipts ?? []),
2849
+ ];
2850
+ let narrationDurationMs = 0;
2851
+ const researchRowsOptIn = route === 'research' && request.researchResultRowsOptIn === true;
2852
+ const rowEgress = resolveProviderResultRowEgressPolicy({
2853
+ projectSetting: projectConfig?.agent?.providerResultRowEgress,
2854
+ researchOptIn: researchRowsOptIn,
2855
+ });
2856
+ // `createBlockStudioAssistProvider` returns null when no AI provider is
2857
+ // configured; a narration failure must never take the governed answer down.
2858
+ const narrationProvider = await createBlockStudioAssistProvider(projectRoot).catch(() => null);
2859
+ const narrationPlan = planAgentRunNarration(governedAnswer, {
2860
+ requestedMode: request.requestedMode,
2861
+ providerAvailable: Boolean(narrationProvider),
2862
+ rowEgress,
2863
+ });
2864
+ const narrationMaxRows = narrationPlan.mode === 'skip' ? 0 : narrationPlan.maxRows;
2865
+ const preview = agentResultToSynthesisPreview(governedAnswer.result);
2866
+ const providerPreview = preview
2867
+ ? { ...preview, rows: redactProviderResultRows(preview.rows, narrationMaxRows) }
2868
+ : undefined;
2869
+ let narrationSource;
2870
+ let narrationValidationFailures = [];
2871
+ if (narrationPlan.mode !== 'skip' && narrationProvider) {
2872
+ const narrationStartedAtMs = Date.now();
2873
+ const draft = governedAnswer.answer ?? governedAnswer.text;
2874
+ const observeNarrationDispatch = (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
2875
+ purpose: 'answer_narration',
2876
+ dispatchPhase: 'narration',
2877
+ optIn: narrationMaxRows > 0,
2878
+ serializedResultShape: {
2879
+ resultRowCount: providerPreview?.rows.length ?? 0,
2880
+ columnCount: providerPreview?.columns.length ?? 0,
2881
+ },
2882
+ });
2883
+ const narrationDispatchOptions = () => ({
2884
+ maxTokens: 350,
2885
+ temperature: 0.3,
2886
+ maxProviderDispatches: 2,
2887
+ ...(agentRunProviderEvidenceContext.getStore()
2888
+ ? { onProviderDispatch: observeNarrationDispatch }
2889
+ : {}),
2890
+ });
2302
2891
  try {
2303
- const provider = route === 'research'
2304
- ? await createBlockStudioAssistProvider(projectRoot)
2305
- : null;
2306
- const preview = agentResultToSynthesisPreview(governedAnswer.result);
2307
- const draft = governedAnswer.answer ?? governedAnswer.text;
2308
- const result = await synthesizeAnswer({
2309
- question: request.question,
2310
- category: routeDecision?.category,
2311
- // The primary Ask reply is always business-facing. Analysts keep
2312
- // the full DQL, SQL, lineage, gates, and grain in the inspector.
2313
- audience: 'stakeholder',
2314
- resultPreview: preview,
2315
- sql: sql,
2316
- draftText: draft,
2317
- gaps: businessNarrativeGaps(governedAnswer.validationWarnings),
2318
- rankingDirection: governedAnswer.contextPack?.questionPlan.requestedShape.rankingDirection,
2319
- // Governed display contract: metric/measure displayFormat metadata
2320
- // beats column-name guessing, so $ and decimals stay consistent
2321
- // between the narration and the table for names like total_bcm.
2322
- ...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
2323
- }, provider
2324
- ? {
2325
- complete: ({ system, user, signal, onDelta }) => streamOrGenerate(provider, [{ role: 'system', content: system }, { role: 'user', content: user }], { maxTokens: 350, temperature: 0.3, signal }, onDelta ?? (() => { })),
2326
- }
2327
- : {});
2328
- if (result.text)
2329
- synthesizedAnswer = result.text;
2892
+ const frame = governedAnswer.resolvedAnalyticalPlan?.analyticalFrame;
2893
+ if (narrationPlan.mode === 'verified_facts' && governedAnswer.analyticalFacts && frame) {
2894
+ // The fact set is the grounding contract: the model drafts, and every
2895
+ // claim is resolved against a fact id before it can reach the user.
2896
+ const composed = await composeVerifiedAnalyticalNarrative({
2897
+ frame,
2898
+ factSet: governedAnswer.analyticalFacts,
2899
+ question: request.question,
2900
+ maxRows: narrationPlan.maxRows,
2901
+ complete: async ({ system, user }) => streamOrGenerate(narrationProvider, [{ role: 'system', content: system }, { role: 'user', content: user }], narrationDispatchOptions(), () => { }),
2902
+ });
2903
+ narrationSource = composed.source;
2904
+ narrationValidationFailures = composed.validationFailures;
2905
+ synthesizedAnswer = composed.source === 'llm'
2906
+ ? composed.narrative.text
2907
+ // A verification failure is not silent: the deterministic join is a
2908
+ // fallback, and the reader is told so rather than handed a dump that
2909
+ // looks like it was written on purpose.
2910
+ : `_Verified narration was unavailable, so this is the deterministic result record._\n\n${composed.narrative.text}`;
2911
+ }
2912
+ else {
2913
+ const result = await synthesizeAnswer({
2914
+ question: request.question,
2915
+ category: routeDecision?.category,
2916
+ // The primary Ask reply is always business-facing. Analysts keep
2917
+ // the full DQL, SQL, lineage, gates, and grain in the inspector.
2918
+ audience: 'stakeholder',
2919
+ resultPreview: providerPreview ?? preview,
2920
+ sql: sql,
2921
+ draftText: draft,
2922
+ gaps: businessNarrativeGaps(governedAnswer.validationWarnings),
2923
+ rankingDirection: governedAnswer.contextPack?.questionPlan.requestedShape.rankingDirection,
2924
+ // Governed display contract: metric/measure displayFormat metadata
2925
+ // beats column-name guessing, so $ and decimals stay consistent
2926
+ // between the narration and the table for names like total_bcm.
2927
+ ...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
2928
+ }, {
2929
+ complete: ({ system, user, signal, onDelta }) => streamOrGenerate(narrationProvider, [{ role: 'system', content: system }, { role: 'user', content: user }], { ...narrationDispatchOptions(), signal }, onDelta ?? (() => { })),
2930
+ });
2931
+ narrationSource = result.source;
2932
+ if (result.text)
2933
+ synthesizedAnswer = result.text;
2934
+ }
2330
2935
  }
2331
2936
  catch {
2332
- // Keep the governed draft on any synthesis failure.
2937
+ // Keep the governed draft on any narration failure.
2333
2938
  synthesizedAnswer = undefined;
2334
2939
  }
2940
+ finally {
2941
+ narrationDurationMs = Date.now() - narrationStartedAtMs;
2942
+ }
2943
+ }
2944
+ // Every run accounts for its narration egress, including the runs that did
2945
+ // not narrate — "no receipt" must never be ambiguous between "no rows left
2946
+ // the host" and "nobody looked".
2947
+ {
2948
+ const narrated = narrationSource !== undefined;
2949
+ const rowsSent = narrated ? providerPreview?.rows.length ?? 0 : 0;
2950
+ providerEgressReceipts.push(createProviderEgressReceipt({
2951
+ purpose: 'answer_narration',
2952
+ provider: narrated ? narrationProvider?.name ?? 'none' : 'none',
2953
+ permittedCategories: rowsSent > 0
2954
+ ? ['instructions', 'question', 'governed_context', 'result_rows']
2955
+ : ['instructions', 'question', 'governed_context'],
2956
+ optIn: rowsSent > 0,
2957
+ redactionPolicyId: rowEgress.policyId,
2958
+ payload: narrated
2959
+ ? { question: request.question, resultPreview: rowsSent > 0 ? providerPreview : undefined }
2960
+ : { resultRows: 0, providerDisabled: !narrationProvider },
2961
+ resultRowCount: rowsSent,
2962
+ columnCount: narrated ? providerPreview?.columns.length ?? 0 : 0,
2963
+ }));
2335
2964
  }
2336
2965
  // A grounding/modeling gap is a REFUSAL. It was computed above but never
2337
2966
  // reached this ternary, so it fell through to `needs_review` — the same
@@ -2415,9 +3044,21 @@ export async function startLocalServer(opts) {
2415
3044
  artifactKind: 'sql_cell',
2416
3045
  }] : []),
2417
3046
  ];
3047
+ const runDispatchEvidence = agentRunProviderEvidenceContext.getStore()?.snapshot(governedAnswer.refusalCode ?? 'none');
3048
+ const finalProviderEgressReceipts = runDispatchEvidence?.providerEgressReceipts
3049
+ ?? providerEgressReceipts;
3050
+ const finalTelemetry = agentRunTelemetryForAnswer(governedAnswer, finalProviderEgressReceipts, Date.now() - runStartedAtMs, narrationDurationMs, attempt);
3051
+ if (runDispatchEvidence) {
3052
+ finalTelemetry.providerRoundTrips = runDispatchEvidence.providerRoundTrips;
3053
+ finalTelemetry.egressReceipts = finalProviderEgressReceipts.length;
3054
+ }
2418
3055
  return {
2419
3056
  resolvedRoute,
2420
- answerRefusalCode: governedAnswer.kind === 'no_answer' ? governedAnswer.refusalCode : undefined,
3057
+ answerRefusalCode: governedAnswer.kind === 'no_answer'
3058
+ ? governedAnswer.refusalCode
3059
+ : semanticAggregationBlocked
3060
+ ? 'policy_blocked'
3061
+ : undefined,
2421
3062
  answerTier: governedAnswer.route?.tier,
2422
3063
  summary: isTerminalFailure
2423
3064
  ? analyticalFailureSummary(governedAnswer.analyticalFailure)
@@ -2477,7 +3118,9 @@ export async function startLocalServer(opts) {
2477
3118
  : needsClarification
2478
3119
  ? 'The answer loop needs more context before producing a governed answer.'
2479
3120
  : isPolicyBlocked
2480
- ? `DQL blocked the generated path under its relationship policy: ${governedAnswer.refusalDetails?.message ?? 'the relationship is not authorized for this analysis.'}`
3121
+ ? semanticAggregationBlocked
3122
+ ? `DQL blocked semantic execution because its exact aggregation proof did not pass: ${governedAnswer.aggregationSafetyProof?.issueCodes.join(', ') || 'required proof evidence is missing.'}`
3123
+ : `DQL blocked the generated path under its relationship policy: ${governedAnswer.refusalDetails?.message ?? 'the relationship is not authorized for this analysis.'}`
2481
3124
  : isExploratory
2482
3125
  ? 'The answer used bounded DBT/schema evidence because no certified modeled relationship path covered the request. It is review-required and was not certified.'
2483
3126
  : 'The answer is generated or semantic-layer backed and remains review-required.', governedAnswer.route),
@@ -2503,10 +3146,13 @@ export async function startLocalServer(opts) {
2503
3146
  },
2504
3147
  ] : []),
2505
3148
  ...(isPolicyBlocked ? [
2506
- agentRunEvaluation('relationship-policy', 'Relationship policy', false, 'warning', governedAnswer.refusalDetails?.message
2507
- ?? 'The selected relationship is not authorized for this analysis.', {
3149
+ agentRunEvaluation(semanticAggregationBlocked ? 'aggregation-safety-proof' : 'relationship-policy', semanticAggregationBlocked ? 'Aggregation safety proof' : 'Relationship policy', false, semanticAggregationBlocked ? 'blocking' : 'warning', semanticAggregationBlocked
3150
+ ? `Semantic execution was refused because the exact aggregation proof did not pass: ${governedAnswer.aggregationSafetyProof?.issueCodes.join(', ') || 'required proof evidence is missing.'}`
3151
+ : governedAnswer.refusalDetails?.message
3152
+ ?? 'The selected relationship is not authorized for this analysis.', {
2508
3153
  refusalCode: governedAnswer.refusalCode,
2509
3154
  refusalDetails: governedAnswer.refusalDetails,
3155
+ aggregationSafetyProof: governedAnswer.aggregationSafetyProof,
2510
3156
  validationWarnings: governedAnswer.validationWarnings,
2511
3157
  route: governedAnswer.route,
2512
3158
  }),
@@ -2516,6 +3162,8 @@ export async function startLocalServer(opts) {
2516
3162
  ] : []),
2517
3163
  ],
2518
3164
  nextActions,
3165
+ providerEgressReceipts: finalProviderEgressReceipts,
3166
+ telemetry: finalTelemetry,
2519
3167
  };
2520
3168
  };
2521
3169
  const conversationRunExecutor = async ({ request, routeDecision, emitAnswerDelta }) => {
@@ -2549,13 +3197,28 @@ export async function startLocalServer(opts) {
2549
3197
  ...(request.history ?? []).slice(-6).map((message) => ({ role: message.role, content: message.text })),
2550
3198
  { role: 'user', content: request.question },
2551
3199
  ];
2552
- text = (await provider.generate(messages, { maxTokens: 320, temperature: 0.6 })).trim();
3200
+ text = (await provider.generate(messages, {
3201
+ maxTokens: 320,
3202
+ temperature: 0.6,
3203
+ maxProviderDispatches: 2,
3204
+ ...(agentRunProviderEvidenceContext.getStore() ? {
3205
+ onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
3206
+ purpose: 'answer_generation',
3207
+ dispatchPhase: 'generation',
3208
+ optIn: false,
3209
+ }),
3210
+ } : {}),
3211
+ })).trim();
2553
3212
  // Perceived-latency: surface the reply as one delta for surfaces wired to stream.
2554
3213
  if (text)
2555
3214
  emitAnswerDelta?.(text);
2556
3215
  }
2557
3216
  }
2558
- catch {
3217
+ catch (error) {
3218
+ if (error && typeof error === 'object'
3219
+ && error.code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED') {
3220
+ throw error;
3221
+ }
2559
3222
  // Provider unavailable / errored — fall through to the deterministic reply.
2560
3223
  text = undefined;
2561
3224
  }
@@ -3094,7 +3757,7 @@ export async function startLocalServer(opts) {
3094
3757
  result: researchResultData,
3095
3758
  evidence: plan.sources,
3096
3759
  reviewRequired: true,
3097
- })
3760
+ }, request.researchResultRowsOptIn === true)
3098
3761
  : undefined;
3099
3762
  const summary = needsClarification
3100
3763
  ? 'Needs clarification before running deeper research.'
@@ -3609,7 +4272,19 @@ export async function startLocalServer(opts) {
3609
4272
  return provider.generate([
3610
4273
  { role: 'system', content: system },
3611
4274
  { role: 'user', content: user },
3612
- ], { maxTokens: 700, temperature: 0.1, signal });
4275
+ ], {
4276
+ maxTokens: 700,
4277
+ temperature: 0.1,
4278
+ signal,
4279
+ maxProviderDispatches: 2,
4280
+ ...(agentRunProviderEvidenceContext.getStore() ? {
4281
+ onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
4282
+ purpose: 'answer_generation',
4283
+ dispatchPhase: 'planning',
4284
+ optIn: false,
4285
+ }),
4286
+ } : {}),
4287
+ });
3613
4288
  },
3614
4289
  getCatalogContext: buildRankedAgentRunCatalogContext,
3615
4290
  });
@@ -3633,6 +4308,14 @@ export async function startLocalServer(opts) {
3633
4308
  // instead of leaving the UI in "Generating and validating SQL" for a
3634
4309
  // minute or more.
3635
4310
  signal: boundedAgentMeaningSignal(signal),
4311
+ maxProviderDispatches: 1,
4312
+ ...(agentRunProviderEvidenceContext.getStore() ? {
4313
+ onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
4314
+ purpose: 'answer_generation',
4315
+ dispatchPhase: 'meaning_resolution',
4316
+ optIn: false,
4317
+ }),
4318
+ } : {}),
3636
4319
  });
3637
4320
  },
3638
4321
  getEvidence: buildAgentRunEvidence,
@@ -3649,6 +4332,8 @@ export async function startLocalServer(opts) {
3649
4332
  // A run may outlive its streaming browser connection, so cancellation is
3650
4333
  // server-owned and keyed by run id rather than relying on fetch abort alone.
3651
4334
  const activeAgentRunControllers = new Map();
4335
+ const activeAnalyticalRepairReservations = new Set();
4336
+ const consumedAnalyticalRepairCapabilities = new Set();
3652
4337
  // Server-side conversation threads: persisted multi-turn state (survives refresh).
3653
4338
  // Lazy so a failed native-sqlite load never blocks the runtime; conversation
3654
4339
  // persistence degrades to the per-request context instead.
@@ -3739,14 +4424,21 @@ export async function startLocalServer(opts) {
3739
4424
  snapshotCells.push({ cellId, status: 'idle', executionCount: 0, executedAt });
3740
4425
  continue;
3741
4426
  }
3742
- const prepared = prepareLocalExecution(executableSql, activeConnection, projectRoot, projectConfig);
3743
4427
  assertAppAccess({ app, domain: resolved.domain ?? app.domain, level: 'execute' });
3744
- const rawResult = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), prepared.connection);
3745
- const result = normalizeQueryResult(rawResult);
4428
+ const execution = await analyticalExecutionService.execute({
4429
+ sql: executableSql,
4430
+ subject: 'App notebook query',
4431
+ connection: activeConnection,
4432
+ sqlParams: plan?.sqlParams,
4433
+ variables: plan?.variables,
4434
+ });
4435
+ const result = execution.result;
3746
4436
  snapshotCells.push({
3747
4437
  cellId,
3748
4438
  status: 'success',
3749
4439
  result,
4440
+ compiledSqlFingerprint: execution.compiledSqlFingerprint,
4441
+ resultFingerprint: execution.resultFingerprint,
3750
4442
  executionCount: 1,
3751
4443
  executedAt,
3752
4444
  });
@@ -3835,44 +4527,81 @@ export async function startLocalServer(opts) {
3835
4527
  const semanticError = semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message;
3836
4528
  throw analyticalError(semanticError ?? `DQL artifact "${metadata.name ?? 'draft'}" produced no executable SQL.`, { origin: 'dql_compilation', stage: 'compile' });
3837
4529
  }
3838
- const preparation = await prepareAnalyticalExecutionSql({
4530
+ const app = loadRuntimeApp(projectRoot, activePersonaAppId());
4531
+ const sourceDomain = metadata.domain ?? source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
4532
+ assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
4533
+ const pinnedSemanticCompile = semanticCompose?.sql
4534
+ ? compiledSemanticQueries.get(executionFingerprint(semanticCompose.sql))
4535
+ : undefined;
4536
+ const semanticExecutionHolder = { value: null };
4537
+ const execution = await analyticalExecutionService.execute({
3839
4538
  sql: semanticCompose?.sql ?? plan.sql,
3840
4539
  subject: 'DQL artifact query',
3841
- executor,
3842
4540
  connection: activeConnection,
3843
- projectRoot,
3844
- projectConfig,
3845
4541
  // Preserve the existing contract: previewed Ask artifacts are explicitly
3846
4542
  // read-only and bounded; saved/manual DQL execution keeps its compiler-
3847
4543
  // governed behavior when no preview bound was requested.
3848
4544
  enforceReadOnly: invocationInput?.rowLimit !== undefined,
3849
4545
  rowLimit: invocationInput?.rowLimit,
4546
+ sqlParams: plan?.sqlParams,
4547
+ variables: { ...(plan?.variables ?? {}), ...invocation.values },
4548
+ executePrepared: async (preparation) => {
4549
+ if (pinnedSemanticCompile) {
4550
+ const semanticExecution = await executeTargetBoundSemanticQuery({
4551
+ executor,
4552
+ connection: activeConnection,
4553
+ projectRoot,
4554
+ plannedAdapter: pinnedSemanticCompile.engine,
4555
+ metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
4556
+ ? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
4557
+ : undefined,
4558
+ compile: async () => pinnedSemanticCompile,
4559
+ prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
4560
+ rowBound: invocationInput?.rowLimit ?? pinnedSemanticCompile.effectiveRequest.limit,
4561
+ });
4562
+ if (semanticExecution) {
4563
+ semanticExecutionHolder.value = semanticExecution;
4564
+ return semanticExecution.result;
4565
+ }
4566
+ }
4567
+ return executor.executeQuery(preparation.executedSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), preparation.connection);
4568
+ },
3850
4569
  });
3851
- const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
4570
+ const semanticExecution = semanticExecutionHolder.value;
4571
+ const { preparation, result: normalized } = execution;
3852
4572
  const executionSql = preparation.executedSql;
3853
- const app = loadRuntimeApp(projectRoot, activePersonaAppId());
3854
- const sourceDomain = metadata.domain ?? source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
3855
- assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
3856
- const pinnedSemanticCompile = semanticCompose?.sql
3857
- ? compiledSemanticQueries.get(executionFingerprint(semanticCompose.sql))
3858
- : undefined;
3859
- const semanticExecution = pinnedSemanticCompile
3860
- ? await executeTargetBoundSemanticQuery({
3861
- executor,
3862
- connection: activeConnection,
3863
- projectRoot,
3864
- plannedAdapter: pinnedSemanticCompile.engine,
3865
- metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
3866
- ? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
3867
- : undefined,
3868
- compile: async () => pinnedSemanticCompile,
3869
- prepareSql: () => ({ sql: executionSql, connection: prepared.connection }),
3870
- rowBound: invocationInput?.rowLimit ?? pinnedSemanticCompile.effectiveRequest.limit,
3871
- })
3872
- : null;
3873
- const rawResult = semanticExecution?.result ?? await executor.executeQuery(executionSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), prepared.connection);
3874
- const normalized = normalizeQueryResult(rawResult);
3875
4573
  const executionReceipt = createDqlArtifactExecutionReceipt(source, executionSql, invocation.values, normalized);
4574
+ const executableArtifact = {
4575
+ version: 1,
4576
+ kind: precompiledSemanticQuery || semanticCompose?.sql ? 'semantic_block' : 'sql_block',
4577
+ dqlFingerprint: executionFingerprint(source),
4578
+ sourceFingerprint: executionFingerprint(stableExecutionValue({
4579
+ name: metadata.name ?? null,
4580
+ path: metadata.path ?? null,
4581
+ domain: sourceDomain ?? null,
4582
+ })),
4583
+ compiledSqlFingerprint: executionFingerprint(semanticCompose?.sql ?? plan?.sql ?? preparation.sourceSql),
4584
+ normalizedSqlFingerprint: executionFingerprint(preparation.decodedSql),
4585
+ parameterFingerprint: executionReceipt.parameterFingerprint,
4586
+ provenanceFingerprint: executionFingerprint(stableExecutionValue(invocation.resolvedParameters.map((parameter) => ({
4587
+ name: parameter.name,
4588
+ source: parameter.source,
4589
+ })))),
4590
+ targetFingerprint: targetGenerationFingerprint(activeConnection, executionConnectionName),
4591
+ snapshotFingerprint: executionFingerprint(projectSnapshot().snapshotId),
4592
+ planFingerprint: executionFingerprint(stableExecutionValue({
4593
+ sqlFingerprint: executionFingerprint(semanticCompose?.sql ?? plan?.sql ?? preparation.sourceSql),
4594
+ parameterCount: plan?.sqlParams?.length ?? 0,
4595
+ variableNames: Object.keys(plan?.variables ?? {}).sort(),
4596
+ chartConfig: plan?.chartConfig ?? null,
4597
+ })),
4598
+ semanticAdapter: pinnedSemanticCompile?.engine ?? 'native',
4599
+ previewPolicy: invocationInput?.rowLimit === undefined
4600
+ ? { mode: 'compiler_governed' }
4601
+ : { mode: 'read_only_bounded', rowLimit: invocationInput.rowLimit },
4602
+ trustState: 'review_required',
4603
+ receipt: executionReceipt,
4604
+ };
3876
4605
  return {
3877
4606
  columns: normalized.columns,
3878
4607
  rows: normalized.rows,
@@ -3885,6 +4614,7 @@ export async function startLocalServer(opts) {
3885
4614
  parameters: invocation.resolvedParameters,
3886
4615
  auditId: invocation.auditId,
3887
4616
  executionReceipt,
4617
+ executableArtifact,
3888
4618
  ...(semanticExecution ? {
3889
4619
  semanticExecutionReceipt: semanticExecution.executionReceipt,
3890
4620
  semanticTargetBinding: semanticExecution.targetBinding,
@@ -3919,6 +4649,9 @@ export async function startLocalServer(opts) {
3919
4649
  trustState: 'certified',
3920
4650
  compiledSql: result.sql,
3921
4651
  executionReceipt: result.executionReceipt,
4652
+ ...(result.executableArtifact ? {
4653
+ executableArtifact: { ...result.executableArtifact, kind: 'certified_block', trustState: 'certified' },
4654
+ } : {}),
3922
4655
  ...(invocationInput?.rowLimit ? { limit: invocationInput.rowLimit } : {}),
3923
4656
  ...(invocationInput?.parameters && Object.keys(invocationInput.parameters).length > 0
3924
4657
  ? { parameterValues: invocationInput.parameters }
@@ -3960,6 +4693,7 @@ export async function startLocalServer(opts) {
3960
4693
  ...(Object.keys(invocation.values).length > 0 ? { parameterValues: invocation.values } : {}),
3961
4694
  compiledSql: result.sql,
3962
4695
  executionReceipt: result.executionReceipt,
4696
+ ...(result.executableArtifact ? { executableArtifact: result.executableArtifact } : {}),
3963
4697
  },
3964
4698
  };
3965
4699
  };
@@ -4036,7 +4770,7 @@ export async function startLocalServer(opts) {
4036
4770
  * string, so the only remaining reasons to differ are the connection and the
4037
4771
  * governance gates, both of which report themselves honestly.
4038
4772
  */
4039
- const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings) => {
4773
+ const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings, executionConnectionName) => {
4040
4774
  const activeConnection = requireActiveConnection(executionConnection);
4041
4775
  const rowBound = clampAnalyticalRowBound(seed?.limit ?? 200);
4042
4776
  const trimmed = sql.trim().replace(/;\s*$/, '').trim();
@@ -4058,18 +4792,6 @@ export async function startLocalServer(opts) {
4058
4792
  if (semantic.unresolvedRefs.length > 0) {
4059
4793
  throw analyticalError(`Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`, { origin: 'dql_compilation', stage: 'compile', code: 'semantic_ref' });
4060
4794
  }
4061
- const preparation = await prepareAnalyticalExecutionSql({
4062
- sql: semantic.sql,
4063
- subject: 'Generated SQL',
4064
- executor,
4065
- connection: activeConnection,
4066
- projectRoot,
4067
- projectConfig,
4068
- enforceReadOnly: true,
4069
- rowLimit: rowBound,
4070
- });
4071
- const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
4072
- const bounded = preparation.rowBound;
4073
4795
  const app = loadRuntimeApp(projectRoot, activePersonaAppId());
4074
4796
  // Generated SQL is governed as 'uncategorized' unless its seed declares a
4075
4797
  // domain — the same value `generatedSqlArtifactContract` stamps into the
@@ -4080,25 +4802,43 @@ export async function startLocalServer(opts) {
4080
4802
  assertAppAccess({ app, domain: sourceDomain, level: 'execute' });
4081
4803
  // A semantic query the loop already compiled (MetricFlow / dbt Cloud) must
4082
4804
  // execute through its pinned target binding, not as loose SQL.
4083
- const pinnedSemanticCompile = compiledSemanticQueries.get(executionFingerprint(prepared.sql))
4084
- ?? compiledSemanticQueries.get(executionFingerprint(trimmed));
4085
- const semanticExecution = pinnedSemanticCompile
4086
- ? await executeTargetBoundSemanticQuery({
4087
- executor,
4088
- connection: activeConnection,
4089
- projectRoot,
4090
- plannedAdapter: pinnedSemanticCompile.engine,
4091
- metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
4092
- ? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
4093
- : undefined,
4094
- compile: async () => pinnedSemanticCompile,
4095
- prepareSql: () => ({ sql: bounded.sql, connection: prepared.connection }),
4096
- rowBound,
4097
- })
4098
- : null;
4099
- const rawResult = semanticExecution?.result
4100
- ?? await executor.executeQuery(preparation.executedSql, bindings?.sqlParams ?? [], runtimeVariables(bindings?.variables ?? {}), preparation.connection);
4101
- const normalized = normalizeQueryResult(rawResult, semantic.semanticRefs);
4805
+ const semanticExecutionHolder = { value: null };
4806
+ const execution = await analyticalExecutionService.execute({
4807
+ sql: semantic.sql,
4808
+ subject: 'Generated SQL',
4809
+ connection: activeConnection,
4810
+ enforceReadOnly: true,
4811
+ rowLimit: rowBound,
4812
+ sqlParams: bindings?.sqlParams,
4813
+ variables: bindings?.variables,
4814
+ semanticRefs: semantic.semanticRefs,
4815
+ executePrepared: async (preparation) => {
4816
+ const pinnedSemanticCompile = compiledSemanticQueries.get(executionFingerprint(preparation.preparedSql))
4817
+ ?? compiledSemanticQueries.get(executionFingerprint(trimmed));
4818
+ if (pinnedSemanticCompile) {
4819
+ const semanticExecution = await executeTargetBoundSemanticQuery({
4820
+ executor,
4821
+ connection: activeConnection,
4822
+ projectRoot,
4823
+ plannedAdapter: pinnedSemanticCompile.engine,
4824
+ metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
4825
+ ? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
4826
+ : undefined,
4827
+ compile: async () => pinnedSemanticCompile,
4828
+ prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
4829
+ rowBound,
4830
+ });
4831
+ if (semanticExecution) {
4832
+ semanticExecutionHolder.value = semanticExecution;
4833
+ return semanticExecution.result;
4834
+ }
4835
+ }
4836
+ return executor.executeQuery(preparation.executedSql, bindings?.sqlParams ?? [], runtimeVariables(bindings?.variables ?? {}), preparation.connection);
4837
+ },
4838
+ });
4839
+ const semanticExecution = semanticExecutionHolder.value;
4840
+ const { preparation, result: normalized } = execution;
4841
+ const bounded = preparation.rowBound;
4102
4842
  // The authoritative bound. `buildRowBoundedSql` may legitimately decline to
4103
4843
  // touch the statement, and only one of 15 drivers honours a maxRows hint, so
4104
4844
  // the host is the only layer correctness may depend on.
@@ -4106,6 +4846,38 @@ export async function startLocalServer(opts) {
4106
4846
  const rows = truncated ? normalized.rows.slice(0, rowBound) : normalized.rows;
4107
4847
  const warnings = [];
4108
4848
  const artifact = buildVerifiedGeneratedArtifact(question, trimmed, bounded.sql, seed, warnings, normalized.columns, activeConnection.driver);
4849
+ const receiptSource = artifact?.source ?? trimmed;
4850
+ const executionBindings = bindings && ((bindings.sqlParams?.length ?? 0) > 0 || Object.keys(bindings.variables ?? {}).length > 0)
4851
+ ? { sqlParams: bindings.sqlParams ?? [], variables: bindings.variables ?? {} }
4852
+ : {};
4853
+ const executionReceipt = createDqlArtifactExecutionReceipt(receiptSource, bounded.sql, executionBindings, { columns: normalized.columns, rows, rowCount: rows.length });
4854
+ const executableArtifact = {
4855
+ version: 1,
4856
+ kind: 'sql_block',
4857
+ dqlFingerprint: executionFingerprint(receiptSource),
4858
+ sourceFingerprint: executionFingerprint(stableExecutionValue({
4859
+ name: artifact?.name ?? seed?.name ?? null,
4860
+ path: artifact?.sourcePath ?? seed?.sourcePath ?? null,
4861
+ })),
4862
+ compiledSqlFingerprint: executionFingerprint(semantic.sql),
4863
+ normalizedSqlFingerprint: executionFingerprint(preparation.decodedSql),
4864
+ parameterFingerprint: executionReceipt.parameterFingerprint,
4865
+ provenanceFingerprint: executionFingerprint(stableExecutionValue({
4866
+ sqlParamCount: bindings?.sqlParams?.length ?? 0,
4867
+ variableNames: Object.keys(bindings?.variables ?? {}).sort(),
4868
+ })),
4869
+ targetFingerprint: targetGenerationFingerprint(activeConnection, executionConnectionName),
4870
+ snapshotFingerprint: executionFingerprint(projectSnapshot().snapshotId),
4871
+ planFingerprint: executionFingerprint(stableExecutionValue({
4872
+ compiledSqlFingerprint: executionFingerprint(semantic.sql),
4873
+ executedSqlFingerprint: executionFingerprint(bounded.sql),
4874
+ rowBound,
4875
+ })),
4876
+ semanticAdapter: semanticExecution?.targetBinding.adapterId ?? 'native',
4877
+ previewPolicy: { mode: 'read_only_bounded', rowLimit: rowBound },
4878
+ trustState: 'review_required',
4879
+ receipt: executionReceipt,
4880
+ };
4109
4881
  return {
4110
4882
  columns: normalized.columns,
4111
4883
  rows,
@@ -4113,9 +4885,10 @@ export async function startLocalServer(opts) {
4113
4885
  executionTime: normalized.executionTime,
4114
4886
  sql: bounded.sql,
4115
4887
  ...(truncated ? { truncated: true } : {}),
4116
- ...(artifact ? { dqlArtifact: { ...artifact, limit: rowBound } } : {}),
4888
+ ...(artifact ? { dqlArtifact: { ...artifact, limit: rowBound, executionReceipt, executableArtifact } } : {}),
4117
4889
  ...(warnings.length > 0 ? { validationWarnings: warnings } : {}),
4118
- executionReceipt: createDqlArtifactExecutionReceipt(artifact?.source ?? trimmed, bounded.sql, {}, { columns: normalized.columns, rows, rowCount: rows.length }),
4890
+ executionReceipt,
4891
+ executableArtifact,
4119
4892
  };
4120
4893
  };
4121
4894
  const executeGeneratedArtifactForAgent = async (question, sql, seed, executionConnection, executionConnectionName) => {
@@ -4125,7 +4898,7 @@ export async function startLocalServer(opts) {
4125
4898
  if (seed && seed.kind !== 'sql_block') {
4126
4899
  return executeArtifactReferenceForAgent({ ...seed, limit: seed.limit ?? 200 }, question, executionConnection, executionConnectionName);
4127
4900
  }
4128
- return executeGeneratedSqlDirect(question, sql, seed, executionConnection);
4901
+ return executeGeneratedSqlDirect(question, sql, seed, executionConnection, undefined, executionConnectionName);
4129
4902
  };
4130
4903
  /**
4131
4904
  * EXP-001: execution host for the deliberately narrow non-governed lane.
@@ -4432,12 +5205,33 @@ export async function startLocalServer(opts) {
4432
5205
  * conversation, writes a DQL file, changes targets, or promotes trust.
4433
5206
  */
4434
5207
  const executeBoundedSqlRepair = async (input) => {
5208
+ let sourceSql = input.sourceSql;
5209
+ let rewrites = [];
5210
+ if (internalRelationIdsInSql(sourceSql).length > 0) {
5211
+ const normalizedSource = await prepareAnalyticalExecutionSql({
5212
+ sql: sourceSql,
5213
+ subject: 'Repair source SQL',
5214
+ executor,
5215
+ connection: input.targetConnection,
5216
+ projectRoot,
5217
+ projectConfig,
5218
+ enforceReadOnly: true,
5219
+ });
5220
+ sourceSql = normalizedSource.decodedSql;
5221
+ rewrites = normalizedSource.rewrites;
5222
+ }
5223
+ const sourceSignature = buildSqlAnalyticalSignature(sourceSql, input.targetConnection.driver);
5224
+ if (!sourceSignature) {
5225
+ throw boundedRepairError('DQL could not prove an immutable analytical signature for the source SQL. Repair it manually.', 409);
5226
+ }
4435
5227
  let result;
4436
- let repairedSql = input.sourceSql;
5228
+ let repairedSql = sourceSql;
4437
5229
  let repairMode = 'deterministic';
4438
5230
  let firstFailure;
5231
+ const providerEgressReceipts = [];
5232
+ let providerRoundTrips = 0;
4439
5233
  try {
4440
- result = await executeGeneratedSqlDirect(input.question, input.sourceSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables });
5234
+ result = await executeGeneratedSqlDirect(input.question, sourceSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables }, input.targetConnectionName);
4441
5235
  }
4442
5236
  catch (error) {
4443
5237
  firstFailure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
@@ -4458,50 +5252,89 @@ export async function startLocalServer(opts) {
4458
5252
  const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
4459
5253
  throw boundedRepairError(failure.redactedMessage, 422, failure);
4460
5254
  }
4461
- const boundedSchema = schemaContext.slice(0, 16).map((table) => ({
5255
+ const boundedSchema = markProviderMetadataArray(schemaContext.slice(0, 16).map((table) => ({
4462
5256
  relation: table.relation,
4463
- columns: table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type })),
4464
- }));
5257
+ columns: markProviderMetadataArray(table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type }))),
5258
+ })));
4465
5259
  // CTX-008: a repair that re-derives joins from column names can undo a
4466
5260
  // join the original query got right. Give it the authored key pairs.
4467
- const governedJoins = governedJoinFactsForRelations(schemaContext.slice(0, 16).map((table) => table.relation));
5261
+ const governedJoins = markProviderMetadataArray(governedJoinFactsForRelations(schemaContext.slice(0, 16).map((table) => table.relation)));
5262
+ const repairPayload = {
5263
+ question: input.question,
5264
+ driver: input.targetConnection.driver,
5265
+ failure: firstFailure.redactedMessage,
5266
+ sql: sourceSql,
5267
+ parameters: input.sqlParams ? markProviderMetadataArray(input.sqlParams.map((parameter) => ({
5268
+ name: parameter.name,
5269
+ placeholder: `$${parameter.position}`,
5270
+ }))) : undefined,
5271
+ schema: boundedSchema,
5272
+ ...(governedJoins.length ? { governedJoins } : {}),
5273
+ };
5274
+ const repairMessages = [
5275
+ {
5276
+ role: 'system',
5277
+ content: [
5278
+ 'Repair one failed read-only analytical SQL statement.',
5279
+ 'Return exactly one JSON object in a ```json fence with keys summary, sql, viz, outputs.',
5280
+ 'Preserve the requested grain, filters, output columns, and aggregation semantics.',
5281
+ input.sqlParams?.length
5282
+ ? 'Preserve every positional parameter placeholder exactly; do not add, remove, or renumber placeholders.'
5283
+ : '',
5284
+ 'Use only relations and columns present in the supplied schema context.',
5285
+ 'When governedJoins is present, join on those exact key pairs rather than inferring a join from column names.',
5286
+ 'Never emit internal DQL relation identities such as source::, model::, seed::, or snapshot::.',
5287
+ 'Do not add writes, DDL, multiple statements, or commentary outside the JSON fence.',
5288
+ ].join(' '),
5289
+ },
5290
+ {
5291
+ role: 'user',
5292
+ content: JSON.stringify(repairPayload),
5293
+ },
5294
+ ];
5295
+ const repairEnvelope = prepareProviderWireEnvelopeForDispatch(provider.name, { messages: repairMessages });
5296
+ assertProviderPayloadAllowed(repairEnvelope, {
5297
+ allowResultRows: false,
5298
+ maxResultRows: 0,
5299
+ purpose: 'repair_sql',
5300
+ });
4468
5301
  let raw;
4469
5302
  try {
4470
- raw = await provider.generate([
4471
- {
4472
- role: 'system',
4473
- content: [
4474
- 'Repair one failed read-only analytical SQL statement.',
4475
- 'Return exactly one JSON object in a ```json fence with keys summary, sql, viz, outputs.',
4476
- 'Preserve the requested grain, filters, output columns, and aggregation semantics.',
4477
- input.sqlParams?.length
4478
- ? 'Preserve every positional parameter placeholder exactly; do not add, remove, or renumber placeholders.'
4479
- : '',
4480
- 'Use only relations and columns present in the supplied schema context.',
4481
- 'When governedJoins is present, join on those exact key pairs rather than inferring a join from column names.',
4482
- 'Never emit internal DQL relation identities such as source::, model::, seed::, or snapshot::.',
4483
- 'Do not add writes, DDL, multiple statements, or commentary outside the JSON fence.',
4484
- ].join(' '),
4485
- },
4486
- {
4487
- role: 'user',
4488
- content: JSON.stringify({
4489
- question: input.question,
4490
- driver: input.targetConnection.driver,
4491
- failure: firstFailure.redactedMessage,
4492
- sql: input.sourceSql,
4493
- parameters: input.sqlParams?.map((parameter) => ({
4494
- name: parameter.name,
4495
- placeholder: `$${parameter.position}`,
4496
- })),
4497
- schema: boundedSchema,
4498
- ...(governedJoins.length ? { governedJoins } : {}),
4499
- }),
5303
+ raw = await provider.generate(repairEnvelope.messages, {
5304
+ maxTokens: 1200,
5305
+ temperature: 0,
5306
+ maxProviderDispatches: 2,
5307
+ onProviderDispatch: (event) => {
5308
+ const envelope = prepareProviderWireEnvelopeForDispatch(provider.name, event.envelope);
5309
+ assertProviderPayloadAllowed(envelope, {
5310
+ allowResultRows: false,
5311
+ maxResultRows: 0,
5312
+ purpose: 'repair_sql',
5313
+ });
5314
+ providerRoundTrips += 1;
5315
+ providerEgressReceipts.push(createProviderDispatchEgressReceipt({
5316
+ purpose: 'repair_sql',
5317
+ dispatchPhase: 'repair',
5318
+ provider: provider.name,
5319
+ permittedCategories: ['instructions', 'question', 'schema_metadata', 'governed_context'],
5320
+ optIn: false,
5321
+ envelope,
5322
+ }));
5323
+ return envelope;
4500
5324
  },
4501
- ], { maxTokens: 1200, temperature: 0 });
5325
+ });
4502
5326
  }
4503
5327
  catch {
4504
- throw boundedRepairError('The AI provider could not complete this repair. Check Provider Settings and retry.', 502);
5328
+ throw Object.assign(boundedRepairError('The AI provider could not complete this repair. Check Provider Settings and retry.', 502), {
5329
+ providerDispatchEvidence: {
5330
+ providerEgressReceipts: [...providerEgressReceipts],
5331
+ providerRoundTrips,
5332
+ toolCalls: 0,
5333
+ sqlExecutions: 0,
5334
+ repairs: 1,
5335
+ fallbackReason: 'repair_provider_error',
5336
+ },
5337
+ });
4505
5338
  }
4506
5339
  const proposal = parseProposal(raw);
4507
5340
  if (!proposal.sql?.trim()) {
@@ -4512,9 +5345,17 @@ export async function startLocalServer(opts) {
4512
5345
  if (requiredPlaceholders.some((placeholder) => !new RegExp(`${placeholder.replace('$', '\\$')}\\b`).test(repairedSql))) {
4513
5346
  throw boundedRepairError('The repair changed this cell\'s parameter contract, so DQL stopped it before execution.', 422);
4514
5347
  }
5348
+ const repairedSignature = buildSqlAnalyticalSignature(repairedSql, input.targetConnection.driver);
5349
+ if (!repairedSignature || stableExecutionValue(repairedSignature) !== stableExecutionValue(sourceSignature)) {
5350
+ throw boundedRepairError('The provider repair changed or could not prove the immutable analytical plan, so DQL stopped before execution.', 409);
5351
+ }
5352
+ const aggregationProof = buildAggregationSafetyProof(repairedSql, undefined, input.targetConnection.driver);
5353
+ if (aggregationProof.status !== 'safe') {
5354
+ throw boundedRepairError(`The repaired SQL lacks complete aggregation-safety evidence (${aggregationProof.issueCodes.join(', ')}). Repair it manually.`, 409);
5355
+ }
4515
5356
  repairMode = 'ai';
4516
5357
  try {
4517
- result = await executeGeneratedSqlDirect(input.question, repairedSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables });
5358
+ result = await executeGeneratedSqlDirect(input.question, repairedSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables }, input.targetConnectionName);
4518
5359
  }
4519
5360
  catch (error) {
4520
5361
  const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
@@ -4524,7 +5365,6 @@ export async function startLocalServer(opts) {
4524
5365
  // Keep preview-only row bounds out of editable notebook source. The one
4525
5366
  // deterministic source rewrite we do persist is decoding internal graph
4526
5367
  // identities, because those can never execute as warehouse SQL.
4527
- let rewrites = [];
4528
5368
  if (internalRelationIdsInSql(repairedSql).length > 0) {
4529
5369
  const normalizedSource = await prepareAnalyticalExecutionSql({
4530
5370
  sql: repairedSql,
@@ -4536,9 +5376,9 @@ export async function startLocalServer(opts) {
4536
5376
  enforceReadOnly: true,
4537
5377
  });
4538
5378
  repairedSql = normalizedSource.decodedSql;
4539
- rewrites = normalizedSource.rewrites;
5379
+ rewrites.push(...normalizedSource.rewrites);
4540
5380
  }
4541
- return { result, repairedSql, repairMode, firstFailure, rewrites };
5381
+ return { result, repairedSql, repairMode, firstFailure, providerEgressReceipts, providerRoundTrips, rewrites };
4542
5382
  };
4543
5383
  /**
4544
5384
  * Repair a DQL wrapper that cannot compile far enough to expose executable
@@ -4551,6 +5391,8 @@ export async function startLocalServer(opts) {
4551
5391
  if (!provider) {
4552
5392
  throw boundedRepairError('Configure an available AI provider in Settings to repair this DQL draft.', 409);
4553
5393
  }
5394
+ const providerEgressReceipts = [];
5395
+ let providerRoundTrips = 0;
4554
5396
  let schemaContext;
4555
5397
  try {
4556
5398
  // Include source identifiers in retrieval so a terse cell title still
@@ -4596,10 +5438,41 @@ export async function startLocalServer(opts) {
4596
5438
  ...(governedJoins.length ? { governedJoins } : {}),
4597
5439
  }),
4598
5440
  },
4599
- ], { maxTokens: 2400, temperature: 0 });
5441
+ ], {
5442
+ maxTokens: 2400,
5443
+ temperature: 0,
5444
+ maxProviderDispatches: 2,
5445
+ onProviderDispatch: (event) => {
5446
+ const envelope = prepareProviderWireEnvelopeForDispatch(provider.name, event.envelope);
5447
+ assertProviderPayloadAllowed(envelope, {
5448
+ allowResultRows: false,
5449
+ maxResultRows: 0,
5450
+ purpose: 'repair_sql',
5451
+ });
5452
+ providerRoundTrips += 1;
5453
+ providerEgressReceipts.push(createProviderDispatchEgressReceipt({
5454
+ purpose: 'repair_sql',
5455
+ dispatchPhase: 'repair',
5456
+ provider: provider.name,
5457
+ permittedCategories: ['instructions', 'question', 'schema_metadata', 'governed_context'],
5458
+ optIn: false,
5459
+ envelope,
5460
+ }));
5461
+ return envelope;
5462
+ },
5463
+ });
4600
5464
  }
4601
5465
  catch {
4602
- throw boundedRepairError('The AI provider could not complete this DQL repair. Check Provider Settings and retry.', 502);
5466
+ throw Object.assign(boundedRepairError('The AI provider could not complete this DQL repair. Check Provider Settings and retry.', 502), {
5467
+ providerDispatchEvidence: {
5468
+ providerEgressReceipts: [...providerEgressReceipts],
5469
+ providerRoundTrips,
5470
+ toolCalls: 0,
5471
+ sqlExecutions: 0,
5472
+ repairs: 1,
5473
+ fallbackReason: 'repair_provider_error',
5474
+ },
5475
+ });
4603
5476
  }
4604
5477
  let repairedSource;
4605
5478
  try {
@@ -4703,6 +5576,8 @@ export async function startLocalServer(opts) {
4703
5576
  repairedSql: plan.sql,
4704
5577
  repairedSource,
4705
5578
  repairMode: 'ai',
5579
+ providerEgressReceipts,
5580
+ providerRoundTrips,
4706
5581
  rewrites: [],
4707
5582
  sqlParams: plan.sqlParams,
4708
5583
  variables: { ...plan.variables, ...invocation.values },
@@ -5701,11 +6576,18 @@ export async function startLocalServer(opts) {
5701
6576
  tableMapping,
5702
6577
  semanticSql: semanticCompose?.sql ?? undefined,
5703
6578
  });
5704
- const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan?.sql ?? executableSql, activeConnection, projectRoot, projectConfig);
5705
- const result = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), prepared.connection);
6579
+ const execution = await analyticalExecutionService.execute({
6580
+ sql: semanticCompose?.sql ?? plan?.sql ?? executableSql,
6581
+ subject: 'Block Studio preview query',
6582
+ connection: activeConnection,
6583
+ sqlParams: plan?.sqlParams,
6584
+ variables: { ...(plan?.variables ?? {}), ...invocation.values },
6585
+ });
5706
6586
  return {
5707
- sql: prepared.sql,
5708
- result: normalizeQueryResult(result),
6587
+ sql: execution.preparation.executedSql,
6588
+ result: execution.result,
6589
+ compiledSqlFingerprint: execution.compiledSqlFingerprint,
6590
+ resultFingerprint: execution.resultFingerprint,
5709
6591
  chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
5710
6592
  invocation,
5711
6593
  tests: plan?.tests ?? [],
@@ -5756,12 +6638,14 @@ export async function startLocalServer(opts) {
5756
6638
  if (tests.length === 0) {
5757
6639
  return { passed: 0, failed: 0, skipped: 0, duration: Date.now() - start, assertions: [], runAt: new Date() };
5758
6640
  }
5759
- const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan.sql, activeConnection, projectRoot, projectConfig);
5760
- const rawResult = await executor.executeQuery(prepared.sql, plan.sqlParams ?? [], runtimeVariables(plan.variables ?? {}), prepared.connection);
5761
- const rows = Array.isArray(rawResult?.rows) ? rawResult.rows : [];
5762
- const columns = Array.isArray(rawResult?.columns)
5763
- ? rawResult.columns.map((column) => typeof column === 'string' ? column : column?.name ?? String(column))
5764
- : [];
6641
+ const execution = await analyticalExecutionService.execute({
6642
+ sql: semanticCompose?.sql ?? plan.sql,
6643
+ subject: 'Block Studio certification test query',
6644
+ connection: activeConnection,
6645
+ sqlParams: plan.sqlParams,
6646
+ variables: plan.variables,
6647
+ });
6648
+ const { rows, columns } = execution.result;
5765
6649
  return evaluateBlockStudioTests(tests, rows, columns, start);
5766
6650
  };
5767
6651
  const saveDqlGenerationDraft = (importId, candidate) => saveDqlGenerationDraftForProject(projectRoot, importId, candidate);
@@ -7243,23 +8127,81 @@ export async function startLocalServer(opts) {
7243
8127
  res.end(serializeJSON({ error: 'Agent run not found.' }));
7244
8128
  return;
7245
8129
  }
8130
+ const capability = normalizeAnalyticalRepairCapabilityV1(sourceRun.repairCapability);
8131
+ if (!capability?.automatic.eligible || capability.automatic.attemptsRemaining < 1) {
8132
+ const manualActions = capability?.manualActions?.length
8133
+ ? capability.manualActions
8134
+ : ['edit_dql', 'open_sql_notebook'];
8135
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
8136
+ res.end(serializeJSON({
8137
+ code: 'REPAIR_CAPABILITY_REQUIRED',
8138
+ error: 'This run does not retain eligible automatic execution-repair authority.',
8139
+ manualActions,
8140
+ ...(capability?.ineligibilityReason ? { ineligibilityReason: capability.ineligibilityReason } : {}),
8141
+ }));
8142
+ return;
8143
+ }
8144
+ // Rebind every server-issued capability identity to the currently
8145
+ // persisted immutable source run before checking idempotence or reserving
8146
+ // an attempt. A copied/stale capability must never authorize provider or
8147
+ // SQL work against a mutated plan, failure, or source artifact.
8148
+ const canonicalCapability = analyticalRepairCapabilityForAgentRun(sourceRun, capability.targetFingerprint);
8149
+ const bindingMatches = canonicalCapability
8150
+ && canonicalCapability.automatic.eligible
8151
+ && canonicalCapability.planFingerprint === capability.planFingerprint
8152
+ && canonicalCapability.failureFingerprint === capability.failureFingerprint
8153
+ && canonicalCapability.sourceFingerprint === capability.sourceFingerprint;
8154
+ if (!bindingMatches) {
8155
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
8156
+ res.end(serializeJSON({
8157
+ code: 'REPAIR_CAPABILITY_REQUIRED',
8158
+ error: 'The persisted source plan, failure, or artifact no longer matches its server-issued repair capability.',
8159
+ manualActions: capability.manualActions,
8160
+ }));
8161
+ return;
8162
+ }
7246
8163
  const derivedRunId = `${sourceRun.id}:repair:1`;
8164
+ if (activeAnalyticalRepairReservations.has(derivedRunId)) {
8165
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
8166
+ res.end(serializeJSON({
8167
+ code: 'REPAIR_IN_PROGRESS',
8168
+ error: 'The single permitted repair attempt is already in progress.',
8169
+ manualActions: capability.manualActions,
8170
+ }));
8171
+ return;
8172
+ }
7247
8173
  const existing = await agentRunStore.get(derivedRunId);
7248
8174
  if (existing) {
7249
- res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
7250
- res.end(serializeJSON({ run: existing }));
8175
+ const succeeded = existing.status === 'needs_review' && existing.trustState === 'review_required';
8176
+ res.writeHead(succeeded ? 200 : 409, { 'Content-Type': 'application/json; charset=utf-8' });
8177
+ res.end(serializeJSON(succeeded
8178
+ ? { run: existing }
8179
+ : {
8180
+ code: 'REPAIR_ATTEMPT_EXHAUSTED',
8181
+ error: 'The single permitted repair attempt was already consumed and did not complete successfully.',
8182
+ manualActions: capability.manualActions,
8183
+ run: existing,
8184
+ }));
7251
8185
  return;
7252
8186
  }
7253
- const sourceSql = repairableSqlFromAgentRun(sourceRun);
7254
8187
  const sourceDqlArtifact = repairableDqlArtifactFromAgentRun(sourceRun);
7255
8188
  const retainedFailure = warehouseFailureFromAgentRun(sourceRun);
7256
- if ((!sourceSql && !sourceDqlArtifact) || !analyticalFailureAllowsDeterministicRetry(retainedFailure)) {
8189
+ if (!sourceDqlArtifact || !validAskRepairDqlWrapper(sourceDqlArtifact.source)) {
8190
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
8191
+ res.end(serializeJSON({
8192
+ code: 'REPAIR_CAPABILITY_REQUIRED',
8193
+ error: 'The retained DQL wrapper is unavailable or invalid. Keep it editable and repair it manually.',
8194
+ manualActions: capability.manualActions,
8195
+ }));
8196
+ return;
8197
+ }
8198
+ const capabilityHash = (value) => `sha256:${createHash('sha256').update(stableExecutionValue(value)).digest('hex')}`;
8199
+ if (capability.dqlFingerprint !== capabilityHash(sourceDqlArtifact.source.trim())) {
7257
8200
  res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
7258
8201
  res.end(serializeJSON({
7259
- error: sourceSql || sourceDqlArtifact
7260
- ? 'This failure requires authorized access or a policy change and cannot be repaired by AI.'
7261
- : 'This run has no retained SQL statement or editable DQL artifact to repair.',
7262
- ...(retainedFailure ? { warehouseFailure: retainedFailure } : {}),
8202
+ code: 'REPAIR_CAPABILITY_REQUIRED',
8203
+ error: 'The retained DQL source no longer matches its immutable repair capability.',
8204
+ manualActions: capability.manualActions,
7263
8205
  }));
7264
8206
  return;
7265
8207
  }
@@ -7282,32 +8224,250 @@ export async function startLocalServer(opts) {
7282
8224
  : sourceRun.executionTarget?.target === 'local'
7283
8225
  ? 'local'
7284
8226
  : projectConfig.defaultConnectionName;
8227
+ const resolvedTargetFingerprint = targetGenerationFingerprint(targetConnection, targetConnectionName);
8228
+ if (resolvedTargetFingerprint !== capability.targetFingerprint) {
8229
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
8230
+ res.end(serializeJSON({
8231
+ code: 'REPAIR_TARGET_DRIFT',
8232
+ error: 'The named execution target now resolves to a different connection generation. Automatic repair was not attempted.',
8233
+ manualActions: capability.manualActions,
8234
+ }));
8235
+ return;
8236
+ }
8237
+ if (consumedAnalyticalRepairCapabilities.has(derivedRunId)) {
8238
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
8239
+ res.end(serializeJSON({
8240
+ code: 'REPAIR_ATTEMPT_EXHAUSTED',
8241
+ error: 'The single permitted repair attempt has already been consumed.',
8242
+ manualActions: capability.manualActions,
8243
+ }));
8244
+ return;
8245
+ }
8246
+ activeAnalyticalRepairReservations.add(derivedRunId);
8247
+ consumedAnalyticalRepairCapabilities.add(derivedRunId);
8248
+ const releaseRepairReservation = () => { activeAnalyticalRepairReservations.delete(derivedRunId); };
8249
+ res.once('finish', releaseRepairReservation);
8250
+ res.once('close', releaseRepairReservation);
8251
+ const repairStartedAt = new Date().toISOString();
8252
+ const repairStartedAtMs = Date.now();
8253
+ const sourceFailureForReservation = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
8254
+ const reservationEvent = {
8255
+ id: `${derivedRunId}:repair`,
8256
+ runId: derivedRunId,
8257
+ type: 'repair.attempted',
8258
+ at: repairStartedAt,
8259
+ message: 'Reserved the single bounded repair attempt.',
8260
+ route: 'generated_answer',
8261
+ };
8262
+ const reservedRun = {
8263
+ id: derivedRunId,
8264
+ question: sourceRun.question,
8265
+ requestedMode: sourceRun.requestedMode,
8266
+ route: 'generated_answer',
8267
+ status: 'blocked',
8268
+ trustState: 'blocked',
8269
+ stopReason: 'blocked',
8270
+ startedAt: repairStartedAt,
8271
+ completedAt: repairStartedAt,
8272
+ selectedObject: sourceRun.selectedObject,
8273
+ executionTarget: sourceRun.executionTarget,
8274
+ routeDecision: sourceRun.routeDecision,
8275
+ plan: sourceRun.plan,
8276
+ steps: [],
8277
+ summary: 'The single repair attempt was reserved but has not completed successfully.',
8278
+ artifacts: [],
8279
+ evaluations: [],
8280
+ events: [reservationEvent],
8281
+ nextActions: sourceRun.nextActions,
8282
+ repairAttempts: 1,
8283
+ lifecycle: {
8284
+ version: 1,
8285
+ state: 'terminal',
8286
+ phase: 'repair.attempted',
8287
+ revision: 1,
8288
+ eventCursor: 1,
8289
+ startedAt: repairStartedAt,
8290
+ updatedAt: repairStartedAt,
8291
+ completedAt: repairStartedAt,
8292
+ },
8293
+ derivation: {
8294
+ version: 1,
8295
+ kind: 'analytical_repair',
8296
+ sourceRunId: sourceRun.id,
8297
+ ...(sourceFailureForReservation?.failureId ? { sourceFailureId: sourceFailureForReservation.failureId } : {}),
8298
+ attempt: 1,
8299
+ },
8300
+ };
8301
+ try {
8302
+ await agentRunStore.save(reservedRun);
8303
+ }
8304
+ catch (error) {
8305
+ releaseRepairReservation();
8306
+ throw error;
8307
+ }
7285
8308
  let repaired;
7286
8309
  try {
7287
- // Prefer the editable DQL contract even when the failed run also
7288
- // retained compiled SQL. This keeps parameters, visualization, tests,
7289
- // and the corrected wrapper attached to the repaired Ask result.
7290
- repaired = sourceDqlArtifact
7291
- ? await executeBoundedDqlRepair({
7292
- question: sourceRun.question,
7293
- source: sourceDqlArtifact.source,
7294
- failure: retainedFailure?.redactedMessage
7295
- ?? sourceRun.diagnosticReceipt?.failure?.message
7296
- ?? sourceRun.summary,
7297
- targetConnection,
7298
- targetConnectionName,
7299
- parameters: sourceDqlArtifact.parameterValues,
7300
- artifactName: sourceDqlArtifact.name,
7301
- })
7302
- : await executeBoundedSqlRepair({
7303
- question: sourceRun.question,
7304
- sourceSql: sourceSql,
7305
- targetConnection,
7306
- targetConnectionName,
7307
- });
8310
+ // Compile the immutable original wrapper first. A provider receives only
8311
+ // this embedded SQL, never the DQL wrapper or its surrounding fields.
8312
+ const invocation = prepareBlockInvocation({
8313
+ source: sourceDqlArtifact.source,
8314
+ parameters: sourceDqlArtifact.parameterValues,
8315
+ question: sourceRun.question,
8316
+ surface: 'ask_ai',
8317
+ });
8318
+ if (invocation.errors.length || invocation.unresolvedParameters.length) {
8319
+ throw boundedRepairError(invocation.errors.join(' ') || `Provide required parameters before retrying: ${invocation.unresolvedParameters.join(', ')}.`, 409);
8320
+ }
8321
+ const originalPlan = buildExecutionPlan({
8322
+ id: 'ask-execution-repair-source',
8323
+ type: 'dql',
8324
+ source: sourceDqlArtifact.source,
8325
+ title: sourceDqlArtifact.name,
8326
+ }, {
8327
+ semanticLayer,
8328
+ driver: targetConnection.driver,
8329
+ parameters: invocation.values,
8330
+ });
8331
+ if (!originalPlan?.sql?.trim())
8332
+ throw boundedRepairError('The original DQL wrapper did not compile to SQL.', 409);
8333
+ if (capability.sqlFingerprint !== capabilityHash(originalPlan.sql.trim())) {
8334
+ throw boundedRepairError('The original compiled SQL no longer matches its immutable repair capability.', 409);
8335
+ }
8336
+ const sqlRepair = await executeBoundedSqlRepair({
8337
+ question: sourceRun.question,
8338
+ sourceSql: originalPlan.sql,
8339
+ targetConnection,
8340
+ targetConnectionName,
8341
+ sqlParams: originalPlan.sqlParams,
8342
+ variables: { ...originalPlan.variables, ...invocation.values },
8343
+ });
8344
+ const embeddedSql = restoreNotebookDqlParameterInterpolations(sqlRepair.repairedSql, originalPlan.sqlParams);
8345
+ const repairedSource = replaceNotebookDqlQueryForRepair(sourceDqlArtifact.source, embeddedSql);
8346
+ if (!repairedSource)
8347
+ throw boundedRepairError('DQL could not reinsert the repaired SQL into the immutable wrapper.', 422);
8348
+ const repairedInvocation = prepareBlockInvocation({
8349
+ source: repairedSource,
8350
+ parameters: sourceDqlArtifact.parameterValues,
8351
+ question: sourceRun.question,
8352
+ surface: 'ask_ai',
8353
+ });
8354
+ if (repairedInvocation.errors.length || repairedInvocation.unresolvedParameters.length) {
8355
+ throw boundedRepairError('The SQL-only repair did not preserve a compilable DQL wrapper.', 422);
8356
+ }
8357
+ const repairedPlan = buildExecutionPlan({
8358
+ id: 'ask-execution-repair-derived',
8359
+ type: 'dql',
8360
+ source: repairedSource,
8361
+ title: sourceDqlArtifact.name,
8362
+ }, {
8363
+ semanticLayer,
8364
+ driver: targetConnection.driver,
8365
+ parameters: repairedInvocation.values,
8366
+ });
8367
+ if (!repairedPlan?.sql?.trim() || normalizeSqlForComparison(repairedPlan.sql) !== normalizeSqlForComparison(sqlRepair.repairedSql)) {
8368
+ throw boundedRepairError('The repaired wrapper recompiled to different SQL, so DQL stopped before publishing it.', 422);
8369
+ }
8370
+ const repairedExecutionReceipt = createDqlArtifactExecutionReceipt(repairedSource, sqlRepair.result.sql ?? repairedPlan.sql, repairedInvocation.values, sqlRepair.result);
8371
+ const repairedExecutableArtifact = sqlRepair.result.executableArtifact
8372
+ ? {
8373
+ ...sqlRepair.result.executableArtifact,
8374
+ kind: 'sql_block',
8375
+ dqlFingerprint: executionFingerprint(repairedSource),
8376
+ sourceFingerprint: executionFingerprint(stableExecutionValue({
8377
+ name: sourceDqlArtifact.name ?? null,
8378
+ path: sourceDqlArtifact.sourcePath ?? null,
8379
+ })),
8380
+ compiledSqlFingerprint: executionFingerprint(repairedPlan.sql),
8381
+ normalizedSqlFingerprint: executionFingerprint(sqlRepair.repairedSql),
8382
+ parameterFingerprint: repairedExecutionReceipt.parameterFingerprint,
8383
+ provenanceFingerprint: executionFingerprint(stableExecutionValue(repairedInvocation.resolvedParameters.map((parameter) => ({
8384
+ name: parameter.name,
8385
+ source: parameter.source,
8386
+ })))),
8387
+ planFingerprint: executionFingerprint(stableExecutionValue({
8388
+ compiledSqlFingerprint: executionFingerprint(repairedPlan.sql),
8389
+ parameterCount: repairedPlan.sqlParams?.length ?? 0,
8390
+ variableNames: Object.keys(repairedPlan.variables ?? {}).sort(),
8391
+ })),
8392
+ trustState: 'review_required',
8393
+ receipt: repairedExecutionReceipt,
8394
+ }
8395
+ : undefined;
8396
+ const repairedArtifact = {
8397
+ ...sourceDqlArtifact,
8398
+ source: repairedSource,
8399
+ compiledSql: repairedPlan.sql,
8400
+ trustState: 'review_required',
8401
+ persistence: 'transient',
8402
+ executionReceipt: repairedExecutionReceipt,
8403
+ ...(repairedExecutableArtifact ? { executableArtifact: repairedExecutableArtifact } : {}),
8404
+ ...(repairedInvocation.parameters.length ? { parameters: repairedInvocation.parameters } : {}),
8405
+ ...(Object.keys(repairedInvocation.values).length ? { parameterValues: repairedInvocation.values } : {}),
8406
+ };
8407
+ repaired = {
8408
+ ...sqlRepair,
8409
+ result: {
8410
+ ...sqlRepair.result,
8411
+ executionReceipt: repairedExecutionReceipt,
8412
+ ...(repairedExecutableArtifact ? { executableArtifact: repairedExecutableArtifact } : {}),
8413
+ dqlArtifact: repairedArtifact,
8414
+ },
8415
+ repairedSql: repairedPlan.sql,
8416
+ repairedSource,
8417
+ sqlParams: repairedPlan.sqlParams,
8418
+ variables: { ...repairedPlan.variables, ...repairedInvocation.values },
8419
+ };
7308
8420
  }
7309
8421
  catch (error) {
7310
8422
  const repairError = error;
8423
+ const dispatchEvidence = providerDispatchTerminalEvidence(error);
8424
+ const failedAt = new Date().toISOString();
8425
+ const failedTelemetry = {
8426
+ version: 1,
8427
+ stageDurationsMs: { repair: Date.now() - repairStartedAtMs, total: Date.now() - repairStartedAtMs },
8428
+ providerRoundTrips: dispatchEvidence?.providerRoundTrips ?? 0,
8429
+ toolCalls: dispatchEvidence?.toolCalls ?? 0,
8430
+ sqlExecutions: dispatchEvidence?.sqlExecutions ?? 0,
8431
+ repairs: Math.max(1, dispatchEvidence?.repairs ?? 0),
8432
+ egressReceipts: dispatchEvidence?.providerEgressReceipts.length ?? 0,
8433
+ fallbackReason: dispatchEvidence?.fallbackReason ?? 'repair_failed',
8434
+ };
8435
+ const failedReceipts = dispatchEvidence?.providerEgressReceipts ?? [];
8436
+ await agentRunStore.save({
8437
+ ...reservedRun,
8438
+ completedAt: failedAt,
8439
+ summary: repairError.message,
8440
+ lifecycle: reservedRun.lifecycle ? {
8441
+ ...reservedRun.lifecycle,
8442
+ phase: 'run.failed',
8443
+ updatedAt: failedAt,
8444
+ completedAt: failedAt,
8445
+ } : undefined,
8446
+ events: [
8447
+ reservationEvent,
8448
+ {
8449
+ id: `${derivedRunId}:failed`,
8450
+ runId: derivedRunId,
8451
+ type: 'run.failed',
8452
+ at: failedAt,
8453
+ message: repairError.message,
8454
+ route: 'generated_answer',
8455
+ status: 'blocked',
8456
+ trustState: 'blocked',
8457
+ },
8458
+ ],
8459
+ telemetry: failedTelemetry,
8460
+ ...(failedReceipts.length ? { providerEgressReceipts: failedReceipts } : {}),
8461
+ diagnosticReceiptV2: {
8462
+ version: 2,
8463
+ runId: derivedRunId,
8464
+ route: 'generated_answer',
8465
+ status: 'blocked',
8466
+ telemetry: failedTelemetry,
8467
+ providerEgressReceiptFingerprints: failedReceipts.map((receipt) => executionFingerprint(stableExecutionValue(receipt))),
8468
+ repairCapabilityFingerprint: executionFingerprint(stableExecutionValue(capability)),
8469
+ },
8470
+ });
7311
8471
  res.writeHead(repairError.status ?? 500, { 'Content-Type': 'application/json; charset=utf-8' });
7312
8472
  res.end(serializeJSON({
7313
8473
  error: repairError.message,
@@ -7315,27 +8475,39 @@ export async function startLocalServer(opts) {
7315
8475
  }));
7316
8476
  return;
7317
8477
  }
7318
- const { result, repairedSql, repairMode, firstFailure } = repaired;
8478
+ const { result, repairedSql, repairMode, firstFailure, providerEgressReceipts, providerRoundTrips } = repaired;
7319
8479
  const completedAt = new Date().toISOString();
7320
- const preview = agentResultToSynthesisPreview(result);
7321
- const synthesis = await synthesizeAnswer({
7322
- question: sourceRun.question,
7323
- audience: 'stakeholder',
7324
- resultPreview: preview,
7325
- sql: repairedSql,
7326
- draftText: `The repaired query returned ${result.rowCount} row${result.rowCount === 1 ? '' : 's'}.`,
7327
- ...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
7328
- });
8480
+ // Repair never grants a second answer/narration call and never sends rows
8481
+ // to a provider. Present the validated contract deterministically.
8482
+ const presentationText = `The repaired query returned ${result.rowCount} row${result.rowCount === 1 ? '' : 's'}. Review the derived SQL before reuse.`;
7329
8483
  const sourceFailure = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
7330
8484
  const presentationContext = repairPresentationContextFromAgentRun(sourceRun);
7331
8485
  const executionResultFingerprint = result.executionReceipt?.resultFingerprint;
8486
+ const repairTotalDurationMs = Date.now() - repairStartedAtMs;
8487
+ const repairTelemetry = {
8488
+ version: 1,
8489
+ stageDurationsMs: {
8490
+ retrieval: 0,
8491
+ meaning: 0,
8492
+ repair: repairTotalDurationMs,
8493
+ ...(typeof result.executionTime === 'number' ? { execution: result.executionTime } : {}),
8494
+ narration: 0,
8495
+ total: repairTotalDurationMs,
8496
+ },
8497
+ providerRoundTrips,
8498
+ toolCalls: 0,
8499
+ sqlExecutions: firstFailure ? 2 : 1,
8500
+ repairs: 1,
8501
+ egressReceipts: providerEgressReceipts.length,
8502
+ ...(typeof result.executionTime === 'number' ? { warehouseDurationMs: result.executionTime } : {}),
8503
+ };
7332
8504
  const artifact = agentRunArtifact('answer', 'Repaired analytical answer', {
7333
8505
  kind: 'uncertified',
7334
8506
  certification: 'analyst_review_required',
7335
8507
  reviewStatus: 'analyst_review_required',
7336
8508
  trustLabel: 'AI-generated · review required',
7337
- answer: synthesis.text,
7338
- text: synthesis.text,
8509
+ answer: presentationText,
8510
+ text: presentationText,
7339
8511
  ...presentationContext,
7340
8512
  result,
7341
8513
  proposedSql: repairedSql,
@@ -7356,10 +8528,11 @@ export async function startLocalServer(opts) {
7356
8528
  rowCount: result.rowCount,
7357
8529
  ...(executionResultFingerprint ? { resultFingerprint: executionResultFingerprint } : {}),
7358
8530
  },
8531
+ providerEgressReceipts,
7359
8532
  },
7360
8533
  previousAttempt: {
7361
8534
  sourceRunId: sourceRun.id,
7362
- ...(sourceSql ? { sql: sourceSql } : {}),
8535
+ sql: repairableSqlFromAgentRun(sourceRun),
7363
8536
  ...(sourceDqlArtifact?.source ? { dql: sourceDqlArtifact.source } : {}),
7364
8537
  failure: retainedFailure ?? firstFailure,
7365
8538
  },
@@ -7386,7 +8559,7 @@ export async function startLocalServer(opts) {
7386
8559
  artifacts: [artifact],
7387
8560
  };
7388
8561
  const events = [
7389
- { id: `${derivedRunId}:repair`, runId: derivedRunId, type: 'repair.attempted', at: completedAt, message: 'Started one bounded query repair.', route: 'generated_answer' },
8562
+ { id: `${derivedRunId}:repair`, runId: derivedRunId, type: 'repair.attempted', at: repairStartedAt, message: 'Started one bounded query repair.', route: 'generated_answer' },
7390
8563
  { id: `${derivedRunId}:artifact`, runId: derivedRunId, type: 'artifact.created', at: completedAt, message: 'Created the repaired result artifact.', route: 'generated_answer', trustState: 'review_required' },
7391
8564
  { id: `${derivedRunId}:complete`, runId: derivedRunId, type: 'run.completed', at: completedAt, message: 'The repaired query executed successfully.', route: 'generated_answer', status: 'needs_review', trustState: 'review_required' },
7392
8565
  ];
@@ -7398,15 +8571,15 @@ export async function startLocalServer(opts) {
7398
8571
  status: 'needs_review',
7399
8572
  trustState: 'review_required',
7400
8573
  stopReason: 'generated_review_required',
7401
- startedAt: completedAt,
8574
+ startedAt: repairStartedAt,
7402
8575
  completedAt,
7403
8576
  selectedObject: sourceRun.selectedObject,
7404
8577
  executionTarget: sourceRun.executionTarget,
7405
8578
  routeDecision: sourceRun.routeDecision,
7406
8579
  plan: sourceRun.plan,
7407
8580
  steps: [repairStep],
7408
- summary: synthesis.text,
7409
- answer: synthesis.text,
8581
+ summary: presentationText,
8582
+ answer: presentationText,
7410
8583
  answerKind: 'governed',
7411
8584
  artifacts: [artifact],
7412
8585
  evaluations: [evaluation],
@@ -7419,7 +8592,7 @@ export async function startLocalServer(opts) {
7419
8592
  phase: 'run.completed',
7420
8593
  revision: 1,
7421
8594
  eventCursor: events.length,
7422
- startedAt: completedAt,
8595
+ startedAt: repairStartedAt,
7423
8596
  updatedAt: completedAt,
7424
8597
  completedAt,
7425
8598
  },
@@ -7432,7 +8605,19 @@ export async function startLocalServer(opts) {
7432
8605
  steps: [repairStep],
7433
8606
  artifacts: [artifact],
7434
8607
  evaluations: [evaluation],
8608
+ providerEgressReceipts,
7435
8609
  },
8610
+ diagnosticReceiptV2: {
8611
+ version: 2,
8612
+ runId: derivedRunId,
8613
+ route: 'generated_answer',
8614
+ status: 'needs_review',
8615
+ telemetry: repairTelemetry,
8616
+ providerEgressReceiptFingerprints: providerEgressReceipts.map((receipt) => executionFingerprint(stableExecutionValue(receipt))),
8617
+ repairCapabilityFingerprint: executionFingerprint(stableExecutionValue(capability)),
8618
+ },
8619
+ telemetry: repairTelemetry,
8620
+ providerEgressReceipts,
7436
8621
  derivation: {
7437
8622
  version: 1,
7438
8623
  kind: 'analytical_repair',
@@ -7578,21 +8763,59 @@ export async function startLocalServer(opts) {
7578
8763
  let runError;
7579
8764
  try {
7580
8765
  const executeRun = async (operationSignal, report) => {
7581
- parsed.request.signal = AbortSignal.any([
8766
+ const ingressSignal = AbortSignal.any([
7582
8767
  runController.signal,
7583
8768
  ...(operationSignal ? [operationSignal] : []),
7584
- AbortSignal.timeout(agentRunDeadlineMs(parsed.request, process.env, getActiveProvider(projectRoot))),
7585
8769
  ]);
8770
+ parsed.request.runBudget = createAgentRunBudget({
8771
+ requestedMode: parsed.request.requestedMode,
8772
+ inheritedSignal: ingressSignal,
8773
+ });
8774
+ parsed.request.signal = parsed.request.runBudget.hardSignal;
8775
+ // Ordinary Ask needs room to think, write, and recover: meaning (1)
8776
+ // + planning/generation (3) + narration (2) + one repair. The old
8777
+ // `total: 2` left nothing for narration once generation had run,
8778
+ // which is why the answer arrived as a deterministic fact-join.
8779
+ const runProviderEvidence = new RunScopedProviderDispatchEvidence(parsed.request.requestedMode === 'research'
8780
+ ? { total: 14, meaningResolution: 1, generationGroup: 11, narration: 2, repair: 1 }
8781
+ : { total: 6, meaningResolution: 1, generationGroup: 3, narration: 2, repair: 1 }, parsed.request.runBudget, resolveProviderResultRowEgressPolicy({
8782
+ projectSetting: projectConfig?.agent?.providerResultRowEgress,
8783
+ researchOptIn: parsed.request.requestedMode === 'research'
8784
+ && parsed.request.researchResultRowsOptIn === true,
8785
+ }));
7586
8786
  report?.({ phase: 'reasoning', progress: 8, message: 'Grounding the request and selecting the governed route.' });
7587
8787
  try {
7588
- const run = await agentRunEngine.run(parsed.request, (event) => {
8788
+ const engineRun = await agentRunProviderEvidenceContext.run(runProviderEvidence, () => agentRunEngine.run(parsed.request, (event) => {
8789
+ runProviderEvidence.setRoute(event.route);
7589
8790
  if (wantsStream)
7590
8791
  writeStream('agent-run-event', event);
7591
8792
  report?.({ phase: event.type, progress: 45, message: event.message });
7592
8793
  }, (delta) => {
7593
8794
  if (wantsStream)
7594
8795
  writeStream('agent-run-answer-delta', { runId: parsed.request.runId, delta });
7595
- });
8796
+ }));
8797
+ const rawRun = mergeRunScopedProviderDispatchEvidence(engineRun, runProviderEvidence.snapshot(engineRun.telemetry?.fallbackReason ?? engineRun.stopReason));
8798
+ let resolvedTargetFingerprint;
8799
+ try {
8800
+ const resolvedTarget = await resolveAgentRunExecutionConnection({
8801
+ question: rawRun.question,
8802
+ executionTarget: rawRun.executionTarget,
8803
+ });
8804
+ const resolvedTargetName = rawRun.executionTarget?.target === 'connection'
8805
+ ? rawRun.executionTarget.connectionName
8806
+ : rawRun.executionTarget?.target === 'local'
8807
+ ? 'local'
8808
+ : projectConfig.defaultConnectionName;
8809
+ resolvedTargetFingerprint = targetGenerationFingerprint(resolvedTarget, resolvedTargetName);
8810
+ }
8811
+ catch {
8812
+ // Missing target-generation evidence keeps automatic repair
8813
+ // ineligible; the failed run itself remains fully inspectable.
8814
+ }
8815
+ const run = attachAnalyticalRepairCapability(rawRun, resolvedTargetFingerprint);
8816
+ // The engine persists before the host can validate the DQL wrapper;
8817
+ // replace that row with the server-owned capability atomically.
8818
+ await agentRunStore.save(run);
7596
8819
  completedRun = run;
7597
8820
  recordConversationTurn(conversationStore, parsed.request.threadId, run);
7598
8821
  return { runId: run.id, threadId: parsed.request.threadId, status: run.status };
@@ -9658,13 +10881,22 @@ export async function startLocalServer(opts) {
9658
10881
  });
9659
10882
  if (!composed)
9660
10883
  throw new Error('The governed semantic query could not be composed.');
9661
- const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
9662
10884
  let normalizedResult;
9663
10885
  let repair;
9664
10886
  let executedSql = composed.sql;
10887
+ let executionFingerprints;
9665
10888
  try {
9666
- const result = await executor.executeQuery(prepared.sql, [], {}, prepared.connection);
9667
- normalizedResult = normalizeQueryResult(result);
10889
+ const execution = await analyticalExecutionService.execute({
10890
+ sql: composed.sql,
10891
+ subject: 'App tile semantic query',
10892
+ connection: targetConnection,
10893
+ });
10894
+ normalizedResult = execution.result;
10895
+ executedSql = execution.preparation.executedSql;
10896
+ executionFingerprints = {
10897
+ compiledSqlFingerprint: execution.compiledSqlFingerprint,
10898
+ resultFingerprint: execution.resultFingerprint,
10899
+ };
9668
10900
  }
9669
10901
  catch (executionError) {
9670
10902
  const repaired = await repairFailedAppTileExecution({
@@ -9706,6 +10938,7 @@ export async function startLocalServer(opts) {
9706
10938
  title: item.title ?? item.semantic.id,
9707
10939
  viz: item.viz,
9708
10940
  result: normalizedResult,
10941
+ ...(executionFingerprints ?? {}),
9709
10942
  trustState: 'review_required',
9710
10943
  citation: { kind: repair ? 'semantic_query_repaired' : 'semantic_query', name: item.semantic.id },
9711
10944
  ...(repair ? { repair } : {}),
@@ -9839,8 +11072,13 @@ export async function startLocalServer(opts) {
9839
11072
  tileId: item.i,
9840
11073
  tileFilterBindings: item.filterBindings,
9841
11074
  });
9842
- const prepared = prepareLocalExecution(blockFilterApplication.sql, blockTargetConnection, projectRoot, projectConfig);
9843
- const result = await executor.executeQuery(prepared.sql, blockFilterApplication.sqlParams, runtimeVariables({ ...blockFilterApplication.variables, ...blockInvocation.values }), prepared.connection);
11075
+ const execution = await analyticalExecutionService.execute({
11076
+ sql: blockFilterApplication.sql,
11077
+ subject: 'App tile block query',
11078
+ connection: blockTargetConnection,
11079
+ sqlParams: blockFilterApplication.sqlParams,
11080
+ variables: { ...blockFilterApplication.variables, ...blockInvocation.values },
11081
+ });
9844
11082
  tiles.push({
9845
11083
  tileId: item.i,
9846
11084
  status: 'ok',
@@ -9850,7 +11088,9 @@ export async function startLocalServer(opts) {
9850
11088
  title: item.title ?? block.name,
9851
11089
  viz: item.viz,
9852
11090
  chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
9853
- result: normalizeQueryResult(result),
11091
+ result: execution.result,
11092
+ compiledSqlFingerprint: execution.compiledSqlFingerprint,
11093
+ resultFingerprint: execution.resultFingerprint,
9854
11094
  filters: {
9855
11095
  applied: blockFilterApplication.appliedFilters,
9856
11096
  skipped: blockFilterApplication.skippedFilters,
@@ -14761,20 +16001,18 @@ export async function startLocalServer(opts) {
14761
16001
  }));
14762
16002
  return;
14763
16003
  }
14764
- const preparation = await prepareAnalyticalExecutionSql({
16004
+ const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
16005
+ const domain = typeof body.domain === 'string' ? body.domain : app?.domain;
16006
+ assertAppAccess({ app, domain, level: 'execute' });
16007
+ const execution = await analyticalExecutionService.execute({
14765
16008
  sql: semantic.sql,
14766
16009
  subject: 'Notebook query',
14767
- executor,
14768
16010
  connection: executionConnection,
14769
- projectRoot,
14770
- projectConfig,
16011
+ sqlParams: Array.isArray(body.sqlParams) ? body.sqlParams : [],
16012
+ variables: body.variables && typeof body.variables === 'object' ? body.variables : {},
16013
+ semanticRefs: semantic.semanticRefs,
14771
16014
  });
14772
- const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
14773
- const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
14774
- const domain = typeof body.domain === 'string' ? body.domain : app?.domain;
14775
- assertAppAccess({ app, domain, level: 'execute' });
14776
- const result = await executor.executeQuery(prepared.sql, Array.isArray(body.sqlParams) ? body.sqlParams : [], runtimeVariables(body.variables && typeof body.variables === 'object' ? body.variables : {}), prepared.connection);
14777
- const normalized = normalizeQueryResult(result, semantic.semanticRefs);
16015
+ const { preparation, result: normalized } = execution;
14778
16016
  if (execContext?.notebookPath) {
14779
16017
  recordNotebookQueryRun(projectRoot, {
14780
16018
  notebookPath: execContext.notebookPath,
@@ -15448,7 +16686,7 @@ export async function startLocalServer(opts) {
15448
16686
  const invocation = prepareBlockInvocation({ source, surface: 'ask_ai' });
15449
16687
  if (invocation.errors.length > 0)
15450
16688
  throw new Error(invocation.errors.join(' '));
15451
- const { executionReceipt: _receipt, compiledSql: _compiled, ...unexecuted } = artifact;
16689
+ const { executionReceipt: _receipt, executableArtifact: _executable, compiledSql: _compiled, ...unexecuted } = artifact;
15452
16690
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
15453
16691
  res.end(serializeJSON({
15454
16692
  artifact: {
@@ -15564,6 +16802,13 @@ export async function startLocalServer(opts) {
15564
16802
  trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
15565
16803
  compiledSql: result.sql,
15566
16804
  executionReceipt: result.executionReceipt,
16805
+ ...(result.executableArtifact ? {
16806
+ executableArtifact: {
16807
+ ...result.executableArtifact,
16808
+ kind: certified ? 'certified_block' : semantic ? 'semantic_block' : 'sql_block',
16809
+ trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
16810
+ },
16811
+ } : {}),
15567
16812
  ...(requestedArtifact?.limit ? { limit: requestedArtifact.limit } : {}),
15568
16813
  },
15569
16814
  }));
@@ -15686,37 +16931,43 @@ export async function startLocalServer(opts) {
15686
16931
  res.end(serializeJSON({ cellType: cell.type, result: null }));
15687
16932
  return;
15688
16933
  }
15689
- const preparation = await prepareAnalyticalExecutionSql({
16934
+ const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
16935
+ assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
16936
+ const semanticExecutionHolder = { value: null };
16937
+ const execution = await analyticalExecutionService.execute({
15690
16938
  sql: executableSql,
15691
16939
  subject: cell.type === 'dql' ? 'DQL query' : 'Notebook query',
15692
- executor,
15693
16940
  connection: cellConnection,
15694
- projectRoot,
15695
- projectConfig,
16941
+ sqlParams: plan?.sqlParams,
16942
+ variables: { ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) },
16943
+ executePrepared: semanticCompose?.compiled
16944
+ ? async (preparation) => {
16945
+ const semanticExecution = await executeTargetBoundSemanticQuery({
16946
+ executor,
16947
+ connection: cellConnection,
16948
+ projectRoot,
16949
+ plannedAdapter: semanticCompose.compiled.engine,
16950
+ metricFlow: semanticCompose.compiled.engine === 'metricflow-cli'
16951
+ ? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
16952
+ : undefined,
16953
+ // Compilation already produced the exact pinned adapter result
16954
+ // used by the plan. The gateway validates target identity before
16955
+ // accepting that result for preflight and execution.
16956
+ compile: async () => semanticCompose.compiled,
16957
+ prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
16958
+ rowBound: semanticCompose.compiled.effectiveRequest.limit,
16959
+ });
16960
+ if (semanticExecution) {
16961
+ semanticExecutionHolder.value = semanticExecution;
16962
+ return semanticExecution.result;
16963
+ }
16964
+ return executor.executeQuery(preparation.executedSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) }), preparation.connection);
16965
+ }
16966
+ : undefined,
15696
16967
  });
16968
+ const { preparation, result: normalized } = execution;
15697
16969
  const decodedSql = preparation.decodedSql;
15698
- const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
15699
- const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
15700
- assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
15701
- const semanticExecution = semanticCompose?.compiled
15702
- ? await executeTargetBoundSemanticQuery({
15703
- executor,
15704
- connection: cellConnection,
15705
- projectRoot,
15706
- plannedAdapter: semanticCompose.compiled.engine,
15707
- metricFlow: semanticCompose.compiled.engine === 'metricflow-cli'
15708
- ? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
15709
- : undefined,
15710
- // Compilation already produced the exact pinned adapter result
15711
- // used by the plan. The gateway validates target identity before
15712
- // accepting that result for preflight and execution.
15713
- compile: async () => semanticCompose.compiled,
15714
- prepareSql: () => ({ sql: prepared.sql, connection: prepared.connection }),
15715
- rowBound: semanticCompose.compiled.effectiveRequest.limit,
15716
- })
15717
- : null;
15718
- const rawResult = semanticExecution?.result ?? await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) }), prepared.connection);
15719
- const normalized = normalizeQueryResult(rawResult);
16970
+ const semanticExecution = semanticExecutionHolder.value;
15720
16971
  // Enforce the block's declared invariants against the result set. This
15721
16972
  // is additive: blocks without invariants produce `null` and the
15722
16973
  // response is unchanged. The agent surface (`query_via_block`) reads
@@ -15967,6 +17218,26 @@ table: ${table}${tagList}
15967
17218
  });
15968
17219
  });
15969
17220
  }
17221
+ function providerDispatchTerminalEvidence(value) {
17222
+ if (!value || typeof value !== 'object')
17223
+ return undefined;
17224
+ const candidate = value.providerDispatchEvidence;
17225
+ if (!candidate || typeof candidate !== 'object')
17226
+ return undefined;
17227
+ const evidence = candidate;
17228
+ if (!Array.isArray(evidence.providerEgressReceipts)
17229
+ || !Number.isInteger(evidence.providerRoundTrips)
17230
+ || (evidence.providerRoundTrips ?? -1) < 0)
17231
+ return undefined;
17232
+ return {
17233
+ providerEgressReceipts: evidence.providerEgressReceipts,
17234
+ providerRoundTrips: evidence.providerRoundTrips,
17235
+ toolCalls: Number.isInteger(evidence.toolCalls) && (evidence.toolCalls ?? -1) >= 0 ? evidence.toolCalls : 0,
17236
+ sqlExecutions: Number.isInteger(evidence.sqlExecutions) && (evidence.sqlExecutions ?? -1) >= 0 ? evidence.sqlExecutions : 0,
17237
+ repairs: Number.isInteger(evidence.repairs) && (evidence.repairs ?? -1) >= 0 ? evidence.repairs : 0,
17238
+ fallbackReason: typeof evidence.fallbackReason === 'string' ? evidence.fallbackReason : 'provider_error',
17239
+ };
17240
+ }
15970
17241
  async function validateModelingRelationship(relationship, manifest, execute, quoteIdentifier = quoteSqlIdentifier) {
15971
17242
  const fromEntity = manifest.modeling?.entities[relationship.from];
15972
17243
  const toEntity = manifest.modeling?.entities[relationship.to];
@@ -18651,9 +19922,104 @@ function createDqlArtifactExecutionReceipt(source, compiledSql, parameters, resu
18651
19922
  })),
18652
19923
  };
18653
19924
  }
19925
+ function agentRunTelemetryForAnswer(answer, egressReceipts, totalDurationMs, narrationDurationMs, engineRepairAttempts) {
19926
+ const timings = new Map((answer.evidence?.timings ?? []).map((timing) => [timing.phase, timing.durationMs]));
19927
+ const toolCalls = answer.evidence?.toolCalls ?? [];
19928
+ const toolDurationMs = toolCalls.reduce((sum, call) => sum + Math.max(0, call.durationMs ?? 0), 0);
19929
+ const warehouseDurationMs = answer.evidence?.execution?.executionTime ?? answer.result?.executionTime;
19930
+ const counters = answer.evidence?.runtimeCounters;
19931
+ const stageDurationsMs = {
19932
+ ...(timings.has('project_state') ? { snapshot: timings.get('project_state') } : {}),
19933
+ ...(timings.has('context_retrieval') ? { retrieval: timings.get('context_retrieval') } : {}),
19934
+ ...(timings.has('runtime_schema') ? { schema: timings.get('runtime_schema') } : {}),
19935
+ ...(timings.has('source_search') ? { meaning: timings.get('source_search') } : {}),
19936
+ ...(timings.has('answer_resolution') ? { provider: timings.get('answer_resolution') } : {}),
19937
+ ...(toolCalls.length > 0 ? { tools: toolDurationMs } : {}),
19938
+ ...(typeof warehouseDurationMs === 'number' ? { execution: Math.max(0, warehouseDurationMs) } : {}),
19939
+ ...(narrationDurationMs > 0 ? { narration: narrationDurationMs } : {}),
19940
+ total: Math.max(0, totalDurationMs),
19941
+ };
19942
+ return {
19943
+ version: 1,
19944
+ stageDurationsMs,
19945
+ providerRoundTrips: counters?.providerRoundTrips ?? 0,
19946
+ toolCalls: counters?.toolCalls ?? toolCalls.length,
19947
+ sqlExecutions: counters?.sqlExecutions ?? 0,
19948
+ repairs: Math.max(engineRepairAttempts, counters?.repairs ?? 0),
19949
+ egressReceipts: egressReceipts.length,
19950
+ ...(typeof warehouseDurationMs === 'number' ? { warehouseDurationMs: Math.max(0, warehouseDurationMs) } : {}),
19951
+ ...(answer.refusalCode ? { fallbackReason: answer.refusalCode } : {}),
19952
+ };
19953
+ }
18654
19954
  function executionFingerprint(value) {
18655
19955
  return createHash('sha256').update(value).digest('hex');
18656
19956
  }
19957
+ function buildSemanticAggregationCompilerReceipt(input) {
19958
+ const plan = input.plan;
19959
+ const metricId = plan?.analyticalFrame?.metricConceptIds[0];
19960
+ if (input.metricNames.length !== 1 || !plan || !metricId || !plan.executionId
19961
+ || !plan.selectedCapabilityFingerprint || !input.targetFingerprint)
19962
+ return undefined;
19963
+ const metric = input.semanticLayer.getMetric(input.metricNames[0]);
19964
+ if (!metric || (metric.aggregation ?? metric.metricType)?.toLowerCase() !== 'ratio')
19965
+ return undefined;
19966
+ const refName = (value) => {
19967
+ if (typeof value === 'string' && value.trim())
19968
+ return value.trim();
19969
+ if (!value || typeof value !== 'object' || Array.isArray(value))
19970
+ return undefined;
19971
+ const name = value.name;
19972
+ return typeof name === 'string' && name.trim() ? name.trim() : undefined;
19973
+ };
19974
+ const numeratorName = refName(metric.typeParams?.numerator);
19975
+ const denominatorName = refName(metric.typeParams?.denominator);
19976
+ if (!numeratorName || !denominatorName || !metric.sql.trim())
19977
+ return undefined;
19978
+ const measures = [numeratorName, denominatorName].map((name) => {
19979
+ const matches = input.semanticLayer.listMeasures().filter((measure) => measure.name === name
19980
+ && (!metric.cube || !measure.cube || measure.cube === metric.cube));
19981
+ return matches.length === 1 ? matches[0] : undefined;
19982
+ });
19983
+ if (measures.some((measure) => !measure))
19984
+ return undefined;
19985
+ const relations = Array.from(new Set(measures.map((measure) => measure.table.trim()).filter(Boolean)));
19986
+ if (relations.length !== 1)
19987
+ return undefined;
19988
+ const outputAlias = metricId.split(/[:.]/).at(-1);
19989
+ if (!outputAlias)
19990
+ return undefined;
19991
+ const orderedMeasureIds = measures.map((measure) => `semantic:${measure.domain || 'uncategorized'}:measure:${measure.cube ? `${measure.cube}.` : ''}${measure.name}`);
19992
+ const compiledSqlFingerprint = semanticExecutionFingerprint(input.compiledSql);
19993
+ const compiledOutputExpression = buildSqlOutputExpressionSignature(input.compiledSql, outputAlias);
19994
+ if (!compiledOutputExpression)
19995
+ return undefined;
19996
+ const compiledExpressionFingerprint = semanticExecutionFingerprint(compiledOutputExpression.canonicalExpression);
19997
+ const compiledSignature = buildGeneratedAnalyticalSqlSignature(input.compiledSql);
19998
+ if (!compiledSignature)
19999
+ return undefined;
20000
+ const body = {
20001
+ version: 1,
20002
+ executionId: plan.executionId,
20003
+ capabilityFingerprint: plan.selectedCapabilityFingerprint,
20004
+ metricId,
20005
+ metricExpressionSql: metric.sql,
20006
+ operator: 'ratio',
20007
+ orderedMeasureIds,
20008
+ physicalRelation: relations[0],
20009
+ relationAliases: Array.from(new Set([metric.cube, ...measures.map((measure) => measure.cube)].filter((value) => Boolean(value)))),
20010
+ outputAlias,
20011
+ dimensionIds: [...new Set(plan.analyticalFrame?.dimensions.map((dimension) => dimension.dimensionId) ?? [])].sort(),
20012
+ relationshipPathIds: [...plan.relationshipPathIds].sort(),
20013
+ relationshipProofFingerprints: [...new Set(plan.relationshipProofs?.map((proof) => proof.authorityFingerprint) ?? [])].sort(),
20014
+ orderBy: compiledSignature.orderBy,
20015
+ compiledSqlFingerprint,
20016
+ compiledExpressionFingerprint,
20017
+ planFingerprint: plan.fingerprint,
20018
+ snapshotId: plan.snapshotId,
20019
+ targetFingerprint: input.targetFingerprint,
20020
+ };
20021
+ return { ...body, receiptFingerprint: semanticExecutionFingerprint(body) };
20022
+ }
18657
20023
  /**
18658
20024
  * Older clients normalized executable DQL with trim(), while the receipt was
18659
20025
  * correctly created from the original renderer output ending in one newline.
@@ -19071,6 +20437,43 @@ export function prepareLocalExecution(sql, connection, projectRoot, projectConfi
19071
20437
  connection: normalizedConnection,
19072
20438
  };
19073
20439
  }
20440
+ /**
20441
+ * The single connector boundary for Ask, Notebook, and artifact execution.
20442
+ * Callers compile their route-specific source first; this service owns target
20443
+ * preparation, parameters, execution, and result normalization after that.
20444
+ */
20445
+ export class ExecutionService {
20446
+ host;
20447
+ constructor(host) {
20448
+ this.host = host;
20449
+ }
20450
+ async execute(input) {
20451
+ const preparation = await prepareAnalyticalExecutionSql({
20452
+ sql: input.sql,
20453
+ subject: input.subject,
20454
+ executor: this.host.executor,
20455
+ connection: input.connection,
20456
+ projectRoot: this.host.projectRoot,
20457
+ projectConfig: this.host.projectConfig(),
20458
+ enforceReadOnly: input.enforceReadOnly,
20459
+ rowLimit: input.rowLimit,
20460
+ });
20461
+ const raw = input.executePrepared
20462
+ ? await input.executePrepared(preparation)
20463
+ : await this.host.executor.executeQuery(preparation.executedSql, input.sqlParams ?? [], runtimeVariables(input.variables ?? {}), preparation.connection);
20464
+ const result = normalizeQueryResult(raw, input.semanticRefs);
20465
+ return {
20466
+ preparation,
20467
+ result,
20468
+ compiledSqlFingerprint: executionFingerprint(preparation.executedSql),
20469
+ resultFingerprint: executionFingerprint(stableExecutionValue({
20470
+ columns: result.columns,
20471
+ rows: result.rows,
20472
+ rowCount: result.rowCount,
20473
+ })),
20474
+ };
20475
+ }
20476
+ }
19074
20477
  /**
19075
20478
  * One host-owned SQL preparation boundary shared by Ask and Notebook.
19076
20479
  *