@duckcodeailabs/dql-agent 1.14.2 → 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 (196) hide show
  1. package/dist/agent-run-engine.d.ts +239 -4
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +2924 -85
  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-planner.d.ts +6 -0
  9. package/dist/agent-run-planner.d.ts.map +1 -1
  10. package/dist/agent-run-planner.js +2 -0
  11. package/dist/agent-run-planner.js.map +1 -1
  12. package/dist/agent-run-store.d.ts.map +1 -1
  13. package/dist/agent-run-store.js +591 -11
  14. package/dist/agent-run-store.js.map +1 -1
  15. package/dist/agentic/analyst-loop.d.ts +9 -1
  16. package/dist/agentic/analyst-loop.d.ts.map +1 -1
  17. package/dist/agentic/analyst-loop.js +204 -4
  18. package/dist/agentic/analyst-loop.js.map +1 -1
  19. package/dist/agentic/assumptions.d.ts +1 -1
  20. package/dist/agentic/assumptions.d.ts.map +1 -1
  21. package/dist/agentic/assumptions.js.map +1 -1
  22. package/dist/agentic/ledger-tools.d.ts.map +1 -1
  23. package/dist/agentic/ledger-tools.js +8 -1
  24. package/dist/agentic/ledger-tools.js.map +1 -1
  25. package/dist/agentic/orchestrator-policy.d.ts +3 -1
  26. package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
  27. package/dist/agentic/orchestrator-policy.js +1 -1
  28. package/dist/agentic/orchestrator-policy.js.map +1 -1
  29. package/dist/agentic/research-agent.d.ts.map +1 -1
  30. package/dist/agentic/research-agent.js +10 -1
  31. package/dist/agentic/research-agent.js.map +1 -1
  32. package/dist/agentic/sql-authorization.d.ts +8 -0
  33. package/dist/agentic/sql-authorization.d.ts.map +1 -1
  34. package/dist/agentic/sql-authorization.js +3 -0
  35. package/dist/agentic/sql-authorization.js.map +1 -1
  36. package/dist/agentic/tool-loop.d.ts +13 -1
  37. package/dist/agentic/tool-loop.d.ts.map +1 -1
  38. package/dist/agentic/tool-loop.js +441 -32
  39. package/dist/agentic/tool-loop.js.map +1 -1
  40. package/dist/analytical-compatibility.d.ts.map +1 -1
  41. package/dist/analytical-compatibility.js +8 -1
  42. package/dist/analytical-compatibility.js.map +1 -1
  43. package/dist/analytical-failure-repair.d.ts.map +1 -1
  44. package/dist/analytical-failure-repair.js +6 -2
  45. package/dist/analytical-failure-repair.js.map +1 -1
  46. package/dist/analytical-frame.d.ts +42 -1
  47. package/dist/analytical-frame.d.ts.map +1 -1
  48. package/dist/analytical-frame.js +490 -26
  49. package/dist/analytical-frame.js.map +1 -1
  50. package/dist/analytical-orchestration.d.ts +1259 -9
  51. package/dist/analytical-orchestration.d.ts.map +1 -1
  52. package/dist/analytical-orchestration.js +970 -84
  53. package/dist/analytical-orchestration.js.map +1 -1
  54. package/dist/answer-loop.d.ts +230 -10
  55. package/dist/answer-loop.d.ts.map +1 -1
  56. package/dist/answer-loop.js +1182 -85
  57. package/dist/answer-loop.js.map +1 -1
  58. package/dist/answer-shape.d.ts +27 -0
  59. package/dist/answer-shape.d.ts.map +1 -1
  60. package/dist/answer-shape.js +55 -0
  61. package/dist/answer-shape.js.map +1 -1
  62. package/dist/ask-observability/index.d.ts +11 -0
  63. package/dist/ask-observability/index.d.ts.map +1 -0
  64. package/dist/ask-observability/index.js +7 -0
  65. package/dist/ask-observability/index.js.map +1 -0
  66. package/dist/ask-observability/instrumentation.d.ts +35 -0
  67. package/dist/ask-observability/instrumentation.d.ts.map +1 -0
  68. package/dist/ask-observability/instrumentation.js +523 -0
  69. package/dist/ask-observability/instrumentation.js.map +1 -0
  70. package/dist/ask-observability/observer.d.ts +62 -0
  71. package/dist/ask-observability/observer.d.ts.map +1 -0
  72. package/dist/ask-observability/observer.js +537 -0
  73. package/dist/ask-observability/observer.js.map +1 -0
  74. package/dist/ask-observability/portable.d.ts +98 -0
  75. package/dist/ask-observability/portable.d.ts.map +1 -0
  76. package/dist/ask-observability/portable.js +1089 -0
  77. package/dist/ask-observability/portable.js.map +1 -0
  78. package/dist/ask-observability/store.d.ts +130 -0
  79. package/dist/ask-observability/store.d.ts.map +1 -0
  80. package/dist/ask-observability/store.js +1114 -0
  81. package/dist/ask-observability/store.js.map +1 -0
  82. package/dist/ask-observability/types.d.ts +380 -0
  83. package/dist/ask-observability/types.d.ts.map +1 -0
  84. package/dist/ask-observability/types.js +2 -0
  85. package/dist/ask-observability/types.js.map +1 -0
  86. package/dist/ask-observability/utils.d.ts +21 -0
  87. package/dist/ask-observability/utils.d.ts.map +1 -0
  88. package/dist/ask-observability/utils.js +102 -0
  89. package/dist/ask-observability/utils.js.map +1 -0
  90. package/dist/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
  91. package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
  92. package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
  93. package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
  94. package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
  95. package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
  96. package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
  97. package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
  98. package/dist/conversation/session-store.d.ts +31 -0
  99. package/dist/conversation/session-store.d.ts.map +1 -1
  100. package/dist/conversation/session-store.js +38 -0
  101. package/dist/conversation/session-store.js.map +1 -1
  102. package/dist/conversation/snapshot.d.ts +4 -1
  103. package/dist/conversation/snapshot.d.ts.map +1 -1
  104. package/dist/conversation/snapshot.js +85 -3
  105. package/dist/conversation/snapshot.js.map +1 -1
  106. package/dist/conversation/turn-trust.d.ts.map +1 -1
  107. package/dist/conversation/turn-trust.js +2 -0
  108. package/dist/conversation/turn-trust.js.map +1 -1
  109. package/dist/embeddings/provider.d.ts +1 -0
  110. package/dist/embeddings/provider.d.ts.map +1 -1
  111. package/dist/embeddings/provider.js +18 -0
  112. package/dist/embeddings/provider.js.map +1 -1
  113. package/dist/generated-analytical-proposal.d.ts +37 -1
  114. package/dist/generated-analytical-proposal.d.ts.map +1 -1
  115. package/dist/generated-analytical-proposal.js +246 -3
  116. package/dist/generated-analytical-proposal.js.map +1 -1
  117. package/dist/governed-relational-compiler.d.ts +15 -0
  118. package/dist/governed-relational-compiler.d.ts.map +1 -1
  119. package/dist/governed-relational-compiler.js +99 -1
  120. package/dist/governed-relational-compiler.js.map +1 -1
  121. package/dist/index.d.ts +21 -12
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +14 -7
  124. package/dist/index.js.map +1 -1
  125. package/dist/intent-controller.d.ts +87 -1
  126. package/dist/intent-controller.d.ts.map +1 -1
  127. package/dist/intent-controller.js +2 -2
  128. package/dist/intent-controller.js.map +1 -1
  129. package/dist/kg/build.js +32 -2
  130. package/dist/kg/build.js.map +1 -1
  131. package/dist/meaning-resolution.d.ts +207 -6
  132. package/dist/meaning-resolution.d.ts.map +1 -1
  133. package/dist/meaning-resolution.js +439 -53
  134. package/dist/meaning-resolution.js.map +1 -1
  135. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  136. package/dist/metadata/analysis-planner.js +78 -7
  137. package/dist/metadata/analysis-planner.js.map +1 -1
  138. package/dist/metadata/catalog.d.ts +14 -0
  139. package/dist/metadata/catalog.d.ts.map +1 -1
  140. package/dist/metadata/catalog.js +29 -2
  141. package/dist/metadata/catalog.js.map +1 -1
  142. package/dist/metadata/meaning-evidence.d.ts +28 -2
  143. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  144. package/dist/metadata/meaning-evidence.js +373 -16
  145. package/dist/metadata/meaning-evidence.js.map +1 -1
  146. package/dist/plan-execution-adapter.d.ts +9 -1
  147. package/dist/plan-execution-adapter.d.ts.map +1 -1
  148. package/dist/plan-execution-adapter.js +93 -13
  149. package/dist/plan-execution-adapter.js.map +1 -1
  150. package/dist/provider-egress.d.ts +28 -11
  151. package/dist/provider-egress.d.ts.map +1 -1
  152. package/dist/provider-egress.js +72 -22
  153. package/dist/provider-egress.js.map +1 -1
  154. package/dist/providers/claude.d.ts +2 -2
  155. package/dist/providers/claude.d.ts.map +1 -1
  156. package/dist/providers/claude.js +201 -37
  157. package/dist/providers/claude.js.map +1 -1
  158. package/dist/providers/dispatch.d.ts +27 -1
  159. package/dist/providers/dispatch.d.ts.map +1 -1
  160. package/dist/providers/dispatch.js +63 -1
  161. package/dist/providers/dispatch.js.map +1 -1
  162. package/dist/providers/gemini.js +10 -10
  163. package/dist/providers/gemini.js.map +1 -1
  164. package/dist/providers/index.d.ts +2 -2
  165. package/dist/providers/index.d.ts.map +1 -1
  166. package/dist/providers/index.js +1 -1
  167. package/dist/providers/index.js.map +1 -1
  168. package/dist/providers/ollama.js +15 -15
  169. package/dist/providers/ollama.js.map +1 -1
  170. package/dist/providers/openai.d.ts +2 -2
  171. package/dist/providers/openai.d.ts.map +1 -1
  172. package/dist/providers/openai.js +213 -43
  173. package/dist/providers/openai.js.map +1 -1
  174. package/dist/providers/types.d.ts +131 -1
  175. package/dist/providers/types.d.ts.map +1 -1
  176. package/dist/providers/types.js.map +1 -1
  177. package/dist/relationship-proof.d.ts +81 -0
  178. package/dist/relationship-proof.d.ts.map +1 -1
  179. package/dist/relationship-proof.js +228 -4
  180. package/dist/relationship-proof.js.map +1 -1
  181. package/dist/requirement-clauses.d.ts +23 -0
  182. package/dist/requirement-clauses.d.ts.map +1 -0
  183. package/dist/requirement-clauses.js +70 -0
  184. package/dist/requirement-clauses.js.map +1 -0
  185. package/dist/resolved-analytical-plan.d.ts +18 -4
  186. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  187. package/dist/resolved-analytical-plan.js +953 -50
  188. package/dist/resolved-analytical-plan.js.map +1 -1
  189. package/dist/router.d.ts +118 -2
  190. package/dist/router.d.ts.map +1 -1
  191. package/dist/router.js +2481 -232
  192. package/dist/router.js.map +1 -1
  193. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  194. package/dist/semantic-bridge/compose.js +23 -4
  195. package/dist/semantic-bridge/compose.js.map +1 -1
  196. package/package.json +4 -4
@@ -9,10 +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';
15
+ import { normalizeProviderEgressReceiptV1 } from '@duckcodeailabs/dql-core';
14
16
  const DEFAULT_MAX_RUNS = 300;
15
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
+ ]);
16
34
  export function resolveAgentRunRetention(env = process.env) {
17
35
  const configured = Number(env.DQL_AGENT_RUN_RETENTION);
18
36
  if (!Number.isFinite(configured) || configured <= 0)
@@ -48,6 +66,7 @@ export class SqliteAgentRunStore {
48
66
  this.migrateLegacyJson(options.legacyJsonPath);
49
67
  }
50
68
  save(run) {
69
+ const persistedRun = normalizeRunProviderEgressReceipts(run);
51
70
  const now = new Date().toISOString();
52
71
  this.db.prepare(`
53
72
  INSERT INTO agent_runs (id, question, route, status, started_at, completed_at, compacted, payload_json, updated_at)
@@ -61,12 +80,13 @@ export class SqliteAgentRunStore {
61
80
  compacted = 0,
62
81
  payload_json = excluded.payload_json,
63
82
  updated_at = excluded.updated_at
64
- `).run(run.id, run.question, run.route, run.status ?? null, run.startedAt, run.completedAt ?? null, JSON.stringify(run), now);
83
+ `).run(persistedRun.id, persistedRun.question, persistedRun.route, persistedRun.status ?? null, persistedRun.startedAt, persistedRun.completedAt ?? null, JSON.stringify(persistedRun), now);
65
84
  this.enforceRetention();
66
85
  this.compactOldRuns();
67
86
  }
68
87
  saveProgress(progress) {
69
- const updatedAt = progress.lifecycle.updatedAt;
88
+ const persistedProgress = sanitizeProgressRoleCoverage(progress);
89
+ const updatedAt = persistedProgress.lifecycle.updatedAt;
70
90
  this.db.prepare(`
71
91
  INSERT INTO agent_runs (id, question, route, status, started_at, completed_at, compacted, payload_json, updated_at)
72
92
  VALUES (?, ?, ?, NULL, ?, NULL, 0, ?, ?)
@@ -80,7 +100,7 @@ export class SqliteAgentRunStore {
80
100
  payload_json = excluded.payload_json,
81
101
  updated_at = excluded.updated_at
82
102
  WHERE agent_runs.completed_at IS NULL
83
- `).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);
84
104
  }
85
105
  get(id) {
86
106
  const row = this.db.prepare('SELECT payload_json FROM agent_runs WHERE id = ?').get(id);
@@ -125,12 +145,16 @@ export class SqliteAgentRunStore {
125
145
  const userCancelled = progress.lifecycle.state === 'cancelling'
126
146
  || progress.events.some((event) => event.type === 'run.cancelled');
127
147
  const route = progress.route ?? (userCancelled ? 'cancelled' : 'blocked');
148
+ const retainedArtifacts = retainedInterruptedArtifacts(progress, userCancelled);
149
+ const retainedIndependentResult = retainedArtifacts.some((artifact) => artifact.trustState !== 'blocked');
128
150
  const failure = {
129
151
  code: userCancelled ? 'RUN_CANCELLED' : 'RUN_INTERRUPTED',
130
152
  phase: progress.lifecycle.phase,
131
153
  message: userCancelled
132
154
  ? 'Stopped by user.'
133
- : '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.',
134
158
  recoverable: !userCancelled,
135
159
  safeActions: userCancelled ? [] : ['retry_same_request'],
136
160
  };
@@ -141,16 +165,24 @@ export class SqliteAgentRunStore {
141
165
  route,
142
166
  plan: progress.plan,
143
167
  steps: progress.steps,
144
- artifacts: progress.artifacts,
168
+ artifacts: retainedArtifacts,
145
169
  evaluations: progress.evaluations,
146
170
  failure,
147
171
  };
172
+ const runtimeReceipt = interruptedRuntimeReceiptV5(progress, userCancelled, route);
173
+ const runtimeReceiptV6 = runtimeReceipt
174
+ ? interruptedRuntimeReceiptV6(progress, userCancelled, runtimeReceipt)
175
+ : undefined;
148
176
  const diagnosticArtifact = {
149
177
  id: `${progress.id}:diagnostic`,
150
178
  kind: 'answer',
151
179
  title: userCancelled ? 'Cancelled agent run' : 'Interrupted agent run',
152
180
  trustState: 'blocked',
153
- payload: { diagnosticReceipt: receipt },
181
+ payload: {
182
+ diagnosticReceipt: receipt,
183
+ ...(runtimeReceipt ? { diagnosticReceiptV5: runtimeReceipt } : {}),
184
+ ...(runtimeReceiptV6 ? { diagnosticReceiptV6: runtimeReceiptV6 } : {}),
185
+ },
154
186
  };
155
187
  const run = {
156
188
  id: progress.id,
@@ -167,8 +199,8 @@ export class SqliteAgentRunStore {
167
199
  steps: progress.steps,
168
200
  summary: failure.message,
169
201
  artifacts: userCancelled
170
- ? progress.artifacts
171
- : [...progress.artifacts.filter((artifact) => artifact.trustState === 'blocked'), diagnosticArtifact],
202
+ ? retainedArtifacts
203
+ : [...retainedArtifacts, diagnosticArtifact],
172
204
  evaluations: [
173
205
  ...progress.evaluations,
174
206
  userCancelled
@@ -207,6 +239,16 @@ export class SqliteAgentRunStore {
207
239
  repairAttempts: 0,
208
240
  escalationAttempts: 0,
209
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 } : {}),
210
252
  lifecycle: {
211
253
  ...progress.lifecycle,
212
254
  state: 'terminal',
@@ -259,7 +301,9 @@ export class SqliteAgentRunStore {
259
301
  return;
260
302
  try {
261
303
  const parsed = JSON.parse(readFileSync(legacyJsonPath, 'utf-8'));
262
- const runs = Array.isArray(parsed.runs) ? parsed.runs.filter(isAgentRunRecord) : [];
304
+ const runs = Array.isArray(parsed.runs)
305
+ ? parsed.runs.filter(isAgentRunRecord).map(normalizeRunProviderEgressReceipts)
306
+ : [];
263
307
  const insert = this.db.prepare(`
264
308
  INSERT OR IGNORE INTO agent_runs (id, question, route, status, started_at, completed_at, compacted, payload_json, updated_at)
265
309
  VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
@@ -285,27 +329,563 @@ export class SqliteAgentRunStore {
285
329
  }
286
330
  }
287
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
+ }
288
528
  export function defaultAgentRunSqlitePath(projectRoot) {
289
529
  return join(projectRoot, '.dql', 'local', 'agent-runs.sqlite');
290
530
  }
291
531
  function parseRun(payload) {
292
532
  try {
293
533
  const value = JSON.parse(payload);
294
- return isAgentRunRecord(value) ? value : undefined;
534
+ return isAgentRunRecord(value) ? normalizeRunProviderEgressReceipts(value) : undefined;
295
535
  }
296
536
  catch {
297
537
  return undefined;
298
538
  }
299
539
  }
540
+ /**
541
+ * Durable run JSON is local and therefore untrusted on import/read. Keep an
542
+ * invalid provider receipt from advertising historical row egress through the
543
+ * primary run field, diagnostic receipt, or its diagnostic artifact payload.
544
+ */
545
+ function normalizeRunProviderEgressReceipts(run) {
546
+ const normalizeReceiptList = (value) => Array.isArray(value)
547
+ ? value.flatMap((receipt) => {
548
+ const normalized = normalizeProviderEgressReceiptV1(receipt);
549
+ return normalized ? [normalized] : [];
550
+ })
551
+ : undefined;
552
+ const normalizeReceiptContainer = (value) => {
553
+ if (!value || typeof value !== 'object' || Array.isArray(value))
554
+ return value;
555
+ const record = value;
556
+ const providerEgressReceipts = normalizeReceiptList(record.providerEgressReceipts);
557
+ return providerEgressReceipts === undefined
558
+ ? value
559
+ : { ...record, providerEgressReceipts };
560
+ };
561
+ const rootReceipts = normalizeReceiptList(run.providerEgressReceipts);
562
+ const diagnosticReceipt = normalizeReceiptContainer(run.diagnosticReceipt);
563
+ const artifacts = run.artifacts.map((artifact) => {
564
+ if (!artifact.payload || typeof artifact.payload !== 'object' || Array.isArray(artifact.payload))
565
+ return artifact;
566
+ const payload = artifact.payload;
567
+ const normalizedDiagnosticReceipt = normalizeReceiptContainer(payload.diagnosticReceipt);
568
+ return normalizedDiagnosticReceipt === payload.diagnosticReceipt
569
+ ? artifact
570
+ : { ...artifact, payload: { ...payload, diagnosticReceipt: normalizedDiagnosticReceipt } };
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;
582
+ return {
583
+ ...durableRun,
584
+ ...(rootReceipts === undefined ? {} : { providerEgressReceipts: rootReceipts }),
585
+ ...(diagnosticReceipt === run.diagnosticReceipt
586
+ ? {}
587
+ : { diagnosticReceipt: diagnosticReceipt }),
588
+ artifacts,
589
+ ...(askAnalystState === run.askAnalystState ? {} : { askAnalystState }),
590
+ ...(taskReceipts.outcomes ? { analyticalTaskOutcomes: taskReceipts.outcomes } : {}),
591
+ ...(taskReceipts.summary ? { analyticalTaskOutcomeSummary: taskReceipts.summary } : {}),
592
+ };
593
+ }
300
594
  function parseProgress(payload) {
301
595
  try {
302
596
  const value = JSON.parse(payload);
303
- return isAgentRunProgressRecord(value) ? value : undefined;
597
+ return isAgentRunProgressRecord(value) ? sanitizeProgressRoleCoverage(value) : undefined;
304
598
  }
305
599
  catch {
306
600
  return undefined;
307
601
  }
308
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
+ }
309
889
  function isAgentRunRecord(value) {
310
890
  if (!value || typeof value !== 'object' || Array.isArray(value))
311
891
  return false;