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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/agent-run-engine.d.ts +177 -3
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +2061 -57
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-gates.d.ts.map +1 -1
  6. package/dist/agent-run-gates.js +51 -3
  7. package/dist/agent-run-gates.js.map +1 -1
  8. package/dist/agent-run-store.d.ts.map +1 -1
  9. package/dist/agent-run-store.js +544 -9
  10. package/dist/agent-run-store.js.map +1 -1
  11. package/dist/agentic/analyst-loop.d.ts +2 -0
  12. package/dist/agentic/analyst-loop.d.ts.map +1 -1
  13. package/dist/agentic/analyst-loop.js +41 -4
  14. package/dist/agentic/analyst-loop.js.map +1 -1
  15. package/dist/agentic/ledger-tools.d.ts.map +1 -1
  16. package/dist/agentic/ledger-tools.js +8 -1
  17. package/dist/agentic/ledger-tools.js.map +1 -1
  18. package/dist/agentic/orchestrator-policy.d.ts +3 -1
  19. package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
  20. package/dist/agentic/orchestrator-policy.js +1 -1
  21. package/dist/agentic/orchestrator-policy.js.map +1 -1
  22. package/dist/agentic/tool-loop.d.ts +13 -1
  23. package/dist/agentic/tool-loop.d.ts.map +1 -1
  24. package/dist/agentic/tool-loop.js +441 -32
  25. package/dist/agentic/tool-loop.js.map +1 -1
  26. package/dist/analytical-frame.d.ts +19 -0
  27. package/dist/analytical-frame.d.ts.map +1 -1
  28. package/dist/analytical-frame.js +170 -12
  29. package/dist/analytical-frame.js.map +1 -1
  30. package/dist/analytical-orchestration.d.ts +949 -10
  31. package/dist/analytical-orchestration.d.ts.map +1 -1
  32. package/dist/analytical-orchestration.js +499 -56
  33. package/dist/analytical-orchestration.js.map +1 -1
  34. package/dist/answer-loop.d.ts +166 -2
  35. package/dist/answer-loop.d.ts.map +1 -1
  36. package/dist/answer-loop.js +654 -18
  37. package/dist/answer-loop.js.map +1 -1
  38. package/dist/answer-shape.d.ts +27 -0
  39. package/dist/answer-shape.d.ts.map +1 -1
  40. package/dist/answer-shape.js +55 -0
  41. package/dist/answer-shape.js.map +1 -1
  42. package/dist/ask-observability/instrumentation.d.ts.map +1 -1
  43. package/dist/ask-observability/instrumentation.js +52 -1
  44. package/dist/ask-observability/instrumentation.js.map +1 -1
  45. package/dist/ask-observability/portable.js +562 -0
  46. package/dist/ask-observability/portable.js.map +1 -1
  47. package/dist/ask-observability/types.d.ts +26 -3
  48. package/dist/ask-observability/types.d.ts.map +1 -1
  49. package/dist/ask-observability/types.js.map +1 -1
  50. package/dist/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
  51. package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
  52. package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
  53. package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
  54. package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
  55. package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
  56. package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
  57. package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
  58. package/dist/conversation/session-store.d.ts +31 -0
  59. package/dist/conversation/session-store.d.ts.map +1 -1
  60. package/dist/conversation/session-store.js +38 -0
  61. package/dist/conversation/session-store.js.map +1 -1
  62. package/dist/conversation/snapshot.d.ts +4 -1
  63. package/dist/conversation/snapshot.d.ts.map +1 -1
  64. package/dist/conversation/snapshot.js +85 -3
  65. package/dist/conversation/snapshot.js.map +1 -1
  66. package/dist/conversation/turn-trust.d.ts.map +1 -1
  67. package/dist/conversation/turn-trust.js +2 -0
  68. package/dist/conversation/turn-trust.js.map +1 -1
  69. package/dist/embeddings/provider.d.ts +1 -0
  70. package/dist/embeddings/provider.d.ts.map +1 -1
  71. package/dist/embeddings/provider.js +18 -0
  72. package/dist/embeddings/provider.js.map +1 -1
  73. package/dist/generated-analytical-proposal.js +37 -2
  74. package/dist/generated-analytical-proposal.js.map +1 -1
  75. package/dist/index.d.ts +15 -8
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +9 -4
  78. package/dist/index.js.map +1 -1
  79. package/dist/intent-controller.d.ts +77 -1
  80. package/dist/intent-controller.d.ts.map +1 -1
  81. package/dist/intent-controller.js +1 -1
  82. package/dist/intent-controller.js.map +1 -1
  83. package/dist/kg/build.js +32 -2
  84. package/dist/kg/build.js.map +1 -1
  85. package/dist/meaning-resolution.d.ts +129 -0
  86. package/dist/meaning-resolution.d.ts.map +1 -1
  87. package/dist/meaning-resolution.js +108 -15
  88. package/dist/meaning-resolution.js.map +1 -1
  89. package/dist/metadata/analysis-planner.js +57 -6
  90. package/dist/metadata/analysis-planner.js.map +1 -1
  91. package/dist/metadata/catalog.d.ts +14 -0
  92. package/dist/metadata/catalog.d.ts.map +1 -1
  93. package/dist/metadata/catalog.js +28 -1
  94. package/dist/metadata/catalog.js.map +1 -1
  95. package/dist/metadata/meaning-evidence.d.ts +19 -0
  96. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  97. package/dist/metadata/meaning-evidence.js +113 -6
  98. package/dist/metadata/meaning-evidence.js.map +1 -1
  99. package/dist/provider-egress.d.ts +4 -1
  100. package/dist/provider-egress.d.ts.map +1 -1
  101. package/dist/provider-egress.js +50 -5
  102. package/dist/provider-egress.js.map +1 -1
  103. package/dist/providers/claude.d.ts +2 -2
  104. package/dist/providers/claude.d.ts.map +1 -1
  105. package/dist/providers/claude.js +191 -28
  106. package/dist/providers/claude.js.map +1 -1
  107. package/dist/providers/index.d.ts +1 -1
  108. package/dist/providers/index.d.ts.map +1 -1
  109. package/dist/providers/index.js.map +1 -1
  110. package/dist/providers/openai.d.ts +2 -2
  111. package/dist/providers/openai.d.ts.map +1 -1
  112. package/dist/providers/openai.js +191 -21
  113. package/dist/providers/openai.js.map +1 -1
  114. package/dist/providers/types.d.ts +79 -1
  115. package/dist/providers/types.d.ts.map +1 -1
  116. package/dist/providers/types.js.map +1 -1
  117. package/dist/requirement-clauses.d.ts +23 -0
  118. package/dist/requirement-clauses.d.ts.map +1 -0
  119. package/dist/requirement-clauses.js +70 -0
  120. package/dist/requirement-clauses.js.map +1 -0
  121. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  122. package/dist/resolved-analytical-plan.js +201 -15
  123. package/dist/resolved-analytical-plan.js.map +1 -1
  124. package/dist/router.d.ts +109 -2
  125. package/dist/router.d.ts.map +1 -1
  126. package/dist/router.js +753 -76
  127. package/dist/router.js.map +1 -1
  128. package/package.json +4 -4
@@ -9,11 +9,28 @@
9
9
  * a user actually reopens stay complete while history stops growing unbounded.
10
10
  */
11
11
  import Database from 'better-sqlite3';
12
+ import { createHash } from 'node:crypto';
12
13
  import { existsSync, mkdirSync, readFileSync, renameSync } from 'node:fs';
13
14
  import { dirname, join } from 'node:path';
14
15
  import { normalizeProviderEgressReceiptV1 } from '@duckcodeailabs/dql-core';
15
16
  const DEFAULT_MAX_RUNS = 300;
16
17
  const DEFAULT_FULL_PAYLOAD_RUNS = 50;
18
+ const EVIDENCE_CANDIDATE_ROLES = new Set([
19
+ 'metric',
20
+ 'entity_key',
21
+ 'entity_label',
22
+ 'categorical_dimension',
23
+ 'time_dimension',
24
+ 'member',
25
+ 'relationship',
26
+ 'context',
27
+ ]);
28
+ const ANALYTICAL_TASK_OUTCOME_STATUSES = new Set([
29
+ 'completed', 'partial', 'gap', 'blocked', 'dependency_blocked',
30
+ ]);
31
+ const ANALYTICAL_TASK_OUTCOME_TRUST_STATES = new Set([
32
+ 'certified', 'governed', 'review_required', 'blocked', 'not_applicable',
33
+ ]);
17
34
  export function resolveAgentRunRetention(env = process.env) {
18
35
  const configured = Number(env.DQL_AGENT_RUN_RETENTION);
19
36
  if (!Number.isFinite(configured) || configured <= 0)
@@ -68,7 +85,8 @@ export class SqliteAgentRunStore {
68
85
  this.compactOldRuns();
69
86
  }
70
87
  saveProgress(progress) {
71
- const updatedAt = progress.lifecycle.updatedAt;
88
+ const persistedProgress = sanitizeProgressRoleCoverage(progress);
89
+ const updatedAt = persistedProgress.lifecycle.updatedAt;
72
90
  this.db.prepare(`
73
91
  INSERT INTO agent_runs (id, question, route, status, started_at, completed_at, compacted, payload_json, updated_at)
74
92
  VALUES (?, ?, ?, NULL, ?, NULL, 0, ?, ?)
@@ -82,7 +100,7 @@ export class SqliteAgentRunStore {
82
100
  payload_json = excluded.payload_json,
83
101
  updated_at = excluded.updated_at
84
102
  WHERE agent_runs.completed_at IS NULL
85
- `).run(progress.id, progress.question, progress.route ?? 'blocked', progress.lifecycle.startedAt, JSON.stringify(progress), updatedAt);
103
+ `).run(persistedProgress.id, persistedProgress.question, persistedProgress.route ?? 'blocked', persistedProgress.lifecycle.startedAt, JSON.stringify(persistedProgress), updatedAt);
86
104
  }
87
105
  get(id) {
88
106
  const row = this.db.prepare('SELECT payload_json FROM agent_runs WHERE id = ?').get(id);
@@ -127,12 +145,16 @@ export class SqliteAgentRunStore {
127
145
  const userCancelled = progress.lifecycle.state === 'cancelling'
128
146
  || progress.events.some((event) => event.type === 'run.cancelled');
129
147
  const route = progress.route ?? (userCancelled ? 'cancelled' : 'blocked');
148
+ const retainedArtifacts = retainedInterruptedArtifacts(progress, userCancelled);
149
+ const retainedIndependentResult = retainedArtifacts.some((artifact) => artifact.trustState !== 'blocked');
130
150
  const failure = {
131
151
  code: userCancelled ? 'RUN_CANCELLED' : 'RUN_INTERRUPTED',
132
152
  phase: progress.lifecycle.phase,
133
153
  message: userCancelled
134
154
  ? 'Stopped by user.'
135
- : 'The local DQL runtime restarted before this agent run completed. No result was accepted.',
155
+ : retainedIndependentResult
156
+ ? 'The local DQL runtime restarted before this agent run completed. Completed independent task results remain available for inspection.'
157
+ : 'The local DQL runtime restarted before this agent run completed. No result was accepted.',
136
158
  recoverable: !userCancelled,
137
159
  safeActions: userCancelled ? [] : ['retry_same_request'],
138
160
  };
@@ -143,16 +165,24 @@ export class SqliteAgentRunStore {
143
165
  route,
144
166
  plan: progress.plan,
145
167
  steps: progress.steps,
146
- artifacts: progress.artifacts,
168
+ artifacts: retainedArtifacts,
147
169
  evaluations: progress.evaluations,
148
170
  failure,
149
171
  };
172
+ const runtimeReceipt = interruptedRuntimeReceiptV5(progress, userCancelled, route);
173
+ const runtimeReceiptV6 = runtimeReceipt
174
+ ? interruptedRuntimeReceiptV6(progress, userCancelled, runtimeReceipt)
175
+ : undefined;
150
176
  const diagnosticArtifact = {
151
177
  id: `${progress.id}:diagnostic`,
152
178
  kind: 'answer',
153
179
  title: userCancelled ? 'Cancelled agent run' : 'Interrupted agent run',
154
180
  trustState: 'blocked',
155
- payload: { diagnosticReceipt: receipt },
181
+ payload: {
182
+ diagnosticReceipt: receipt,
183
+ ...(runtimeReceipt ? { diagnosticReceiptV5: runtimeReceipt } : {}),
184
+ ...(runtimeReceiptV6 ? { diagnosticReceiptV6: runtimeReceiptV6 } : {}),
185
+ },
156
186
  };
157
187
  const run = {
158
188
  id: progress.id,
@@ -169,8 +199,8 @@ export class SqliteAgentRunStore {
169
199
  steps: progress.steps,
170
200
  summary: failure.message,
171
201
  artifacts: userCancelled
172
- ? progress.artifacts
173
- : [...progress.artifacts.filter((artifact) => artifact.trustState === 'blocked'), diagnosticArtifact],
202
+ ? retainedArtifacts
203
+ : [...retainedArtifacts, diagnosticArtifact],
174
204
  evaluations: [
175
205
  ...progress.evaluations,
176
206
  userCancelled
@@ -209,6 +239,16 @@ export class SqliteAgentRunStore {
209
239
  repairAttempts: 0,
210
240
  escalationAttempts: 0,
211
241
  diagnosticReceipt: receipt,
242
+ ...(runtimeReceipt ? { diagnosticReceiptV5: runtimeReceipt } : {}),
243
+ ...(runtimeReceiptV6 ? { diagnosticReceiptV6: runtimeReceiptV6 } : {}),
244
+ ...(progress.askAnalystState ? { askAnalystState: progress.askAnalystState } : {}),
245
+ ...(progress.analyticalTaskOutcomes?.length
246
+ ? { analyticalTaskOutcomes: progress.analyticalTaskOutcomes }
247
+ : {}),
248
+ ...(progress.analyticalTaskOutcomeSummary
249
+ ? { analyticalTaskOutcomeSummary: progress.analyticalTaskOutcomeSummary }
250
+ : {}),
251
+ ...(progress.traceReference ? { traceReference: progress.traceReference } : {}),
212
252
  lifecycle: {
213
253
  ...progress.lifecycle,
214
254
  state: 'terminal',
@@ -289,6 +329,202 @@ export class SqliteAgentRunStore {
289
329
  }
290
330
  }
291
331
  }
332
+ /**
333
+ * Restart recovery retains the typed continuation state for a deliberate retry,
334
+ * but its diagnostic artifact/export receipt must remain content-free. Never
335
+ * copy the question, filter values, member values, answer, SQL, or rows here.
336
+ */
337
+ function interruptedRuntimeReceiptV5(progress, cancelled, route) {
338
+ const state = progress.askAnalystState;
339
+ if (!state)
340
+ return undefined;
341
+ const diagnosticState = interruptedDiagnosticState(state);
342
+ const summaryInput = {
343
+ version: 2,
344
+ runtimeMode: state.mode,
345
+ whatHappened: cancelled
346
+ ? 'The Ask runtime was cancelled before its final result was accepted.'
347
+ : 'The Ask runtime was interrupted before its final result was accepted.',
348
+ why: cancelled
349
+ ? 'The local run was cancelled before finalization.'
350
+ : 'The local DQL runtime restarted before finalization.',
351
+ impact: 'No executable data answer was accepted for this run.',
352
+ nextAction: cancelled ? 'none' : 'retry_same_plan',
353
+ ...(state.resolvedPlan?.compiler ? { selectedCompiler: state.resolvedPlan.compiler } : {}),
354
+ programTaskCount: state.program.taskIds.length,
355
+ admittedCandidateCount: state.workspace.admittedCandidateIds.length,
356
+ toolCallCount: state.toolCalls,
357
+ executionAttempts: state.executionAttempts,
358
+ };
359
+ return {
360
+ version: 5,
361
+ runId: progress.id,
362
+ state: diagnosticState,
363
+ summary: {
364
+ ...summaryInput,
365
+ summaryFingerprint: `sha256:${createHash('sha256').update(JSON.stringify(summaryInput)).digest('hex')}`,
366
+ },
367
+ businessAnswer: {
368
+ version: 1,
369
+ mode: 'deterministic_fallback',
370
+ trustState: cancelled ? 'not_applicable' : 'blocked',
371
+ factIds: [],
372
+ limitationCount: 1,
373
+ },
374
+ finalStopReason: cancelled ? 'cancelled' : 'blocked',
375
+ };
376
+ }
377
+ /**
378
+ * Restart recovery has no live trace collector, so V6 is projected only from
379
+ * the persisted V2 state and its content-free V5 envelope. Do not infer a
380
+ * connection or SQL attempt from elapsed time/prose: an interrupted execution
381
+ * count is retained separately and the connection boundary remains skipped
382
+ * unless a terminal run receipt recorded it before restart.
383
+ */
384
+ function interruptedRuntimeReceiptV6(progress, cancelled, receipt) {
385
+ const state = progress.askAnalystState;
386
+ const planning = state.version === 2 || state.version === 3
387
+ ? state.planningReceipt
388
+ : undefined;
389
+ const plannerTool = state.workspace.tools.find((tool) => tool.kind === 'provider_meaning');
390
+ const recoveryTool = state.workspace.tools.find((tool) => tool.kind === 'candidate_extension');
391
+ const planFrozen = state.resolvedPlan?.planFrozen === true;
392
+ const verification = planning?.verification ?? {
393
+ version: 1,
394
+ status: state.phase === 'clarify' ? 'ambiguous' : state.phase === 'blocked' ? 'invalid' : 'valid',
395
+ missingRoles: [],
396
+ candidateIds: [],
397
+ reasonCode: state.phase === 'clarify'
398
+ ? 'persisted_clarification_state'
399
+ : state.phase === 'blocked'
400
+ ? 'persisted_blocked_state'
401
+ : 'persisted_program_state',
402
+ };
403
+ // A durable failed provider tool is a planner attempt too. This restores a
404
+ // truthful concise V6 restart story for V2 checkpoints saved between
405
+ // dispatch and planning-receipt finalization.
406
+ const plannerAttempted = plannerTool?.status === 'completed' || plannerTool?.status === 'failed';
407
+ const plannerCalls = Math.max(planning?.plannerCalls ?? 0, plannerAttempted ? Math.max(1, state.planningContinuations) : 0);
408
+ const revisionCalls = planning?.revisionCalls
409
+ ?? (recoveryTool?.status === 'completed' && plannerCalls > 1 ? 1 : 0);
410
+ const planningMode = planning?.mode
411
+ ?? (plannerCalls === 0
412
+ ? 'deterministic_binding'
413
+ : revisionCalls > 0 ? 'targeted_revision' : 'initial_planner');
414
+ const executionAttempts = state.executionAttempts;
415
+ return {
416
+ ...receipt,
417
+ version: 6,
418
+ planning: {
419
+ version: 1,
420
+ mode: planningMode,
421
+ plannerCalls,
422
+ revisionCalls,
423
+ verification,
424
+ },
425
+ // V2 stores count-only admission coverage, so reload can retain the
426
+ // meaningful "required versus admitted" story without exposing labels,
427
+ // prompts, or result values. Older V1 states remain content-free.
428
+ roleCoverage: state.workspace.version === 2
429
+ ? (state.workspace.roleCoverage ?? [])
430
+ .filter((entry) => Number.isFinite(entry.candidateCount) && entry.candidateCount >= 0)
431
+ .map((entry) => ({
432
+ role: entry.role,
433
+ candidateCount: entry.candidateCount,
434
+ ...(entry.state === 'alternatives' || entry.state === 'proven' ? { state: entry.state } : {}),
435
+ }))
436
+ : [],
437
+ cascade: {
438
+ attempts: [],
439
+ ...(state.resolvedPlan?.selectedTier ? { selectedTier: state.resolvedPlan.selectedTier } : {}),
440
+ ...(state.resolvedPlan?.compiler && state.resolvedPlan.compiler !== 'none'
441
+ ? { stopReason: `persisted_${state.resolvedPlan.compiler}_plan` }
442
+ : {}),
443
+ planFrozen,
444
+ },
445
+ connection: { attempted: false },
446
+ execution: { attempts: executionAttempts },
447
+ facts: { factCount: 0 },
448
+ safeNextAction: receipt.summary.nextAction,
449
+ story: [
450
+ { stage: 'retrieval', status: state.workspace.snapshotId ? 'completed' : 'skipped', reasonCode: state.workspace.snapshotId ? 'persisted_snapshot' : 'snapshot_not_retained' },
451
+ {
452
+ stage: 'role_coverage',
453
+ status: state.workspace.version === 2 && (state.workspace.roleCoverage?.length ?? 0) > 0
454
+ ? 'completed'
455
+ : 'skipped',
456
+ reasonCode: state.workspace.version === 2 && (state.workspace.roleCoverage?.length ?? 0) > 0
457
+ ? 'persisted_role_admission_coverage'
458
+ : 'content_free_restart_projection',
459
+ },
460
+ {
461
+ stage: 'planner',
462
+ status: plannerTool?.status === 'failed' ? 'blocked' : plannerCalls > 0 ? 'completed' : 'skipped',
463
+ reasonCode: plannerTool?.reasonCode ?? planningMode,
464
+ },
465
+ { stage: 'verification', status: verification.status === 'valid' ? 'completed' : 'blocked', reasonCode: verification.reasonCode },
466
+ { stage: 'targeted_recovery', status: recoveryTool?.status === 'completed' ? 'completed' : 'skipped', reasonCode: recoveryTool?.reasonCode ?? 'not_required' },
467
+ { stage: 'cascade', status: planFrozen ? 'completed' : state.phase === 'blocked' ? 'blocked' : 'skipped', reasonCode: state.resolvedPlan?.compiler ?? 'no_persisted_compiler' },
468
+ { stage: 'freeze', status: planFrozen ? 'completed' : 'skipped', reasonCode: planFrozen ? 'persisted_plan_frozen' : 'no_persisted_plan' },
469
+ { stage: 'connection', status: 'skipped', reasonCode: 'connection_boundary_not_retained_on_restart' },
470
+ { stage: 'execution', status: executionAttempts > 0 ? 'blocked' : 'skipped', reasonCode: executionAttempts > 0 ? 'interrupted_after_execution_attempt' : 'execution_not_attempted' },
471
+ { stage: 'facts', status: 'skipped', reasonCode: cancelled ? 'run_cancelled_before_final_facts' : 'run_interrupted_before_final_facts' },
472
+ ],
473
+ };
474
+ }
475
+ function interruptedDiagnosticState(state) {
476
+ return {
477
+ version: 1,
478
+ mode: state.mode,
479
+ phase: state.phase,
480
+ questionFingerprint: state.frame.questionFingerprint,
481
+ kind: state.frame.kind,
482
+ requirementCounts: {
483
+ measures: state.frame.requirements.measures.length,
484
+ dimensions: state.frame.requirements.dimensions.length,
485
+ entityTerms: state.frame.requirements.entityTerms.length + state.frame.requirements.entityDisplayTerms.length,
486
+ members: state.frame.requirements.memberTerms.length,
487
+ filters: state.program.filters.length,
488
+ },
489
+ mission: {
490
+ mode: state.mission.mode,
491
+ taskCount: state.mission.tasks.length,
492
+ deferredTaskCount: state.mission.deferredTasks?.length ?? 0,
493
+ hypothesisCount: state.mission.hypotheses.length,
494
+ },
495
+ workspace: {
496
+ ...(state.workspace.snapshotId ? { snapshotId: state.workspace.snapshotId } : {}),
497
+ ...(state.workspace.sourceFingerprint ? { sourceFingerprint: state.workspace.sourceFingerprint } : {}),
498
+ admittedCandidateCount: state.workspace.admittedCandidateIds.length,
499
+ excludedCandidateCount: state.workspace.excludedCandidates.length,
500
+ sourceCoverage: state.workspace.sourceCoverage.map((coverage) => ({
501
+ source: coverage.source,
502
+ status: coverage.status,
503
+ candidateCount: coverage.candidateIds.length,
504
+ })),
505
+ tools: state.workspace.tools.map((tool) => ({
506
+ id: tool.id,
507
+ kind: tool.kind,
508
+ status: tool.status,
509
+ reasonCode: tool.reasonCode,
510
+ })),
511
+ },
512
+ program: {
513
+ id: state.program.id,
514
+ taskCount: state.program.taskIds.length,
515
+ candidateCount: state.program.candidateIds.length,
516
+ requiredRoles: state.program.requiredRoles,
517
+ outputAssertionCount: state.program.outputs.assertions.length,
518
+ },
519
+ ...(state.resolvedPlan ? { resolvedPlan: state.resolvedPlan } : {}),
520
+ counters: {
521
+ planningContinuations: state.planningContinuations,
522
+ toolCalls: state.toolCalls,
523
+ executionAttempts: state.executionAttempts,
524
+ repairAttempts: state.repairAttempts,
525
+ },
526
+ };
527
+ }
292
528
  export function defaultAgentRunSqlitePath(projectRoot) {
293
529
  return join(projectRoot, '.dql', 'local', 'agent-runs.sqlite');
294
530
  }
@@ -333,24 +569,323 @@ function normalizeRunProviderEgressReceipts(run) {
333
569
  ? artifact
334
570
  : { ...artifact, payload: { ...payload, diagnosticReceipt: normalizedDiagnosticReceipt } };
335
571
  });
572
+ const askAnalystState = sanitizeAskAnalystStateRoleCoverage(run.askAnalystState);
573
+ const taskReceipts = sanitizeTaskOutcomeReceipts({
574
+ outcomes: run.analyticalTaskOutcomes,
575
+ summary: run.analyticalTaskOutcomeSummary,
576
+ steps: run.steps,
577
+ });
578
+ // Do not spread untrusted V3 receipts back onto the returned object. Older
579
+ // V1/V2 runs simply omit the additive fields; malformed V3 JSON is stripped
580
+ // or reduced to a blocked receipt before the notebook/API can render it.
581
+ const { analyticalTaskOutcomes: _rawTaskOutcomes, analyticalTaskOutcomeSummary: _rawTaskOutcomeSummary, ...durableRun } = run;
336
582
  return {
337
- ...run,
583
+ ...durableRun,
338
584
  ...(rootReceipts === undefined ? {} : { providerEgressReceipts: rootReceipts }),
339
585
  ...(diagnosticReceipt === run.diagnosticReceipt
340
586
  ? {}
341
587
  : { diagnosticReceipt: diagnosticReceipt }),
342
588
  artifacts,
589
+ ...(askAnalystState === run.askAnalystState ? {} : { askAnalystState }),
590
+ ...(taskReceipts.outcomes ? { analyticalTaskOutcomes: taskReceipts.outcomes } : {}),
591
+ ...(taskReceipts.summary ? { analyticalTaskOutcomeSummary: taskReceipts.summary } : {}),
343
592
  };
344
593
  }
345
594
  function parseProgress(payload) {
346
595
  try {
347
596
  const value = JSON.parse(payload);
348
- return isAgentRunProgressRecord(value) ? value : undefined;
597
+ return isAgentRunProgressRecord(value) ? sanitizeProgressRoleCoverage(value) : undefined;
349
598
  }
350
599
  catch {
351
600
  return undefined;
352
601
  }
353
602
  }
603
+ /**
604
+ * V2 role coverage is an observability summary, not a payload lane. Persist
605
+ * only the typed count tuples so an injected label, raw query fragment, or
606
+ * result value cannot survive a restart through the Ask state or diagnostic
607
+ * projection. The rest of the local continuation state remains intact.
608
+ */
609
+ function sanitizeAskAnalystStateRoleCoverage(state) {
610
+ if (!state || (state.version !== 2 && state.version !== 3) || !Array.isArray(state.workspace.roleCoverage))
611
+ return state;
612
+ const roleCoverage = state.workspace.roleCoverage.flatMap((entry) => {
613
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
614
+ return [];
615
+ const record = entry;
616
+ if (typeof record.role !== 'string'
617
+ || !EVIDENCE_CANDIDATE_ROLES.has(record.role)
618
+ || !Number.isFinite(record.candidateCount)
619
+ || Number(record.candidateCount) < 0)
620
+ return [];
621
+ const coverageState = record.state === 'alternatives' || record.state === 'proven'
622
+ ? record.state
623
+ : undefined;
624
+ return [{
625
+ role: record.role,
626
+ candidateCount: Math.floor(Number(record.candidateCount)),
627
+ ...(coverageState ? { state: coverageState } : {}),
628
+ }];
629
+ });
630
+ return {
631
+ ...state,
632
+ workspace: {
633
+ ...state.workspace,
634
+ roleCoverage,
635
+ },
636
+ };
637
+ }
638
+ function sanitizeProgressRoleCoverage(progress) {
639
+ const askAnalystState = sanitizeAskAnalystStateRoleCoverage(progress.askAnalystState);
640
+ const taskReceipts = sanitizeTaskOutcomeReceipts({
641
+ outcomes: progress.analyticalTaskOutcomes,
642
+ summary: progress.analyticalTaskOutcomeSummary,
643
+ steps: progress.steps,
644
+ });
645
+ const { analyticalTaskOutcomes: _rawTaskOutcomes, analyticalTaskOutcomeSummary: _rawTaskOutcomeSummary, ...durableProgress } = progress;
646
+ return {
647
+ ...durableProgress,
648
+ ...(askAnalystState === progress.askAnalystState ? {} : { askAnalystState }),
649
+ ...(taskReceipts.outcomes ? { analyticalTaskOutcomes: taskReceipts.outcomes } : {}),
650
+ ...(taskReceipts.summary ? { analyticalTaskOutcomeSummary: taskReceipts.summary } : {}),
651
+ };
652
+ }
653
+ function isDurableString(value) {
654
+ return typeof value === 'string' && value.trim().length > 0 && value.length <= 1_000;
655
+ }
656
+ function durableStringList(value, maxItems = 32) {
657
+ if (!Array.isArray(value))
658
+ return [];
659
+ return [...new Set(value.filter(isDurableString).map((item) => item.trim()))].slice(0, maxItems);
660
+ }
661
+ function durableRecord(value) {
662
+ return value && typeof value === 'object' && !Array.isArray(value)
663
+ ? value
664
+ : undefined;
665
+ }
666
+ function taskReceiptTrustForArtifact(value) {
667
+ if (value === 'grounded')
668
+ return 'governed';
669
+ return ANALYTICAL_TASK_OUTCOME_TRUST_STATES.has(value)
670
+ ? value
671
+ : undefined;
672
+ }
673
+ function canonicalTaskResultProofForArtifact(value) {
674
+ const artifact = durableRecord(value);
675
+ const trustState = taskReceiptTrustForArtifact(artifact?.trustState);
676
+ if (!artifact || artifact.kind !== 'answer' || !trustState || trustState === 'blocked')
677
+ return undefined;
678
+ const payload = durableRecord(artifact.payload);
679
+ const result = durableRecord(payload?.result);
680
+ const fingerprint = result?.resultFingerprint;
681
+ if (!result || !isDurableString(fingerprint)
682
+ || !Array.isArray(result.columns) || result.columns.length === 0
683
+ || !Array.isArray(result.rows))
684
+ return undefined;
685
+ return { resultFingerprint: fingerprint, trustState };
686
+ }
687
+ function canonicalTaskResultProofsByTask(steps) {
688
+ const byTask = new Map();
689
+ if (!Array.isArray(steps))
690
+ return byTask;
691
+ for (const rawStep of steps) {
692
+ const step = durableRecord(rawStep);
693
+ const taskId = step?.askAnalystTaskId;
694
+ if (!step || !isDurableString(taskId) || !Array.isArray(step.artifacts))
695
+ continue;
696
+ const proofs = step.artifacts
697
+ .map(canonicalTaskResultProofForArtifact)
698
+ .filter((proof) => Boolean(proof));
699
+ if (proofs.length > 0) {
700
+ const byFingerprint = new Map();
701
+ for (const proof of proofs) {
702
+ const existing = byFingerprint.get(proof.resultFingerprint);
703
+ byFingerprint.set(proof.resultFingerprint, existing
704
+ ? leastTrustedTaskReceiptState([existing, proof.trustState])
705
+ : proof.trustState);
706
+ }
707
+ byTask.set(taskId, byFingerprint);
708
+ }
709
+ }
710
+ return byTask;
711
+ }
712
+ function sanitizeTaskOutcomeFailure(value) {
713
+ const failure = durableRecord(value);
714
+ if (!failure || !isDurableString(failure.code) || !isDurableString(failure.message))
715
+ return undefined;
716
+ if (failure.phase !== 'planning' && failure.phase !== 'execution' && failure.phase !== 'dependency')
717
+ return undefined;
718
+ return { version: 1, code: failure.code, message: failure.message, phase: failure.phase };
719
+ }
720
+ function sanitizeTaskOutcomeGap(value) {
721
+ const gap = durableRecord(value);
722
+ const allowedCodes = new Set([
723
+ 'MISSING_MEASURE', 'MISSING_DIMENSION', 'MISSING_ATTRIBUTE', 'MISSING_RELATIONSHIP',
724
+ 'MISSING_RUNTIME_CAPABILITY', 'RESULT_CONTRACT_MISMATCH', 'PROVIDER_UNAVAILABLE',
725
+ 'EXECUTION_FAILED', 'AMBIGUOUS_MEANING', 'POLICY_BLOCKED',
726
+ ]);
727
+ const allowedPhases = new Set([
728
+ 'retrieval', 'meaning', 'planning', 'compilation', 'execution', 'presentation',
729
+ ]);
730
+ const allowedRoutes = new Set([
731
+ 'certified', 'semantic', 'governed_relational', 'generated', 'research',
732
+ ]);
733
+ if (!gap || !allowedCodes.has(gap.code)
734
+ || !allowedPhases.has(gap.phase)
735
+ || !isDurableString(gap.message) || typeof gap.recoverable !== 'boolean'
736
+ || typeof gap.planFrozen !== 'boolean')
737
+ return undefined;
738
+ const attemptedRoutes = Array.isArray(gap.attemptedRoutes)
739
+ ? gap.attemptedRoutes.filter((route) => typeof route === 'string' && allowedRoutes.has(route))
740
+ : [];
741
+ return {
742
+ version: 1,
743
+ code: gap.code,
744
+ phase: gap.phase,
745
+ message: gap.message,
746
+ searchedSources: durableStringList(gap.searchedSources),
747
+ attemptedRoutes,
748
+ missing: durableStringList(gap.missing),
749
+ recoverable: gap.recoverable,
750
+ planFrozen: gap.planFrozen,
751
+ nextActions: durableStringList(gap.nextActions),
752
+ };
753
+ }
754
+ function leastTrustedTaskReceiptState(states) {
755
+ if (states.length === 0)
756
+ return 'blocked';
757
+ const score = {
758
+ certified: 4, governed: 3, review_required: 2, not_applicable: 1, blocked: 0,
759
+ };
760
+ return states.reduce((least, candidate) => score[candidate] < score[least] ? candidate : least);
761
+ }
762
+ /**
763
+ * Read local V3 task receipts as untrusted JSON. Successful status is valid
764
+ * only when the same task step contains a canonical immutable result artifact
765
+ * with the advertised fingerprint. This also keeps V1/V2 records readable:
766
+ * absent additive fields remain absent.
767
+ */
768
+ function sanitizeTaskOutcomeReceipts(input) {
769
+ const rawSummary = durableRecord(input.summary);
770
+ const rawOutcomes = Array.isArray(input.outcomes) ? input.outcomes : undefined;
771
+ if (!rawOutcomes && !rawSummary)
772
+ return {};
773
+ const canonicalByTask = canonicalTaskResultProofsByTask(input.steps);
774
+ const outcomes = [];
775
+ const seenTaskIds = new Set();
776
+ for (const rawOutcome of rawOutcomes ?? []) {
777
+ const outcome = durableRecord(rawOutcome);
778
+ const taskId = outcome?.taskId;
779
+ const status = outcome?.status;
780
+ if (!outcome || !isDurableString(taskId)
781
+ || !ANALYTICAL_TASK_OUTCOME_STATUSES.has(status)
782
+ || seenTaskIds.has(taskId))
783
+ continue;
784
+ seenTaskIds.add(taskId);
785
+ const persistedTrustState = ANALYTICAL_TASK_OUTCOME_TRUST_STATES.has(outcome.trustState)
786
+ ? outcome.trustState
787
+ : undefined;
788
+ const requestedSuccess = status === 'completed' || status === 'partial';
789
+ const resultFingerprint = isDurableString(outcome.resultFingerprint) ? outcome.resultFingerprint : undefined;
790
+ const canonicalTrustState = resultFingerprint
791
+ ? canonicalByTask.get(taskId)?.get(resultFingerprint)
792
+ : undefined;
793
+ const canonicalResult = Boolean(canonicalTrustState);
794
+ if (requestedSuccess && !canonicalResult) {
795
+ outcomes.push({
796
+ version: 1,
797
+ taskId,
798
+ status: 'blocked',
799
+ trustState: 'blocked',
800
+ summary: 'This persisted task result has no matching immutable canonical artifact.',
801
+ failure: {
802
+ version: 1,
803
+ code: 'TASK_RESULT_ARTIFACT_UNVERIFIED',
804
+ message: 'This persisted task result has no matching immutable canonical artifact.',
805
+ phase: 'execution',
806
+ },
807
+ });
808
+ continue;
809
+ }
810
+ const trustState = canonicalTrustState
811
+ ? persistedTrustState
812
+ ? leastTrustedTaskReceiptState([persistedTrustState, canonicalTrustState])
813
+ : canonicalTrustState
814
+ : persistedTrustState ?? 'blocked';
815
+ outcomes.push({
816
+ version: 1,
817
+ taskId,
818
+ status: status,
819
+ trustState,
820
+ ...(isDurableString(outcome.summary) ? { summary: outcome.summary } : {}),
821
+ ...(canonicalResult && resultFingerprint ? { resultFingerprint } : {}),
822
+ ...(sanitizeTaskOutcomeGap(outcome.gap) ? { gap: sanitizeTaskOutcomeGap(outcome.gap) } : {}),
823
+ ...(sanitizeTaskOutcomeFailure(outcome.failure) ? { failure: sanitizeTaskOutcomeFailure(outcome.failure) } : {}),
824
+ ...(status === 'dependency_blocked' ? { dependencyTaskIds: durableStringList(outcome.dependencyTaskIds) } : {}),
825
+ });
826
+ }
827
+ const requestedTaskCount = typeof rawSummary?.taskCount === 'number' && Number.isFinite(rawSummary.taskCount)
828
+ ? Math.max(0, Math.min(100, Math.floor(rawSummary.taskCount)))
829
+ : 0;
830
+ // Preserve an execution-pending compiler checkpoint only when it makes no
831
+ // success claim. It becomes execution-final after the first child step.
832
+ const mayKeepEmptySummary = outcomes.length === 0
833
+ && requestedTaskCount > 0
834
+ && durableStringList(rawSummary?.successfulTaskIds).length === 0;
835
+ if (outcomes.length === 0 && !mayKeepEmptySummary)
836
+ return {};
837
+ const successfulTaskIds = outcomes
838
+ .filter((outcome) => outcome.status === 'completed' || outcome.status === 'partial')
839
+ .map((outcome) => outcome.taskId);
840
+ const failedTaskIds = outcomes
841
+ .filter((outcome) => outcome.status !== 'completed' && outcome.status !== 'partial' && outcome.status !== 'dependency_blocked')
842
+ .map((outcome) => outcome.taskId);
843
+ const dependencyBlockedTaskIds = outcomes
844
+ .filter((outcome) => outcome.status === 'dependency_blocked')
845
+ .map((outcome) => outcome.taskId);
846
+ const taskCount = Math.max(requestedTaskCount, outcomes.length);
847
+ const summary = {
848
+ version: 1,
849
+ status: successfulTaskIds.length === 0
850
+ ? 'blocked'
851
+ : failedTaskIds.length > 0 || dependencyBlockedTaskIds.length > 0 || successfulTaskIds.length < taskCount
852
+ ? 'partial'
853
+ : 'completed',
854
+ trustState: leastTrustedTaskReceiptState(outcomes
855
+ .filter((outcome) => outcome.status === 'completed' || outcome.status === 'partial')
856
+ .map((outcome) => outcome.trustState ?? 'blocked')),
857
+ taskCount,
858
+ successfulTaskIds,
859
+ failedTaskIds,
860
+ dependencyBlockedTaskIds,
861
+ };
862
+ return {
863
+ ...(outcomes.length > 0 ? { outcomes } : {}),
864
+ summary,
865
+ };
866
+ }
867
+ /**
868
+ * A restart never turns an in-flight Ask into a completed parent answer, but
869
+ * it must not erase a completed independent child whose canonical result was
870
+ * checkpointed before the interruption. Keep only blocked diagnostics plus
871
+ * artifact/result pairs proved by the V3 task receipt.
872
+ */
873
+ function retainedInterruptedArtifacts(progress, userCancelled) {
874
+ if (userCancelled)
875
+ return progress.artifacts;
876
+ const retainedIds = new Set(progress.artifacts
877
+ .filter((artifact) => artifact.trustState === 'blocked')
878
+ .map((artifact) => artifact.id));
879
+ for (const outcome of progress.analyticalTaskOutcomes ?? []) {
880
+ if ((outcome.status !== 'completed' && outcome.status !== 'partial') || !outcome.resultFingerprint)
881
+ continue;
882
+ const step = progress.steps.find((candidate) => candidate.askAnalystTaskId === outcome.taskId);
883
+ const artifact = step?.artifacts.find((candidate) => canonicalTaskResultProofForArtifact(candidate)?.resultFingerprint === outcome.resultFingerprint);
884
+ if (artifact)
885
+ retainedIds.add(artifact.id);
886
+ }
887
+ return progress.artifacts.filter((artifact) => retainedIds.has(artifact.id));
888
+ }
354
889
  function isAgentRunRecord(value) {
355
890
  if (!value || typeof value !== 'object' || Array.isArray(value))
356
891
  return false;