@duckcodeailabs/dql-cli 1.13.3 → 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 (91) hide show
  1. package/dist/apps-api.d.ts +1 -0
  2. package/dist/apps-api.d.ts.map +1 -1
  3. package/dist/apps-api.js +31 -23
  4. package/dist/apps-api.js.map +1 -1
  5. package/dist/assets/dql-notebook/assets/{AgentLogPage-B3ugEb3H.js → AgentLogPage-BzQOKjyV.js} +1 -1
  6. package/dist/assets/dql-notebook/assets/{AiBuildDialog-BK_TDNcM.js → AiBuildDialog-CTxha499.js} +1 -1
  7. package/dist/assets/dql-notebook/assets/{AiBuildResult-Cob_Lj05.js → AiBuildResult--MuD_I6g.js} +3 -3
  8. package/dist/assets/dql-notebook/assets/{AiSidePanel-CzIgxmP9.js → AiSidePanel-DcI4PMJ1.js} +1 -1
  9. package/dist/assets/dql-notebook/assets/{AnalyticsHome-C2vJ2fxY.js → AnalyticsHome-Dq54vjNz.js} +1 -1
  10. package/dist/assets/dql-notebook/assets/AppsView-0SlwWYex.js +5595 -0
  11. package/dist/assets/dql-notebook/assets/{BlockStudio-CaDNTKMM.js → BlockStudio-CxxXZD3M.js} +12 -12
  12. package/dist/assets/dql-notebook/assets/{BusinessArtifactView-zwUOtddi.js → BusinessArtifactView-DxdAmeUN.js} +1 -1
  13. package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-BlnhTMu3.js → DbtFirstModelingPage-y81gFg_b.js} +4 -4
  14. package/dist/assets/dql-notebook/assets/{GitPage-Coevr9NJ.js → GitPage-GQtcwncb.js} +3 -3
  15. package/dist/assets/dql-notebook/assets/{GlobalAiRail-BTZS11x5.js → GlobalAiRail-DM4wkxR_.js} +1 -1
  16. package/dist/assets/dql-notebook/assets/{GovernedContextPage-CjyyM_ne.js → GovernedContextPage-B8Ft6JES.js} +3 -3
  17. package/dist/assets/dql-notebook/assets/{HelpDocsPage-C1UeP85v.js → HelpDocsPage-DnD4nMuF.js} +1 -1
  18. package/dist/assets/dql-notebook/assets/{HomePage-zGPFIvOY.js → HomePage-Ehb0ITj-.js} +1 -1
  19. package/dist/assets/dql-notebook/assets/{LineageDAG-DbXnQWu3.js → LineageDAG-Lvjc2AQX.js} +1 -1
  20. package/dist/assets/dql-notebook/assets/{LineageDetailView-Ch1Db01I.js → LineageDetailView-DX32pfbp.js} +1 -1
  21. package/dist/assets/dql-notebook/assets/{LineageDrawer-DLxqJO6D.js → LineageDrawer-CFq3jPfk.js} +1 -1
  22. package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-B2tNdkr_.js → LineagePathBreadcrumb-CVHXuHls.js} +1 -1
  23. package/dist/assets/dql-notebook/assets/{MiniLineageGraph-0gY-ZwCy.js → MiniLineageGraph-BKaeT5Kt.js} +1 -1
  24. package/dist/assets/dql-notebook/assets/{NewBlockModal-DKVkXD20.js → NewBlockModal-DI-JDzFH.js} +1 -1
  25. package/dist/assets/dql-notebook/assets/{NewNotebookModal-BUIByvYT.js → NewNotebookModal-Csw6eF74.js} +1 -1
  26. package/dist/assets/dql-notebook/assets/{NotebookEditor-D2fIUxjE.js → NotebookEditor-FEqs3789.js} +1 -1
  27. package/dist/assets/dql-notebook/assets/{ReadinessPage-Ci4LyNiQ.js → ReadinessPage-Bk0S_MEA.js} +1 -1
  28. package/dist/assets/dql-notebook/assets/{SetupOnboarding-BF7ai_0t.js → SetupOnboarding-DyLaPFGn.js} +1 -1
  29. package/dist/assets/dql-notebook/assets/{SkillsPage-BgYrgirJ.js → SkillsPage-DLHyvhci.js} +1 -1
  30. package/dist/assets/dql-notebook/assets/{TrustBadge-Bob1Laiq.js → TrustBadge-CIDLj2A6.js} +1 -1
  31. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-C0oKTU6G.js +88 -0
  32. package/dist/assets/dql-notebook/assets/{answer-to-notebook-Dk42N0pU.js → answer-to-notebook-CFEJHLvs.js} +1 -1
  33. package/dist/assets/dql-notebook/assets/{arrow-left-DKuuTw9l.js → arrow-left-B-Zdcyvm.js} +1 -1
  34. package/dist/assets/dql-notebook/assets/{arrow-right-TH8_ufKG.js → arrow-right-Cn8TM7cp.js} +1 -1
  35. package/dist/assets/dql-notebook/assets/{book-open-text-B-dcHHn0.js → book-open-text-Bko2NNUs.js} +1 -1
  36. package/dist/assets/dql-notebook/assets/{circle-x-BOc2KCZQ.js → circle-x-AWJAmUBB.js} +1 -1
  37. package/dist/assets/dql-notebook/assets/{dagre.esm-CPT_vn5p.js → dagre.esm-Cl_ucrRh.js} +1 -1
  38. package/dist/assets/dql-notebook/assets/{external-link-DtOfpYc6.js → external-link-DN57tb5f.js} +1 -1
  39. package/dist/assets/dql-notebook/assets/{grip-vertical-B8joDDH9.js → grip-vertical-BOWwFFva.js} +1 -1
  40. package/dist/assets/dql-notebook/assets/{index-DJRMz64S.js → index-Ck-wqvV2.js} +153 -153
  41. package/dist/assets/dql-notebook/assets/{link-2-DMZPHawg.js → link-2-xUhbfBs8.js} +1 -1
  42. package/dist/assets/dql-notebook/assets/{list-tree-Dw0qlxPK.js → list-tree-BDWcBr67.js} +1 -1
  43. package/dist/assets/dql-notebook/assets/{minimize-2-DUiSrPS3.js → minimize-2-CEeNoMTS.js} +1 -1
  44. package/dist/assets/dql-notebook/assets/{panel-right-open-BrrFrRZQ.js → panel-right-open-DnSeUvOY.js} +1 -1
  45. package/dist/assets/dql-notebook/assets/{play-Ced0HT9g.js → play-EoDj8-fa.js} +1 -1
  46. package/dist/assets/dql-notebook/assets/{rotate-ccw-D_9U9o7X.js → rotate-ccw-BAMxWT9A.js} +1 -1
  47. package/dist/assets/dql-notebook/assets/{semantic-fields-BlDOaonV.js → semantic-fields-BwLOs1kl.js} +1 -1
  48. package/dist/assets/dql-notebook/assets/{sliders-horizontal-BDt6KRGR.js → sliders-horizontal-BYkm8SWW.js} +1 -1
  49. package/dist/assets/dql-notebook/assets/{star-CxUbF_BW.js → star-sVexqNCs.js} +1 -1
  50. package/dist/assets/dql-notebook/assets/{triangle-alert-COWnsSdh.js → triangle-alert-D2njv4o4.js} +1 -1
  51. package/dist/assets/dql-notebook/assets/{upload-DSFC2kJX.js → upload-CXTMxIA8.js} +1 -1
  52. package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-DYdx896E.js → usePersistedAgentThreadId-CPDeaAgL.js} +1 -1
  53. package/dist/assets/dql-notebook/assets/{user-round-Bfu7fosn.js → user-round-zVsI_uud.js} +1 -1
  54. package/dist/assets/dql-notebook/assets/{wand-sparkles-DSpG0c0j.js → wand-sparkles-BMIwIzXT.js} +1 -1
  55. package/dist/assets/dql-notebook/assets/{workflow-B1_pj9gq.js → workflow-Dj9y1sNk.js} +1 -1
  56. package/dist/assets/dql-notebook/assets/{wrench-CvfM03KZ.js → wrench-DPQi6zrs.js} +1 -1
  57. package/dist/assets/dql-notebook/assets/{x-B4coLiOl.js → x-XhhbtinL.js} +1 -1
  58. package/dist/assets/dql-notebook/index.html +1 -1
  59. package/dist/llm/answer-loop-tools.d.ts +3 -1
  60. package/dist/llm/answer-loop-tools.d.ts.map +1 -1
  61. package/dist/llm/answer-loop-tools.js +31 -23
  62. package/dist/llm/answer-loop-tools.js.map +1 -1
  63. package/dist/llm/providers/dql-agent-provider.d.ts +3 -1
  64. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
  65. package/dist/llm/providers/dql-agent-provider.js +278 -19
  66. package/dist/llm/providers/dql-agent-provider.js.map +1 -1
  67. package/dist/llm/types.d.ts +29 -2
  68. package/dist/llm/types.d.ts.map +1 -1
  69. package/dist/local-operation-coordinator.d.ts.map +1 -1
  70. package/dist/local-operation-coordinator.js +16 -2
  71. package/dist/local-operation-coordinator.js.map +1 -1
  72. package/dist/local-runtime.d.ts +184 -4
  73. package/dist/local-runtime.d.ts.map +1 -1
  74. package/dist/local-runtime.js +1858 -387
  75. package/dist/local-runtime.js.map +1 -1
  76. package/dist/package.json +10 -10
  77. package/dist/providers/oauth/claude-oauth.d.ts +1 -1
  78. package/dist/providers/oauth/claude-oauth.d.ts.map +1 -1
  79. package/dist/providers/oauth/claude-oauth.js +18 -11
  80. package/dist/providers/oauth/claude-oauth.js.map +1 -1
  81. package/dist/providers/oauth/codex-oauth.d.ts +1 -1
  82. package/dist/providers/oauth/codex-oauth.d.ts.map +1 -1
  83. package/dist/providers/oauth/codex-oauth.js +9 -1
  84. package/dist/providers/oauth/codex-oauth.js.map +1 -1
  85. package/dist/providers/subscription-cli.d.ts +1 -1
  86. package/dist/providers/subscription-cli.d.ts.map +1 -1
  87. package/dist/providers/subscription-cli.js +33 -4
  88. package/dist/providers/subscription-cli.js.map +1 -1
  89. package/package.json +10 -10
  90. package/dist/assets/dql-notebook/assets/AppsView-DEWQ0sHT.js +0 -5588
  91. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-IxDUKSG8.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';
@@ -48,6 +49,7 @@ import { buildSemanticTargetBinding } from './semantic-execution/target-binding.
48
49
  import { discoverWarehouseMetadataScopes, normalizeConnectionMetadataScope, syncWarehouseMetadata, warehouseMetadataStatus, } from './warehouse-metadata.js';
49
50
  import { NotebookDatasetWorkspace, } from "./notebook-datasets.js";
50
51
  import { prepareBlockInvocation } from './block-invocation.js';
52
+ export const APP_SOURCE_REUSABLE_TAG = 'app-source';
51
53
  const NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT = 500;
52
54
  const NOTEBOOK_FAVICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#6d5dfc"/><path d="M9 9h14v14H9z" fill="none" stroke="#fff" stroke-width="2"/><path d="M13 13h6M13 17h6M13 21h4" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>';
53
55
  export function resolveProjectSemanticConfig(projectConfig, projectRoot) {
@@ -290,6 +292,12 @@ export function parseAgentRunRequestBody(body) {
290
292
  const workspaceContext = agentRunRecord(record.workspaceContext) ?? agentRunRecord(record.context);
291
293
  const signals = agentRunRecord(record.signals);
292
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
+ }
293
301
  const audience = record.audience === 'stakeholder' || record.audience === 'analyst'
294
302
  ? record.audience
295
303
  : undefined;
@@ -305,29 +313,47 @@ export function parseAgentRunRequestBody(body) {
305
313
  selectedObject,
306
314
  executionTarget,
307
315
  workspaceContext,
308
- conversationContext: agentRunRecord(record.conversationContext),
316
+ conversationContext: sanitizeClientConversationContext(agentRunRecord(record.conversationContext)),
309
317
  history: parseAgentRunHistory(record.history),
310
318
  threadId: agentRunString(record.threadId),
311
319
  runId: agentRunString(record.runId),
312
320
  reasoningEffort: parseAgentRunReasoningEffort(record.reasoningEffort),
313
321
  analysisDepth: parseAgentRunAnalysisDepth(record.analysisDepth) ?? parseAgentRunAnalysisDepth(record.depth),
314
322
  thinkingMode: coerceThinkingMode(record.thinkingMode),
323
+ researchResultRowsOptIn: requestedMode === 'research' && record.researchResultRowsOptIn === true,
315
324
  },
316
325
  };
317
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
+ }
318
350
  const AGENT_LOOKUP_DEADLINE_MS = 45_000;
319
351
  const AGENT_RESEARCH_DEADLINE_MS = 120_000;
320
- /** Env-tunable run deadline (e.g. slow subscription-CLI providers), clamped to sane bounds. */
321
- function resolveAgentDeadlineMs(envKey, fallback, env = process.env) {
322
- const configured = Number(env[envKey]);
323
- if (!Number.isFinite(configured) || configured <= 0)
324
- return fallback;
325
- return Math.max(15_000, Math.min(600_000, Math.floor(configured)));
326
- }
327
352
  /**
328
353
  * PERF-002: one wall-clock budget follows the request through routing, provider
329
354
  * calls, repair, and execution. Ordinary Ask never inherits Research's budget
330
- * 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.
331
357
  */
332
358
  /** Resolve governed display formats for result columns from the semantic layer (best-effort, cached per call). */
333
359
  function agentColumnDisplayFormats(projectRoot, columns) {
@@ -345,91 +371,63 @@ function agentColumnDisplayFormats(projectRoot, columns) {
345
371
  catch { /* formatting is presentation-only; never fail an answer over it */ }
346
372
  return formats;
347
373
  }
348
- /** Subscription CLI providers spawn a cold external process per LLM call (2-15s
349
- * startup each) across a multi-call loop, so the API-sized 45s default made even
350
- * simple questions "reach the bounded execution deadline" on office laptops with
351
- * no env override. The defaults must fit the transport's physics. */
352
- const AGENT_CLI_LOOKUP_DEADLINE_MS = 150_000;
353
- const AGENT_CLI_RESEARCH_DEADLINE_MS = 300_000;
354
- /** High reasoning effort makes every LLM call materially slower, so the same
355
- * multi-step build that fits a budget at medium effort overruns it at high. The
356
- * budget must scale with effort or "High · thorough" reliably times out. */
357
- function reasoningEffortBudgetMultiplier(effort) {
358
- 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;
359
380
  }
360
381
  /**
361
- * A ranking/breakdown/filtered question needs LLM SQL generation (the grain
362
- * dimension usually lives on a joined model the semantic layer can't compose),
363
- * and that multi-call build does NOT fit the bare lookup budget on a slow or
364
- * high-effort provider it dead-ends at the deadline. It is not "research", but
365
- * it needs research-sized time to finish. Detected from the requested SHAPE (a
366
- * top-N, a member filter, a breakdown dimension, or a time window) rather than a
367
- * recognized metric word, since office questions like "top bcm customers from
368
- * the south region" name no metric the planner knows yet. A bare scalar
369
- * ("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.
370
395
  */
371
- function isAnalyticalBuildQuestion(plan) {
372
- if (!plan.needsGeneratedSql)
373
- return false;
374
- return plan.requestedShape.dimensions.length > 0
375
- || plan.dimensionTerms.length > 0
376
- || plan.requestedShape.filters.length > 0
377
- || plan.timeTerms.length > 0
378
- || Boolean(plan.requestedShape.topN);
379
- }
380
- export function agentRunDeadlineMs(request, env = process.env, activeProviderId) {
381
- const cliProvider = activeProviderId === 'claude-code' || activeProviderId === 'codex';
382
- const lookupDeadline = resolveAgentDeadlineMs('DQL_AGENT_LOOKUP_DEADLINE_MS', cliProvider ? AGENT_CLI_LOOKUP_DEADLINE_MS : AGENT_LOOKUP_DEADLINE_MS, env);
383
- const researchDeadline = resolveAgentDeadlineMs('DQL_AGENT_RESEARCH_DEADLINE_MS', cliProvider ? AGENT_CLI_RESEARCH_DEADLINE_MS : AGENT_RESEARCH_DEADLINE_MS, env);
384
- // The "Thinking" control sends a thinkingMode; explicit reasoningEffort/
385
- // analysisDepth (CLI flags) take precedence but the mode is the common case.
386
- // The deadline MUST see the resolved values — "High" both slows every call
387
- // AND means deep — or picking High reliably times out at the lookup budget.
388
- const resolvedMode = request.thinkingMode ? resolveThinkingMode(request.thinkingMode) : {};
389
- const effectiveEffort = request.reasoningEffort ?? resolvedMode.reasoningEffort;
390
- const effectiveDepth = request.analysisDepth ?? resolvedMode.analysisDepth;
391
- // The budget is a CEILING, not a target — a fast answer returns immediately;
392
- // the larger tiers only matter when the build genuinely needs the time.
393
- const scale = (ms) => Math.min(600_000, Math.round(ms * reasoningEffortBudgetMultiplier(effectiveEffort)));
394
- if (request.requestedMode === 'research' || effectiveDepth === 'deep') {
395
- return scale(researchDeadline);
396
- }
397
- // A structured clarification chip displays a compact metric/path label, but
398
- // the engine restores and executes the original analytical question. Budget
399
- // from that same original question as well. Otherwise a complex ranking or
400
- // cross-model request can incorrectly inherit the 45s scalar-lookup budget
401
- // merely because the selected label is short, then time out after the user
402
- // already supplied the exact governed meaning.
403
- const deadlineQuestion = request.selectedEvidenceId
404
- ? request.clarificationSourceQuestion?.trim() || request.question
405
- : request.question;
406
- const plan = buildAnalysisQuestionPlan(deadlineQuestion);
407
- if (plan.needsResearchWorkspace || isAnalyticalBuildQuestion(plan)) {
408
- return scale(researchDeadline);
409
- }
410
- return scale(lookupDeadline);
411
- }
412
- 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.
413
399
  if (governedAnswer.kind === 'no_answer')
414
- return false;
415
- // The executed rows are the presentation boundary: certified, semantic,
416
- // DQL-first, and exploratory routes all deserve a separate stakeholder-facing
417
- // explanation grounded in those values. Trust and query mechanics stay in the
418
- // 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 };
419
408
  if (governedAnswer.result)
420
- return true;
421
- if (governedAnswer.kind === 'certified' || governedAnswer.certification === 'certified')
422
- return false;
423
- // EXP-001: exploratory prose is part of the trust boundary. A free-form
424
- // synthesis pass can accidentally relabel an entity-level lifetime measure
425
- // as a child-level allocation or total repeated values. Keep the host's
426
- // deterministic grain statement instead.
427
- if (governedAnswer.exploratoryCandidate)
428
- return false;
429
- const finalText = (governedAnswer.answer ?? governedAnswer.text ?? '').trim();
430
- if (governedAnswer.dqlArtifact && finalText)
431
- return false;
432
- 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';
433
431
  }
434
432
  export function agentAnswerHasExecutionFailure(governedAnswer) {
435
433
  return typeof governedAnswer.executionError === 'string'
@@ -543,6 +541,194 @@ export function warehouseFailureFromAgentRun(run) {
543
541
  }
544
542
  return undefined;
545
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
+ }
546
732
  /**
547
733
  * Retain planning and retrieval evidence for a successful derived repair while
548
734
  * deliberately dropping the source failure and any result-bound facts. This
@@ -655,7 +841,7 @@ async function conversationContextFromThread(store, threadId, clientContext, que
655
841
  });
656
842
  }
657
843
  return {
658
- ...(clientContext ?? {}),
844
+ ...(sanitizeClientConversationContext(clientContext) ?? {}),
659
845
  conversationStateVersion: 1,
660
846
  threadId,
661
847
  ...(serverSnapshot ? { conversationEnvelope: serverSnapshot } : {}),
@@ -782,7 +968,9 @@ export function conversationTurnInputFromRun(run) {
782
968
  ?? run.artifacts.find((candidate) => candidate.kind === 'research_run')
783
969
  ?? run.artifacts[0];
784
970
  const payload = agentRunRecord(artifact?.payload);
785
- 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);
786
974
  const columns = conversationResultColumns(result?.columns);
787
975
  // The visual preview stays tiny, but member resolution needs a wider bounded
788
976
  // value window. Deriving dimensions from only the eight preview rows caused a
@@ -923,6 +1111,230 @@ function apiErrorCode(error, fallback) {
923
1111
  function apiErrorMessage(error) {
924
1112
  return error instanceof Error ? error.message : String(error);
925
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
+ }
926
1338
  export async function startLocalServer(opts) {
927
1339
  const { rootDir, executor, connection: rawConnection, preferredPort, projectRoot = process.cwd() } = opts;
928
1340
  const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
@@ -944,6 +1356,11 @@ export async function startLocalServer(opts) {
944
1356
  if (gitRoot)
945
1357
  ensureLocalRuntimeGitignore(projectRoot);
946
1358
  let projectConfig = loadProjectConfig(projectRoot);
1359
+ const analyticalExecutionService = new ExecutionService({
1360
+ executor,
1361
+ projectRoot,
1362
+ projectConfig: () => projectConfig,
1363
+ });
947
1364
  recordAgentRuntimeVersion(projectRoot, runtimeVersion);
948
1365
  // A clone carries governed Hint Graph files in Git, never the rebuildable
949
1366
  // `.dql/cache` database. Materialize that projection as soon as the Notebook
@@ -1565,23 +1982,81 @@ export async function startLocalServer(opts) {
1565
1982
  };
1566
1983
  // Provider-backed narration for stakeholder stories. Reuses the same provider
1567
1984
  // adapter as the planner; narrateResult always returns (deterministic fallback).
1568
- const narrateForAgentRun = async (input) => narrateResult(input, {
1569
- complete: async ({ system, user, signal }) => {
1570
- const provider = await createBlockStudioAssistProvider(projectRoot);
1571
- if (!provider)
1572
- throw new Error('No AI provider configured for narration.');
1573
- return provider.generate([{ role: 'system', content: system }, { role: 'user', content: user }], { maxTokens: 600, temperature: 0.2, signal });
1574
- },
1575
- });
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
+ };
1576
2034
  async function runGovernedAgentAnswerForRun(request, repair, route = 'generated_answer', onProgress, routeDecision) {
1577
2035
  const governed = resolveGovernedAnswerRunner(projectRoot);
1578
- const resolvedProvider = governed?.provider ?? null;
1579
- 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
+ }
1580
2054
  if (!resolvedProvider || !runner) {
1581
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.');
1582
2056
  }
1583
2057
  let governedAnswer;
1584
2058
  let providerError;
2059
+ let providerDispatchEvidence;
1585
2060
  const isRepair = (repair?.attempt ?? 0) > 0 && Boolean(repair?.repairHint);
1586
2061
  // The chat composer sends a `thinkingMode` (auto/low/medium/high); resolve it
1587
2062
  // into the effort+depth bundle it stands for. An explicit `reasoningEffort` /
@@ -1641,6 +2116,9 @@ export async function startLocalServer(opts) {
1641
2116
  const semanticTableMapping = semanticLayer && semanticConnection
1642
2117
  ? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer, projectRoot, semanticConnectionName)
1643
2118
  : undefined;
2119
+ const generatedProposalTargetIdentity = semanticConnection
2120
+ ? await observeWarehouseTargetIdentity(executor, semanticConnection)
2121
+ : undefined;
1644
2122
  const requestedDomain = normalizeAgentRunDomain(agentRunWorkspaceValue(request, 'domain'));
1645
2123
  const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
1646
2124
  const requestedModelAreaId = agentRunWorkspaceValue(request, 'modelAreaId');
@@ -1658,6 +2136,9 @@ export async function startLocalServer(opts) {
1658
2136
  : undefined;
1659
2137
  await runner.run({
1660
2138
  provider: resolvedProvider,
2139
+ ...(agentRunProviderEvidenceContext.getStore()
2140
+ ? { providerDispatchEvidenceSink: agentRunProviderEvidenceContext.getStore() }
2141
+ : {}),
1661
2142
  messages: [
1662
2143
  // No-thread fallback path: bound the raw client history so follow-ups
1663
2144
  // cannot inflate every provider call (8 turns × 4k chars).
@@ -1677,6 +2158,8 @@ export async function startLocalServer(opts) {
1677
2158
  },
1678
2159
  reasoningEffort,
1679
2160
  ...(analysisDepth ? { analysisDepth } : {}),
2161
+ allowProviderSemanticMemberSelection: route === 'research',
2162
+ researchResultRowsOptIn: route === 'research' && request.researchResultRowsOptIn === true,
1680
2163
  projectRoot,
1681
2164
  preparedContextPack: preparedAgentContextPacks.get(request),
1682
2165
  domainContext,
@@ -1798,9 +2281,20 @@ export async function startLocalServer(opts) {
1798
2281
  : projectConfig.dbt?.projectDir;
1799
2282
  throw new SemanticRuntimeRequiredError(await explainMissingSemanticRuntime(projectRoot, dbtProjectPath));
1800
2283
  }
1801
- 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
+ });
1802
2296
  return {
1803
- sql: compiled.sql,
2297
+ sql: governedCompiledSql,
1804
2298
  engine: compiled.engine,
1805
2299
  selection: {
1806
2300
  ...selection,
@@ -1813,7 +2307,9 @@ export async function startLocalServer(opts) {
1813
2307
  trace: {
1814
2308
  ...compiled.semanticTrace,
1815
2309
  ...(targetBinding ? { targetBinding } : {}),
2310
+ ...(aggregationCompilerReceipt ? { aggregationCompilerReceipt } : {}),
1816
2311
  },
2312
+ ...(aggregationCompilerReceipt ? { aggregationCompilerReceipt } : {}),
1817
2313
  };
1818
2314
  },
1819
2315
  } : {}),
@@ -1826,6 +2322,9 @@ export async function startLocalServer(opts) {
1826
2322
  ...(routeDecision?.resolvedAnalyticalPlan
1827
2323
  ? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
1828
2324
  : {}),
2325
+ ...(generatedProposalTargetIdentity?.identityFingerprint
2326
+ ? { generatedProposalTargetFingerprint: generatedProposalTargetIdentity.identityFingerprint }
2327
+ : {}),
1829
2328
  analyticalReferenceInstant: new Date().toISOString(),
1830
2329
  executeCertifiedBlock: (node, invocation) => executeCertifiedBlockForAgent(node, invocation, semanticConnection, semanticConnectionName),
1831
2330
  executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName),
@@ -1842,10 +2341,11 @@ export async function startLocalServer(opts) {
1842
2341
  }
1843
2342
  if (turn.kind === 'error') {
1844
2343
  providerError = turn.message;
2344
+ providerDispatchEvidence = turn.dispatchEvidence;
1845
2345
  }
1846
2346
  }, runSignal);
1847
2347
  if (!governedAnswer) {
1848
- 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 }] : []));
1849
2349
  }
1850
2350
  return governedAnswer;
1851
2351
  }
@@ -2072,7 +2572,11 @@ export async function startLocalServer(opts) {
2072
2572
  surface: 'ask_ai',
2073
2573
  });
2074
2574
  const certified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
2075
- 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);
2076
2580
  governedAnswer.dqlArtifact = {
2077
2581
  ...artifact,
2078
2582
  ...(invocation.parameters.length > 0 ? { parameters: invocation.parameters } : {}),
@@ -2081,11 +2585,19 @@ export async function startLocalServer(opts) {
2081
2585
  trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
2082
2586
  ...(governedAnswer.result?.sql ? { compiledSql: governedAnswer.result.sql } : originalSql ? { compiledSql: originalSql } : {}),
2083
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
+ } : {}),
2084
2595
  };
2085
2596
  if (governedAnswer.result)
2086
2597
  delete governedAnswer.result.dqlArtifact;
2087
2598
  }
2088
2599
  const answerRunExecutor = async ({ request, route, routeDecision, attempt, repairHint, emit }) => {
2600
+ const runStartedAtMs = Date.now();
2089
2601
  let governedAnswer;
2090
2602
  try {
2091
2603
  governedAnswer = await runGovernedAgentAnswerForRun(request, { attempt, repairHint }, route, (message) => emit({ type: 'executor.started', message, route }), routeDecision);
@@ -2221,39 +2733,67 @@ export async function startLocalServer(opts) {
2221
2733
  // Deadline/cancellation propagates to the engine, which renders the
2222
2734
  // graceful "bounded execution deadline" outcome — never a provider error.
2223
2735
  rethrowIfCancelled(error, request.signal);
2224
- 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');
2225
2743
  return {
2226
2744
  summary: message,
2227
2745
  status: 'blocked',
2228
2746
  trustState: 'blocked',
2229
2747
  stopReason: 'blocked',
2230
- artifacts: [agentRunArtifact('answer', 'AI answer provider failed', {
2748
+ artifacts: [agentRunArtifact('answer', dispatchBudgetExhausted ? 'Internal orchestration budget exhausted' : 'AI answer provider failed', {
2231
2749
  kind: 'no_answer',
2232
- refusalCode: 'provider_error',
2750
+ refusalCode: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'provider_error',
2233
2751
  text: message,
2234
2752
  answer: message,
2235
2753
  ...(routeDecision?.resolvedAnalyticalPlan
2236
2754
  ? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
2237
2755
  : {}),
2238
2756
  providerFailure: {
2239
- code: 'AI_PROVIDER_FAILURE',
2757
+ code: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'AI_PROVIDER_FAILURE',
2240
2758
  message,
2241
- recoverable: true,
2759
+ recoverable: !dispatchBudgetExhausted,
2242
2760
  },
2243
2761
  }, undefined, 'blocked')],
2244
2762
  evaluations: [
2245
2763
  agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.'),
2246
- 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 }),
2247
2765
  ],
2248
2766
  nextActions: [
2249
- { id: 'retry-ask-after-provider', label: 'Retry after provider setup', route: 'generated_answer' },
2250
- { 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
+ ]),
2251
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
+ },
2252
2789
  };
2253
2790
  }
2254
2791
  const answeredRoute = resolvedRunRouteFromAnswer(governedAnswer) ?? route;
2255
2792
  const isCertified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
2256
- 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;
2257
2797
  const requestedNotebookDataset = findMentionedNotebookDataset(request.question, datasetWorkspace.list());
2258
2798
  const governedAssetMissesLocalSource = Boolean(requestedNotebookDataset && (isCertified
2259
2799
  || isSemantic
@@ -2270,7 +2810,8 @@ export async function startLocalServer(opts) {
2270
2810
  // governance boundary, not an ambiguous user question and not a repairable
2271
2811
  // retrieval miss. Keep the precise policy detail visible, but never burn two
2272
2812
  // more provider calls retrying the same incompatible candidate.
2273
- const isPolicyBlocked = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked';
2813
+ const isPolicyBlocked = (governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked')
2814
+ || semanticAggregationBlocked;
2274
2815
  const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure || isPolicyBlocked;
2275
2816
  // The model tried to compose a governed query and declined despite having usable
2276
2817
  // context (e.g. it wasn't confident about a multi-table join). The answer loop
@@ -2289,48 +2830,137 @@ export async function startLocalServer(opts) {
2289
2830
  // reply ran context-free and was clarified again: the reported clarify loop.
2290
2831
  const resolvedRoute = needsClarification ? 'clarify' : answeredRoute;
2291
2832
  const sql = governedAnswer.proposedSql ?? governedAnswer.sql;
2292
- 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)
2293
2837
  ? undefined
2294
2838
  : sql;
2295
- // Render executed rows deterministically for ordinary lookups. A second LLM
2296
- // call is reserved for an explicit research route; certified, semantic, and
2297
- // generated lookup answers must not pay another provider round-trip merely
2298
- // 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.
2299
2846
  let synthesizedAnswer;
2300
- 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
+ });
2301
2891
  try {
2302
- const provider = route === 'research'
2303
- ? await createBlockStudioAssistProvider(projectRoot)
2304
- : null;
2305
- const preview = agentResultToSynthesisPreview(governedAnswer.result);
2306
- const draft = governedAnswer.answer ?? governedAnswer.text;
2307
- const result = await synthesizeAnswer({
2308
- question: request.question,
2309
- category: routeDecision?.category,
2310
- // The primary Ask reply is always business-facing. Analysts keep
2311
- // the full DQL, SQL, lineage, gates, and grain in the inspector.
2312
- audience: 'stakeholder',
2313
- resultPreview: preview,
2314
- sql: sql,
2315
- draftText: draft,
2316
- gaps: businessNarrativeGaps(governedAnswer.validationWarnings),
2317
- rankingDirection: governedAnswer.contextPack?.questionPlan.requestedShape.rankingDirection,
2318
- // Governed display contract: metric/measure displayFormat metadata
2319
- // beats column-name guessing, so $ and decimals stay consistent
2320
- // between the narration and the table for names like total_bcm.
2321
- ...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
2322
- }, provider
2323
- ? {
2324
- complete: ({ system, user, signal, onDelta }) => streamOrGenerate(provider, [{ role: 'system', content: system }, { role: 'user', content: user }], { maxTokens: 350, temperature: 0.3, signal }, onDelta ?? (() => { })),
2325
- }
2326
- : {});
2327
- if (result.text)
2328
- 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
+ }
2329
2935
  }
2330
2936
  catch {
2331
- // Keep the governed draft on any synthesis failure.
2937
+ // Keep the governed draft on any narration failure.
2332
2938
  synthesizedAnswer = undefined;
2333
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
+ }));
2334
2964
  }
2335
2965
  // A grounding/modeling gap is a REFUSAL. It was computed above but never
2336
2966
  // reached this ternary, so it fell through to `needs_review` — the same
@@ -2414,9 +3044,21 @@ export async function startLocalServer(opts) {
2414
3044
  artifactKind: 'sql_cell',
2415
3045
  }] : []),
2416
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
+ }
2417
3055
  return {
2418
3056
  resolvedRoute,
2419
- answerRefusalCode: governedAnswer.kind === 'no_answer' ? governedAnswer.refusalCode : undefined,
3057
+ answerRefusalCode: governedAnswer.kind === 'no_answer'
3058
+ ? governedAnswer.refusalCode
3059
+ : semanticAggregationBlocked
3060
+ ? 'policy_blocked'
3061
+ : undefined,
2420
3062
  answerTier: governedAnswer.route?.tier,
2421
3063
  summary: isTerminalFailure
2422
3064
  ? analyticalFailureSummary(governedAnswer.analyticalFailure)
@@ -2476,7 +3118,9 @@ export async function startLocalServer(opts) {
2476
3118
  : needsClarification
2477
3119
  ? 'The answer loop needs more context before producing a governed answer.'
2478
3120
  : isPolicyBlocked
2479
- ? `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.'}`
2480
3124
  : isExploratory
2481
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.'
2482
3126
  : 'The answer is generated or semantic-layer backed and remains review-required.', governedAnswer.route),
@@ -2502,10 +3146,13 @@ export async function startLocalServer(opts) {
2502
3146
  },
2503
3147
  ] : []),
2504
3148
  ...(isPolicyBlocked ? [
2505
- agentRunEvaluation('relationship-policy', 'Relationship policy', false, 'warning', governedAnswer.refusalDetails?.message
2506
- ?? '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.', {
2507
3153
  refusalCode: governedAnswer.refusalCode,
2508
3154
  refusalDetails: governedAnswer.refusalDetails,
3155
+ aggregationSafetyProof: governedAnswer.aggregationSafetyProof,
2509
3156
  validationWarnings: governedAnswer.validationWarnings,
2510
3157
  route: governedAnswer.route,
2511
3158
  }),
@@ -2515,6 +3162,8 @@ export async function startLocalServer(opts) {
2515
3162
  ] : []),
2516
3163
  ],
2517
3164
  nextActions,
3165
+ providerEgressReceipts: finalProviderEgressReceipts,
3166
+ telemetry: finalTelemetry,
2518
3167
  };
2519
3168
  };
2520
3169
  const conversationRunExecutor = async ({ request, routeDecision, emitAnswerDelta }) => {
@@ -2548,13 +3197,28 @@ export async function startLocalServer(opts) {
2548
3197
  ...(request.history ?? []).slice(-6).map((message) => ({ role: message.role, content: message.text })),
2549
3198
  { role: 'user', content: request.question },
2550
3199
  ];
2551
- 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();
2552
3212
  // Perceived-latency: surface the reply as one delta for surfaces wired to stream.
2553
3213
  if (text)
2554
3214
  emitAnswerDelta?.(text);
2555
3215
  }
2556
3216
  }
2557
- catch {
3217
+ catch (error) {
3218
+ if (error && typeof error === 'object'
3219
+ && error.code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED') {
3220
+ throw error;
3221
+ }
2558
3222
  // Provider unavailable / errored — fall through to the deterministic reply.
2559
3223
  text = undefined;
2560
3224
  }
@@ -3093,7 +3757,7 @@ export async function startLocalServer(opts) {
3093
3757
  result: researchResultData,
3094
3758
  evidence: plan.sources,
3095
3759
  reviewRequired: true,
3096
- })
3760
+ }, request.researchResultRowsOptIn === true)
3097
3761
  : undefined;
3098
3762
  const summary = needsClarification
3099
3763
  ? 'Needs clarification before running deeper research.'
@@ -3608,7 +4272,19 @@ export async function startLocalServer(opts) {
3608
4272
  return provider.generate([
3609
4273
  { role: 'system', content: system },
3610
4274
  { role: 'user', content: user },
3611
- ], { 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
+ });
3612
4288
  },
3613
4289
  getCatalogContext: buildRankedAgentRunCatalogContext,
3614
4290
  });
@@ -3632,6 +4308,14 @@ export async function startLocalServer(opts) {
3632
4308
  // instead of leaving the UI in "Generating and validating SQL" for a
3633
4309
  // minute or more.
3634
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
+ } : {}),
3635
4319
  });
3636
4320
  },
3637
4321
  getEvidence: buildAgentRunEvidence,
@@ -3648,6 +4332,8 @@ export async function startLocalServer(opts) {
3648
4332
  // A run may outlive its streaming browser connection, so cancellation is
3649
4333
  // server-owned and keyed by run id rather than relying on fetch abort alone.
3650
4334
  const activeAgentRunControllers = new Map();
4335
+ const activeAnalyticalRepairReservations = new Set();
4336
+ const consumedAnalyticalRepairCapabilities = new Set();
3651
4337
  // Server-side conversation threads: persisted multi-turn state (survives refresh).
3652
4338
  // Lazy so a failed native-sqlite load never blocks the runtime; conversation
3653
4339
  // persistence degrades to the per-request context instead.
@@ -3738,14 +4424,21 @@ export async function startLocalServer(opts) {
3738
4424
  snapshotCells.push({ cellId, status: 'idle', executionCount: 0, executedAt });
3739
4425
  continue;
3740
4426
  }
3741
- const prepared = prepareLocalExecution(executableSql, activeConnection, projectRoot, projectConfig);
3742
4427
  assertAppAccess({ app, domain: resolved.domain ?? app.domain, level: 'execute' });
3743
- const rawResult = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), prepared.connection);
3744
- 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;
3745
4436
  snapshotCells.push({
3746
4437
  cellId,
3747
4438
  status: 'success',
3748
4439
  result,
4440
+ compiledSqlFingerprint: execution.compiledSqlFingerprint,
4441
+ resultFingerprint: execution.resultFingerprint,
3749
4442
  executionCount: 1,
3750
4443
  executedAt,
3751
4444
  });
@@ -3834,44 +4527,81 @@ export async function startLocalServer(opts) {
3834
4527
  const semanticError = semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message;
3835
4528
  throw analyticalError(semanticError ?? `DQL artifact "${metadata.name ?? 'draft'}" produced no executable SQL.`, { origin: 'dql_compilation', stage: 'compile' });
3836
4529
  }
3837
- 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({
3838
4538
  sql: semanticCompose?.sql ?? plan.sql,
3839
4539
  subject: 'DQL artifact query',
3840
- executor,
3841
4540
  connection: activeConnection,
3842
- projectRoot,
3843
- projectConfig,
3844
4541
  // Preserve the existing contract: previewed Ask artifacts are explicitly
3845
4542
  // read-only and bounded; saved/manual DQL execution keeps its compiler-
3846
4543
  // governed behavior when no preview bound was requested.
3847
4544
  enforceReadOnly: invocationInput?.rowLimit !== undefined,
3848
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
+ },
3849
4569
  });
3850
- const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
4570
+ const semanticExecution = semanticExecutionHolder.value;
4571
+ const { preparation, result: normalized } = execution;
3851
4572
  const executionSql = preparation.executedSql;
3852
- const app = loadRuntimeApp(projectRoot, activePersonaAppId());
3853
- const sourceDomain = metadata.domain ?? source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
3854
- assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
3855
- const pinnedSemanticCompile = semanticCompose?.sql
3856
- ? compiledSemanticQueries.get(executionFingerprint(semanticCompose.sql))
3857
- : undefined;
3858
- const semanticExecution = pinnedSemanticCompile
3859
- ? await executeTargetBoundSemanticQuery({
3860
- executor,
3861
- connection: activeConnection,
3862
- projectRoot,
3863
- plannedAdapter: pinnedSemanticCompile.engine,
3864
- metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
3865
- ? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
3866
- : undefined,
3867
- compile: async () => pinnedSemanticCompile,
3868
- prepareSql: () => ({ sql: executionSql, connection: prepared.connection }),
3869
- rowBound: invocationInput?.rowLimit ?? pinnedSemanticCompile.effectiveRequest.limit,
3870
- })
3871
- : null;
3872
- const rawResult = semanticExecution?.result ?? await executor.executeQuery(executionSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), prepared.connection);
3873
- const normalized = normalizeQueryResult(rawResult);
3874
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
+ };
3875
4605
  return {
3876
4606
  columns: normalized.columns,
3877
4607
  rows: normalized.rows,
@@ -3884,6 +4614,7 @@ export async function startLocalServer(opts) {
3884
4614
  parameters: invocation.resolvedParameters,
3885
4615
  auditId: invocation.auditId,
3886
4616
  executionReceipt,
4617
+ executableArtifact,
3887
4618
  ...(semanticExecution ? {
3888
4619
  semanticExecutionReceipt: semanticExecution.executionReceipt,
3889
4620
  semanticTargetBinding: semanticExecution.targetBinding,
@@ -3918,6 +4649,9 @@ export async function startLocalServer(opts) {
3918
4649
  trustState: 'certified',
3919
4650
  compiledSql: result.sql,
3920
4651
  executionReceipt: result.executionReceipt,
4652
+ ...(result.executableArtifact ? {
4653
+ executableArtifact: { ...result.executableArtifact, kind: 'certified_block', trustState: 'certified' },
4654
+ } : {}),
3921
4655
  ...(invocationInput?.rowLimit ? { limit: invocationInput.rowLimit } : {}),
3922
4656
  ...(invocationInput?.parameters && Object.keys(invocationInput.parameters).length > 0
3923
4657
  ? { parameterValues: invocationInput.parameters }
@@ -3959,6 +4693,7 @@ export async function startLocalServer(opts) {
3959
4693
  ...(Object.keys(invocation.values).length > 0 ? { parameterValues: invocation.values } : {}),
3960
4694
  compiledSql: result.sql,
3961
4695
  executionReceipt: result.executionReceipt,
4696
+ ...(result.executableArtifact ? { executableArtifact: result.executableArtifact } : {}),
3962
4697
  },
3963
4698
  };
3964
4699
  };
@@ -4035,7 +4770,7 @@ export async function startLocalServer(opts) {
4035
4770
  * string, so the only remaining reasons to differ are the connection and the
4036
4771
  * governance gates, both of which report themselves honestly.
4037
4772
  */
4038
- const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings) => {
4773
+ const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings, executionConnectionName) => {
4039
4774
  const activeConnection = requireActiveConnection(executionConnection);
4040
4775
  const rowBound = clampAnalyticalRowBound(seed?.limit ?? 200);
4041
4776
  const trimmed = sql.trim().replace(/;\s*$/, '').trim();
@@ -4057,18 +4792,6 @@ export async function startLocalServer(opts) {
4057
4792
  if (semantic.unresolvedRefs.length > 0) {
4058
4793
  throw analyticalError(`Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`, { origin: 'dql_compilation', stage: 'compile', code: 'semantic_ref' });
4059
4794
  }
4060
- const preparation = await prepareAnalyticalExecutionSql({
4061
- sql: semantic.sql,
4062
- subject: 'Generated SQL',
4063
- executor,
4064
- connection: activeConnection,
4065
- projectRoot,
4066
- projectConfig,
4067
- enforceReadOnly: true,
4068
- rowLimit: rowBound,
4069
- });
4070
- const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
4071
- const bounded = preparation.rowBound;
4072
4795
  const app = loadRuntimeApp(projectRoot, activePersonaAppId());
4073
4796
  // Generated SQL is governed as 'uncategorized' unless its seed declares a
4074
4797
  // domain — the same value `generatedSqlArtifactContract` stamps into the
@@ -4079,25 +4802,43 @@ export async function startLocalServer(opts) {
4079
4802
  assertAppAccess({ app, domain: sourceDomain, level: 'execute' });
4080
4803
  // A semantic query the loop already compiled (MetricFlow / dbt Cloud) must
4081
4804
  // execute through its pinned target binding, not as loose SQL.
4082
- const pinnedSemanticCompile = compiledSemanticQueries.get(executionFingerprint(prepared.sql))
4083
- ?? compiledSemanticQueries.get(executionFingerprint(trimmed));
4084
- const semanticExecution = pinnedSemanticCompile
4085
- ? await executeTargetBoundSemanticQuery({
4086
- executor,
4087
- connection: activeConnection,
4088
- projectRoot,
4089
- plannedAdapter: pinnedSemanticCompile.engine,
4090
- metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
4091
- ? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
4092
- : undefined,
4093
- compile: async () => pinnedSemanticCompile,
4094
- prepareSql: () => ({ sql: bounded.sql, connection: prepared.connection }),
4095
- rowBound,
4096
- })
4097
- : null;
4098
- const rawResult = semanticExecution?.result
4099
- ?? await executor.executeQuery(preparation.executedSql, bindings?.sqlParams ?? [], runtimeVariables(bindings?.variables ?? {}), preparation.connection);
4100
- 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;
4101
4842
  // The authoritative bound. `buildRowBoundedSql` may legitimately decline to
4102
4843
  // touch the statement, and only one of 15 drivers honours a maxRows hint, so
4103
4844
  // the host is the only layer correctness may depend on.
@@ -4105,6 +4846,38 @@ export async function startLocalServer(opts) {
4105
4846
  const rows = truncated ? normalized.rows.slice(0, rowBound) : normalized.rows;
4106
4847
  const warnings = [];
4107
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
+ };
4108
4881
  return {
4109
4882
  columns: normalized.columns,
4110
4883
  rows,
@@ -4112,9 +4885,10 @@ export async function startLocalServer(opts) {
4112
4885
  executionTime: normalized.executionTime,
4113
4886
  sql: bounded.sql,
4114
4887
  ...(truncated ? { truncated: true } : {}),
4115
- ...(artifact ? { dqlArtifact: { ...artifact, limit: rowBound } } : {}),
4888
+ ...(artifact ? { dqlArtifact: { ...artifact, limit: rowBound, executionReceipt, executableArtifact } } : {}),
4116
4889
  ...(warnings.length > 0 ? { validationWarnings: warnings } : {}),
4117
- executionReceipt: createDqlArtifactExecutionReceipt(artifact?.source ?? trimmed, bounded.sql, {}, { columns: normalized.columns, rows, rowCount: rows.length }),
4890
+ executionReceipt,
4891
+ executableArtifact,
4118
4892
  };
4119
4893
  };
4120
4894
  const executeGeneratedArtifactForAgent = async (question, sql, seed, executionConnection, executionConnectionName) => {
@@ -4124,7 +4898,7 @@ export async function startLocalServer(opts) {
4124
4898
  if (seed && seed.kind !== 'sql_block') {
4125
4899
  return executeArtifactReferenceForAgent({ ...seed, limit: seed.limit ?? 200 }, question, executionConnection, executionConnectionName);
4126
4900
  }
4127
- return executeGeneratedSqlDirect(question, sql, seed, executionConnection);
4901
+ return executeGeneratedSqlDirect(question, sql, seed, executionConnection, undefined, executionConnectionName);
4128
4902
  };
4129
4903
  /**
4130
4904
  * EXP-001: execution host for the deliberately narrow non-governed lane.
@@ -4431,12 +5205,33 @@ export async function startLocalServer(opts) {
4431
5205
  * conversation, writes a DQL file, changes targets, or promotes trust.
4432
5206
  */
4433
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
+ }
4434
5227
  let result;
4435
- let repairedSql = input.sourceSql;
5228
+ let repairedSql = sourceSql;
4436
5229
  let repairMode = 'deterministic';
4437
5230
  let firstFailure;
5231
+ const providerEgressReceipts = [];
5232
+ let providerRoundTrips = 0;
4438
5233
  try {
4439
- 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);
4440
5235
  }
4441
5236
  catch (error) {
4442
5237
  firstFailure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
@@ -4457,50 +5252,89 @@ export async function startLocalServer(opts) {
4457
5252
  const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
4458
5253
  throw boundedRepairError(failure.redactedMessage, 422, failure);
4459
5254
  }
4460
- const boundedSchema = schemaContext.slice(0, 16).map((table) => ({
5255
+ const boundedSchema = markProviderMetadataArray(schemaContext.slice(0, 16).map((table) => ({
4461
5256
  relation: table.relation,
4462
- columns: table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type })),
4463
- }));
5257
+ columns: markProviderMetadataArray(table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type }))),
5258
+ })));
4464
5259
  // CTX-008: a repair that re-derives joins from column names can undo a
4465
5260
  // join the original query got right. Give it the authored key pairs.
4466
- 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
+ });
4467
5301
  let raw;
4468
5302
  try {
4469
- raw = await provider.generate([
4470
- {
4471
- role: 'system',
4472
- content: [
4473
- 'Repair one failed read-only analytical SQL statement.',
4474
- 'Return exactly one JSON object in a ```json fence with keys summary, sql, viz, outputs.',
4475
- 'Preserve the requested grain, filters, output columns, and aggregation semantics.',
4476
- input.sqlParams?.length
4477
- ? 'Preserve every positional parameter placeholder exactly; do not add, remove, or renumber placeholders.'
4478
- : '',
4479
- 'Use only relations and columns present in the supplied schema context.',
4480
- 'When governedJoins is present, join on those exact key pairs rather than inferring a join from column names.',
4481
- 'Never emit internal DQL relation identities such as source::, model::, seed::, or snapshot::.',
4482
- 'Do not add writes, DDL, multiple statements, or commentary outside the JSON fence.',
4483
- ].join(' '),
4484
- },
4485
- {
4486
- role: 'user',
4487
- content: JSON.stringify({
4488
- question: input.question,
4489
- driver: input.targetConnection.driver,
4490
- failure: firstFailure.redactedMessage,
4491
- sql: input.sourceSql,
4492
- parameters: input.sqlParams?.map((parameter) => ({
4493
- name: parameter.name,
4494
- placeholder: `$${parameter.position}`,
4495
- })),
4496
- schema: boundedSchema,
4497
- ...(governedJoins.length ? { governedJoins } : {}),
4498
- }),
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;
4499
5324
  },
4500
- ], { maxTokens: 1200, temperature: 0 });
5325
+ });
4501
5326
  }
4502
5327
  catch {
4503
- 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
+ });
4504
5338
  }
4505
5339
  const proposal = parseProposal(raw);
4506
5340
  if (!proposal.sql?.trim()) {
@@ -4511,9 +5345,17 @@ export async function startLocalServer(opts) {
4511
5345
  if (requiredPlaceholders.some((placeholder) => !new RegExp(`${placeholder.replace('$', '\\$')}\\b`).test(repairedSql))) {
4512
5346
  throw boundedRepairError('The repair changed this cell\'s parameter contract, so DQL stopped it before execution.', 422);
4513
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
+ }
4514
5356
  repairMode = 'ai';
4515
5357
  try {
4516
- 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);
4517
5359
  }
4518
5360
  catch (error) {
4519
5361
  const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
@@ -4523,7 +5365,6 @@ export async function startLocalServer(opts) {
4523
5365
  // Keep preview-only row bounds out of editable notebook source. The one
4524
5366
  // deterministic source rewrite we do persist is decoding internal graph
4525
5367
  // identities, because those can never execute as warehouse SQL.
4526
- let rewrites = [];
4527
5368
  if (internalRelationIdsInSql(repairedSql).length > 0) {
4528
5369
  const normalizedSource = await prepareAnalyticalExecutionSql({
4529
5370
  sql: repairedSql,
@@ -4535,9 +5376,9 @@ export async function startLocalServer(opts) {
4535
5376
  enforceReadOnly: true,
4536
5377
  });
4537
5378
  repairedSql = normalizedSource.decodedSql;
4538
- rewrites = normalizedSource.rewrites;
5379
+ rewrites.push(...normalizedSource.rewrites);
4539
5380
  }
4540
- return { result, repairedSql, repairMode, firstFailure, rewrites };
5381
+ return { result, repairedSql, repairMode, firstFailure, providerEgressReceipts, providerRoundTrips, rewrites };
4541
5382
  };
4542
5383
  /**
4543
5384
  * Repair a DQL wrapper that cannot compile far enough to expose executable
@@ -4550,6 +5391,8 @@ export async function startLocalServer(opts) {
4550
5391
  if (!provider) {
4551
5392
  throw boundedRepairError('Configure an available AI provider in Settings to repair this DQL draft.', 409);
4552
5393
  }
5394
+ const providerEgressReceipts = [];
5395
+ let providerRoundTrips = 0;
4553
5396
  let schemaContext;
4554
5397
  try {
4555
5398
  // Include source identifiers in retrieval so a terse cell title still
@@ -4595,10 +5438,41 @@ export async function startLocalServer(opts) {
4595
5438
  ...(governedJoins.length ? { governedJoins } : {}),
4596
5439
  }),
4597
5440
  },
4598
- ], { 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
+ });
4599
5464
  }
4600
5465
  catch {
4601
- 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
+ });
4602
5476
  }
4603
5477
  let repairedSource;
4604
5478
  try {
@@ -4702,6 +5576,8 @@ export async function startLocalServer(opts) {
4702
5576
  repairedSql: plan.sql,
4703
5577
  repairedSource,
4704
5578
  repairMode: 'ai',
5579
+ providerEgressReceipts,
5580
+ providerRoundTrips,
4705
5581
  rewrites: [],
4706
5582
  sqlParams: plan.sqlParams,
4707
5583
  variables: { ...plan.variables, ...invocation.values },
@@ -5700,11 +6576,18 @@ export async function startLocalServer(opts) {
5700
6576
  tableMapping,
5701
6577
  semanticSql: semanticCompose?.sql ?? undefined,
5702
6578
  });
5703
- const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan?.sql ?? executableSql, activeConnection, projectRoot, projectConfig);
5704
- 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
+ });
5705
6586
  return {
5706
- sql: prepared.sql,
5707
- result: normalizeQueryResult(result),
6587
+ sql: execution.preparation.executedSql,
6588
+ result: execution.result,
6589
+ compiledSqlFingerprint: execution.compiledSqlFingerprint,
6590
+ resultFingerprint: execution.resultFingerprint,
5708
6591
  chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
5709
6592
  invocation,
5710
6593
  tests: plan?.tests ?? [],
@@ -5755,12 +6638,14 @@ export async function startLocalServer(opts) {
5755
6638
  if (tests.length === 0) {
5756
6639
  return { passed: 0, failed: 0, skipped: 0, duration: Date.now() - start, assertions: [], runAt: new Date() };
5757
6640
  }
5758
- const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan.sql, activeConnection, projectRoot, projectConfig);
5759
- const rawResult = await executor.executeQuery(prepared.sql, plan.sqlParams ?? [], runtimeVariables(plan.variables ?? {}), prepared.connection);
5760
- const rows = Array.isArray(rawResult?.rows) ? rawResult.rows : [];
5761
- const columns = Array.isArray(rawResult?.columns)
5762
- ? rawResult.columns.map((column) => typeof column === 'string' ? column : column?.name ?? String(column))
5763
- : [];
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;
5764
6649
  return evaluateBlockStudioTests(tests, rows, columns, start);
5765
6650
  };
5766
6651
  const saveDqlGenerationDraft = (importId, candidate) => saveDqlGenerationDraftForProject(projectRoot, importId, candidate);
@@ -5809,10 +6694,20 @@ export async function startLocalServer(opts) {
5809
6694
  };
5810
6695
  };
5811
6696
  const certifyBlockStudioSource = async (source, blockPath, options = {}) => {
6697
+ options.reportProgress?.({
6698
+ phase: 'validating',
6699
+ progress: 10,
6700
+ message: 'Validating the saved DQL source.',
6701
+ });
5812
6702
  let validation = validateBlockStudioSource(source, semanticLayer);
5813
6703
  let preview = null;
5814
6704
  let testResults = null;
5815
6705
  const blockers = [];
6706
+ options.reportProgress?.({
6707
+ phase: 'previewing',
6708
+ progress: 30,
6709
+ message: 'Compiling and previewing the saved block.',
6710
+ });
5816
6711
  try {
5817
6712
  preview = await runBlockStudioPreviewSource(source);
5818
6713
  validation = preview.validation;
@@ -5820,6 +6715,11 @@ export async function startLocalServer(opts) {
5820
6715
  catch (error) {
5821
6716
  blockers.push(error instanceof Error ? error.message : String(error));
5822
6717
  }
6718
+ options.reportProgress?.({
6719
+ phase: 'testing',
6720
+ progress: 60,
6721
+ message: 'Evaluating declared block tests.',
6722
+ });
5823
6723
  try {
5824
6724
  testResults = preview
5825
6725
  ? await runBlockStudioTestSummary(source, undefined, preview)
@@ -5835,6 +6735,11 @@ export async function startLocalServer(opts) {
5835
6735
  runAt: new Date(),
5836
6736
  };
5837
6737
  }
6738
+ options.reportProgress?.({
6739
+ phase: 'assessing',
6740
+ progress: 75,
6741
+ message: 'Assessing certification rules and evidence.',
6742
+ });
5838
6743
  const parsed = parseBlockSourceMetadata(source);
5839
6744
  const record = {
5840
6745
  id: parsed.name || 'local',
@@ -7222,23 +8127,81 @@ export async function startLocalServer(opts) {
7222
8127
  res.end(serializeJSON({ error: 'Agent run not found.' }));
7223
8128
  return;
7224
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
+ }
7225
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
+ }
7226
8173
  const existing = await agentRunStore.get(derivedRunId);
7227
8174
  if (existing) {
7228
- res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
7229
- 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
+ }));
7230
8185
  return;
7231
8186
  }
7232
- const sourceSql = repairableSqlFromAgentRun(sourceRun);
7233
8187
  const sourceDqlArtifact = repairableDqlArtifactFromAgentRun(sourceRun);
7234
8188
  const retainedFailure = warehouseFailureFromAgentRun(sourceRun);
7235
- 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())) {
7236
8200
  res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
7237
8201
  res.end(serializeJSON({
7238
- error: sourceSql || sourceDqlArtifact
7239
- ? 'This failure requires authorized access or a policy change and cannot be repaired by AI.'
7240
- : 'This run has no retained SQL statement or editable DQL artifact to repair.',
7241
- ...(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,
7242
8205
  }));
7243
8206
  return;
7244
8207
  }
@@ -7261,32 +8224,250 @@ export async function startLocalServer(opts) {
7261
8224
  : sourceRun.executionTarget?.target === 'local'
7262
8225
  ? 'local'
7263
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
+ }
7264
8308
  let repaired;
7265
8309
  try {
7266
- // Prefer the editable DQL contract even when the failed run also
7267
- // retained compiled SQL. This keeps parameters, visualization, tests,
7268
- // and the corrected wrapper attached to the repaired Ask result.
7269
- repaired = sourceDqlArtifact
7270
- ? await executeBoundedDqlRepair({
7271
- question: sourceRun.question,
7272
- source: sourceDqlArtifact.source,
7273
- failure: retainedFailure?.redactedMessage
7274
- ?? sourceRun.diagnosticReceipt?.failure?.message
7275
- ?? sourceRun.summary,
7276
- targetConnection,
7277
- targetConnectionName,
7278
- parameters: sourceDqlArtifact.parameterValues,
7279
- artifactName: sourceDqlArtifact.name,
7280
- })
7281
- : await executeBoundedSqlRepair({
7282
- question: sourceRun.question,
7283
- sourceSql: sourceSql,
7284
- targetConnection,
7285
- targetConnectionName,
7286
- });
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
+ };
7287
8420
  }
7288
8421
  catch (error) {
7289
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
+ });
7290
8471
  res.writeHead(repairError.status ?? 500, { 'Content-Type': 'application/json; charset=utf-8' });
7291
8472
  res.end(serializeJSON({
7292
8473
  error: repairError.message,
@@ -7294,27 +8475,39 @@ export async function startLocalServer(opts) {
7294
8475
  }));
7295
8476
  return;
7296
8477
  }
7297
- const { result, repairedSql, repairMode, firstFailure } = repaired;
8478
+ const { result, repairedSql, repairMode, firstFailure, providerEgressReceipts, providerRoundTrips } = repaired;
7298
8479
  const completedAt = new Date().toISOString();
7299
- const preview = agentResultToSynthesisPreview(result);
7300
- const synthesis = await synthesizeAnswer({
7301
- question: sourceRun.question,
7302
- audience: 'stakeholder',
7303
- resultPreview: preview,
7304
- sql: repairedSql,
7305
- draftText: `The repaired query returned ${result.rowCount} row${result.rowCount === 1 ? '' : 's'}.`,
7306
- ...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
7307
- });
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.`;
7308
8483
  const sourceFailure = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
7309
8484
  const presentationContext = repairPresentationContextFromAgentRun(sourceRun);
7310
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
+ };
7311
8504
  const artifact = agentRunArtifact('answer', 'Repaired analytical answer', {
7312
8505
  kind: 'uncertified',
7313
8506
  certification: 'analyst_review_required',
7314
8507
  reviewStatus: 'analyst_review_required',
7315
8508
  trustLabel: 'AI-generated · review required',
7316
- answer: synthesis.text,
7317
- text: synthesis.text,
8509
+ answer: presentationText,
8510
+ text: presentationText,
7318
8511
  ...presentationContext,
7319
8512
  result,
7320
8513
  proposedSql: repairedSql,
@@ -7335,10 +8528,11 @@ export async function startLocalServer(opts) {
7335
8528
  rowCount: result.rowCount,
7336
8529
  ...(executionResultFingerprint ? { resultFingerprint: executionResultFingerprint } : {}),
7337
8530
  },
8531
+ providerEgressReceipts,
7338
8532
  },
7339
8533
  previousAttempt: {
7340
8534
  sourceRunId: sourceRun.id,
7341
- ...(sourceSql ? { sql: sourceSql } : {}),
8535
+ sql: repairableSqlFromAgentRun(sourceRun),
7342
8536
  ...(sourceDqlArtifact?.source ? { dql: sourceDqlArtifact.source } : {}),
7343
8537
  failure: retainedFailure ?? firstFailure,
7344
8538
  },
@@ -7365,7 +8559,7 @@ export async function startLocalServer(opts) {
7365
8559
  artifacts: [artifact],
7366
8560
  };
7367
8561
  const events = [
7368
- { 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' },
7369
8563
  { id: `${derivedRunId}:artifact`, runId: derivedRunId, type: 'artifact.created', at: completedAt, message: 'Created the repaired result artifact.', route: 'generated_answer', trustState: 'review_required' },
7370
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' },
7371
8565
  ];
@@ -7377,15 +8571,15 @@ export async function startLocalServer(opts) {
7377
8571
  status: 'needs_review',
7378
8572
  trustState: 'review_required',
7379
8573
  stopReason: 'generated_review_required',
7380
- startedAt: completedAt,
8574
+ startedAt: repairStartedAt,
7381
8575
  completedAt,
7382
8576
  selectedObject: sourceRun.selectedObject,
7383
8577
  executionTarget: sourceRun.executionTarget,
7384
8578
  routeDecision: sourceRun.routeDecision,
7385
8579
  plan: sourceRun.plan,
7386
8580
  steps: [repairStep],
7387
- summary: synthesis.text,
7388
- answer: synthesis.text,
8581
+ summary: presentationText,
8582
+ answer: presentationText,
7389
8583
  answerKind: 'governed',
7390
8584
  artifacts: [artifact],
7391
8585
  evaluations: [evaluation],
@@ -7398,7 +8592,7 @@ export async function startLocalServer(opts) {
7398
8592
  phase: 'run.completed',
7399
8593
  revision: 1,
7400
8594
  eventCursor: events.length,
7401
- startedAt: completedAt,
8595
+ startedAt: repairStartedAt,
7402
8596
  updatedAt: completedAt,
7403
8597
  completedAt,
7404
8598
  },
@@ -7411,7 +8605,19 @@ export async function startLocalServer(opts) {
7411
8605
  steps: [repairStep],
7412
8606
  artifacts: [artifact],
7413
8607
  evaluations: [evaluation],
8608
+ providerEgressReceipts,
7414
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,
7415
8621
  derivation: {
7416
8622
  version: 1,
7417
8623
  kind: 'analytical_repair',
@@ -7557,21 +8763,59 @@ export async function startLocalServer(opts) {
7557
8763
  let runError;
7558
8764
  try {
7559
8765
  const executeRun = async (operationSignal, report) => {
7560
- parsed.request.signal = AbortSignal.any([
8766
+ const ingressSignal = AbortSignal.any([
7561
8767
  runController.signal,
7562
8768
  ...(operationSignal ? [operationSignal] : []),
7563
- AbortSignal.timeout(agentRunDeadlineMs(parsed.request, process.env, getActiveProvider(projectRoot))),
7564
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
+ }));
7565
8786
  report?.({ phase: 'reasoning', progress: 8, message: 'Grounding the request and selecting the governed route.' });
7566
8787
  try {
7567
- 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);
7568
8790
  if (wantsStream)
7569
8791
  writeStream('agent-run-event', event);
7570
8792
  report?.({ phase: event.type, progress: 45, message: event.message });
7571
8793
  }, (delta) => {
7572
8794
  if (wantsStream)
7573
8795
  writeStream('agent-run-answer-delta', { runId: parsed.request.runId, delta });
7574
- });
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);
7575
8819
  completedRun = run;
7576
8820
  recordConversationTurn(conversationStore, parsed.request.threadId, run);
7577
8821
  return { runId: run.id, threadId: parsed.request.threadId, status: run.status };
@@ -9637,13 +10881,22 @@ export async function startLocalServer(opts) {
9637
10881
  });
9638
10882
  if (!composed)
9639
10883
  throw new Error('The governed semantic query could not be composed.');
9640
- const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
9641
10884
  let normalizedResult;
9642
10885
  let repair;
9643
10886
  let executedSql = composed.sql;
10887
+ let executionFingerprints;
9644
10888
  try {
9645
- const result = await executor.executeQuery(prepared.sql, [], {}, prepared.connection);
9646
- 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
+ };
9647
10900
  }
9648
10901
  catch (executionError) {
9649
10902
  const repaired = await repairFailedAppTileExecution({
@@ -9685,6 +10938,7 @@ export async function startLocalServer(opts) {
9685
10938
  title: item.title ?? item.semantic.id,
9686
10939
  viz: item.viz,
9687
10940
  result: normalizedResult,
10941
+ ...(executionFingerprints ?? {}),
9688
10942
  trustState: 'review_required',
9689
10943
  citation: { kind: repair ? 'semantic_query_repaired' : 'semantic_query', name: item.semantic.id },
9690
10944
  ...(repair ? { repair } : {}),
@@ -9818,8 +11072,13 @@ export async function startLocalServer(opts) {
9818
11072
  tileId: item.i,
9819
11073
  tileFilterBindings: item.filterBindings,
9820
11074
  });
9821
- const prepared = prepareLocalExecution(blockFilterApplication.sql, blockTargetConnection, projectRoot, projectConfig);
9822
- 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
+ });
9823
11082
  tiles.push({
9824
11083
  tileId: item.i,
9825
11084
  status: 'ok',
@@ -9829,7 +11088,9 @@ export async function startLocalServer(opts) {
9829
11088
  title: item.title ?? block.name,
9830
11089
  viz: item.viz,
9831
11090
  chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
9832
- result: normalizeQueryResult(result),
11091
+ result: execution.result,
11092
+ compiledSqlFingerprint: execution.compiledSqlFingerprint,
11093
+ resultFingerprint: execution.resultFingerprint,
9833
11094
  filters: {
9834
11095
  applied: blockFilterApplication.appliedFilters,
9835
11096
  skipped: blockFilterApplication.skippedFilters,
@@ -11689,7 +12950,7 @@ export async function startLocalServer(opts) {
11689
12950
  return;
11690
12951
  }
11691
12952
  }
11692
- const draftSource = setBlockStudioStatusInSource(source, 'draft');
12953
+ const draftSource = markBlockStudioSourceReusable(setBlockStudioStatusInSource(source, 'draft'));
11693
12954
  const parsed = parseBlockSourceMetadata(draftSource);
11694
12955
  const name = typeof suppliedMetadata.name === 'string' && suppliedMetadata.name.trim()
11695
12956
  ? suppliedMetadata.name.trim()
@@ -11710,7 +12971,10 @@ export async function startLocalServer(opts) {
11710
12971
  domain: typeof suppliedMetadata.domain === 'string' ? suppliedMetadata.domain : parsed.domain,
11711
12972
  description: typeof suppliedMetadata.description === 'string' ? suppliedMetadata.description : parsed.description,
11712
12973
  owner: typeof suppliedMetadata.owner === 'string' ? suppliedMetadata.owner : parsed.owner,
11713
- tags: Array.isArray(suppliedMetadata.tags) ? suppliedMetadata.tags.map(String) : parsed.tags,
12974
+ tags: Array.from(new Set([
12975
+ ...(Array.isArray(suppliedMetadata.tags) ? suppliedMetadata.tags.map(String) : parsed.tags),
12976
+ APP_SOURCE_REUSABLE_TAG,
12977
+ ])),
11714
12978
  lineage: Array.isArray(suppliedMetadata.lineage) ? suppliedMetadata.lineage.map(String) : undefined,
11715
12979
  };
11716
12980
  // A saved draft already has a durable identity, even when it lives in
@@ -11747,8 +13011,14 @@ export async function startLocalServer(opts) {
11747
13011
  }
11748
13012
  }
11749
13013
  operationCoordinator.run(operation.id, async (signal, report) => {
11750
- report({ phase: 'validating', progress: 15, message: 'Validating and running the saved draft once.' });
11751
- const result = await certifyBlockStudioSource(draftSource, draftPath, { enterprise: body.enterprise === true });
13014
+ const result = await certifyBlockStudioSource(draftSource, draftPath, {
13015
+ enterprise: body.enterprise === true,
13016
+ reportProgress: (progress) => {
13017
+ if (signal.aborted)
13018
+ throw Object.assign(new Error('Certification cancelled.'), { code: 'OPERATION_CANCELLED' });
13019
+ report(progress);
13020
+ },
13021
+ });
11752
13022
  const blockers = Array.from(new Set(result.checklist.blockers));
11753
13023
  if (signal.aborted)
11754
13024
  throw Object.assign(new Error('Certification cancelled.'), { code: 'OPERATION_CANCELLED' });
@@ -11771,7 +13041,7 @@ export async function startLocalServer(opts) {
11771
13041
  block: { ...openedDraft, validation: result.validation },
11772
13042
  };
11773
13043
  }
11774
- report({ phase: 'publishing', progress: 80, message: 'Publishing the certified block.' });
13044
+ report({ phase: 'publishing', progress: 82, message: 'Publishing the certified block.' });
11775
13045
  const certifiedSource = setBlockStudioStatusInSource(draftSource, 'certified');
11776
13046
  const certifiedMetadata = parseBlockSourceMetadata(certifiedSource);
11777
13047
  const savedPath = saveBlockStudioArtifacts(projectRoot, {
@@ -11813,7 +13083,9 @@ export async function startLocalServer(opts) {
11813
13083
  catch {
11814
13084
  /* best-effort: learning capture must never block certification */
11815
13085
  }
13086
+ report({ phase: 'indexing', progress: 92, message: 'Queueing the certified source index refresh.' });
11816
13087
  const refreshOperation = scheduleProjectRefresh('block-studio-certification');
13088
+ report({ phase: 'indexing', progress: 96, message: 'Certified source saved; index refresh queued.' });
11817
13089
  if (result.preview)
11818
13090
  writeBlockStudioRunSummary(projectRoot, savedPath, certifiedSource, result.preview.result);
11819
13091
  const openedCertified = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
@@ -12493,13 +13765,14 @@ export async function startLocalServer(opts) {
12493
13765
  }));
12494
13766
  return;
12495
13767
  }
13768
+ const reusableSource = markBlockStudioSourceReusable(readiness.candidate.dqlSource);
12496
13769
  const savedPath = saveBlockStudioArtifacts(projectRoot, {
12497
- source: readiness.candidate.dqlSource,
13770
+ source: reusableSource,
12498
13771
  name: readiness.candidate.name,
12499
13772
  domain: readiness.candidate.domain,
12500
13773
  description: readiness.candidate.description,
12501
13774
  owner: readiness.candidate.owner,
12502
- tags: readiness.candidate.tags,
13775
+ tags: Array.from(new Set([...readiness.candidate.tags, APP_SOURCE_REUSABLE_TAG])),
12503
13776
  lineage: readiness.candidate.lineage.sourceTables,
12504
13777
  importMeta: {
12505
13778
  importId,
@@ -12508,7 +13781,13 @@ export async function startLocalServer(opts) {
12508
13781
  sourcePath: readiness.candidate.sourcePath,
12509
13782
  },
12510
13783
  });
12511
- const next = { ...readiness.candidate, reviewStatus: 'saved', savedPath };
13784
+ const next = {
13785
+ ...readiness.candidate,
13786
+ dqlSource: reusableSource,
13787
+ tags: Array.from(new Set([...readiness.candidate.tags, APP_SOURCE_REUSABLE_TAG])),
13788
+ reviewStatus: 'saved',
13789
+ savedPath,
13790
+ };
12512
13791
  writeBlockStudioImportCandidate(projectRoot, importId, next);
12513
13792
  await refreshLocalMetadataCatalog(projectRoot);
12514
13793
  const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
@@ -12751,7 +14030,7 @@ export async function startLocalServer(opts) {
12751
14030
  try {
12752
14031
  const body = await readJSON(req);
12753
14032
  const source = typeof body.source === 'string'
12754
- ? sanitizeAgentBlockDraftSource(body.source)
14033
+ ? markBlockStudioSourceReusable(sanitizeAgentBlockDraftSource(body.source))
12755
14034
  : '';
12756
14035
  const name = typeof body.name === 'string' ? body.name.trim() : '';
12757
14036
  if (!source.trim() || !name) {
@@ -12764,7 +14043,10 @@ export async function startLocalServer(opts) {
12764
14043
  name,
12765
14044
  domain: typeof body.domain === 'string' ? body.domain : undefined,
12766
14045
  description: typeof body.description === 'string' ? body.description : undefined,
12767
- tags: Array.isArray(body.tags) ? body.tags.map(String) : ['ai-generated', 'review-required'],
14046
+ tags: Array.from(new Set([
14047
+ ...(Array.isArray(body.tags) ? body.tags.map(String) : ['ai-generated', 'review-required']),
14048
+ APP_SOURCE_REUSABLE_TAG,
14049
+ ])),
12768
14050
  stableSuffix: typeof body.runId === 'string' ? body.runId : undefined,
12769
14051
  });
12770
14052
  const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
@@ -12780,7 +14062,9 @@ export async function startLocalServer(opts) {
12780
14062
  if (req.method === 'POST' && path === '/api/block-studio/save') {
12781
14063
  try {
12782
14064
  const body = await readJSON(req);
12783
- const source = typeof body.source === 'string' ? body.source : '';
14065
+ const source = typeof body.source === 'string'
14066
+ ? markBlockStudioSourceReusable(body.source)
14067
+ : '';
12784
14068
  const metadata = body.metadata && typeof body.metadata === 'object'
12785
14069
  ? body.metadata
12786
14070
  : {};
@@ -12817,7 +14101,10 @@ export async function startLocalServer(opts) {
12817
14101
  folderPath: metadata.folderPath,
12818
14102
  description: metadata.description,
12819
14103
  owner: metadata.owner,
12820
- tags: Array.isArray(metadata.tags) ? metadata.tags.map(String) : [],
14104
+ tags: Array.from(new Set([
14105
+ ...(Array.isArray(metadata.tags) ? metadata.tags.map(String) : []),
14106
+ APP_SOURCE_REUSABLE_TAG,
14107
+ ])),
12821
14108
  lineage: Array.isArray(metadata.lineage) ? metadata.lineage.map(String) : undefined,
12822
14109
  importMeta: metadata.sourceKind || metadata.sourcePath || metadata.importId || metadata.candidateId
12823
14110
  ? {
@@ -14714,20 +16001,18 @@ export async function startLocalServer(opts) {
14714
16001
  }));
14715
16002
  return;
14716
16003
  }
14717
- 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({
14718
16008
  sql: semantic.sql,
14719
16009
  subject: 'Notebook query',
14720
- executor,
14721
16010
  connection: executionConnection,
14722
- projectRoot,
14723
- projectConfig,
16011
+ sqlParams: Array.isArray(body.sqlParams) ? body.sqlParams : [],
16012
+ variables: body.variables && typeof body.variables === 'object' ? body.variables : {},
16013
+ semanticRefs: semantic.semanticRefs,
14724
16014
  });
14725
- const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
14726
- const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
14727
- const domain = typeof body.domain === 'string' ? body.domain : app?.domain;
14728
- assertAppAccess({ app, domain, level: 'execute' });
14729
- const result = await executor.executeQuery(prepared.sql, Array.isArray(body.sqlParams) ? body.sqlParams : [], runtimeVariables(body.variables && typeof body.variables === 'object' ? body.variables : {}), prepared.connection);
14730
- const normalized = normalizeQueryResult(result, semantic.semanticRefs);
16015
+ const { preparation, result: normalized } = execution;
14731
16016
  if (execContext?.notebookPath) {
14732
16017
  recordNotebookQueryRun(projectRoot, {
14733
16018
  notebookPath: execContext.notebookPath,
@@ -15401,7 +16686,7 @@ export async function startLocalServer(opts) {
15401
16686
  const invocation = prepareBlockInvocation({ source, surface: 'ask_ai' });
15402
16687
  if (invocation.errors.length > 0)
15403
16688
  throw new Error(invocation.errors.join(' '));
15404
- const { executionReceipt: _receipt, compiledSql: _compiled, ...unexecuted } = artifact;
16689
+ const { executionReceipt: _receipt, executableArtifact: _executable, compiledSql: _compiled, ...unexecuted } = artifact;
15405
16690
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
15406
16691
  res.end(serializeJSON({
15407
16692
  artifact: {
@@ -15517,6 +16802,13 @@ export async function startLocalServer(opts) {
15517
16802
  trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
15518
16803
  compiledSql: result.sql,
15519
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
+ } : {}),
15520
16812
  ...(requestedArtifact?.limit ? { limit: requestedArtifact.limit } : {}),
15521
16813
  },
15522
16814
  }));
@@ -15639,37 +16931,43 @@ export async function startLocalServer(opts) {
15639
16931
  res.end(serializeJSON({ cellType: cell.type, result: null }));
15640
16932
  return;
15641
16933
  }
15642
- 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({
15643
16938
  sql: executableSql,
15644
16939
  subject: cell.type === 'dql' ? 'DQL query' : 'Notebook query',
15645
- executor,
15646
16940
  connection: cellConnection,
15647
- projectRoot,
15648
- 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,
15649
16967
  });
16968
+ const { preparation, result: normalized } = execution;
15650
16969
  const decodedSql = preparation.decodedSql;
15651
- const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
15652
- const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
15653
- assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
15654
- const semanticExecution = semanticCompose?.compiled
15655
- ? await executeTargetBoundSemanticQuery({
15656
- executor,
15657
- connection: cellConnection,
15658
- projectRoot,
15659
- plannedAdapter: semanticCompose.compiled.engine,
15660
- metricFlow: semanticCompose.compiled.engine === 'metricflow-cli'
15661
- ? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
15662
- : undefined,
15663
- // Compilation already produced the exact pinned adapter result
15664
- // used by the plan. The gateway validates target identity before
15665
- // accepting that result for preflight and execution.
15666
- compile: async () => semanticCompose.compiled,
15667
- prepareSql: () => ({ sql: prepared.sql, connection: prepared.connection }),
15668
- rowBound: semanticCompose.compiled.effectiveRequest.limit,
15669
- })
15670
- : null;
15671
- const rawResult = semanticExecution?.result ?? await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) }), prepared.connection);
15672
- const normalized = normalizeQueryResult(rawResult);
16970
+ const semanticExecution = semanticExecutionHolder.value;
15673
16971
  // Enforce the block's declared invariants against the result set. This
15674
16972
  // is additive: blocks without invariants produce `null` and the
15675
16973
  // response is unchanged. The agent surface (`query_via_block`) reads
@@ -15920,6 +17218,26 @@ table: ${table}${tagList}
15920
17218
  });
15921
17219
  });
15922
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
+ }
15923
17241
  async function validateModelingRelationship(relationship, manifest, execute, quoteIdentifier = quoteSqlIdentifier) {
15924
17242
  const fromEntity = manifest.modeling?.entities[relationship.from];
15925
17243
  const toEntity = manifest.modeling?.entities[relationship.to];
@@ -18604,9 +19922,104 @@ function createDqlArtifactExecutionReceipt(source, compiledSql, parameters, resu
18604
19922
  })),
18605
19923
  };
18606
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
+ }
18607
19954
  function executionFingerprint(value) {
18608
19955
  return createHash('sha256').update(value).digest('hex');
18609
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
+ }
18610
20023
  /**
18611
20024
  * Older clients normalized executable DQL with trim(), while the receipt was
18612
20025
  * correctly created from the original renderer output ending in one newline.
@@ -19024,6 +20437,43 @@ export function prepareLocalExecution(sql, connection, projectRoot, projectConfi
19024
20437
  connection: normalizedConnection,
19025
20438
  };
19026
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
+ }
19027
20477
  /**
19028
20478
  * One host-owned SQL preparation boundary shared by Ask and Notebook.
19029
20479
  *
@@ -21745,14 +23195,18 @@ export function validateBlockStudioSource(source, semanticLayer) {
21745
23195
  };
21746
23196
  }
21747
23197
  export async function createDqlArtifactGenerationSessionForProject(projectRoot, options, semanticLayer) {
21748
- const dqlArtifact = normalizeDqlArtifactReference(options.dqlArtifact);
21749
- if (!dqlArtifact)
23198
+ const normalizedDqlArtifact = normalizeDqlArtifactReference(options.dqlArtifact);
23199
+ if (!normalizedDqlArtifact)
21750
23200
  throw new Error('A DQL artifact with source is required for DQL draft promotion.');
23201
+ const dqlArtifact = {
23202
+ ...normalizedDqlArtifact,
23203
+ source: markBlockStudioSourceReusable(normalizedDqlArtifact.source),
23204
+ };
21751
23205
  const question = notebookResearchString(options.question) ?? dqlArtifact.name ?? 'Generated DQL artifact';
21752
23206
  const slug = deriveGeneratedDraftSlug(dqlArtifact.name ?? question);
21753
23207
  const domain = notebookResearchString(options.domain) ?? 'misc';
21754
23208
  const owner = notebookResearchString(options.owner) ?? '';
21755
- const tags = Array.from(new Set(['notebook-research', 'review-required', 'dql-artifact', ...(Array.isArray(options.tags) ? options.tags : [])]
23209
+ const tags = Array.from(new Set(['notebook-research', 'review-required', 'dql-artifact', APP_SOURCE_REUSABLE_TAG, ...(Array.isArray(options.tags) ? options.tags : [])]
21756
23210
  .map((tag) => notebookResearchString(tag))
21757
23211
  .filter((tag) => Boolean(tag))));
21758
23212
  const proposedContractId = `${domain}.Unknown.${slug}`;
@@ -22646,6 +24100,23 @@ export function parseBlockSourceMetadata(source) {
22646
24100
  metricsRef: extractStringArray('metrics'),
22647
24101
  };
22648
24102
  }
24103
+ /**
24104
+ * Persist the explicit App-source reuse decision in canonical DQL. Generated
24105
+ * execution stays transient; only Block Studio save/promotion routes call this
24106
+ * helper. The marker is a normal parser-supported tag, so refresh/reindex can
24107
+ * make the participation decision without relying on filenames or prose.
24108
+ */
24109
+ export function markBlockStudioSourceReusable(source) {
24110
+ const parsed = parseBlockSourceMetadata(source);
24111
+ if (parsed.tags.some((tag) => tag.trim().toLowerCase() === APP_SOURCE_REUSABLE_TAG))
24112
+ return source;
24113
+ const tags = Array.from(new Set([...parsed.tags, APP_SOURCE_REUSABLE_TAG]));
24114
+ const rendered = tags.map((tag) => JSON.stringify(tag)).join(', ');
24115
+ if (/\btags\s*=\s*\[[\s\S]*?\]/i.test(source)) {
24116
+ return source.replace(/\btags\s*=\s*\[[\s\S]*?\]/i, `tags = [${rendered}]`);
24117
+ }
24118
+ return source.replace(/(\bblock\s+"[^"]+"\s*\{)/i, `$1\n tags = [${rendered}]`);
24119
+ }
22649
24120
  function compareBlockStudioValues(actual, operator, expected) {
22650
24121
  const expectedValue = normalizeBlockStudioExpected(expected);
22651
24122
  if (operator === '==' || operator === '=')