@inflexa-ai/harness 0.44.0 → 0.45.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 (99) hide show
  1. package/dist/app/approve-plan.d.ts +48 -0
  2. package/dist/app/approve-plan.js +84 -0
  3. package/dist/app/data-profile-view.d.ts +1 -1
  4. package/dist/app/run-activity.js +4 -6
  5. package/dist/contracts/chat-events.d.ts +2 -0
  6. package/dist/contracts/chat-frame.d.ts +7 -0
  7. package/dist/contracts/chat-frame.js +18 -4
  8. package/dist/contracts/chat-parts.d.ts +73 -2
  9. package/dist/contracts/chat-parts.js +3 -1
  10. package/dist/contracts/index.d.ts +3 -1
  11. package/dist/contracts/index.js +1 -0
  12. package/dist/contracts/part-registry.js +3 -0
  13. package/dist/contracts/schemas/chat-events.d.ts +13 -0
  14. package/dist/contracts/schemas/chat-events.js +1 -0
  15. package/dist/contracts/schemas/chat-parts.d.ts +200 -15
  16. package/dist/contracts/schemas/chat-parts.js +63 -12
  17. package/dist/contracts/schemas/index.d.ts +1 -1
  18. package/dist/contracts/schemas/index.js +1 -1
  19. package/dist/contracts/subagent-activity.d.ts +12 -0
  20. package/dist/contracts/subagent-activity.js +76 -0
  21. package/dist/execution/artifact-metadata.js +1 -3
  22. package/dist/execution/run-canceler.js +1 -1
  23. package/dist/execution/run-event-parts.d.ts +6 -5
  24. package/dist/execution/run-event-parts.js +6 -5
  25. package/dist/execution/run-event-stream.d.ts +18 -6
  26. package/dist/execution/run-event-stream.js +67 -17
  27. package/dist/execution/run-synthesis.d.ts +21 -0
  28. package/dist/execution/run-synthesis.js +77 -3
  29. package/dist/index.d.ts +22 -8
  30. package/dist/index.js +16 -2
  31. package/dist/lib/hooks.d.ts +1 -1
  32. package/dist/lib/metrics.d.ts +2 -2
  33. package/dist/loop/run-agent.js +1 -0
  34. package/dist/loop/types.d.ts +2 -0
  35. package/dist/memory/card-builders.d.ts +11 -0
  36. package/dist/memory/card-builders.js +35 -21
  37. package/dist/memory/conversation-display-recorder.d.ts +2 -0
  38. package/dist/memory/conversation-display-recorder.js +10 -1
  39. package/dist/memory/conversation-display-storage.d.ts +9 -2
  40. package/dist/memory/conversation-display-storage.js +18 -3
  41. package/dist/memory/thread-history.d.ts +8 -1
  42. package/dist/memory/thread-history.js +13 -3
  43. package/dist/memory/thread-store.d.ts +13 -0
  44. package/dist/memory/thread-store.js +15 -1
  45. package/dist/prompts/conversation.d.ts +1 -1
  46. package/dist/prompts/conversation.js +3 -1
  47. package/dist/provenance/seam.d.ts +16 -5
  48. package/dist/providers/ai-sdk.js +6 -4
  49. package/dist/report-model/pin-snapshot.d.ts +1 -1
  50. package/dist/report-model/pin-snapshot.js +2 -50
  51. package/dist/runtime/assemble.d.ts +8 -1
  52. package/dist/runtime/assemble.js +11 -0
  53. package/dist/runtime/boot.d.ts +3 -0
  54. package/dist/runtime/boot.js +10 -1
  55. package/dist/sandbox/sandbox-step-translate.d.ts +23 -0
  56. package/dist/sandbox/sandbox-step-translate.js +24 -0
  57. package/dist/schemas/artifact-manifest.d.ts +1 -1
  58. package/dist/schemas/artifact-manifest.js +11 -9
  59. package/dist/schemas/plan-schemas.d.ts +18 -18
  60. package/dist/schemas/run-synthesis.d.ts +2 -2
  61. package/dist/state/analyses-metrics.d.ts +79 -7
  62. package/dist/state/analyses-metrics.js +141 -6
  63. package/dist/state/artifacts.d.ts +50 -3
  64. package/dist/state/artifacts.js +67 -7
  65. package/dist/state/data-profile.d.ts +4 -2
  66. package/dist/state/data-profile.js +7 -5
  67. package/dist/state/index.d.ts +9 -8
  68. package/dist/state/index.js +5 -5
  69. package/dist/state/migrations/20261012120000_index_the_runs_by_start.d.ts +8 -0
  70. package/dist/state/migrations/20261012120000_index_the_runs_by_start.js +12 -0
  71. package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.d.ts +8 -0
  72. package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.js +13 -0
  73. package/dist/state/migrations/20261013180000_narrow_the_active_run_index.d.ts +8 -0
  74. package/dist/state/migrations/20261013180000_narrow_the_active_run_index.js +13 -0
  75. package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.d.ts +8 -0
  76. package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js +13 -0
  77. package/dist/state/migrations/index.js +8 -0
  78. package/dist/state/plans.d.ts +21 -0
  79. package/dist/state/plans.js +25 -0
  80. package/dist/state/runs.d.ts +36 -5
  81. package/dist/state/runs.js +65 -19
  82. package/dist/state/schema.d.ts +2 -0
  83. package/dist/tasks/data-profile-activity.d.ts +7 -1
  84. package/dist/tasks/data-profile-activity.js +18 -9
  85. package/dist/tasks/data-profile.d.ts +15 -1
  86. package/dist/tasks/data-profile.js +16 -11
  87. package/dist/tools/approval/gateway.js +28 -5
  88. package/dist/tools/display/show-user.d.ts +1 -1
  89. package/dist/tools/execute-analysis.d.ts +51 -0
  90. package/dist/tools/execute-analysis.js +241 -109
  91. package/dist/tools/research/inspect-data-profile.d.ts +1 -1
  92. package/dist/tools/research/inspect-run.d.ts +1 -1
  93. package/dist/tools/research/inspect-run.js +7 -7
  94. package/dist/workflows/execute-analysis.d.ts +4 -3
  95. package/dist/workflows/execute-analysis.js +35 -22
  96. package/dist/workflows/plan/graph.d.ts +16 -0
  97. package/dist/workflows/plan/graph.js +36 -0
  98. package/dist/workflows/sandbox-step.js +14 -12
  99. package/package.json +1 -1
@@ -144,7 +144,9 @@ export function recordDataProfileWorkflowId(pool, analysisId, workflowId) {
144
144
  }
145
145
  function terminalWriteOf(rows) {
146
146
  const row = rows[0];
147
- return row === undefined ? { stamped: false } : { stamped: true, startedAt: row.data_profile_started_at ?? null };
147
+ return row === undefined
148
+ ? { stamped: false }
149
+ : { stamped: true, startedAt: row.data_profile_started_at ?? null, completedAt: row.data_profile_completed_at };
148
150
  }
149
151
  /**
150
152
  * Stamp a claimed-`running` row `completed` and record its result — a terminal
@@ -162,7 +164,7 @@ function terminalWriteOf(rows) {
162
164
  * guard only ever refuses a row another writer already moved on from.
163
165
  *
164
166
  * `stamped: true` when this call stamped the row, with the `data_profile_started_at`
165
- * of the claim it closed; `stamped: false` when the guard refused it (the row was
167
+ * of the claim it closed and the `data_profile_completed_at` it stamped; `stamped: false` when the guard refused it (the row was
166
168
  * cleared/expired/replayed away) — a no-op the caller logs, not an error.
167
169
  */
168
170
  export function completeDataProfile(pool, analysisId, result) {
@@ -173,7 +175,7 @@ export function completeDataProfile(pool, analysisId, result) {
173
175
  SET data_profile_status = 'completed', data_profile_completed_at = $1,
174
176
  data_profile_result = $2::jsonb
175
177
  WHERE analysis_id = $3 AND data_profile_status = 'running'
176
- RETURNING data_profile_started_at`,
178
+ RETURNING data_profile_started_at, data_profile_completed_at`,
177
179
  values: [now, result ? JSON.stringify(result) : null, analysisId],
178
180
  });
179
181
  return terminalWriteOf(res.rows);
@@ -188,7 +190,7 @@ export function completeDataProfile(pool, analysisId, result) {
188
190
  * prior profile survives the failure (the re-profile/retry route can keep serving it).
189
191
  *
190
192
  * `stamped: true` when this call stamped the row, with the `data_profile_started_at`
191
- * of the claim it closed; `stamped: false` when the guard refused it — a logged
193
+ * of the claim it closed and the `data_profile_completed_at` it stamped; `stamped: false` when the guard refused it — a logged
192
194
  * no-op, not an error.
193
195
  */
194
196
  export function failDataProfile(pool, analysisId, error) {
@@ -199,7 +201,7 @@ export function failDataProfile(pool, analysisId, error) {
199
201
  SET data_profile_status = 'failed', data_profile_error = $1,
200
202
  data_profile_completed_at = $2
201
203
  WHERE analysis_id = $3 AND data_profile_status = 'running'
202
- RETURNING data_profile_started_at`,
204
+ RETURNING data_profile_started_at, data_profile_completed_at`,
203
205
  values: [error, now, analysisId],
204
206
  });
205
207
  return terminalWriteOf(res.rows);
@@ -9,20 +9,21 @@
9
9
  export { initCortexState } from "./init.js";
10
10
  export type { Querier } from "./db.js";
11
11
  export { upsertAnalysis, suspendAnalysis, resumeAnalysis, loadAnalysisStatus } from "./analyses.js";
12
- export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifactPaths, queryUnsyncedStepArtifacts, updateArtifactId, updateFileIds, countArtifactsForRun, } from "./artifacts.js";
13
- export type { RegisterArtifactInput, InputArtifactMeta, StepArtifactRef } from "./artifacts.js";
14
- export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, markRunCanceledIfActive, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
15
- setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
12
+ export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifactPaths, queryUnsyncedStepArtifacts, updateArtifactId, updateFileIds, queryArtifactPage, countArtifactsByRuns, } from "./artifacts.js";
13
+ export type { RegisterArtifactInput, InputArtifactMeta, StepArtifactRef, ArtifactPage, ArtifactPageEntry, ArtifactPageQuery, ArtifactPageSort, RunArtifactCount, } from "./artifacts.js";
14
+ export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, reconcileOrphanedRuns, ORPHANED_RUN_GRACE_MS, markRunCanceledIfActive, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
15
+ setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, queryRunsByThread, queryRunIdsByPlan, } from "./runs.js";
16
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, clearSandboxRef, reconcileReapedSandbox } from "./active-sandboxes.js";
20
- export { insertPlan, upsertPlan, loadPlan } from "./plans.js";
21
- export type { InsertPlanInput, UpsertPlanInput } from "./plans.js";
20
+ export { insertPlan, upsertPlan, loadPlan, loadPlanRecord } from "./plans.js";
21
+ export type { InsertPlanInput, UpsertPlanInput, PlanRecord } from "./plans.js";
22
22
  export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
23
23
  export type { DataProfileStatus, DataProfileTerminalWrite, DataProfileResult, DataProfileChecked, DataProfileCompanionCompleteness, DataProfileDimension, DataProfileGroup, DataProfileGroupSlot, DataProfileFile, DataProfileMemberAnnotation, DataProfileObservation, DataProfilePartition, DataProfileProbeReport, DataProfileQualityAssessment, DataProfileQuarantine, DataProfileRecipeStep, DataProfileKind, DataProfileAxis, DataProfileInputSignature, DataProfileCoverage, } from "./data-profile.js";
24
- export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses } from "./analyses-metrics.js";
24
+ export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses, queryLatestRunsByAnalyses, queryRunProgressByRuns, queryRunsEndedSince, } from "./analyses-metrics.js";
25
+ export type { DataProfileSummary, EndedRunSummary, LatestRunSummary, RunProgressSummary } from "./analyses-metrics.js";
25
26
  export { createAnalysisPurge } from "./purge-analysis.js";
26
27
  export type { AnalysisPurge, AnalysisPurgeDeps, AnalysisPurgeOutcome } from "./purge-analysis.js";
27
28
  export { AnalysisStateRowSchema, ArtifactRole, ArtifactRowSchema, RunStatus, StepExecutionStatus, SynthesisStatus, CortexRunRowSchema, StepExecutionRowSchema, CortexPlanRowSchema, } from "./schema.js";
28
- export type { AnalysisStateRow, ArtifactRow, CortexRunRow, StepExecutionRow, CortexPlanRow } from "./schema.js";
29
+ export type { AnalysisStateRow, ArtifactRow, CortexRunRow, StepExecutionRow, CortexPlanRow, TerminalRunStatus } from "./schema.js";
@@ -8,14 +8,14 @@
8
8
  */
9
9
  export { initCortexState } from "./init.js";
10
10
  export { upsertAnalysis, suspendAnalysis, resumeAnalysis, loadAnalysisStatus } from "./analyses.js";
11
- export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifactPaths, queryUnsyncedStepArtifacts, updateArtifactId, updateFileIds, countArtifactsForRun, } from "./artifacts.js";
12
- export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, markRunCanceledIfActive, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
13
- setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
11
+ export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifactPaths, queryUnsyncedStepArtifacts, updateArtifactId, updateFileIds, queryArtifactPage, countArtifactsByRuns, } from "./artifacts.js";
12
+ export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, reconcileOrphanedRuns, ORPHANED_RUN_GRACE_MS, markRunCanceledIfActive, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
13
+ setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, queryRunsByThread, queryRunIdsByPlan, } from "./runs.js";
14
14
  export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun } from "./step-executions.js";
15
15
  export { setSandboxRef, clearSandboxRef, reconcileReapedSandbox } from "./active-sandboxes.js";
16
- export { insertPlan, upsertPlan, loadPlan } from "./plans.js";
16
+ export { insertPlan, upsertPlan, loadPlan, loadPlanRecord } from "./plans.js";
17
17
  export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
18
- export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses } from "./analyses-metrics.js";
18
+ export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses, queryLatestRunsByAnalyses, queryRunProgressByRuns, queryRunsEndedSince, } from "./analyses-metrics.js";
19
19
  // Not per-entity: one analysis's whole Postgres footprint, reached from its id
20
20
  // alone, with the workflow ledger behind an injected seam.
21
21
  export { createAnalysisPurge } from "./purge-analysis.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Index the runs of an analysis by their start time, thus the read of the
3
+ * newest run of each analysis of a list reads one index entry for each
4
+ * analysis, and its cost does not grow with the run history. The index on
5
+ * `analysis_id` alone stays: a drop is a separate decision.
6
+ */
7
+ import type { Migration } from "kysely/migration";
8
+ export declare const indexTheRunsByStart: Migration;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Index the runs of an analysis by their start time, thus the read of the
3
+ * newest run of each analysis of a list reads one index entry for each
4
+ * analysis, and its cost does not grow with the run history. The index on
5
+ * `analysis_id` alone stays: a drop is a separate decision.
6
+ */
7
+ import { sql } from "kysely";
8
+ export const indexTheRunsByStart = {
9
+ async up(db) {
10
+ await sql `CREATE INDEX IF NOT EXISTS idx_cortex_runs_analysis_started ON cortex_runs(analysis_id, started_at DESC, run_id DESC)`.execute(db);
11
+ },
12
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Index the runs by their completion time and the live threads by their update
3
+ * time, thus the reads over each analysis walk an index and do not sort the
4
+ * whole history. The thread index is partial on the live rows, the predicate of
5
+ * the read.
6
+ */
7
+ import type { Migration } from "kysely/migration";
8
+ export declare const indexTheEndedRunsAndTheRecentThreads: Migration;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Index the runs by their completion time and the live threads by their update
3
+ * time, thus the reads over each analysis walk an index and do not sort the
4
+ * whole history. The thread index is partial on the live rows, the predicate of
5
+ * the read.
6
+ */
7
+ import { sql } from "kysely";
8
+ export const indexTheEndedRunsAndTheRecentThreads = {
9
+ async up(db) {
10
+ await sql `CREATE INDEX IF NOT EXISTS idx_cortex_runs_completed ON cortex_runs(completed_at DESC, run_id DESC)`.execute(db);
11
+ await sql `CREATE INDEX IF NOT EXISTS idx_cortex_analysis_threads_recent ON cortex_analysis_threads(updated_at DESC, thread_id) WHERE deleted_at IS NULL`.execute(db);
12
+ },
13
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Narrow the one-active-run index to the `running` status. A run that a host
3
+ * suspended is terminal, thus it no longer blocks a new run of its plan. A drop
4
+ * and a new index replace the index, because Postgres cannot change the
5
+ * predicate of an index.
6
+ */
7
+ import type { Migration } from "kysely/migration";
8
+ export declare const narrowTheActiveRunIndex: Migration;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Narrow the one-active-run index to the `running` status. A run that a host
3
+ * suspended is terminal, thus it no longer blocks a new run of its plan. A drop
4
+ * and a new index replace the index, because Postgres cannot change the
5
+ * predicate of an index.
6
+ */
7
+ import { sql } from "kysely";
8
+ export const narrowTheActiveRunIndex = {
9
+ async up(db) {
10
+ await sql `DROP INDEX IF EXISTS idx_cortex_runs_active_plan`.execute(db);
11
+ await sql `CREATE UNIQUE INDEX idx_cortex_runs_active_plan ON cortex_runs(analysis_id, plan_id) WHERE status = 'running'`.execute(db);
12
+ },
13
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Index the plans by their parent, thus the read of the plan that replaced a
3
+ * plan, and the key check when a plan row goes away, read one index entry.
4
+ * Index the runs by their plan, thus the read of the run ids of a plan reads
5
+ * only the runs of that plan.
6
+ */
7
+ import type { Migration } from "kysely/migration";
8
+ export declare const indexThePlansByParentAndTheRunsByPlan: Migration;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Index the plans by their parent, thus the read of the plan that replaced a
3
+ * plan, and the key check when a plan row goes away, read one index entry.
4
+ * Index the runs by their plan, thus the read of the run ids of a plan reads
5
+ * only the runs of that plan.
6
+ */
7
+ import { sql } from "kysely";
8
+ export const indexThePlansByParentAndTheRunsByPlan = {
9
+ async up(db) {
10
+ await sql `CREATE INDEX IF NOT EXISTS idx_cortex_plans_parent ON cortex_plans(parent_plan_id)`.execute(db);
11
+ await sql `CREATE INDEX IF NOT EXISTS idx_cortex_runs_plan ON cortex_runs(analysis_id, plan_id, started_at DESC, run_id DESC)`.execute(db);
12
+ },
13
+ };
@@ -8,10 +8,18 @@
8
8
  import { baseline } from "./20260927120000_baseline.js";
9
9
  import { threadTitleSetByUser } from "./20260927180000_thread_title_set_by_user.js";
10
10
  import { dropActiveExecTracking } from "./20261005120000_drop_the_active_exec_tracking.js";
11
+ import { indexTheRunsByStart } from "./20261012120000_index_the_runs_by_start.js";
12
+ import { indexTheEndedRunsAndTheRecentThreads } from "./20261013120000_index_the_ended_runs_and_the_recent_threads.js";
13
+ import { narrowTheActiveRunIndex } from "./20261013180000_narrow_the_active_run_index.js";
14
+ import { indexThePlansByParentAndTheRunsByPlan } from "./20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js";
11
15
  export function cortexMigrations(logger) {
12
16
  return {
13
17
  "20260927120000_baseline": baseline(logger),
14
18
  "20260927180000_thread_title_set_by_user": threadTitleSetByUser,
15
19
  "20261005120000_drop_the_active_exec_tracking": dropActiveExecTracking,
20
+ "20261012120000_index_the_runs_by_start": indexTheRunsByStart,
21
+ "20261013120000_index_the_ended_runs_and_the_recent_threads": indexTheEndedRunsAndTheRecentThreads,
22
+ "20261013180000_narrow_the_active_run_index": narrowTheActiveRunIndex,
23
+ "20261014120000_index_the_plans_by_parent_and_the_runs_by_plan": indexThePlansByParentAndTheRunsByPlan,
16
24
  };
17
25
  }
@@ -54,3 +54,24 @@ export declare function upsertPlan(pool: Querier, input: UpsertPlanInput): Resul
54
54
  export declare function loadPlan(pool: Querier, planId: string, opts: {
55
55
  analysisId: string;
56
56
  }): ResultAsync<unknown | null, DbError>;
57
+ /** A stored plan with its lineage, as {@link loadPlanRecord} reads it. */
58
+ export interface PlanRecord {
59
+ planId: string;
60
+ /** The stored document, as the store holds it. A reader parses it. */
61
+ plan: unknown;
62
+ parentPlanId: string | null;
63
+ /** The newest plan of the same analysis that names this plan as its parent. */
64
+ supersededBy: string | null;
65
+ createdAt: string;
66
+ }
67
+ /**
68
+ * Analysis-scoped read of one plan with its parent and the newest plan that
69
+ * replaced it, in one statement. Returns `ok(null)` for an unknown planId OR a
70
+ * planId of a different analysis, as {@link loadPlan} does.
71
+ *
72
+ * `created_at` is ISO 8601 text, thus the text order is the time order; the plan
73
+ * id breaks a tie so that the answer is stable.
74
+ */
75
+ export declare function loadPlanRecord(pool: Querier, planId: string, opts: {
76
+ analysisId: string;
77
+ }): ResultAsync<PlanRecord | null, DbError>;
@@ -115,3 +115,28 @@ export function loadPlan(pool, planId, opts) {
115
115
  return result.rows[0]?.plan ?? null;
116
116
  });
117
117
  }
118
+ /**
119
+ * Analysis-scoped read of one plan with its parent and the newest plan that
120
+ * replaced it, in one statement. Returns `ok(null)` for an unknown planId OR a
121
+ * planId of a different analysis, as {@link loadPlan} does.
122
+ *
123
+ * `created_at` is ISO 8601 text, thus the text order is the time order; the plan
124
+ * id breaks a tie so that the answer is stable.
125
+ */
126
+ export function loadPlanRecord(pool, planId, opts) {
127
+ return tryQuery("plans.loadPlanRecord", async () => {
128
+ const result = await pool.query({
129
+ text: `SELECT p.plan, p.parent_plan_id, p.created_at,
130
+ (SELECT c.plan_id FROM cortex_plans c
131
+ WHERE c.parent_plan_id = p.plan_id AND c.analysis_id = p.analysis_id
132
+ ORDER BY c.created_at DESC, c.plan_id DESC LIMIT 1) AS superseded_by
133
+ FROM cortex_plans p
134
+ WHERE p.plan_id = $1 AND p.analysis_id = $2`,
135
+ values: [planId, opts.analysisId],
136
+ });
137
+ const row = result.rows[0];
138
+ if (!row)
139
+ return null;
140
+ return { planId, plan: row.plan, parentPlanId: row.parent_plan_id, supersededBy: row.superseded_by, createdAt: row.created_at };
141
+ });
142
+ }
@@ -77,11 +77,33 @@ export declare function queryActiveRun(pool: Querier, analysisId: string, planId
77
77
  * analysis's live concurrency, not by its history, and a run leaves the set permanently when it
78
78
  * finishes.
79
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.
80
+ * The status set is the one {@link queryActiveRun} uses — non-terminal means `running`. Keep the two
81
+ * in step: a new non-terminal status added to one and not the other makes a live run invisible to
82
+ * exactly one of its readers.
83
83
  */
84
84
  export declare function queryActiveRunsByAnalysis(pool: Querier, analysisId: string): ResultAsync<CortexRunRow[], DbError>;
85
+ /**
86
+ * How long a reserved run may wait for its workflow before {@link reconcileOrphanedRuns} fails it. A launch writes
87
+ * the workflow row seconds after the reservation, and the margin covers a slow authorization gate of a managed host.
88
+ */
89
+ export declare const ORPHANED_RUN_GRACE_MS: number;
90
+ /**
91
+ * Fail each `running` run that started before the grace and that has no DBOS workflow row, and give the ids of
92
+ * the runs that it failed.
93
+ *
94
+ * A launch reserves the run row, then starts the workflow. A host that dies between the two, or a start that throws
95
+ * while the following status write also fails, leaves a `running` row that no workflow will ever settle. That row
96
+ * holds the active-plan slot, thus each later launch of the plan answers `started: false` for a run that never
97
+ * started. Recovery cannot help, because no workflow row exists to recover.
98
+ *
99
+ * Only a run with no workflow row at all is failed. A run whose workflow exists, also a terminal one, is the
100
+ * workflow's to settle: a failed mark could misname a run that completed. Call it after `DBOS.launch()`, thus a
101
+ * recovered workflow is visible to the predicate.
102
+ */
103
+ export declare function reconcileOrphanedRuns(pool: Querier, opts?: {
104
+ now?: number;
105
+ graceMs?: number;
106
+ }): ResultAsync<string[], DbError>;
85
107
  export declare function updateRunStatus(pool: Querier, runId: string, status: "running" | "completed" | "partial" | "failed" | "canceled" | "suspended_insufficient_funds", error?: string | null): ResultAsync<void, DbError>;
86
108
  /**
87
109
  * Conditionally transition an ACTIVE run to `canceled`, stamping `completed_at`
@@ -99,6 +121,16 @@ export declare function queryRunsByAnalysis(pool: Querier, analysisId: string, {
99
121
  limit?: number;
100
122
  offset?: number;
101
123
  }): ResultAsync<CortexRunRow[], DbError>;
124
+ /**
125
+ * The run ids of one plan of one analysis, newest start first, at most `limit`.
126
+ * The run id breaks a tie of start times, highest first. A plan with no run gives
127
+ * an empty list.
128
+ */
129
+ export declare function queryRunIdsByPlan(pool: Querier, opts: {
130
+ analysisId: string;
131
+ planId: string;
132
+ limit: number;
133
+ }): ResultAsync<string[], DbError>;
102
134
  /**
103
135
  * Read the analysis's non-terminal run activity without coupling it to a
104
136
  * conversation thread. The total is counted before the detail limit so a
@@ -106,8 +138,7 @@ export declare function queryRunsByAnalysis(pool: Querier, analysisId: string, {
106
138
  */
107
139
  export declare function queryNonTerminalRunsByAnalysis(pool: Querier, analysisId: string, limit?: number): ResultAsync<RunPage, DbError>;
108
140
  /**
109
- * Read one stable inspection page: running first, then suspended, then
110
- * terminal history. This is deliberately separate from
141
+ * Read one stable inspection page: running first, then each other run. This is deliberately separate from
111
142
  * {@link queryRunsByAnalysis}, whose chronological order is an embedder/UI
112
143
  * contract.
113
144
  */
@@ -132,7 +132,7 @@ export function queryActiveRun(pool, analysisId, planId) {
132
132
  FROM cortex_runs
133
133
  WHERE analysis_id = $1
134
134
  AND plan_id IS NOT DISTINCT FROM $2
135
- AND status IN ('running','suspended_insufficient_funds')`,
135
+ AND status = 'running'`,
136
136
  values: [analysisId, planId],
137
137
  });
138
138
  const row = result.rows[0];
@@ -152,9 +152,9 @@ export function queryActiveRun(pool, analysisId, planId) {
152
152
  * analysis's live concurrency, not by its history, and a run leaves the set permanently when it
153
153
  * finishes.
154
154
  *
155
- * The status set is the one {@link queryActiveRun} uses — non-terminal means still running, or
156
- * suspended awaiting funds. Keep the two in step: a new non-terminal status added to one and not the
157
- * other makes a live run invisible to exactly one of its readers.
155
+ * The status set is the one {@link queryActiveRun} uses — non-terminal means `running`. Keep the two
156
+ * in step: a new non-terminal status added to one and not the other makes a live run invisible to
157
+ * exactly one of its readers.
158
158
  */
159
159
  export function queryActiveRunsByAnalysis(pool, analysisId) {
160
160
  return tryQuery("runs.queryActiveRunsByAnalysis", async () => {
@@ -164,16 +164,52 @@ export function queryActiveRunsByAnalysis(pool, analysisId) {
164
164
  mandate_jti, mandate_expires_at, plan_id -- oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
165
165
  FROM cortex_runs
166
166
  WHERE analysis_id = $1
167
- AND status IN ('running','suspended_insufficient_funds')
167
+ AND status = 'running'
168
168
  ORDER BY started_at DESC`,
169
169
  values: [analysisId],
170
170
  });
171
171
  return result.rows.map(mapRunRow);
172
172
  });
173
173
  }
174
+ /**
175
+ * How long a reserved run may wait for its workflow before {@link reconcileOrphanedRuns} fails it. A launch writes
176
+ * the workflow row seconds after the reservation, and the margin covers a slow authorization gate of a managed host.
177
+ */
178
+ export const ORPHANED_RUN_GRACE_MS = 5 * 60 * 1000;
179
+ /**
180
+ * Fail each `running` run that started before the grace and that has no DBOS workflow row, and give the ids of
181
+ * the runs that it failed.
182
+ *
183
+ * A launch reserves the run row, then starts the workflow. A host that dies between the two, or a start that throws
184
+ * while the following status write also fails, leaves a `running` row that no workflow will ever settle. That row
185
+ * holds the active-plan slot, thus each later launch of the plan answers `started: false` for a run that never
186
+ * started. Recovery cannot help, because no workflow row exists to recover.
187
+ *
188
+ * Only a run with no workflow row at all is failed. A run whose workflow exists, also a terminal one, is the
189
+ * workflow's to settle: a failed mark could misname a run that completed. Call it after `DBOS.launch()`, thus a
190
+ * recovered workflow is visible to the predicate.
191
+ */
192
+ export function reconcileOrphanedRuns(pool, opts = {}) {
193
+ const now = opts.now ?? Date.now();
194
+ const before = new Date(now - (opts.graceMs ?? ORPHANED_RUN_GRACE_MS)).toISOString();
195
+ return tryMutation("runs.reconcileOrphanedRuns", async () => {
196
+ const result = await pool.query({
197
+ text: `UPDATE cortex_runs r
198
+ SET status = 'failed',
199
+ error = 'The workflow of the run never started.',
200
+ completed_at = $1
201
+ WHERE r.status = 'running'
202
+ AND r.started_at < $2
203
+ AND NOT EXISTS (SELECT 1 FROM dbos.workflow_status ws WHERE ws.workflow_uuid = r.run_id)
204
+ RETURNING r.run_id`,
205
+ values: [new Date(now).toISOString(), before],
206
+ });
207
+ return result.rows.map((row) => row.run_id);
208
+ });
209
+ }
174
210
  export function updateRunStatus(pool, runId, status, error) {
175
211
  const now = new Date().toISOString();
176
- const isTerminal = status === "completed" || status === "partial" || status === "failed" || status === "canceled";
212
+ const isTerminal = status === "completed" || status === "partial" || status === "failed" || status === "canceled" || status === "suspended_insufficient_funds";
177
213
  return tryMutation("runs.updateRunStatus", async () => {
178
214
  await pool.query({
179
215
  text: `UPDATE cortex_runs
@@ -198,7 +234,7 @@ export function markRunCanceledIfActive(pool, runId, reason) {
198
234
  const result = await pool.query({
199
235
  text: `UPDATE cortex_runs
200
236
  SET status = 'canceled', completed_at = $2, error = $3
201
- WHERE run_id = $1 AND status IN ('running','suspended_insufficient_funds')`,
237
+ WHERE run_id = $1 AND status = 'running'`,
202
238
  values: [runId, now, reason],
203
239
  });
204
240
  return (result.rowCount ?? 0) > 0;
@@ -242,6 +278,23 @@ export function queryRunsByAnalysis(pool, analysisId, { limit = 50, offset = 0 }
242
278
  return result.rows.map(mapRunRow);
243
279
  });
244
280
  }
281
+ /**
282
+ * The run ids of one plan of one analysis, newest start first, at most `limit`.
283
+ * The run id breaks a tie of start times, highest first. A plan with no run gives
284
+ * an empty list.
285
+ */
286
+ export function queryRunIdsByPlan(pool, opts) {
287
+ return tryQuery("runs.queryRunIdsByPlan", async () => {
288
+ const result = await pool.query({
289
+ text: `SELECT run_id FROM cortex_runs
290
+ WHERE analysis_id = $1 AND plan_id = $2
291
+ ORDER BY started_at DESC, run_id DESC
292
+ LIMIT $3`,
293
+ values: [opts.analysisId, opts.planId, opts.limit],
294
+ });
295
+ return result.rows.map((row) => row.run_id);
296
+ });
297
+ }
245
298
  /**
246
299
  * Read the analysis's non-terminal run activity without coupling it to a
247
300
  * conversation thread. The total is counted before the detail limit so a
@@ -253,7 +306,7 @@ export function queryNonTerminalRunsByAnalysis(pool, analysisId, limit = 20) {
253
306
  text: `SELECT COUNT(*)::int AS total
254
307
  FROM cortex_runs
255
308
  WHERE analysis_id = $1
256
- AND status IN ('running','suspended_insufficient_funds')`,
309
+ AND status = 'running'`,
257
310
  values: [analysisId],
258
311
  });
259
312
  const rowsResult = await pool.query({
@@ -262,13 +315,8 @@ export function queryNonTerminalRunsByAnalysis(pool, analysisId, limit = 20) {
262
315
  mandate_jti, mandate_expires_at, plan_id
263
316
  FROM cortex_runs
264
317
  WHERE analysis_id = $1
265
- AND status IN ('running','suspended_insufficient_funds')
266
- ORDER BY CASE status
267
- WHEN 'running' THEN 0
268
- WHEN 'suspended_insufficient_funds' THEN 1
269
- ELSE 2
270
- END,
271
- started_at DESC,
318
+ AND status = 'running'
319
+ ORDER BY started_at DESC,
272
320
  run_id DESC
273
321
  LIMIT $2`,
274
322
  values: [analysisId, limit],
@@ -280,8 +328,7 @@ export function queryNonTerminalRunsByAnalysis(pool, analysisId, limit = 20) {
280
328
  });
281
329
  }
282
330
  /**
283
- * Read one stable inspection page: running first, then suspended, then
284
- * terminal history. This is deliberately separate from
331
+ * Read one stable inspection page: running first, then each other run. This is deliberately separate from
285
332
  * {@link queryRunsByAnalysis}, whose chronological order is an embedder/UI
286
333
  * contract.
287
334
  */
@@ -299,8 +346,7 @@ export function queryRunsForInspection(pool, analysisId, { limit = 50, offset =
299
346
  WHERE analysis_id = $1
300
347
  ORDER BY CASE status
301
348
  WHEN 'running' THEN 0
302
- WHEN 'suspended_insufficient_funds' THEN 1
303
- ELSE 2
349
+ ELSE 1
304
350
  END,
305
351
  started_at DESC,
306
352
  run_id DESC
@@ -54,6 +54,8 @@ export declare const RunStatus: z.ZodEnum<{
54
54
  suspended_insufficient_funds: "suspended_insufficient_funds";
55
55
  }>;
56
56
  export type RunStatus = z.infer<typeof RunStatus>;
57
+ /** A status that a run keeps once it ended: every status but `running`. */
58
+ export type TerminalRunStatus = Exclude<RunStatus, "running">;
57
59
  export declare const StepExecutionStatus: z.ZodEnum<{
58
60
  failed: "failed";
59
61
  pending: "pending";
@@ -103,5 +103,11 @@ export interface ProfileActivityFrame {
103
103
  * A failing write is swallowed and logged, never propagated: observation is a
104
104
  * diagnostic channel, and a dropped frame must not fail a profile that is otherwise
105
105
  * succeeding. This mirrors the sandbox-step producer's stance on the same write.
106
+ *
107
+ * Each emission writes the reconciling activity part, so a late subscriber folds to the
108
+ * current activity, and then one `data-activity-entry`, so it also gets each earlier line.
109
+ * The two parts carry one time from `now`, which the body binds to the checkpointed clock of
110
+ * its workflow. The two writes fail alone. The sequence number of the entry advances before
111
+ * the clock read and the writes, thus a failure cannot shift a later id.
106
112
  */
107
- export declare function createProfileActivityEmitter(write: ActivityWrite, frame: ProfileActivityFrame, logger: Logger): ProfileActivityEmitter;
113
+ export declare function createProfileActivityEmitter(write: ActivityWrite, frame: ProfileActivityFrame, logger: Logger, now: () => Promise<number>): ProfileActivityEmitter;
@@ -19,7 +19,7 @@
19
19
  * anything that throws on the way there reaches the failure path having emitted
20
20
  * no terminal at all.
21
21
  */
22
- import { activityForTool, stepPartId } from "../sandbox/sandbox-step-translate.js";
22
+ import { activityEmission, activityForTool } from "../sandbox/sandbox-step-translate.js";
23
23
  /**
24
24
  * Build the profile's activity channel over `write`.
25
25
  *
@@ -30,21 +30,30 @@ import { activityForTool, stepPartId } from "../sandbox/sandbox-step-translate.j
30
30
  * A failing write is swallowed and logged, never propagated: observation is a
31
31
  * diagnostic channel, and a dropped frame must not fail a profile that is otherwise
32
32
  * succeeding. This mirrors the sandbox-step producer's stance on the same write.
33
+ *
34
+ * Each emission writes the reconciling activity part, so a late subscriber folds to the
35
+ * current activity, and then one `data-activity-entry`, so it also gets each earlier line.
36
+ * The two parts carry one time from `now`, which the body binds to the checkpointed clock of
37
+ * its workflow. The two writes fail alone. The sequence number of the entry advances before
38
+ * the clock read and the writes, thus a failure cannot shift a later id.
33
39
  */
34
- export function createProfileActivityEmitter(write, frame, logger) {
35
- // One reconciling id for the profile's single step, so the run-stream fold
36
- // collapses every phase transition latest-wins onto one frame rather than
37
- // accumulating them. Minted through the shared construction because that
38
- // sameness across producers is what makes the fold correct.
39
- const id = stepPartId("step-activity", frame.runId, frame.stepId);
40
- const emit = async (phase, activity) => {
40
+ export function createProfileActivityEmitter(write, frame, logger, now) {
41
+ let seq = 0;
42
+ const writeAlone = async (part, phase) => {
41
43
  try {
42
- await write({ type: "data-step-activity", id, runId: frame.runId, stepId: frame.stepId, phase, activity });
44
+ await write(part);
43
45
  }
44
46
  catch (err) {
45
47
  logger.warn("run-event write failed (non-fatal)", { phase, ...logger.errorFields(err) });
46
48
  }
47
49
  };
50
+ const emit = async (phase, activity) => {
51
+ const position = seq++;
52
+ const at = new Date(await now()).toISOString();
53
+ const parts = activityEmission(frame, position, phase, activity, at);
54
+ await writeAlone(parts.activity, phase);
55
+ await writeAlone(parts.entry, phase);
56
+ };
48
57
  return {
49
58
  sandboxInit: () => emit("sandbox-init", "Starting sandbox"),
50
59
  scanning: () => emit("executing", "Scanning input files"),
@@ -28,7 +28,7 @@ import type { SandboxAgentDeps } from "../agents/sandbox/shared.js";
28
28
  import type { BioToolKeys } from "../tools/bio/keys.js";
29
29
  import type { ToolOutputStore } from "../loop/tool-output.js";
30
30
  import type { DbError } from "../lib/db-result.js";
31
- import { type GateRefusal } from "../lib/hooks.js";
31
+ import { type GateRefusal, type NoticeFailure } from "../lib/hooks.js";
32
32
  import type { Logger } from "../lib/logger.js";
33
33
  import type { UsageRecorder } from "../billing/usage-recorder.js";
34
34
  import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
@@ -38,6 +38,15 @@ import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
38
38
  import { type ProfileSubmission } from "../schemas/data-profile-schemas.js";
39
39
  import { type ProfileResolution } from "./data-profile-resolve.js";
40
40
  import { type DataProfileResult } from "../state/index.js";
41
+ /** The settle of one profile claim: the ledger row left `running`, and the terminal write stamped it. */
42
+ export interface DataProfileSettlement {
43
+ readonly analysisId: string;
44
+ readonly status: "completed" | "failed";
45
+ /** The `data_profile_completed_at` that the terminal write stamped, as an ISO 8601 string. */
46
+ readonly completedAt: string;
47
+ }
48
+ /** The notice of a profile settle. The workflow body calls it one time for each settled claim. */
49
+ export type NotifyDataProfileSettled = (settlement: DataProfileSettlement) => ResultAsync<void, NoticeFailure>;
41
50
  /** The body's construction-time deps — closed over at registration. */
42
51
  export interface DataProfileDeps extends EnvironmentStorePaths {
43
52
  /** Operational logging seam; omitted falls back to no-op. */
@@ -73,6 +82,11 @@ export interface DataProfileDeps extends EnvironmentStorePaths {
73
82
  * Unbound keeps the current shape: no link tool, and no error.
74
83
  */
75
84
  readonly extendAnalysisFarm?: ExtendAnalysisFarm;
85
+ /**
86
+ * Notice of each profile settle, for the records of the embedder. Unbound, the
87
+ * body calls nothing. It is not a provenance hook.
88
+ */
89
+ readonly notifyDataProfileSettled?: NotifyDataProfileSettled;
76
90
  }
77
91
  /**
78
92
  * Workflow input. JSON-serialisable — DBOS persists it as the workflow's