@duckcodeailabs/dql-cli 1.13.5 → 1.14.0

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 (73) hide show
  1. package/dist/args.d.ts +2 -0
  2. package/dist/args.d.ts.map +1 -1
  3. package/dist/args.js +5 -0
  4. package/dist/args.js.map +1 -1
  5. package/dist/assets/dql-notebook/assets/{AgentLogPage-BzQOKjyV.js → AgentLogPage-Ch7VK20X.js} +1 -1
  6. package/dist/assets/dql-notebook/assets/{AiBuildDialog-CTxha499.js → AiBuildDialog-DBr5TmyM.js} +1 -1
  7. package/dist/assets/dql-notebook/assets/{AiBuildResult--MuD_I6g.js → AiBuildResult-jLPzQO7O.js} +1 -1
  8. package/dist/assets/dql-notebook/assets/{AiSidePanel-DcI4PMJ1.js → AiSidePanel-CdlGsiVC.js} +1 -1
  9. package/dist/assets/dql-notebook/assets/{AnalyticsHome-Dq54vjNz.js → AnalyticsHome-C0DXbOwY.js} +1 -1
  10. package/dist/assets/dql-notebook/assets/{AppsView-0SlwWYex.js → AppsView-CcpwjApv.js} +4 -4
  11. package/dist/assets/dql-notebook/assets/{BlockStudio-CxxXZD3M.js → BlockStudio-CFYxafw-.js} +1 -1
  12. package/dist/assets/dql-notebook/assets/{BusinessArtifactView-DxdAmeUN.js → BusinessArtifactView-C0kLYg2p.js} +1 -1
  13. package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-y81gFg_b.js → DbtFirstModelingPage-CMwElXD_.js} +1 -1
  14. package/dist/assets/dql-notebook/assets/{GitPage-GQtcwncb.js → GitPage-JjhRDeWY.js} +1 -1
  15. package/dist/assets/dql-notebook/assets/{GlobalAiRail-DM4wkxR_.js → GlobalAiRail-DakE4NdR.js} +1 -1
  16. package/dist/assets/dql-notebook/assets/{GovernedContextPage-B8Ft6JES.js → GovernedContextPage-BokDqG6a.js} +1 -1
  17. package/dist/assets/dql-notebook/assets/{HelpDocsPage-DnD4nMuF.js → HelpDocsPage-CjOv6_gz.js} +1 -1
  18. package/dist/assets/dql-notebook/assets/{HomePage-Ehb0ITj-.js → HomePage-nGaNcwdw.js} +1 -1
  19. package/dist/assets/dql-notebook/assets/{LineageDAG-Lvjc2AQX.js → LineageDAG-CO6CFJRg.js} +1 -1
  20. package/dist/assets/dql-notebook/assets/{LineageDetailView-DX32pfbp.js → LineageDetailView-BnGb7OF7.js} +1 -1
  21. package/dist/assets/dql-notebook/assets/{LineageDrawer-CFq3jPfk.js → LineageDrawer-C5Y0Ht0b.js} +1 -1
  22. package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-CVHXuHls.js → LineagePathBreadcrumb-Cgh3GR3F.js} +1 -1
  23. package/dist/assets/dql-notebook/assets/{MiniLineageGraph-BKaeT5Kt.js → MiniLineageGraph-Kla9PYuj.js} +1 -1
  24. package/dist/assets/dql-notebook/assets/{NewBlockModal-DI-JDzFH.js → NewBlockModal-BR2SnmPT.js} +1 -1
  25. package/dist/assets/dql-notebook/assets/{NewNotebookModal-Csw6eF74.js → NewNotebookModal-BaCHMYeB.js} +1 -1
  26. package/dist/assets/dql-notebook/assets/{NotebookEditor-FEqs3789.js → NotebookEditor-CBLqY8cE.js} +1 -1
  27. package/dist/assets/dql-notebook/assets/{ReadinessPage-Bk0S_MEA.js → ReadinessPage-CiN0IWSS.js} +1 -1
  28. package/dist/assets/dql-notebook/assets/{SetupOnboarding-DyLaPFGn.js → SetupOnboarding-BhiCsYF-.js} +1 -1
  29. package/dist/assets/dql-notebook/assets/{SkillsPage-DLHyvhci.js → SkillsPage-CCSf8VMm.js} +1 -1
  30. package/dist/assets/dql-notebook/assets/{TrustBadge-CIDLj2A6.js → TrustBadge-BgQmFe_x.js} +1 -1
  31. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-Bw5AXNDB.js +88 -0
  32. package/dist/assets/dql-notebook/assets/{answer-to-notebook-CFEJHLvs.js → answer-to-notebook-AeDYUDla.js} +1 -1
  33. package/dist/assets/dql-notebook/assets/{arrow-left-B-Zdcyvm.js → arrow-left-C55x2hq_.js} +1 -1
  34. package/dist/assets/dql-notebook/assets/{arrow-right-Cn8TM7cp.js → arrow-right-C1cJrhOm.js} +1 -1
  35. package/dist/assets/dql-notebook/assets/{book-open-text-Bko2NNUs.js → book-open-text-CQf_sdv2.js} +1 -1
  36. package/dist/assets/dql-notebook/assets/{circle-x-AWJAmUBB.js → circle-x-X8-Z2yLY.js} +1 -1
  37. package/dist/assets/dql-notebook/assets/{dagre.esm-Cl_ucrRh.js → dagre.esm-BjjNYKyY.js} +1 -1
  38. package/dist/assets/dql-notebook/assets/{external-link-DN57tb5f.js → external-link-C2zrz5DH.js} +1 -1
  39. package/dist/assets/dql-notebook/assets/{grip-vertical-BOWwFFva.js → grip-vertical-qGV_PYGU.js} +1 -1
  40. package/dist/assets/dql-notebook/assets/{index-Ck-wqvV2.js → index-ByTDPDaH.js} +4 -4
  41. package/dist/assets/dql-notebook/assets/{link-2-xUhbfBs8.js → link-2-VpyOxQXG.js} +1 -1
  42. package/dist/assets/dql-notebook/assets/{list-tree-BDWcBr67.js → list-tree-CH2Jhwms.js} +1 -1
  43. package/dist/assets/dql-notebook/assets/{minimize-2-CEeNoMTS.js → minimize-2-B2TZJ8BT.js} +1 -1
  44. package/dist/assets/dql-notebook/assets/{panel-right-open-DnSeUvOY.js → panel-right-open-BunF88lt.js} +1 -1
  45. package/dist/assets/dql-notebook/assets/{play-EoDj8-fa.js → play-DAFVF4_G.js} +1 -1
  46. package/dist/assets/dql-notebook/assets/{rotate-ccw-BAMxWT9A.js → rotate-ccw-DGCrrqtY.js} +1 -1
  47. package/dist/assets/dql-notebook/assets/{semantic-fields-BwLOs1kl.js → semantic-fields-Ci-9QL9F.js} +1 -1
  48. package/dist/assets/dql-notebook/assets/{sliders-horizontal-BYkm8SWW.js → sliders-horizontal-BOAlXXbn.js} +1 -1
  49. package/dist/assets/dql-notebook/assets/{star-sVexqNCs.js → star-CkksZXHt.js} +1 -1
  50. package/dist/assets/dql-notebook/assets/{triangle-alert-D2njv4o4.js → triangle-alert-D3mjyJZE.js} +1 -1
  51. package/dist/assets/dql-notebook/assets/{upload-CXTMxIA8.js → upload-CTNOAVEO.js} +1 -1
  52. package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-CPDeaAgL.js → usePersistedAgentThreadId-DiQjc7x-.js} +4 -4
  53. package/dist/assets/dql-notebook/assets/{user-round-zVsI_uud.js → user-round-BWd5tQRg.js} +1 -1
  54. package/dist/assets/dql-notebook/assets/{wand-sparkles-BMIwIzXT.js → wand-sparkles-CqsAv8P-.js} +1 -1
  55. package/dist/assets/dql-notebook/assets/{workflow-Dj9y1sNk.js → workflow-CSqsj-sC.js} +1 -1
  56. package/dist/assets/dql-notebook/assets/{wrench-DPQi6zrs.js → wrench-DWqzqlX8.js} +1 -1
  57. package/dist/assets/dql-notebook/assets/{x-XhhbtinL.js → x-65M5rLCE.js} +1 -1
  58. package/dist/assets/dql-notebook/index.html +1 -1
  59. package/dist/commands/eval.d.ts +15 -1
  60. package/dist/commands/eval.d.ts.map +1 -1
  61. package/dist/commands/eval.js +32 -3
  62. package/dist/commands/eval.js.map +1 -1
  63. package/dist/index.js +1 -1
  64. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
  65. package/dist/llm/providers/dql-agent-provider.js +37 -3
  66. package/dist/llm/providers/dql-agent-provider.js.map +1 -1
  67. package/dist/local-runtime.d.ts +25 -2
  68. package/dist/local-runtime.d.ts.map +1 -1
  69. package/dist/local-runtime.js +508 -88
  70. package/dist/local-runtime.js.map +1 -1
  71. package/dist/package.json +10 -10
  72. package/package.json +10 -10
  73. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-C0oKTU6G.js +0 -88
@@ -25,7 +25,7 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
25
25
  import { resolveRetrievalHealthStatus } from './retrieval-health.js';
26
26
  import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
27
27
  import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
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
+ 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, buildCoverageGap, capResearchBranches, buildResearchEvidenceLedger, buildAnalyticalTurnPlan, DEFAULT_ASK_ROW_EGRESS_POLICY, ZERO_ROW_EGRESS_POLICY, resolveProviderResultRowEgressPolicy, normalizeCanonicalQueryResult, normalizeAnalyticalExecutionFingerprint, normalizeAnalyticalExecutionReceipt, createAgentRunCancellationError, } from '@duckcodeailabs/dql-agent';
29
29
  import { addSqlResultFilter, dashboardFilterableResultColumns, filterableResultColumns, replaceBlockStudioSql } from './sql-result-filter.js';
30
30
  import { gatherProposeEnrichment } from './propose-enrich.js';
31
31
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization, } from './apps-api.js';
@@ -564,7 +564,7 @@ export function validAskRepairDqlWrapper(source) {
564
564
  }
565
565
  /** Build retained automatic-repair authority from analytical failure state only. */
566
566
  export function analyticalRepairCapabilityForAgentRun(run, resolvedTargetFingerprint) {
567
- if (run.status !== 'blocked')
567
+ if (run.status !== 'blocked' || run.stopReason !== 'blocked')
568
568
  return undefined;
569
569
  const failedRun = analyticalFailedRunFromAgentRun(run);
570
570
  const failure = failedRun?.failure;
@@ -963,6 +963,33 @@ export function slimAgentRunForTransport(run) {
963
963
  : {}),
964
964
  };
965
965
  }
966
+ /**
967
+ * INDEX projection for `GET /api/agent-runs` — strictly lighter than the
968
+ * presentation projection above.
969
+ *
970
+ * A run history list renders one ROW per run: question, route, status, trust,
971
+ * timing, summary. It never renders an answer body, an event stream, or a step
972
+ * trace. Shipping those anyway dominated the response: on 300 real stored runs
973
+ * a 20-run page was 47.61 MB whole and still 6.80 MB under the presentation
974
+ * projection, of which 6.35 MB was `artifacts[].payload` alone.
975
+ *
976
+ * Artifact identity (`id`/`kind`/`title`/`trustState`) is kept so a row can say
977
+ * what it produced; only the payload body is dropped. The complete immutable
978
+ * record stays available from `GET /api/agent-runs/:id`.
979
+ *
980
+ * Acceptance: PERF-003, E2E-022.
981
+ */
982
+ export function agentRunListEntryForTransport(run) {
983
+ const slim = slimAgentRunForTransport(run);
984
+ const artifacts = (slim.artifacts ?? []).map((artifact) => {
985
+ const record = agentRunRecord(artifact);
986
+ if (!record)
987
+ return artifact;
988
+ const { payload: _payload, ...rest } = record;
989
+ return rest;
990
+ });
991
+ return { ...slim, artifacts, steps: [], events: [] };
992
+ }
966
993
  export function conversationTurnInputFromRun(run) {
967
994
  const artifact = run.artifacts.find((candidate) => candidate.kind === 'answer')
968
995
  ?? run.artifacts.find((candidate) => candidate.kind === 'research_run')
@@ -970,7 +997,9 @@ export function conversationTurnInputFromRun(run) {
970
997
  const payload = agentRunRecord(artifact?.payload);
971
998
  // A blocked run may retain diagnostic artifacts, but their result-shaped
972
999
  // payload is never accepted conversation evidence or prose authority.
973
- const result = run.status === 'blocked' ? undefined : agentRunRecord(payload?.result);
1000
+ const result = run.status === 'blocked' || run.status === 'cancelled'
1001
+ ? undefined
1002
+ : agentRunRecord(payload?.result);
974
1003
  const columns = conversationResultColumns(result?.columns);
975
1004
  // The visual preview stays tiny, but member resolution needs a wider bounded
976
1005
  // value window. Deriving dimensions from only the eight preview rows caused a
@@ -2598,9 +2627,143 @@ export async function startLocalServer(opts) {
2598
2627
  }
2599
2628
  const answerRunExecutor = async ({ request, route, routeDecision, attempt, repairHint, emit }) => {
2600
2629
  const runStartedAtMs = Date.now();
2630
+ const turnPlan = buildAnalyticalTurnPlan({
2631
+ question: request.question,
2632
+ turnId: request.runId,
2633
+ candidateIds: routeDecision?.retrievalEvidence?.candidateIds ?? [],
2634
+ frozen: routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative',
2635
+ snapshotId: routeDecision?.resolvedAnalyticalPlan?.snapshotId ?? routeDecision?.retrievalEvidence?.snapshotId,
2636
+ sourceFingerprint: routeDecision?.resolvedAnalyticalPlan?.sourceFingerprint ?? routeDecision?.retrievalEvidence?.sourceFingerprint,
2637
+ });
2638
+ const childTurn = Boolean(request.workspaceContext && typeof request.workspaceContext === 'object'
2639
+ && request.workspaceContext.analyticalTaskChild === true);
2640
+ // Compound questions are a bounded task graph, not one query whose answer
2641
+ // is copied into several labels. Independent children share the parent's
2642
+ // signal/deadline and return truthful partial success.
2643
+ if (turnPlan.tasks.length > 1 && !childTurn && (attempt ?? 0) === 0) {
2644
+ const childResults = await Promise.all(turnPlan.tasks.slice(0, 6).map(async (task) => {
2645
+ if (request.signal?.aborted)
2646
+ rethrowIfCancelled(request.signal.reason, request.signal);
2647
+ try {
2648
+ const childRequest = {
2649
+ ...request,
2650
+ question: task.question,
2651
+ workspaceContext: {
2652
+ ...(request.workspaceContext && typeof request.workspaceContext === 'object' ? request.workspaceContext : {}),
2653
+ analyticalTaskChild: true,
2654
+ analyticalParentRunId: request.runId,
2655
+ analyticalTaskId: task.id,
2656
+ },
2657
+ };
2658
+ const answer = await runGovernedAgentAnswerForRun(childRequest, { attempt: 0, repairHint }, route, (message) => emit({ type: 'executor.started', message: `Task ${task.id}: ${message}`, route }), undefined);
2659
+ if (answer.result) {
2660
+ const canonical = normalizeCanonicalQueryResult({
2661
+ ...answer.result,
2662
+ resultFingerprint: answer.result.resultFingerprint ?? answer.result.executionReceipt?.resultFingerprint,
2663
+ executionReceipt: answer.result.executionReceipt,
2664
+ answerTier: answer.route?.tier ?? answer.sourceTier,
2665
+ });
2666
+ answer.result = {
2667
+ ...answer.result,
2668
+ columns: canonical.columns,
2669
+ rows: canonical.rows,
2670
+ rowCount: canonical.rowCount,
2671
+ resultFingerprint: canonical.resultFingerprint,
2672
+ ...(canonical.executionReceipt ? { executionReceipt: canonical.executionReceipt } : {}),
2673
+ ...(canonical.answerTier ? { answerTier: canonical.answerTier } : {}),
2674
+ };
2675
+ }
2676
+ return { task, answer };
2677
+ }
2678
+ catch (error) {
2679
+ rethrowIfCancelled(error, request.signal);
2680
+ return { task, error: error instanceof Error ? error.message : String(error) };
2681
+ }
2682
+ }));
2683
+ const outcomes = childResults.map(({ task, answer, error }) => ({
2684
+ version: 1,
2685
+ taskId: task.id,
2686
+ status: error || answer?.kind === 'no_answer' ? 'gap' : 'completed',
2687
+ ...(answer?.answer || answer?.text ? { summary: answer.answer ?? answer.text } : {}),
2688
+ ...(answer?.result?.resultFingerprint ? { resultFingerprint: answer.result.resultFingerprint } : {}),
2689
+ ...(error || answer?.kind === 'no_answer' ? {
2690
+ gap: buildCoverageGap({
2691
+ code: answer?.refusalCode === 'ambiguous' ? 'AMBIGUOUS_MEANING' : 'EXECUTION_FAILED',
2692
+ phase: answer?.executionError ? 'execution' : 'meaning',
2693
+ message: error ?? answer?.answer ?? answer?.text ?? 'The task did not produce an accepted analytical result.',
2694
+ searchedSources: routeDecision?.retrievalEvidence?.candidateIds ?? [],
2695
+ attemptedRoutes: ['certified', 'semantic', 'governed_relational', 'generated'],
2696
+ missing: [],
2697
+ recoverable: false,
2698
+ planFrozen: turnPlan.frozen,
2699
+ nextActions: ['Review the task context and retry the clause.'],
2700
+ }),
2701
+ } : {}),
2702
+ }));
2703
+ const completedCount = outcomes.filter((outcome) => outcome.status === 'completed').length;
2704
+ const tasks = turnPlan.tasks.map((task) => ({
2705
+ ...task,
2706
+ status: outcomes.find((outcome) => outcome.taskId === task.id)?.status === 'completed' ? 'completed' : 'gap',
2707
+ }));
2708
+ const answerText = childResults.map(({ task, answer, error }) => `${task.question}: ${error ?? answer?.answer ?? answer?.text ?? 'No accepted result was produced.'}`).join('\n\n');
2709
+ return {
2710
+ summary: completedCount === outcomes.length
2711
+ ? `Answered ${completedCount} independent analytical clauses.`
2712
+ : `Answered ${completedCount} of ${outcomes.length} analytical clauses; the remaining clauses need review.`,
2713
+ answer: answerText,
2714
+ status: completedCount === outcomes.length ? 'completed' : completedCount > 0 ? 'needs_review' : 'needs_clarification',
2715
+ trustState: completedCount === outcomes.length ? 'governed' : 'review_required',
2716
+ stopReason: completedCount === outcomes.length ? 'governed_semantic_answer' : 'human_review_required',
2717
+ artifacts: childResults.map(({ task, answer, error }) => agentRunArtifact('answer', `Task: ${task.question}`, {
2718
+ taskId: task.id,
2719
+ question: task.question,
2720
+ answer: answer?.answer ?? answer?.text,
2721
+ resultFingerprint: answer?.result?.resultFingerprint,
2722
+ error,
2723
+ })),
2724
+ evaluations: outcomes.map((outcome) => agentRunEvaluation(`analytical-task-${outcome.taskId}`, `Analytical task ${outcome.taskId}`, outcome.status === 'completed', outcome.status === 'completed' ? 'info' : 'warning', outcome.summary ?? outcome.gap?.message ?? 'Task outcome recorded.', outcome)),
2725
+ analyticalTurnPlan: { ...turnPlan, tasks, frozen: true },
2726
+ analyticalTaskOutcomes: outcomes,
2727
+ };
2728
+ }
2601
2729
  let governedAnswer;
2602
2730
  try {
2603
2731
  governedAnswer = await runGovernedAgentAnswerForRun(request, { attempt, repairHint }, route, (message) => emit({ type: 'executor.started', message, route }), routeDecision);
2732
+ // Keep the canonical result contract on the answer itself, not only on
2733
+ // the narration preview. Conversation persistence, follow-up member
2734
+ // resolution, Apply, and the notebook table all read this payload; if a
2735
+ // connector returned positional rows, dropping normalization here makes
2736
+ // the next question lose its customer/product/region binding (AGT-032).
2737
+ if (governedAnswer.result) {
2738
+ const canonical = normalizeCanonicalQueryResult({
2739
+ ...governedAnswer.result,
2740
+ resultFingerprint: governedAnswer.result.resultFingerprint
2741
+ ?? governedAnswer.result.executionReceipt?.resultFingerprint,
2742
+ executionReceipt: governedAnswer.result.executionReceipt,
2743
+ answerTier: governedAnswer.route?.tier ?? governedAnswer.sourceTier,
2744
+ trustState: governedAnswer.certification === 'certified'
2745
+ ? 'certified'
2746
+ : governedAnswer.kind === 'no_answer'
2747
+ ? 'blocked'
2748
+ : governedAnswer.reviewStatus === 'analyst_review_required'
2749
+ ? 'review_required'
2750
+ : 'governed',
2751
+ });
2752
+ governedAnswer.result = {
2753
+ ...governedAnswer.result,
2754
+ columns: canonical.columns,
2755
+ rows: canonical.rows,
2756
+ rowCount: canonical.rowCount,
2757
+ // Preserve the execution-service/receipt fingerprint. A local UI
2758
+ // digest is only a legacy fallback in normalizeCanonicalQueryResult;
2759
+ // it must never replace the cryptographic identity of this run.
2760
+ resultFingerprint: canonical.resultFingerprint,
2761
+ ...(canonical.executionTime !== undefined ? { executionTime: canonical.executionTime } : {}),
2762
+ ...(canonical.truncated ? { truncated: true } : {}),
2763
+ ...(canonical.executionReceipt ? { executionReceipt: canonical.executionReceipt } : {}),
2764
+ ...(canonical.answerTier ? { answerTier: canonical.answerTier } : {}),
2765
+ };
2766
+ }
2604
2767
  // Surface the approved Hint-Graph corrections that shaped this answer so the
2605
2768
  // UI can show an "applied learnings" chip (memoryContext is already on the answer).
2606
2769
  if (!governedAnswer.appliedHints) {
@@ -2818,6 +2981,38 @@ export async function startLocalServer(opts) {
2818
2981
  // has already spent its one evidence-aware repair. Keep this terminal and
2819
2982
  // inspectable; an ordinary Ask must never silently become a second Research run.
2820
2983
  const isModelDeclined = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'model_declined';
2984
+ // Before an analytical plan is frozen, a generated Ask gap is a recoverable
2985
+ // coverage result rather than a terminal governed error. Let the run engine
2986
+ // consume the typed evaluation and make one bounded cascade decision. Frozen
2987
+ // certified/semantic plans and provider/policy/execution failures remain
2988
+ // fail-closed.
2989
+ const canRecoverPreFreezeGap = (isGroundingGap || isModelDeclined)
2990
+ && route === 'generated_answer'
2991
+ && (request.requestedMode === undefined || request.requestedMode === 'auto' || request.requestedMode === 'ask')
2992
+ && routeDecision?.resolvedAnalyticalPlan?.mode !== 'authoritative';
2993
+ const typedCoverageGap = (isGroundingGap || isModelDeclined)
2994
+ ? buildCoverageGap({
2995
+ code: governedAnswer.refusalCode === 'modeling_gap' ? 'MISSING_RELATIONSHIP' : 'MISSING_RUNTIME_CAPABILITY',
2996
+ phase: 'planning',
2997
+ message: governedAnswer.refusalDetails?.message
2998
+ ?? (isModelDeclined
2999
+ ? 'The available context did not produce a safe analytical tuple.'
3000
+ : 'The retrieved context did not prove the metadata required for this analytical question.'),
3001
+ searchedSources: ['certified_blocks', 'semantic_metrics', 'dbt_manifest', 'relationship_graph', 'warehouse_metadata'],
3002
+ attemptedRoutes: ['certified', 'semantic', 'governed_relational', 'generated'],
3003
+ missing: governedAnswer.refusalDetails?.offending
3004
+ ? [
3005
+ governedAnswer.refusalDetails.offending.relation,
3006
+ governedAnswer.refusalDetails.offending.column,
3007
+ ].filter((value) => Boolean(value))
3008
+ : ['an executable metric/dimension/relationship tuple'],
3009
+ recoverable: canRecoverPreFreezeGap,
3010
+ planFrozen: routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative',
3011
+ nextActions: canRecoverPreFreezeGap
3012
+ ? ['continue through DBT-grounded relational context', 'run review-required generated SQL', 'start bounded Research if coverage remains incomplete']
3013
+ : ['review the retained metadata gap', 'select a governed metric or dimension', 'repair modeling if the relationship is missing'],
3014
+ })
3015
+ : undefined;
2821
3016
  // Only a genuinely AMBIGUOUS question is surfaced as "needs clarification".
2822
3017
  // Grounding/compiler gaps are terminal review states with their evidence trace;
2823
3018
  // provider outages are blocked so the UI can offer an explicit retry.
@@ -2971,7 +3166,7 @@ export async function startLocalServer(opts) {
2971
3166
  ? 'blocked'
2972
3167
  : needsClarification
2973
3168
  ? 'needs_clarification'
2974
- : isGroundingGap || isModelDeclined
3169
+ : (isGroundingGap || isModelDeclined) && !canRecoverPreFreezeGap
2975
3170
  ? 'blocked'
2976
3171
  : isCertified || isSemantic
2977
3172
  ? 'completed'
@@ -2980,7 +3175,7 @@ export async function startLocalServer(opts) {
2980
3175
  ? 'blocked'
2981
3176
  : needsClarification
2982
3177
  ? 'not_applicable'
2983
- : isGroundingGap || isModelDeclined
3178
+ : (isGroundingGap || isModelDeclined) && !canRecoverPreFreezeGap
2984
3179
  ? 'blocked'
2985
3180
  : isCertified
2986
3181
  ? 'certified'
@@ -2994,7 +3189,7 @@ export async function startLocalServer(opts) {
2994
3189
  // A gap is terminal, but it is NOT a provider outage: it still owes the
2995
3190
  // user its evidence trace, and its next-actions below stay the
2996
3191
  // grounding-gap set rather than "retry the provider".
2997
- : isGroundingGap || isModelDeclined
3192
+ : (isGroundingGap || isModelDeclined) && !canRecoverPreFreezeGap
2998
3193
  ? 'human_review_required'
2999
3194
  : isCertified
3000
3195
  ? 'certified_answer_found'
@@ -3083,7 +3278,7 @@ export async function startLocalServer(opts) {
3083
3278
  ? (governedAnswer.semanticExecutionTrace
3084
3279
  ? [agentRunArtifact('answer', governedAnswer.refusalCode === 'ambiguous'
3085
3280
  ? 'Semantic path selection required'
3086
- : 'Semantic compilation details', governedAnswer, undefined, needsClarification ? 'not_applicable' : 'review_required')]
3281
+ : 'Semantic compilation details', typedCoverageGap ? { ...governedAnswer, coverageGap: typedCoverageGap } : governedAnswer, undefined, needsClarification ? 'not_applicable' : 'review_required')]
3087
3282
  : governedAnswer.dqlArtifact && !isProviderError && !isGroundingGap && !isModelDeclined && !isPolicyBlocked
3088
3283
  ? [agentRunArtifact('dql_block_draft', 'DQL draft (review required)', governedAnswer.dqlArtifact, undefined, 'review_required')]
3089
3284
  // Every refusal still owes the user an account of itself. Emitting no
@@ -3092,7 +3287,7 @@ export async function startLocalServer(opts) {
3092
3287
  // answered", the DQL draft and the compiled SQL all become
3093
3288
  // unreachable exactly when the user most needs to see why it stopped
3094
3289
  // and carry the query into a notebook.
3095
- : [agentRunArtifact('answer', 'No answer was accepted', governedAnswer, undefined, needsClarification ? 'not_applicable' : 'blocked')])
3290
+ : [agentRunArtifact('answer', 'No answer was accepted', typedCoverageGap ? { ...governedAnswer, coverageGap: typedCoverageGap } : governedAnswer, undefined, needsClarification ? 'not_applicable' : 'blocked')])
3096
3291
  : [agentRunArtifact('answer', isCertified ? 'Certified answer' : isSemantic ? 'Governed semantic answer' : isExploratory ? 'Exploratory DBT-grounded answer' : 'Review-required answer', governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, isCertified ? 'certified' : isSemantic ? 'governed' : 'review_required')],
3097
3292
  evaluations: [
3098
3293
  agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.', {
@@ -3127,22 +3322,34 @@ export async function startLocalServer(opts) {
3127
3322
  ...(isExecutionFailure ? [agentRunEvaluation('query-execution', 'Query execution', false, 'blocking', `The governed query failed before it produced a result: ${governedAnswer.executionError}`)] : []),
3128
3323
  ...(isGroundingGap ? [
3129
3324
  {
3130
- ...agentRunEvaluation('grounding-gap', 'Metadata grounding', false, 'warning', 'The bounded lookup could not prove the required metadata grounding. No automatic retry or Research escalation was started.', {
3325
+ ...agentRunEvaluation('grounding-gap', 'Metadata grounding', false, 'warning', canRecoverPreFreezeGap
3326
+ ? 'The first governed lookup did not prove the required metadata grounding. Continue through the bounded relational/generated cascade before asking the user to repair modeling.'
3327
+ : 'The bounded lookup could not prove the required metadata grounding. This plan is already frozen, so no automatic Research escalation was started.', {
3131
3328
  refusalCode: governedAnswer.refusalCode,
3132
3329
  refusalDetails: governedAnswer.refusalDetails,
3133
3330
  validationWarnings: governedAnswer.validationWarnings,
3134
3331
  route: governedAnswer.route,
3332
+ coverageGap: typedCoverageGap,
3135
3333
  }),
3334
+ ...(canRecoverPreFreezeGap ? {
3335
+ suggestedRepair: 'Continue the unfrozen Ask cascade through DBT-grounded relational context and review-required generated SQL.',
3336
+ } : {}),
3136
3337
  },
3137
3338
  ] : []),
3138
3339
  ...(isModelDeclined ? [
3139
3340
  {
3140
- ...agentRunEvaluation('declined-despite-context', 'Answer grounding', false, 'blocking', 'The bounded lookup could not compose a governed query after its in-lane repair. Start Research explicitly to investigate beyond this lookup budget.', {
3341
+ ...agentRunEvaluation('declined-despite-context', 'Answer grounding', false, 'blocking', canRecoverPreFreezeGap
3342
+ ? 'The governed lookup could not compose a query after its bounded in-lane repair. Continue with the Research context ledger before returning a typed gap.'
3343
+ : 'The bounded lookup could not compose a governed query after its in-lane repair. Start Research explicitly to investigate beyond this lookup budget.', {
3141
3344
  refusalCode: governedAnswer.refusalCode,
3142
3345
  refusalDetails: governedAnswer.refusalDetails,
3143
3346
  validationWarnings: governedAnswer.validationWarnings,
3144
3347
  route: governedAnswer.route,
3348
+ coverageGap: typedCoverageGap,
3145
3349
  }),
3350
+ ...(canRecoverPreFreezeGap ? {
3351
+ suggestedRepair: 'Search the surrounding metadata and relationship context before giving up on the analytical turn.',
3352
+ } : {}),
3146
3353
  },
3147
3354
  ] : []),
3148
3355
  ...(isPolicyBlocked ? [
@@ -3160,6 +3367,14 @@ export async function startLocalServer(opts) {
3160
3367
  ...(governedAnswer.executionError ? [
3161
3368
  agentRunEvaluation('execution-error', 'Execution error', false, 'warning', governedAnswer.executionError),
3162
3369
  ] : []),
3370
+ // A rejected narration must say WHY it was rejected. The failures were
3371
+ // computed and then dropped, so a reader saw "Verified narration was
3372
+ // unavailable" with no way to tell whether the model invented a number,
3373
+ // cited a fact id that does not exist, or the provider simply failed.
3374
+ // The fallback itself stays: this only makes its reason inspectable.
3375
+ ...(narrationSource === 'deterministic' && narrationValidationFailures.length > 0 ? [
3376
+ agentRunEvaluation('narration-verification', 'Narration verification', false, 'warning', `The drafted narration was rejected against the result fact set, so the deterministic record was shown instead: ${narrationValidationFailures.join('; ')}`, { narrationSource, validationFailures: narrationValidationFailures }),
3377
+ ] : []),
3163
3378
  ],
3164
3379
  nextActions,
3165
3380
  providerEgressReceipts: finalProviderEgressReceipts,
@@ -3687,6 +3902,7 @@ export async function startLocalServer(opts) {
3687
3902
  plan,
3688
3903
  };
3689
3904
  let researchRun;
3905
+ const researchRuns = [];
3690
3906
  let researchWorkspaceError;
3691
3907
  if (!needsClarification) {
3692
3908
  try {
@@ -3711,26 +3927,106 @@ export async function startLocalServer(opts) {
3711
3927
  });
3712
3928
  emit({
3713
3929
  type: 'artifact.created',
3714
- message: 'Saved notebook research workspace record.',
3930
+ message: 'Saved the immutable root research plan; executing bounded child branches.',
3715
3931
  route: 'research',
3716
3932
  trustState: 'review_required',
3717
- payload: { researchRunId: created.id, notebookPath },
3933
+ payload: { researchRunId: created.id, notebookPath, branchCap: 6 },
3718
3934
  });
3719
- const executed = await runNotebookResearch(storage, created, {
3720
- domain: agentRunWorkspaceValue(request, 'domain'),
3721
- owner: agentRunWorkspaceValue(request, 'owner'),
3722
- sourceCellFingerprint,
3723
- question: request.question,
3724
- intent: researchIntent,
3725
- context: researchContextEnvelope,
3726
- executionConnection: researchExecutionConnection,
3727
- executionConnectionName: researchExecutionConnectionName,
3728
- signal: request.signal,
3729
- baselineSql: agentRunString(researchSource?.sql),
3730
- baselineDqlArtifact: researchSource?.dqlArtifact,
3731
- baselineRunId: agentRunString(researchSource?.runId),
3732
- });
3733
- researchRun = withNotebookResearchChecklist(executed);
3935
+ // The root record is a plan/dossier parent. Only child runs are
3936
+ // observed research executions. This prevents one root result from
3937
+ // being copied into six fabricated ledger entries (AGT-016/033).
3938
+ // An explicit Research request still gets one real child when the
3939
+ // catalog planner has no grounded step (for example, an empty
3940
+ // starter project). The child is an observed metadata/baseline
3941
+ // attempt, not a fabricated successful finding; its durable status
3942
+ // and receipt determine the ledger entry.
3943
+ const fallbackBranch = {
3944
+ thought: 'Inspect the requested analytical question against the frozen root context.',
3945
+ action: {
3946
+ kind: 'lookup_metric',
3947
+ target: routeDecision?.resolvedAnalyticalPlan?.executionId ?? request.question,
3948
+ },
3949
+ expectation: 'Whether the frozen context contains enough evidence for a bounded answer.',
3950
+ };
3951
+ const branches = capResearchBranches(plan.steps.length > 0 ? plan.steps : [fallbackBranch], 6);
3952
+ for (let index = 0; index < branches.length; index += 1) {
3953
+ const step = branches[index];
3954
+ if (request.signal?.aborted)
3955
+ rethrowIfCancelled(request.signal.reason, request.signal);
3956
+ const branchId = `${step.action.kind}:${step.action.target}`;
3957
+ const branchQuestion = `${request.question}\nResearch branch ${index + 1} (${branchId}): ${step.expectation}`;
3958
+ const childId = `${created.id}:research:${index + 1}`;
3959
+ const child = storage.createRun({
3960
+ id: childId,
3961
+ notebookPath,
3962
+ title: `${agentRunTitle(request.question, 'Agent research')} · branch ${index + 1}`,
3963
+ question: branchQuestion,
3964
+ sourceCell,
3965
+ sourceCellId,
3966
+ sourceCellName,
3967
+ sourceCellFingerprint,
3968
+ intent: researchIntent,
3969
+ domain: agentRunWorkspaceValue(request, 'domain'),
3970
+ owner: agentRunWorkspaceValue(request, 'owner'),
3971
+ context: {
3972
+ ...researchContextEnvelope,
3973
+ rootRunId: created.id,
3974
+ rootPlanId: plan.rootPlanId,
3975
+ branch: {
3976
+ id: branchId,
3977
+ index: index + 1,
3978
+ expectation: step.expectation,
3979
+ action: step.action,
3980
+ },
3981
+ },
3982
+ });
3983
+ emit({
3984
+ type: 'artifact.created',
3985
+ message: `Started research branch ${index + 1} of ${branches.length}.`,
3986
+ route: 'research',
3987
+ trustState: 'review_required',
3988
+ payload: { researchRunId: child.id, parentResearchRunId: created.id, branchId },
3989
+ });
3990
+ try {
3991
+ const executed = await runNotebookResearch(storage, child, {
3992
+ domain: agentRunWorkspaceValue(request, 'domain'),
3993
+ owner: agentRunWorkspaceValue(request, 'owner'),
3994
+ sourceCellFingerprint,
3995
+ question: branchQuestion,
3996
+ intent: researchIntent,
3997
+ context: {
3998
+ ...researchContextEnvelope,
3999
+ rootRunId: created.id,
4000
+ rootPlanId: plan.rootPlanId,
4001
+ branch: { id: branchId, index: index + 1, expectation: step.expectation, action: step.action },
4002
+ },
4003
+ executionConnection: researchExecutionConnection,
4004
+ executionConnectionName: researchExecutionConnectionName,
4005
+ signal: request.signal,
4006
+ baselineSql: agentRunString(researchSource?.sql),
4007
+ baselineDqlArtifact: researchSource?.dqlArtifact,
4008
+ baselineRunId: agentRunString(researchSource?.runId),
4009
+ });
4010
+ researchRuns.push(withNotebookResearchChecklist(executed));
4011
+ }
4012
+ catch (error) {
4013
+ // A child is a real durable run even when cancellation stops the
4014
+ // shared branch budget. Persist the truthful stop before
4015
+ // propagating cancellation to the parent run.
4016
+ const message = error instanceof Error ? error.message : String(error);
4017
+ storage.updateRun(child.id, {
4018
+ status: 'error',
4019
+ error: message,
4020
+ summary: 'Research branch stopped before producing a result.',
4021
+ reviewStatus: 'needs_review',
4022
+ });
4023
+ const stopped = storage.getRun(child.id);
4024
+ if (stopped)
4025
+ researchRuns.push(withNotebookResearchChecklist(stopped));
4026
+ rethrowIfCancelled(error, request.signal);
4027
+ }
4028
+ }
4029
+ researchRun = researchRuns[0];
3734
4030
  }
3735
4031
  finally {
3736
4032
  storage.close();
@@ -3741,15 +4037,81 @@ export async function startLocalServer(opts) {
3741
4037
  researchWorkspaceError = formatNotebookResearchStorageError(error);
3742
4038
  }
3743
4039
  }
3744
- const researchResultPreview = researchRun?.resultPreview;
4040
+ const researchResultPreview = researchRuns
4041
+ .map((run) => run.resultPreview)
4042
+ // Keep zero-row executions in the proof path. `coerceNarrateResultData`
4043
+ // intentionally omits empty row sets for prose, but an empty result is
4044
+ // still an executed result when it carries its execution identity.
4045
+ .find((preview) => {
4046
+ const record = agentRunRecord(preview);
4047
+ return Boolean(record && Array.isArray(record.rows));
4048
+ })
4049
+ ?? researchRun?.resultPreview;
3745
4050
  const researchResultData = coerceNarrateResultData(researchResultPreview);
3746
4051
  const researchResultRecord = agentRunRecord(researchResultPreview);
4052
+ // Keep each bounded research branch inspectable as an evidence ledger.
4053
+ // The notebook workspace remains the durable execution record; this
4054
+ // additive projection gives Ask/Research narration a stable list of
4055
+ // observations, failures, facts, and receipts without exposing provider
4056
+ // chain-of-thought. Six is the hard branch budget for one root question.
4057
+ const researchLedger = buildResearchEvidenceLedger({
4058
+ rootQuestion: request.question,
4059
+ planId: plan.rootPlanId,
4060
+ snapshotId: routeDecision?.resolvedAnalyticalPlan?.snapshotId,
4061
+ entries: researchRuns.slice(0, 6).map((branch, index) => {
4062
+ const branchContext = agentRunRecord(branch.context)?.branch;
4063
+ const branchPreviewRecord = agentRunRecord(branch.resultPreview);
4064
+ const branchPreview = coerceNarrateResultData(branchPreviewRecord);
4065
+ const previewRecord = branchPreviewRecord;
4066
+ const executionReceipt = normalizeAnalyticalExecutionReceipt(previewRecord?.executionReceipt);
4067
+ const resultFingerprint = normalizeAnalyticalExecutionFingerprint(previewRecord?.resultFingerprint)
4068
+ ?? executionReceipt?.resultFingerprint;
4069
+ // A child run ID or context-pack ID proves that planning happened,
4070
+ // not that a query executed. Only the canonical result fingerprint
4071
+ // (on the result or its execution receipt) can make a branch
4072
+ // observed (AGT-016/033).
4073
+ const executionProof = resultFingerprint;
4074
+ const observed = branch.status === 'ready' && Boolean(executionProof);
4075
+ return {
4076
+ id: branch.id,
4077
+ branchId: agentRunString(branchContext?.id) ?? `branch:${index + 1}`,
4078
+ question: branch.question,
4079
+ status: observed ? 'observed' : branch.status === 'error' ? 'failed' : 'skipped',
4080
+ ...(branchPreviewRecord && Array.isArray(branchPreviewRecord.rows)
4081
+ ? { rowCount: branchPreviewRecord.rows.length }
4082
+ : {}),
4083
+ ...(resultFingerprint ? { resultFingerprint } : {}),
4084
+ ...(executionReceipt ? { executionReceipt } : {}),
4085
+ facts: [agentRunString(branchContext?.expectation) ?? branch.question, ...(branch.evidence?.citations ?? []).flatMap((item) => typeof item === 'string' ? [item] : [])].slice(0, 8),
4086
+ // A context-pack ID or child run ID is not execution evidence. Keep
4087
+ // the receipt list empty until the execution service supplies a
4088
+ // receipt/fingerprint (AGT-016/033).
4089
+ receipts: executionProof ? [executionProof] : [],
4090
+ ...(!observed ? {
4091
+ error: branch.error
4092
+ ?? (branch.status === 'error'
4093
+ ? branch.summary
4094
+ : 'Research branch did not produce an execution receipt or result fingerprint.'),
4095
+ } : {}),
4096
+ };
4097
+ }),
4098
+ stoppingReason: needsClarification
4099
+ ? 'not_started'
4100
+ : researchRuns.some((run) => run.status === 'error')
4101
+ ? 'insufficient_evidence'
4102
+ : plan.steps.length > 6
4103
+ ? 'budget'
4104
+ : 'completed',
4105
+ });
3747
4106
  // A query that ran and matched 0 rows STILL executed — treat it as a clean,
3748
4107
  // grounded execution (not "no result"), so an empty answer is surfaced as
3749
4108
  // "0 rows matched" rather than silently downgraded to review-required.
3750
4109
  const researchDidExecute = Boolean(researchResultData) || Boolean(researchResultRecord && Array.isArray(researchResultRecord.rows));
3751
4110
  const researchZeroRows = !researchResultData && researchDidExecute;
3752
- const researchExecutedCleanly = researchDidExecute && !researchWorkspaceError && researchRun?.status !== 'error';
4111
+ const researchExecutedCleanly = researchDidExecute
4112
+ && !researchWorkspaceError
4113
+ && researchRuns.length > 0
4114
+ && researchRuns.every((run) => run.status === 'ready');
3753
4115
  const narration = !needsClarification && researchResultData
3754
4116
  ? await narrateForAgentRun({
3755
4117
  question: request.question,
@@ -3785,8 +4147,11 @@ export async function startLocalServer(opts) {
3785
4147
  ? []
3786
4148
  : [agentRunArtifact('research_run', 'Research plan', {
3787
4149
  plan,
4150
+ researchLedger,
3788
4151
  researchRun,
4152
+ researchRuns,
3789
4153
  researchRunId: researchRun?.id,
4154
+ researchRunIds: researchRuns.map((run) => run.id),
3790
4155
  notebookPath,
3791
4156
  workspaceError: researchWorkspaceError,
3792
4157
  routeDecision,
@@ -3817,7 +4182,7 @@ export async function startLocalServer(opts) {
3817
4182
  : [
3818
4183
  ...(researchRun?.id ? [{ id: 'open-research', label: 'Open research dossier', artifactKind: 'research_run' }] : []),
3819
4184
  { id: 'create-block', label: 'Review DQL draft', route: 'dql_block_draft', artifactKind: 'dql_block_draft' },
3820
- ...(researchRun?.generatedSql || researchRun?.reviewedSql ? [{ id: 'insert-sql', label: 'Insert SQL preview', route: 'sql_cell', artifactKind: 'sql_cell' }] : []),
4185
+ ...(researchRuns.some((run) => run.generatedSql || run.reviewedSql) ? [{ id: 'insert-sql', label: 'Insert SQL preview', route: 'sql_cell', artifactKind: 'sql_cell' }] : []),
3821
4186
  ],
3822
4187
  };
3823
4188
  },
@@ -4288,11 +4653,12 @@ export async function startLocalServer(opts) {
4288
4653
  },
4289
4654
  getCatalogContext: buildRankedAgentRunCatalogContext,
4290
4655
  });
4291
- // Hybrid router: keep the deterministic decision when it is confident (certified
4292
- // fast paths + greetings stay 0-LLM); spend one cheap classification call only for
4293
- // the ambiguous middle so Auto reliably picks quick-answer vs deep research without
4294
- // the user clicking "Dig deeper". Same provider completion as the planner.
4656
+ // Explicit selections and conversation-only turns remain deterministic;
4657
+ // each fresh natural-language analytical turn gets one bounded candidate-ID
4658
+ // interpretation call by default. The rollback is host-owned and cannot be
4659
+ // supplied by an HTTP/MCP client.
4295
4660
  const agentRunRouter = createHybridRouter({
4661
+ requireMeaningCallForNaturalLanguage: opts.requireMeaningCallForNaturalLanguage ?? true,
4296
4662
  complete: async ({ system, user, signal }) => {
4297
4663
  const provider = await createBlockStudioAssistProvider(projectRoot);
4298
4664
  if (!provider)
@@ -4332,6 +4698,25 @@ export async function startLocalServer(opts) {
4332
4698
  // A run may outlive its streaming browser connection, so cancellation is
4333
4699
  // server-owned and keyed by run id rather than relying on fetch abort alone.
4334
4700
  const activeAgentRunControllers = new Map();
4701
+ const cancelActiveAgentRun = (id) => {
4702
+ const controller = activeAgentRunControllers.get(id);
4703
+ if (!controller)
4704
+ return false;
4705
+ const progress = agentRunStore.getProgress(id);
4706
+ if (progress) {
4707
+ agentRunStore.saveProgress({
4708
+ ...progress,
4709
+ lifecycle: {
4710
+ ...progress.lifecycle,
4711
+ state: 'cancelling',
4712
+ revision: progress.lifecycle.revision + 1,
4713
+ updatedAt: new Date().toISOString(),
4714
+ },
4715
+ });
4716
+ }
4717
+ controller.abort(createAgentRunCancellationError());
4718
+ return true;
4719
+ };
4335
4720
  const activeAnalyticalRepairReservations = new Set();
4336
4721
  const consumedAnalyticalRepairCapabilities = new Set();
4337
4722
  // Server-side conversation threads: persisted multi-turn state (survives refresh).
@@ -6113,6 +6498,19 @@ export async function startLocalServer(opts) {
6113
6498
  : notebookResearchString(governedAnswer?.answer)
6114
6499
  ?? notebookResearchString(governedAnswer?.text)
6115
6500
  ?? notebookResearchSummary(question, resultPreview, previewError);
6501
+ const previewRecord = agentRunRecord(resultPreview);
6502
+ const executionReceipt = normalizeAnalyticalExecutionReceipt(previewRecord?.executionReceipt);
6503
+ // Do not treat a child run ID as execution evidence. The canonical
6504
+ // fingerprint is the only proof that this Research branch produced a
6505
+ // result; planning, SQL text, and a durable run record remain review
6506
+ // required until that proof exists (AGT-016/033).
6507
+ const executionProof = normalizeAnalyticalExecutionFingerprint(previewRecord?.resultFingerprint)
6508
+ ?? executionReceipt?.resultFingerprint;
6509
+ const executionUnavailable = !previewError && !executionProof;
6510
+ const terminalError = previewError
6511
+ ?? (executionUnavailable
6512
+ ? 'Research did not produce an executed result or execution receipt; the branch remains review-required.'
6513
+ : undefined);
6116
6514
  const recommendation = previewError
6117
6515
  ? 'Review the SQL, selected metadata, and connection context before rerunning.'
6118
6516
  : dqlArtifact && !reviewedSql
@@ -6137,8 +6535,14 @@ export async function startLocalServer(opts) {
6137
6535
  question,
6138
6536
  intent,
6139
6537
  context,
6140
- status: previewError ? 'error' : 'ready',
6141
- summary,
6538
+ // A plan, generated SQL, or DQL artifact is not an observed execution.
6539
+ // Only a result carrying an execution fingerprint/receipt may become
6540
+ // `ready`; otherwise persist a failed review state with no fabricated
6541
+ // evidence (AGT-016/033).
6542
+ status: terminalError ? 'error' : 'ready',
6543
+ summary: terminalError && executionUnavailable
6544
+ ? 'Research branch stopped without an executed result; no observation was recorded.'
6545
+ : summary,
6142
6546
  recommendation,
6143
6547
  resultPreview,
6144
6548
  evidence,
@@ -6158,7 +6562,7 @@ export async function startLocalServer(opts) {
6158
6562
  ...(display && display.ok ? display.warnings : []),
6159
6563
  ],
6160
6564
  reviewStatus: 'needs_review',
6161
- error: previewError,
6565
+ error: terminalError,
6162
6566
  lastRunAt: startedAt,
6163
6567
  }) ?? run;
6164
6568
  }
@@ -6985,7 +7389,12 @@ export async function startLocalServer(opts) {
6985
7389
  return;
6986
7390
  }
6987
7391
  if (operationMatch && req.method === 'DELETE') {
6988
- const operation = operationCoordinator.cancel(decodeURIComponent(operationMatch[1]));
7392
+ const operationId = decodeURIComponent(operationMatch[1]);
7393
+ const existingOperation = operationCoordinator.get(operationId);
7394
+ if (existingOperation?.type === 'agent_run' && existingOperation.scope.startsWith('agent-run:')) {
7395
+ cancelActiveAgentRun(existingOperation.scope.slice('agent-run:'.length));
7396
+ }
7397
+ const operation = operationCoordinator.cancel(operationId);
6989
7398
  res.writeHead(operation ? 200 : 404, { 'Content-Type': 'application/json; charset=utf-8' });
6990
7399
  res.end(serializeJSON(operation ?? { error: 'Operation not found.' }));
6991
7400
  return;
@@ -8098,12 +8507,17 @@ export async function startLocalServer(opts) {
8098
8507
  const limit = Number.isFinite(rawLimit) && rawLimit > 0
8099
8508
  ? Math.min(200, Math.floor(rawLimit))
8100
8509
  : 50;
8101
- const runs = agentRunStore
8102
- .list()
8510
+ const stored = agentRunStore.list();
8511
+ // This is an INDEX payload: one row per run, no answer bodies. Shipping
8512
+ // stored runs whole meant a measured 47.61 MB for 20 real runs.
8513
+ // `GET /api/agent-runs/:id` still serves the complete immutable record.
8514
+ const runs = stored
8515
+ .slice()
8103
8516
  .sort((a, b) => b.startedAt.localeCompare(a.startedAt))
8104
- .slice(0, limit);
8517
+ .slice(0, limit)
8518
+ .map(agentRunListEntryForTransport);
8105
8519
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
8106
- res.end(serializeJSON({ runs, total: agentRunStore.list().length, limit }));
8520
+ res.end(serializeJSON({ runs, total: stored.length, limit }));
8107
8521
  return;
8108
8522
  }
8109
8523
  /**
@@ -8641,6 +9055,14 @@ export async function startLocalServer(opts) {
8641
9055
  res.end(serializeJSON({ error: 'Agent run not found.' }));
8642
9056
  return;
8643
9057
  }
9058
+ if (run.status !== 'blocked' || run.stopReason !== 'blocked') {
9059
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
9060
+ res.end(serializeJSON({
9061
+ code: 'REPAIR_CAPABILITY_REQUIRED',
9062
+ error: 'Only a terminal blocked run with a blocked stop reason may derive an analytical repair.',
9063
+ }));
9064
+ return;
9065
+ }
8644
9066
  const source = analyticalFailedRunFromAgentRun(run);
8645
9067
  if (!source) {
8646
9068
  res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -8679,25 +9101,11 @@ export async function startLocalServer(opts) {
8679
9101
  if (req.method === 'POST' && /^\/api\/agent-runs\/[^/]+\/cancel$/.test(path)) {
8680
9102
  const match = path.match(/^\/api\/agent-runs\/([^/]+)\/cancel$/);
8681
9103
  const id = decodeURIComponent(match?.[1] ?? '');
8682
- const controller = activeAgentRunControllers.get(id);
8683
- if (!controller) {
9104
+ if (!cancelActiveAgentRun(id)) {
8684
9105
  res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
8685
9106
  res.end(serializeJSON({ ok: false, error: 'This run is no longer active.' }));
8686
9107
  return;
8687
9108
  }
8688
- const progress = agentRunStore.getProgress(id);
8689
- if (progress) {
8690
- agentRunStore.saveProgress({
8691
- ...progress,
8692
- lifecycle: {
8693
- ...progress.lifecycle,
8694
- state: 'cancelling',
8695
- revision: progress.lifecycle.revision + 1,
8696
- updatedAt: new Date().toISOString(),
8697
- },
8698
- });
8699
- }
8700
- controller.abort(new Error('Stopped by user.'));
8701
9109
  res.writeHead(202, { 'Content-Type': 'application/json; charset=utf-8' });
8702
9110
  res.end(serializeJSON({ ok: true, id }));
8703
9111
  return;
@@ -8842,7 +9250,10 @@ export async function startLocalServer(opts) {
8842
9250
  return;
8843
9251
  }
8844
9252
  res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
8845
- res.end(serializeJSON({ run: completedRun }));
9253
+ // Same run, same reader as the stream above, so it ships the same
9254
+ // projection. Sending the stored record here instead made an ordinary
9255
+ // completed Ask a 4.66 MB reply.
9256
+ res.end(serializeJSON({ run: slimAgentRunForTransport(completedRun) }));
8846
9257
  }
8847
9258
  finally {
8848
9259
  if (runId)
@@ -17527,21 +17938,31 @@ function connectionDriverLabel(connection) {
17527
17938
  * The notebook SPA expects columns as string[] (just names).
17528
17939
  */
17529
17940
  function normalizeQueryResult(result, semanticRefs) {
17530
- const rawCols = Array.isArray(result?.columns) ? result.columns : [];
17531
- const columns = rawCols.map((c) => typeof c === 'string' ? c : typeof c?.name === 'string' ? c.name : String(c));
17941
+ const canonical = normalizeCanonicalQueryResult({
17942
+ columns: result?.columns,
17943
+ rows: result?.rows,
17944
+ rowCount: result?.rowCount,
17945
+ executionTime: result?.executionTime,
17946
+ executionTimeMs: result?.executionTimeMs,
17947
+ truncated: result?.truncated,
17948
+ resultFingerprint: result?.resultFingerprint,
17949
+ executionReceipt: result?.executionReceipt,
17950
+ trustState: result?.trustState,
17951
+ answerTier: result?.answerTier,
17952
+ });
17532
17953
  const rawRows = Array.isArray(result?.rows) ? result.rows : [];
17533
- const rows = rawRows.slice(0, NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT);
17954
+ const rows = canonical.rows.slice(0, NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT);
17534
17955
  const hasRefs = semanticRefs && (semanticRefs.metrics.length > 0 || semanticRefs.dimensions.length > 0);
17535
17956
  return {
17536
- columns,
17957
+ columns: canonical.columns,
17537
17958
  rows,
17538
- rowCount: typeof result?.rowCount === 'number' ? result.rowCount : rawRows.length,
17539
- executionTime: typeof result?.executionTimeMs === 'number'
17540
- ? result.executionTimeMs
17541
- : typeof result?.executionTime === 'number'
17542
- ? result.executionTime
17543
- : 0,
17544
- ...(rawRows.length > rows.length ? { truncated: true } : {}),
17959
+ rowCount: canonical.rowCount,
17960
+ resultFingerprint: canonical.resultFingerprint,
17961
+ executionTime: canonical.executionTime ?? 0,
17962
+ ...(rawRows.length > rows.length || canonical.truncated ? { truncated: true } : {}),
17963
+ ...(canonical.executionReceipt ? { executionReceipt: canonical.executionReceipt } : {}),
17964
+ ...(canonical.trustState ? { trustState: canonical.trustState } : {}),
17965
+ ...(canonical.answerTier ? { answerTier: canonical.answerTier } : {}),
17545
17966
  ...(hasRefs ? { semanticRefs } : {}),
17546
17967
  };
17547
17968
  }
@@ -28888,26 +29309,25 @@ function notebookResearchContextPreview(contextPack) {
28888
29309
  };
28889
29310
  }
28890
29311
  function normalizeNotebookAgentResult(result) {
28891
- const columns = Array.isArray(result.columns)
28892
- ? result.columns.map((column) => {
28893
- if (typeof column === 'string')
28894
- return column;
28895
- if (column && typeof column === 'object' && typeof column.name === 'string') {
28896
- return String(column.name);
28897
- }
28898
- return String(column);
28899
- })
28900
- : [];
28901
- const rows = Array.isArray(result.rows)
28902
- ? result.rows
28903
- .filter((row) => Boolean(row && typeof row === 'object' && !Array.isArray(row)))
28904
- .map((row) => row)
28905
- : [];
29312
+ const canonical = normalizeCanonicalQueryResult({
29313
+ columns: result.columns,
29314
+ rows: result.rows,
29315
+ rowCount: result.rowCount,
29316
+ executionTime: result.executionTime,
29317
+ resultFingerprint: result.resultFingerprint,
29318
+ executionReceipt: result.executionReceipt,
29319
+ trustState: result.executableArtifact?.trustState,
29320
+ answerTier: result.answerTier,
29321
+ });
28906
29322
  return {
28907
- columns,
28908
- rows,
28909
- rowCount: typeof result.rowCount === 'number' ? result.rowCount : rows.length,
28910
- executionTime: typeof result.executionTime === 'number' ? result.executionTime : 0,
29323
+ columns: canonical.columns,
29324
+ rows: canonical.rows,
29325
+ rowCount: canonical.rowCount,
29326
+ resultFingerprint: canonical.resultFingerprint,
29327
+ executionTime: canonical.executionTime ?? 0,
29328
+ ...(canonical.truncated ? { truncated: true } : {}),
29329
+ ...(canonical.executionReceipt ? { executionReceipt: canonical.executionReceipt } : {}),
29330
+ ...(canonical.answerTier ? { answerTier: canonical.answerTier } : {}),
28911
29331
  };
28912
29332
  }
28913
29333
  function notebookResearchSummary(question, result, error) {