@inflexa-ai/harness 0.13.0 → 0.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 (162) hide show
  1. package/dist/agents/conversation-agent.js +23 -16
  2. package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
  3. package/dist/agents/sandbox/chromatin-agent.js +1 -1
  4. package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
  5. package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
  6. package/dist/agents/sandbox/enrichment-agent.js +1 -1
  7. package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
  8. package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
  9. package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
  10. package/dist/agents/sandbox/network-agent.js +1 -1
  11. package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
  12. package/dist/agents/sandbox/scientific-executor.js +1 -1
  13. package/dist/agents/sandbox/shared.js +7 -13
  14. package/dist/agents/sandbox/translational-safety-agent.js +4 -7
  15. package/dist/agents/sandbox/types.d.ts +5 -5
  16. package/dist/agents/sandbox/types.js +4 -4
  17. package/dist/app/chat-turn.js +4 -1
  18. package/dist/app/message-assembly.d.ts +5 -2
  19. package/dist/app/message-assembly.js +7 -2
  20. package/dist/app/run-activity.d.ts +13 -0
  21. package/dist/app/run-activity.js +51 -0
  22. package/dist/contracts/target-dossier.d.ts +10 -10
  23. package/dist/execution/report-runner.js +1 -1
  24. package/dist/execution/run-event-parts.d.ts +44 -0
  25. package/dist/execution/run-event-parts.js +0 -0
  26. package/dist/execution/run-event-stream.d.ts +91 -0
  27. package/dist/execution/run-event-stream.js +212 -0
  28. package/dist/execution/run-synthesis.js +1 -1
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +20 -1
  31. package/dist/input-sanitization.d.ts +14 -4
  32. package/dist/input-sanitization.js +27 -4
  33. package/dist/lib/storage.js +4 -1
  34. package/dist/loop/budget-exceeded.d.ts +18 -2
  35. package/dist/loop/budget-exceeded.js +49 -2
  36. package/dist/loop/run-agent.d.ts +22 -1
  37. package/dist/loop/run-agent.js +50 -5
  38. package/dist/loop/run-to-terminal.d.ts +0 -2
  39. package/dist/loop/run-to-terminal.js +8 -1
  40. package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
  41. package/dist/memory/ai-sdk-message-storage.js +45 -4
  42. package/dist/memory/content-to-cortex.js +19 -3
  43. package/dist/memory/message-backfill.js +1 -1
  44. package/dist/memory/thread-history.js +34 -8
  45. package/dist/prompts/conversation.d.ts +1 -1
  46. package/dist/prompts/conversation.js +15 -6
  47. package/dist/prompts/literature-reviewer.d.ts +1 -1
  48. package/dist/prompts/literature-reviewer.js +16 -14
  49. package/dist/prompts/planner.js +15 -20
  50. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  51. package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
  52. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  53. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
  54. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  55. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
  56. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  57. package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
  58. package/dist/prompts/synthesis-agent.d.ts +1 -1
  59. package/dist/prompts/synthesis-agent.js +7 -7
  60. package/dist/providers/errors.d.ts +7 -0
  61. package/dist/providers/errors.js +88 -3
  62. package/dist/runtime/pools.d.ts +15 -1
  63. package/dist/runtime/pools.js +15 -1
  64. package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
  65. package/dist/sandbox/sandbox-step-translate.js +17 -2
  66. package/dist/schemas/workflow-state.d.ts +2 -2
  67. package/dist/schemas/workflow-state.js +2 -2
  68. package/dist/state/data-profile.d.ts +53 -1
  69. package/dist/state/data-profile.js +66 -6
  70. package/dist/state/index.d.ts +3 -3
  71. package/dist/state/index.js +2 -2
  72. package/dist/state/init.js +37 -2
  73. package/dist/state/runs.d.ts +39 -0
  74. package/dist/state/runs.js +103 -0
  75. package/dist/tasks/data-profile-activity.d.ts +87 -0
  76. package/dist/tasks/data-profile-activity.js +56 -0
  77. package/dist/tasks/data-profile.js +66 -4
  78. package/dist/tools/bio/chembl.d.ts +1 -1
  79. package/dist/tools/bio/chembl.js +51 -54
  80. package/dist/tools/bio/comptox.js +35 -33
  81. package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
  82. package/dist/tools/bio/drug-gene-interactions.js +175 -0
  83. package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
  84. package/dist/tools/bio/gene-disease-evidence.js +187 -0
  85. package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
  86. package/dist/tools/bio/gene-preclinical-profile.js +125 -0
  87. package/dist/tools/bio/index.d.ts +23 -16
  88. package/dist/tools/bio/index.js +29 -22
  89. package/dist/tools/bio/keys.d.ts +16 -9
  90. package/dist/tools/bio/keys.js +22 -13
  91. package/dist/tools/bio/lookup-annotation.d.ts +48 -0
  92. package/dist/tools/bio/lookup-annotation.js +173 -0
  93. package/dist/tools/bio/opentargets.d.ts +7 -11
  94. package/dist/tools/bio/opentargets.js +24 -35
  95. package/dist/tools/bio/pubchem.js +6 -4
  96. package/dist/tools/bio/pubmed.d.ts +15 -7
  97. package/dist/tools/bio/pubmed.js +79 -20
  98. package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
  99. package/dist/tools/bio/search-clinical-trials.js +26 -5
  100. package/dist/tools/bio/search-faers.js +9 -3
  101. package/dist/tools/bio/search-gene.d.ts +0 -1
  102. package/dist/tools/bio/search-gene.js +4 -5
  103. package/dist/tools/bio/search-geo-datasets.js +10 -4
  104. package/dist/tools/bio/search-interactions.d.ts +8 -4
  105. package/dist/tools/bio/search-interactions.js +28 -12
  106. package/dist/tools/bio/target-safety.d.ts +59 -0
  107. package/dist/tools/bio/target-safety.js +191 -0
  108. package/dist/tools/execute-analysis.d.ts +1 -0
  109. package/dist/tools/execute-analysis.js +5 -5
  110. package/dist/tools/lib/dgidb-client.d.ts +49 -0
  111. package/dist/tools/lib/dgidb-client.js +168 -0
  112. package/dist/tools/lib/disgenet-client.d.ts +56 -0
  113. package/dist/tools/lib/disgenet-client.js +59 -0
  114. package/dist/tools/lib/drugbank-client.d.ts +83 -0
  115. package/dist/tools/lib/drugbank-client.js +95 -0
  116. package/dist/tools/lib/ensembl-client.d.ts +4 -1
  117. package/dist/tools/lib/ensembl-client.js +5 -3
  118. package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
  119. package/dist/tools/lib/gwas-catalog-client.js +136 -0
  120. package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
  121. package/dist/tools/lib/pharmgkb-client.js +32 -0
  122. package/dist/tools/lib/pubmed-client.d.ts +57 -4
  123. package/dist/tools/lib/pubmed-client.js +57 -3
  124. package/dist/tools/lib/string-client.d.ts +25 -4
  125. package/dist/tools/lib/string-client.js +22 -5
  126. package/dist/tools/research/generate-plan.d.ts +16 -9
  127. package/dist/tools/research/generate-plan.js +115 -69
  128. package/dist/tools/research/inspect-data-profile.d.ts +31 -6
  129. package/dist/tools/research/inspect-data-profile.js +23 -7
  130. package/dist/tools/research/inspect-run.d.ts +33 -15
  131. package/dist/tools/research/inspect-run.js +155 -66
  132. package/dist/tools/research/literature-reviewer.js +7 -11
  133. package/dist/workflows/execute-analysis.d.ts +63 -0
  134. package/dist/workflows/execute-analysis.js +73 -2
  135. package/dist/workflows/sandbox-step.js +1 -11
  136. package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
  137. package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
  138. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
  139. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
  140. package/package.json +1 -1
  141. package/dist/tools/bio/check-safety-panel.d.ts +0 -35
  142. package/dist/tools/bio/check-safety-panel.js +0 -76
  143. package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
  144. package/dist/tools/bio/get-impc-ko-profile.js +0 -23
  145. package/dist/tools/bio/lookup-go-term.d.ts +0 -31
  146. package/dist/tools/bio/lookup-go-term.js +0 -116
  147. package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
  148. package/dist/tools/bio/search-bgee-expression.js +0 -30
  149. package/dist/tools/bio/search-clinvar.d.ts +0 -13
  150. package/dist/tools/bio/search-clinvar.js +0 -28
  151. package/dist/tools/bio/search-dgidb.d.ts +0 -53
  152. package/dist/tools/bio/search-dgidb.js +0 -213
  153. package/dist/tools/bio/search-disgenet.d.ts +0 -30
  154. package/dist/tools/bio/search-disgenet.js +0 -84
  155. package/dist/tools/bio/search-drugbank.d.ts +0 -38
  156. package/dist/tools/bio/search-drugbank.js +0 -109
  157. package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
  158. package/dist/tools/bio/search-gwas-catalog.js +0 -172
  159. package/dist/tools/bio/search-pathway.d.ts +0 -12
  160. package/dist/tools/bio/search-pathway.js +0 -32
  161. package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
  162. package/dist/tools/bio/search-pharmgkb.js +0 -52
@@ -77,6 +77,13 @@ export interface ProviderErrorClassification {
77
77
  */
78
78
  readonly retryable: boolean;
79
79
  }
80
+ /**
81
+ * Walk the `cause` chain for the first numeric HTTP status. The single status
82
+ * walker: `classifyProviderError` keys on what this returns, so a caller asking
83
+ * "did the provider layer have a status to classify on?" must ask it here rather
84
+ * than re-implement the traversal.
85
+ */
86
+ export declare function extractStatus(err: unknown): number | undefined;
80
87
  /**
81
88
  * Classify a provider failure by origin.
82
89
  *
@@ -15,6 +15,7 @@
15
15
  * re-authenticating fixes it — which is a different remedy from every other
16
16
  * non-retryable 4xx, so it is a different kind.
17
17
  */
18
+ import { redactSecrets } from "../input-sanitization.js";
18
19
  /**
19
20
  * Structural guard for an already-constructed `ProviderError` value. A
20
21
  * `ProviderError` is a plain object, not an `Error`, so it cannot be
@@ -71,7 +72,67 @@ export function toProviderError(e, workload) {
71
72
  cause: e,
72
73
  };
73
74
  }
74
- return { type: "provider", retryable, message: detail, cause: e };
75
+ // The `provider` arm composes like its three siblings rather than forwarding
76
+ // the SDK message verbatim. It has to: when a 4xx body does not parse against
77
+ // the configured provider's error schema, the AI SDK falls back to
78
+ // `response.statusText`, so the "detail" is a bare HTTP reason phrase —
79
+ // `"Bad Request"` — that names neither the call nor the cause, while the
80
+ // status, the workload, and the body the SDK captured are all still on the
81
+ // `cause` chain right here.
82
+ //
83
+ // Composed strictly AFTER `classifyProviderError` has returned, so message
84
+ // text can never become a classification input: the harness-providers spec
85
+ // fixes classification on the HTTP status alone.
86
+ const status = extractStatus(e);
87
+ const lead = status === undefined ? `Provider call failed for ${workload}` : `Provider call failed for ${workload} (HTTP ${status})`;
88
+ // A transport that carries no reason phrase (an HTTP/2 hop yields
89
+ // `statusText === ""`) leaves `detail` empty; appending it regardless would
90
+ // trail a bare `": "` on an otherwise complete message.
91
+ const reason = detail.trim();
92
+ const diagnosed = reason ? `${lead}: ${reason}` : lead;
93
+ const body = extractResponseBody(e);
94
+ return {
95
+ type: "provider",
96
+ retryable,
97
+ // The excerpt trails deliberately — workload and status lead, so a
98
+ // downstream truncation (`profileFailureReason` cuts the line at 200)
99
+ // eats the least diagnostic content first.
100
+ message: body ? `${diagnosed} — response body: ${excerptResponseBody(body)}` : diagnosed,
101
+ cause: e,
102
+ };
103
+ }
104
+ /**
105
+ * Max characters of a captured provider response body carried in a message.
106
+ *
107
+ * Derived, not picked: the tightest downstream consumer (`profileFailureReason`,
108
+ * `tasks/data-profile.ts`) truncates the whole line at 200, and the lead this
109
+ * excerpt trails — a workload label plus `HTTP <status>` plus a reason phrase —
110
+ * runs to roughly 80. 120 is therefore the largest round value that cannot evict
111
+ * that lead.
112
+ *
113
+ * Deliberately NOT shared with that consumer's `PROFILE_ERROR_MAX_LEN`: that one
114
+ * bounds a `varchar` ledger column, this one bounds a message many non-ledger
115
+ * consumers read, and coupling them would make either one unmovable.
116
+ */
117
+ const PROVIDER_BODY_EXCERPT_MAX_LEN = 120;
118
+ /**
119
+ * Single-lined, secret-redacted, length-capped rendering of a captured response body. Bounding happens
120
+ * here rather than at a consumer because most `ProviderError` consumers truncate nothing at all, and the
121
+ * one that does would let an unbounded body crowd out the workload and status ahead of it.
122
+ *
123
+ * The body is an UNTRUSTED upstream/proxy payload, and this excerpt lands verbatim in the ledger's
124
+ * `data_profile_error` (surfaced to the UI) and the logs, whereas the composed lead does not — so
125
+ * secrets are redacted here and nowhere else on the message. Redaction runs on the FULL body before
126
+ * truncation: a token straddling the cut would otherwise be sliced into an unmatchable fragment that
127
+ * leaks. `redactSecrets` matches only prefixed formats (API keys, JWTs, bearer tokens, connection
128
+ * strings) and is safe on biological data, so a legitimate error body passes through intact.
129
+ */
130
+ function excerptResponseBody(body) {
131
+ const singleLine = redactSecrets(body).replace(/\s+/g, " ").trim();
132
+ // Slice by CODE POINT, not UTF-16 unit: a raw byte body can carry astral characters, and a plain
133
+ // `.slice` could bisect a surrogate pair into a lone half that renders as U+FFFD.
134
+ const chars = [...singleLine];
135
+ return chars.length > PROVIDER_BODY_EXCERPT_MAX_LEN ? chars.slice(0, PROVIDER_BODY_EXCERPT_MAX_LEN - 1).join("") + "…" : singleLine;
75
136
  }
76
137
  /** Max links walked on the `cause` chain looking for a structured status. */
77
138
  const MAX_CAUSE_HOPS = 5;
@@ -88,8 +149,13 @@ const CONNECTION_ERROR_CODES = new Set([
88
149
  "UND_ERR_SOCKET",
89
150
  ]);
90
151
  const CONNECTION_ERROR_PATTERN = /fetch failed|socket hang up|network error|connection (?:refused|reset|error|closed)|econnrefused|enotfound|etimedout|terminated/i;
91
- /** Walk the `cause` chain for the first numeric HTTP status. */
92
- function extractStatus(err) {
152
+ /**
153
+ * Walk the `cause` chain for the first numeric HTTP status. The single status
154
+ * walker: `classifyProviderError` keys on what this returns, so a caller asking
155
+ * "did the provider layer have a status to classify on?" must ask it here rather
156
+ * than re-implement the traversal.
157
+ */
158
+ export function extractStatus(err) {
93
159
  let cursor = err;
94
160
  for (let i = 0; i < MAX_CAUSE_HOPS && cursor; i++) {
95
161
  const e = cursor;
@@ -101,6 +167,25 @@ function extractStatus(err) {
101
167
  }
102
168
  return undefined;
103
169
  }
170
+ /**
171
+ * Walk the `cause` chain for the first captured provider response body.
172
+ *
173
+ * This is what recovers a proxy-minted or otherwise non-conforming error body:
174
+ * the AI SDK keeps the raw bytes on `APICallError.responseBody` even when its
175
+ * own parse against the provider's error schema fails and its `message` degrades
176
+ * to the HTTP reason phrase. A blank body is treated as absent — an empty
177
+ * `— response body:` segment says less than no segment at all.
178
+ */
179
+ function extractResponseBody(err) {
180
+ let cursor = err;
181
+ for (let i = 0; i < MAX_CAUSE_HOPS && cursor; i++) {
182
+ const e = cursor;
183
+ if (typeof e.responseBody === "string" && e.responseBody.trim() !== "")
184
+ return e.responseBody;
185
+ cursor = e.cause;
186
+ }
187
+ return undefined;
188
+ }
104
189
  /** Does any link of the `cause` chain look like a transport-level failure? */
105
190
  function looksLikeConnectionError(err) {
106
191
  let cursor = err;
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Per-pod Postgres pool sizing — the single source of truth.
2
+ * Per-pod Postgres pool sizing and acquisition bounds — the single source of
3
+ * truth.
3
4
  *
4
5
  * Cortex runs two pools per pod against the same cluster (see the postgres-storage-backend spec): the
5
6
  * application pool (`lib/storage.ts::createPool()`) and the DBOS system-DB
@@ -21,5 +22,18 @@ export declare const DEFAULT_APP_POOL_SIZE = 12;
21
22
  * overlap during DBOS recovery, replication / monitoring users.
22
23
  */
23
24
  export declare const SAFETY_MARGIN = 5;
25
+ /**
26
+ * How long a caller may wait for a connection from the app pool before the
27
+ * acquisition fails.
28
+ *
29
+ * The driver waits forever by default, which leaves a caller on a saturated
30
+ * pool with no failure mode at all — only an indefinite hang, indistinguishable
31
+ * from work still in progress and therefore able to stall the caller's own
32
+ * in-flight guard permanently. A bound turns that silent stall into a surfaced
33
+ * error the caller can degrade on. 30s is long enough that a healthy pool under
34
+ * burst never trips it, and short enough that a wedged caller surfaces well
35
+ * inside a human's patience.
36
+ */
37
+ export declare const APP_POOL_ACQUIRE_TIMEOUT_MS = 30000;
24
38
  /** Resolve the app pool size from the optional `DB_POOL_MAX` override. */
25
39
  export declare function resolveAppPoolSize(dbPoolMax: string | undefined): number;
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Per-pod Postgres pool sizing — the single source of truth.
2
+ * Per-pod Postgres pool sizing and acquisition bounds — the single source of
3
+ * truth.
3
4
  *
4
5
  * Cortex runs two pools per pod against the same cluster (see the postgres-storage-backend spec): the
5
6
  * application pool (`lib/storage.ts::createPool()`) and the DBOS system-DB
@@ -21,6 +22,19 @@ export const DEFAULT_APP_POOL_SIZE = 12;
21
22
  * overlap during DBOS recovery, replication / monitoring users.
22
23
  */
23
24
  export const SAFETY_MARGIN = 5;
25
+ /**
26
+ * How long a caller may wait for a connection from the app pool before the
27
+ * acquisition fails.
28
+ *
29
+ * The driver waits forever by default, which leaves a caller on a saturated
30
+ * pool with no failure mode at all — only an indefinite hang, indistinguishable
31
+ * from work still in progress and therefore able to stall the caller's own
32
+ * in-flight guard permanently. A bound turns that silent stall into a surfaced
33
+ * error the caller can degrade on. 30s is long enough that a healthy pool under
34
+ * burst never trips it, and short enough that a wedged caller surfaces well
35
+ * inside a human's patience.
36
+ */
37
+ export const APP_POOL_ACQUIRE_TIMEOUT_MS = 30_000;
24
38
  /** Resolve the app pool size from the optional `DB_POOL_MAX` override. */
25
39
  export function resolveAppPoolSize(dbPoolMax) {
26
40
  if (dbPoolMax === undefined || dbPoolMax === "")
@@ -1,10 +1,23 @@
1
1
  /**
2
2
  * Pure translation between the agent-loop / sandbox event stream and the
3
3
  * typed per-step parts the frontend renders. No DBOS, no per-run state — just the
4
- * label mapping, the file-tree delta extraction/fold, and the chat-data-part
5
- * narrowing the `sandbox-step` workflow body composes into its emitters.
4
+ * reconciliation-id construction, the label mapping, the file-tree delta
5
+ * extraction/fold, and the chat-data-part narrowing a workflow body composes
6
+ * into its emitters.
6
7
  */
7
8
  import type { ChatDataPart, EmitFn } from "../loop/types.js";
9
+ /**
10
+ * Stable reconciliation id for a step's reconciling parts
11
+ * (`data-step-activity`, `data-step-file-tree`). One id per (runId, stepId)
12
+ * so the run-stream fold collapses every phase transition latest-wins onto
13
+ * a single frame. The client keys these by `stepId`, but the server fold
14
+ * keys by part `id`, so the id MUST be unique per step within the run.
15
+ *
16
+ * Every producer of these parts mints its ids here: the id is a
17
+ * reconciliation contract, and the fold collapses correctly only while all
18
+ * producers construct it identically.
19
+ */
20
+ export declare function stepPartId(kind: string, runId: string, stepId: string): string;
8
21
  export declare function activityForTool(name: string, input?: unknown): string;
9
22
  /** On-change working-tree delta the sandbox executor posts per exec (paths
10
23
  * only; the executor skips directories). */
@@ -1,9 +1,24 @@
1
1
  /**
2
2
  * Pure translation between the agent-loop / sandbox event stream and the
3
3
  * typed per-step parts the frontend renders. No DBOS, no per-run state — just the
4
- * label mapping, the file-tree delta extraction/fold, and the chat-data-part
5
- * narrowing the `sandbox-step` workflow body composes into its emitters.
4
+ * reconciliation-id construction, the label mapping, the file-tree delta
5
+ * extraction/fold, and the chat-data-part narrowing a workflow body composes
6
+ * into its emitters.
6
7
  */
8
+ /**
9
+ * Stable reconciliation id for a step's reconciling parts
10
+ * (`data-step-activity`, `data-step-file-tree`). One id per (runId, stepId)
11
+ * so the run-stream fold collapses every phase transition latest-wins onto
12
+ * a single frame. The client keys these by `stepId`, but the server fold
13
+ * keys by part `id`, so the id MUST be unique per step within the run.
14
+ *
15
+ * Every producer of these parts mints its ids here: the id is a
16
+ * reconciliation contract, and the fold collapses correctly only while all
17
+ * producers construct it identically.
18
+ */
19
+ export function stepPartId(kind, runId, stepId) {
20
+ return `${kind}-${runId}-${stepId}`;
21
+ }
7
22
  /** Friendly live-activity label per sandbox tool id. Falls back to the raw
8
23
  * tool name so a newly added tool still reads sensibly. */
9
24
  const TOOL_ACTIVITY = {
@@ -7,8 +7,8 @@
7
7
  import { z } from "zod";
8
8
  /**
9
9
  * A step of an analysis plan. `PlanStepSchema` (schemas/plan-schemas.ts) narrows
10
- * this into the planner's output contract and emits it as the `validate_plan` /
11
- * `submit_plan` arg schema — so each field's meaning belongs in `.describe()`,
10
+ * this into the planner's output contract and emits it as the `submit_plan`
11
+ * arg schema — so each field's meaning belongs in `.describe()`,
12
12
  * not in a comment the model never sees.
13
13
  */
14
14
  export declare const AnalysisStepSchema: z.ZodObject<{
@@ -8,8 +8,8 @@ import { z } from "zod";
8
8
  // ── Plan structures ─────────────────────────────────────────────────
9
9
  /**
10
10
  * A step of an analysis plan. `PlanStepSchema` (schemas/plan-schemas.ts) narrows
11
- * this into the planner's output contract and emits it as the `validate_plan` /
12
- * `submit_plan` arg schema — so each field's meaning belongs in `.describe()`,
11
+ * this into the planner's output contract and emits it as the `submit_plan`
12
+ * arg schema — so each field's meaning belongs in `.describe()`,
13
13
  * not in a comment the model never sees.
14
14
  */
15
15
  export const AnalysisStepSchema = z.object({
@@ -101,6 +101,13 @@ export interface DataProfileStatus {
101
101
  startedAt: string | null;
102
102
  completedAt: string | null;
103
103
  result: DataProfileResult | null;
104
+ /**
105
+ * The DBOS workflow id of the profile attempt that owns this row — the durable
106
+ * event stream a consumer subscribes to for this profile's activity. `null` means
107
+ * this profile's stream is not addressable (see
108
+ * {@link recordDataProfileWorkflowId}).
109
+ */
110
+ workflowId: string | null;
104
111
  seedInputFileIds: string[] | null;
105
112
  }
106
113
  /**
@@ -117,14 +124,59 @@ export interface DataProfileStatus {
117
124
  * unseeded — neither is an error, both stay in the ok channel.
118
125
  */
119
126
  export declare function tryStartDataProfile(pool: Querier, analysisId: string): ResultAsync<boolean, DbError>;
120
- /** Claim a `failed` row back into `running` (the deliberate-retry route). Carries {@link SEEDED}. */
127
+ /**
128
+ * Claim a `failed` row back into `running` (the deliberate-retry route). Carries {@link SEEDED}.
129
+ *
130
+ * Clears `data_profile_workflow_id`, because the claim IS the moment a new attempt takes the row
131
+ * and the prior attempt's workflow is finished. Left in place it would leave the row `running`
132
+ * while naming a stream that has already drained, so a consumer would subscribe and observe
133
+ * nothing without knowing why; NULL says "not addressable yet", which is exactly true until the
134
+ * new body records its own id (see {@link recordDataProfileWorkflowId}).
135
+ */
121
136
  export declare function tryRetryDataProfile(pool: Querier, analysisId: string): ResultAsync<boolean, DbError>;
122
137
  /**
123
138
  * Claim a `completed` row back into `running` (the re-profile route). `data_profile_result`
124
139
  * is deliberately preserved so a consumer can keep serving the prior profile while the new
125
140
  * one runs. Carries {@link SEEDED}.
141
+ *
142
+ * `data_profile_workflow_id` is NOT preserved alongside it, and the asymmetry is the point: the
143
+ * result is CONTENT that stays valid until replaced, while the id is a POINTER to a live stream
144
+ * whose workflow is now finished. Keeping it would leave the row `running` while naming a drained
145
+ * stream — see {@link tryRetryDataProfile} for the same reasoning.
126
146
  */
127
147
  export declare function tryRerunDataProfile(pool: Querier, analysisId: string): ResultAsync<boolean, DbError>;
148
+ /**
149
+ * Record the DBOS workflow id of the attempt that owns a claimed-`running` row, so a
150
+ * consumer resolves which durable event stream carries this profile's activity from the
151
+ * ledger row alone — no durability-engine query, no id reconstructed from the workflow-id
152
+ * string format. The workflow BODY calls this, never the trigger: the claim CAS runs
153
+ * before the workflow id is minted, so only the body can report the id of the attempt
154
+ * that actually started.
155
+ *
156
+ * `AND data_profile_status = 'running'` buys exactly one thing: a write that lands late
157
+ * cannot stamp a row that has already settled, which would otherwise point a consumer at
158
+ * a workflow for a profile that is already finished. What it does NOT buy is
159
+ * disambiguation between two attempts that each believe they are the running one — the
160
+ * stale-expiry claim admits precisely that, since a row whose `data_profile_started_at`
161
+ * has aged past the timeout can be claimed by a second attempt while the first body is
162
+ * still alive but has not completed its first step, and that first step then overwrites
163
+ * the second's id.
164
+ *
165
+ * That residue is deliberately left open rather than closed. Its worst outcome is already
166
+ * a specified-normal state: a consumer subscribed to a superseded workflow reads a stream
167
+ * that has already drained, so it observes no activity — indistinguishable from "running,
168
+ * nothing reported yet". A missing activity line for one profile is acceptable; a wrong
169
+ * one would not be. And closing it costs more than it buys: either the three claim
170
+ * functions widen their `Result<boolean>` returns to carry the id — their callers branch
171
+ * on truthiness (`if (!retried)`), so an object return makes every such branch
172
+ * always-taken, a silent behaviour change in the embedder's recovery path — or the
173
+ * embedder mints the workflow ids, putting a harness-internal construction in a
174
+ * consumer's hands.
175
+ *
176
+ * `ok(true)` when this call stamped the row; `ok(false)` when the CAS refused it — a
177
+ * normal in-band outcome, not an error.
178
+ */
179
+ export declare function recordDataProfileWorkflowId(pool: Querier, analysisId: string, workflowId: string): ResultAsync<boolean, DbError>;
128
180
  /**
129
181
  * Stamp a claimed-`running` row `completed` and record its result — a terminal
130
182
  * write CAS'd on `data_profile_status = 'running'`.
@@ -38,21 +38,31 @@ export function tryStartDataProfile(pool, analysisId) {
38
38
  return tryMutation("dataProfile.tryStartDataProfile", async () => {
39
39
  const result = await pool.query({
40
40
  text: `UPDATE cortex_analysis_state
41
- SET data_profile_status = 'running', data_profile_started_at = $1
41
+ SET data_profile_status = 'running', data_profile_started_at = $1,
42
+ data_profile_workflow_id = NULL
42
43
  WHERE analysis_id = $2 AND (data_profile_status = 'pending' OR data_profile_status IS NULL) AND ${SEEDED}`,
43
44
  values: [now, analysisId],
44
45
  });
45
46
  return (result.rowCount ?? 0) > 0;
46
47
  });
47
48
  }
48
- /** Claim a `failed` row back into `running` (the deliberate-retry route). Carries {@link SEEDED}. */
49
+ /**
50
+ * Claim a `failed` row back into `running` (the deliberate-retry route). Carries {@link SEEDED}.
51
+ *
52
+ * Clears `data_profile_workflow_id`, because the claim IS the moment a new attempt takes the row
53
+ * and the prior attempt's workflow is finished. Left in place it would leave the row `running`
54
+ * while naming a stream that has already drained, so a consumer would subscribe and observe
55
+ * nothing without knowing why; NULL says "not addressable yet", which is exactly true until the
56
+ * new body records its own id (see {@link recordDataProfileWorkflowId}).
57
+ */
49
58
  export function tryRetryDataProfile(pool, analysisId) {
50
59
  const now = new Date().toISOString();
51
60
  return tryMutation("dataProfile.tryRetryDataProfile", async () => {
52
61
  const result = await pool.query({
53
62
  text: `UPDATE cortex_analysis_state
54
63
  SET data_profile_status = 'running', data_profile_started_at = $1,
55
- data_profile_error = NULL, data_profile_completed_at = NULL
64
+ data_profile_error = NULL, data_profile_completed_at = NULL,
65
+ data_profile_workflow_id = NULL
56
66
  WHERE analysis_id = $2 AND data_profile_status = 'failed' AND ${SEEDED}`,
57
67
  values: [now, analysisId],
58
68
  });
@@ -63,6 +73,11 @@ export function tryRetryDataProfile(pool, analysisId) {
63
73
  * Claim a `completed` row back into `running` (the re-profile route). `data_profile_result`
64
74
  * is deliberately preserved so a consumer can keep serving the prior profile while the new
65
75
  * one runs. Carries {@link SEEDED}.
76
+ *
77
+ * `data_profile_workflow_id` is NOT preserved alongside it, and the asymmetry is the point: the
78
+ * result is CONTENT that stays valid until replaced, while the id is a POINTER to a live stream
79
+ * whose workflow is now finished. Keeping it would leave the row `running` while naming a drained
80
+ * stream — see {@link tryRetryDataProfile} for the same reasoning.
66
81
  */
67
82
  export function tryRerunDataProfile(pool, analysisId) {
68
83
  const now = new Date().toISOString();
@@ -70,13 +85,56 @@ export function tryRerunDataProfile(pool, analysisId) {
70
85
  const result = await pool.query({
71
86
  text: `UPDATE cortex_analysis_state
72
87
  SET data_profile_status = 'running', data_profile_started_at = $1,
73
- data_profile_error = NULL, data_profile_completed_at = NULL
88
+ data_profile_error = NULL, data_profile_completed_at = NULL,
89
+ data_profile_workflow_id = NULL
74
90
  WHERE analysis_id = $2 AND data_profile_status = 'completed' AND ${SEEDED}`,
75
91
  values: [now, analysisId],
76
92
  });
77
93
  return (result.rowCount ?? 0) > 0;
78
94
  });
79
95
  }
96
+ /**
97
+ * Record the DBOS workflow id of the attempt that owns a claimed-`running` row, so a
98
+ * consumer resolves which durable event stream carries this profile's activity from the
99
+ * ledger row alone — no durability-engine query, no id reconstructed from the workflow-id
100
+ * string format. The workflow BODY calls this, never the trigger: the claim CAS runs
101
+ * before the workflow id is minted, so only the body can report the id of the attempt
102
+ * that actually started.
103
+ *
104
+ * `AND data_profile_status = 'running'` buys exactly one thing: a write that lands late
105
+ * cannot stamp a row that has already settled, which would otherwise point a consumer at
106
+ * a workflow for a profile that is already finished. What it does NOT buy is
107
+ * disambiguation between two attempts that each believe they are the running one — the
108
+ * stale-expiry claim admits precisely that, since a row whose `data_profile_started_at`
109
+ * has aged past the timeout can be claimed by a second attempt while the first body is
110
+ * still alive but has not completed its first step, and that first step then overwrites
111
+ * the second's id.
112
+ *
113
+ * That residue is deliberately left open rather than closed. Its worst outcome is already
114
+ * a specified-normal state: a consumer subscribed to a superseded workflow reads a stream
115
+ * that has already drained, so it observes no activity — indistinguishable from "running,
116
+ * nothing reported yet". A missing activity line for one profile is acceptable; a wrong
117
+ * one would not be. And closing it costs more than it buys: either the three claim
118
+ * functions widen their `Result<boolean>` returns to carry the id — their callers branch
119
+ * on truthiness (`if (!retried)`), so an object return makes every such branch
120
+ * always-taken, a silent behaviour change in the embedder's recovery path — or the
121
+ * embedder mints the workflow ids, putting a harness-internal construction in a
122
+ * consumer's hands.
123
+ *
124
+ * `ok(true)` when this call stamped the row; `ok(false)` when the CAS refused it — a
125
+ * normal in-band outcome, not an error.
126
+ */
127
+ export function recordDataProfileWorkflowId(pool, analysisId, workflowId) {
128
+ return tryMutation("dataProfile.recordDataProfileWorkflowId", async () => {
129
+ const result = await pool.query({
130
+ text: `UPDATE cortex_analysis_state
131
+ SET data_profile_workflow_id = $1
132
+ WHERE analysis_id = $2 AND data_profile_status = 'running'`,
133
+ values: [workflowId, analysisId],
134
+ });
135
+ return (result.rowCount ?? 0) > 0;
136
+ });
137
+ }
80
138
  /**
81
139
  * Stamp a claimed-`running` row `completed` and record its result — a terminal
82
140
  * write CAS'd on `data_profile_status = 'running'`.
@@ -213,7 +271,8 @@ export function clearDataProfile(pool, analysisId) {
213
271
  text: `UPDATE cortex_analysis_state
214
272
  SET data_profile_status = NULL, data_profile_error = NULL,
215
273
  data_profile_started_at = NULL, data_profile_completed_at = NULL,
216
- data_profile_result = NULL, seed_input_file_ids = NULL
274
+ data_profile_result = NULL, data_profile_workflow_id = NULL,
275
+ seed_input_file_ids = NULL
217
276
  WHERE analysis_id = $1 AND data_profile_status IS DISTINCT FROM 'running'`,
218
277
  values: [analysisId],
219
278
  });
@@ -242,7 +301,7 @@ export function loadDataProfileStatus(pool, analysisId) {
242
301
  const result = await pool.query({
243
302
  text: `SELECT data_profile_status, data_profile_error,
244
303
  data_profile_started_at, data_profile_completed_at,
245
- data_profile_result, seed_input_file_ids
304
+ data_profile_result, data_profile_workflow_id, seed_input_file_ids
246
305
  FROM cortex_analysis_state WHERE analysis_id = $1`,
247
306
  values: [analysisId],
248
307
  });
@@ -255,6 +314,7 @@ export function loadDataProfileStatus(pool, analysisId) {
255
314
  startedAt: row.data_profile_started_at ?? null,
256
315
  completedAt: row.data_profile_completed_at ?? null,
257
316
  result: row.data_profile_result ?? null,
317
+ workflowId: row.data_profile_workflow_id ?? null,
258
318
  seedInputFileIds: row.seed_input_file_ids ?? null,
259
319
  };
260
320
  });
@@ -12,15 +12,15 @@ export { upsertAnalysis, resolveAnalysisBilling, suspendAnalysis, resumeAnalysis
12
12
  export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifactPaths, queryUnsyncedStepArtifacts, updateArtifactId, updateFileIds, countArtifactsForRun, } from "./artifacts.js";
13
13
  export type { RegisterArtifactInput, InputArtifactMeta, StepArtifactRef } from "./artifacts.js";
14
14
  export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
15
- setRunSynthesisOutcome, queryRun, queryActiveRun, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
16
- export type { InsertRunInput, RunReservation } from "./runs.js";
15
+ setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
16
+ export type { InsertRunInput, RunPage, RunReservation } from "./runs.js";
17
17
  export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun } from "./step-executions.js";
18
18
  export type { InsertStepExecutionInput, SeedStepExecutionRow, UpdateStepExecutionInput } from "./step-executions.js";
19
19
  export { setSandboxRef, setActiveExecId, clearSandboxRef, queryActiveSandboxes, reconcileReapedSandbox } from "./active-sandboxes.js";
20
20
  export type { ActiveSandboxRow } from "./active-sandboxes.js";
21
21
  export { insertPlan, upsertPlan, loadPlan } from "./plans.js";
22
22
  export type { InsertPlanInput, UpsertPlanInput } from "./plans.js";
23
- export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, } from "./data-profile.js";
23
+ export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
24
24
  export type { DataProfileStatus, DataProfileResult, DataProfileInputFile, DataProfileFile, DataProfileQualityAssessment } from "./data-profile.js";
25
25
  export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses } from "./analyses-metrics.js";
26
26
  export { AnalysisStateRowSchema, ArtifactRole, ArtifactRowSchema, RunStatus, StepExecutionStatus, SynthesisStatus, CortexRunRowSchema, StepExecutionRowSchema, CortexPlanRowSchema, } from "./schema.js";
@@ -10,11 +10,11 @@ export { initCortexState } from "./init.js";
10
10
  export { upsertAnalysis, resolveAnalysisBilling, suspendAnalysis, resumeAnalysis, loadAnalysisStatus } from "./analyses.js";
11
11
  export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifactPaths, queryUnsyncedStepArtifacts, updateArtifactId, updateFileIds, countArtifactsForRun, } from "./artifacts.js";
12
12
  export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
13
- setRunSynthesisOutcome, queryRun, queryActiveRun, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
13
+ setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
14
14
  export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun } from "./step-executions.js";
15
15
  export { setSandboxRef, setActiveExecId, clearSandboxRef, queryActiveSandboxes, reconcileReapedSandbox } from "./active-sandboxes.js";
16
16
  export { insertPlan, upsertPlan, loadPlan } from "./plans.js";
17
- export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, } from "./data-profile.js";
17
+ export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
18
18
  export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses } from "./analyses-metrics.js";
19
19
  export { AnalysisStateRowSchema, ArtifactRole, ArtifactRowSchema, RunStatus, StepExecutionStatus, SynthesisStatus, CortexRunRowSchema, StepExecutionRowSchema, CortexPlanRowSchema, } from "./schema.js";
20
20
  export { insertAssessment, updateProgress, setDossier, markFailed, getAssessment, listAssessmentsByOrg, softDeleteAssessment, TargetAssessmentStatusSchema, TargetAssessmentErrorSchema, TargetAssessmentRowSchema, } from "./target-assessments.js";
@@ -27,10 +27,22 @@ CREATE TABLE IF NOT EXISTS cortex_analysis_state (
27
27
  data_profile_started_at TEXT,
28
28
  data_profile_completed_at TEXT,
29
29
  data_profile_result JSONB,
30
+ data_profile_workflow_id TEXT,
30
31
  created_at TEXT NOT NULL,
31
32
  updated_at TEXT NOT NULL
32
33
  );
33
34
 
35
+ -- DBOS workflow id of the profile attempt that owns the row — what a consumer
36
+ -- subscribes to in order to observe that profile's activity. Written by the
37
+ -- workflow body, not the trigger: the ledger claim happens before the workflow id
38
+ -- is minted, so only the body knows the id of the attempt that actually started.
39
+ -- Nullable, and absence is a normal state rather than a defect, with two ordinary
40
+ -- causes: a row claimed whose body has not yet recorded its id, and a row written
41
+ -- before this column existed. Both mean "this profile's stream is not
42
+ -- addressable", which is true in each case — hence no backfill.
43
+ ALTER TABLE cortex_analysis_state
44
+ ADD COLUMN IF NOT EXISTS data_profile_workflow_id TEXT;
45
+
34
46
  -- File registry — artifact_id (provenance) + file_id (S3), cross-run.
35
47
  CREATE TABLE IF NOT EXISTS cortex_artifacts (
36
48
  analysis_id TEXT NOT NULL,
@@ -207,12 +219,20 @@ CREATE INDEX IF NOT EXISTS idx_cortex_regulatory_chunks_metadata
207
219
  -- workflow and sandbox agent loops use the DBOS step cache, never this
208
220
  -- table (see the harness-thread-store spec). The (thread_id, seq) primary key already serves
209
221
  -- (thread_id, seq DESC) reads, so no separate index is needed.
222
+ --
223
+ -- message_envelope is JSON, not JSONB, and must stay that way. JSONB stores a
224
+ -- re-sorted parse tree rather than the document it was given, so a tool call's
225
+ -- input — and every other free-form payload, which has no schema to be
226
+ -- normalized back against on read — comes back key-reordered. Those bytes are
227
+ -- re-sent to the provider on the next turn, where a reordered prefix misses the
228
+ -- prompt cache. JSON stores the text verbatim. Nothing indexes this column, and
229
+ -- the JSON operators retractLastTurn uses work the same on either type.
210
230
  CREATE TABLE IF NOT EXISTS messages (
211
231
  thread_id TEXT NOT NULL,
212
232
  seq BIGINT NOT NULL,
213
233
  role TEXT,
214
234
  content_jsonb JSONB,
215
- message_envelope JSONB,
235
+ message_envelope JSON,
216
236
  tokens INTEGER NOT NULL,
217
237
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
218
238
  PRIMARY KEY (thread_id, seq)
@@ -411,7 +431,22 @@ export async function initCortexState(pool, injected) {
411
431
  // carry the NOT NULL floor, which would reject a clear. Idempotent:
412
432
  // dropping an absent NOT NULL no-ops, so this is a no-op on fresh DBs.
413
433
  "ALTER TABLE cortex_analysis_state ALTER COLUMN data_profile_status DROP NOT NULL",
414
- "ALTER TABLE messages ADD COLUMN IF NOT EXISTS message_envelope JSONB",
434
+ "ALTER TABLE messages ADD COLUMN IF NOT EXISTS message_envelope JSON",
435
+ // Demote an existing JSONB envelope column to JSON (see the CREATE
436
+ // TABLE comment). Type-guarded because ALTER ... TYPE rewrites the
437
+ // whole table and this runs at every boot; scoped to
438
+ // current_schema() so parallel test schemas never see each other.
439
+ // Rows already written as JSONB carry over as they stand — their
440
+ // original key order is not recoverable.
441
+ `DO $$ BEGIN
442
+ IF EXISTS (
443
+ SELECT 1 FROM information_schema.columns
444
+ WHERE table_schema = current_schema() AND table_name = 'messages'
445
+ AND column_name = 'message_envelope' AND data_type = 'jsonb'
446
+ ) THEN
447
+ ALTER TABLE messages ALTER COLUMN message_envelope TYPE JSON USING message_envelope::text::json;
448
+ END IF;
449
+ END $$`,
415
450
  // Grant key an 'always' records the standing grant under. Nullable
416
451
  // and unbackfilled: every new row writes it (command when absent), and
417
452
  // orphaned legacy pending rows are swept to 'expired' before any answer
@@ -26,6 +26,11 @@ export interface RunReservation {
26
26
  readonly row: CortexRunRow;
27
27
  readonly inserted: boolean;
28
28
  }
29
+ /** Bounded run rows paired with the true count before limit/offset. */
30
+ export interface RunPage {
31
+ readonly runs: CortexRunRow[];
32
+ readonly total: number;
33
+ }
29
34
  export declare class RunIdentityCollisionError extends Error {
30
35
  readonly runId: string;
31
36
  constructor(runId: string);
@@ -59,6 +64,24 @@ export declare function reserveRunById(pool: Querier, input: InsertRunInput): Re
59
64
  * most one match.
60
65
  */
61
66
  export declare function queryActiveRun(pool: Querier, analysisId: string, planId: string | null): ResultAsync<CortexRunRow | null, DbError>;
67
+ /**
68
+ * Every NON-TERMINAL run of an analysis, newest-first and deliberately unbounded.
69
+ *
70
+ * The counterpart to {@link queryRunsByAnalysis}, which answers "the newest N runs" — a windowed
71
+ * question, and the wrong one for a host driving live surfaces. Ordering by `started_at DESC` and
72
+ * capping means the run most worth watching is the first to fall out: a long analysis still going
73
+ * while ten short ones start and finish after it leaves the window entirely, so a host reading only
74
+ * that window loses its progress AND never learns it terminated.
75
+ *
76
+ * Unbounded is safe here in a way it is not for the general listing: this result is capped by the
77
+ * analysis's live concurrency, not by its history, and a run leaves the set permanently when it
78
+ * finishes.
79
+ *
80
+ * The status set is the one {@link queryActiveRun} uses — non-terminal means still running, or
81
+ * suspended awaiting funds. Keep the two in step: a new non-terminal status added to one and not the
82
+ * other makes a live run invisible to exactly one of its readers.
83
+ */
84
+ export declare function queryActiveRunsByAnalysis(pool: Querier, analysisId: string): ResultAsync<CortexRunRow[], DbError>;
62
85
  export declare function updateRunStatus(pool: Querier, runId: string, status: "running" | "completed" | "partial" | "failed" | "canceled" | "suspended_insufficient_funds", error?: string | null): ResultAsync<void, DbError>;
63
86
  export declare function promoteFailedToPartial(pool: Querier, runId: string): ResultAsync<boolean, DbError>;
64
87
  export declare function queryRun(pool: Querier, runId: string): ResultAsync<CortexRunRow | null, DbError>;
@@ -66,6 +89,22 @@ export declare function queryRunsByAnalysis(pool: Querier, analysisId: string, {
66
89
  limit?: number;
67
90
  offset?: number;
68
91
  }): ResultAsync<CortexRunRow[], DbError>;
92
+ /**
93
+ * Read the analysis's non-terminal run activity without coupling it to a
94
+ * conversation thread. The total is counted before the detail limit so a
95
+ * bounded prompt projection can disclose omitted rows.
96
+ */
97
+ export declare function queryNonTerminalRunsByAnalysis(pool: Querier, analysisId: string, limit?: number): ResultAsync<RunPage, DbError>;
98
+ /**
99
+ * Read one stable inspection page: running first, then suspended, then
100
+ * terminal history. This is deliberately separate from
101
+ * {@link queryRunsByAnalysis}, whose chronological order is an embedder/UI
102
+ * contract.
103
+ */
104
+ export declare function queryRunsForInspection(pool: Querier, analysisId: string, { limit, offset }?: {
105
+ limit?: number;
106
+ offset?: number;
107
+ }): ResultAsync<RunPage, DbError>;
69
108
  export declare function queryRunsByThread(pool: Querier, analysisId: string, threadId: string, { limit, offset }?: {
70
109
  limit?: number;
71
110
  offset?: number;