@inflexa-ai/harness 0.44.0 → 0.45.1
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.
- package/dist/agents/conversation-agent.d.ts +10 -0
- package/dist/agents/conversation-agent.js +19 -1
- package/dist/agents/sandbox/shared.d.ts +23 -2
- package/dist/agents/sandbox/shared.js +51 -18
- package/dist/agents/sandbox/translational-safety-agent.js +26 -0
- package/dist/agents/sandbox/types.d.ts +24 -1
- package/dist/agents/sandbox-catalog.d.ts +14 -0
- package/dist/agents/sandbox-catalog.js +19 -0
- package/dist/app/approve-plan.d.ts +51 -0
- package/dist/app/approve-plan.js +84 -0
- package/dist/app/chat-turn.js +6 -3
- package/dist/app/data-profile-view.d.ts +1 -1
- package/dist/app/run-activity.js +4 -6
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +14 -2
- package/dist/contracts/chat-events.js +1 -1
- package/dist/contracts/chat-frame.d.ts +7 -0
- package/dist/contracts/chat-frame.js +24 -6
- package/dist/contracts/chat-parts.d.ts +73 -2
- package/dist/contracts/chat-parts.js +3 -1
- package/dist/contracts/index.d.ts +4 -2
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/part-registry.js +3 -0
- package/dist/contracts/schemas/chat-events.d.ts +38 -0
- package/dist/contracts/schemas/chat-events.js +8 -0
- package/dist/contracts/schemas/chat-parts.d.ts +239 -15
- package/dist/contracts/schemas/chat-parts.js +80 -12
- package/dist/contracts/schemas/index.d.ts +2 -2
- package/dist/contracts/schemas/index.js +2 -2
- package/dist/contracts/subagent-activity.d.ts +12 -0
- package/dist/contracts/subagent-activity.js +76 -0
- package/dist/execution/artifact-metadata.js +1 -3
- package/dist/execution/dbos-workflow-purger.d.ts +2 -0
- package/dist/execution/dbos-workflow-purger.js +21 -14
- package/dist/execution/run-canceler.d.ts +33 -11
- package/dist/execution/run-canceler.js +130 -28
- package/dist/execution/run-end-record.d.ts +30 -0
- package/dist/execution/run-end-record.js +27 -0
- package/dist/execution/run-event-parts.d.ts +6 -5
- package/dist/execution/run-event-parts.js +6 -5
- package/dist/execution/run-event-stream.d.ts +18 -6
- package/dist/execution/run-event-stream.js +67 -17
- package/dist/execution/run-synthesis.d.ts +21 -0
- package/dist/execution/run-synthesis.js +77 -3
- package/dist/index.d.ts +28 -10
- package/dist/index.js +26 -2
- package/dist/lib/hooks.d.ts +1 -1
- package/dist/lib/metrics.d.ts +2 -2
- package/dist/loop/call-guard.d.ts +50 -0
- package/dist/loop/call-guard.js +90 -0
- package/dist/loop/compaction.d.ts +5 -0
- package/dist/loop/compaction.js +17 -0
- package/dist/loop/run-agent.d.ts +9 -0
- package/dist/loop/run-agent.js +16 -1
- package/dist/loop/run-to-terminal.js +4 -1
- package/dist/loop/types.d.ts +8 -0
- package/dist/memory/card-builders.d.ts +11 -0
- package/dist/memory/card-builders.js +35 -21
- package/dist/memory/conversation-display-recorder.d.ts +2 -2
- package/dist/memory/conversation-display-recorder.js +38 -4
- package/dist/memory/conversation-display-storage.d.ts +9 -2
- package/dist/memory/conversation-display-storage.js +18 -3
- package/dist/memory/thread-history.d.ts +42 -8
- package/dist/memory/thread-history.js +164 -45
- package/dist/memory/thread-store.d.ts +13 -0
- package/dist/memory/thread-store.js +15 -1
- package/dist/prompts/briefing.d.ts +57 -3
- package/dist/prompts/briefing.js +129 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +3 -1
- package/dist/prompts/planner.js +13 -2
- package/dist/provenance/seam.d.ts +39 -20
- package/dist/provenance/seam.js +7 -5
- package/dist/providers/ai-sdk.js +93 -11
- package/dist/providers/types.d.ts +6 -0
- package/dist/reference-data/catalog.js +141 -2
- package/dist/report-model/authoring-grammar.d.ts +26 -26
- package/dist/report-model/pin-snapshot.d.ts +1 -1
- package/dist/report-model/pin-snapshot.js +2 -50
- package/dist/runtime/assemble.d.ts +8 -1
- package/dist/runtime/assemble.js +11 -0
- package/dist/runtime/boot.d.ts +3 -0
- package/dist/runtime/boot.js +10 -1
- package/dist/sandbox/image-packages.d.ts +26 -4
- package/dist/sandbox/image-packages.js +18 -4
- package/dist/sandbox/sandbox-step-translate.d.ts +23 -0
- package/dist/sandbox/sandbox-step-translate.js +24 -0
- package/dist/schemas/artifact-manifest.d.ts +1 -1
- package/dist/schemas/artifact-manifest.js +11 -9
- package/dist/schemas/data-profile-schemas.d.ts +12 -12
- package/dist/schemas/plan-schemas.d.ts +75 -21
- package/dist/schemas/plan-schemas.js +1 -1
- package/dist/schemas/run-synthesis.d.ts +2 -2
- package/dist/schemas/workflow-state.d.ts +113 -0
- package/dist/schemas/workflow-state.js +54 -0
- package/dist/state/analyses-metrics.d.ts +79 -7
- package/dist/state/analyses-metrics.js +141 -6
- package/dist/state/artifacts.d.ts +66 -3
- package/dist/state/artifacts.js +85 -7
- package/dist/state/data-profile.d.ts +4 -2
- package/dist/state/data-profile.js +7 -5
- package/dist/state/index.d.ts +9 -8
- package/dist/state/index.js +5 -5
- package/dist/state/migrations/20261012120000_index_the_runs_by_start.d.ts +8 -0
- package/dist/state/migrations/20261012120000_index_the_runs_by_start.js +12 -0
- package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.d.ts +8 -0
- package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.js +13 -0
- package/dist/state/migrations/20261013180000_narrow_the_active_run_index.d.ts +8 -0
- package/dist/state/migrations/20261013180000_narrow_the_active_run_index.js +13 -0
- package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.d.ts +8 -0
- package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js +13 -0
- package/dist/state/migrations/index.js +8 -0
- package/dist/state/plans.d.ts +21 -0
- package/dist/state/plans.js +25 -0
- package/dist/state/runs.d.ts +79 -14
- package/dist/state/runs.js +150 -33
- package/dist/state/schema.d.ts +2 -0
- package/dist/state/step-executions.d.ts +9 -0
- package/dist/state/step-executions.js +22 -0
- package/dist/tasks/data-profile-activity.d.ts +7 -1
- package/dist/tasks/data-profile-activity.js +18 -9
- package/dist/tasks/data-profile.d.ts +15 -1
- package/dist/tasks/data-profile.js +16 -11
- package/dist/tools/ad-hoc-router.d.ts +2 -0
- package/dist/tools/ad-hoc-router.js +18 -2
- package/dist/tools/approval/gateway.js +28 -5
- package/dist/tools/capability-data.d.ts +36 -0
- package/dist/tools/capability-data.js +73 -0
- package/dist/tools/display/show-user.d.ts +1 -1
- package/dist/tools/execute-analysis.d.ts +80 -1
- package/dist/tools/execute-analysis.js +352 -110
- package/dist/tools/knowledge/check.d.ts +60 -0
- package/dist/tools/knowledge/check.js +92 -0
- package/dist/tools/knowledge/client.d.ts +382 -0
- package/dist/tools/knowledge/client.js +302 -0
- package/dist/tools/knowledge/environment.d.ts +78 -0
- package/dist/tools/knowledge/environment.js +105 -0
- package/dist/tools/knowledge/index.d.ts +49 -0
- package/dist/tools/knowledge/index.js +70 -0
- package/dist/tools/knowledge/local-slots.d.ts +62 -0
- package/dist/tools/knowledge/local-slots.js +177 -0
- package/dist/tools/knowledge/pin.d.ts +14 -0
- package/dist/tools/knowledge/pin.js +17 -0
- package/dist/tools/knowledge/recommend.d.ts +92 -0
- package/dist/tools/knowledge/recommend.js +98 -0
- package/dist/tools/knowledge/situation.d.ts +143 -0
- package/dist/tools/knowledge/situation.js +164 -0
- package/dist/tools/knowledge/skeleton.d.ts +69 -0
- package/dist/tools/knowledge/skeleton.js +202 -0
- package/dist/tools/knowledge/template.d.ts +137 -0
- package/dist/tools/knowledge/template.js +267 -0
- package/dist/tools/research/generate-plan.d.ts +23 -0
- package/dist/tools/research/generate-plan.js +203 -12
- package/dist/tools/research/guarded-address.d.ts +20 -0
- package/dist/tools/research/guarded-address.js +148 -0
- package/dist/tools/research/inspect-data-profile.d.ts +1 -1
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/research/inspect-run.js +7 -7
- package/dist/tools/research/searxng-engine.d.ts +40 -0
- package/dist/tools/research/searxng-engine.js +120 -0
- package/dist/tools/research/web-fetch.d.ts +124 -0
- package/dist/tools/research/web-fetch.js +627 -0
- package/dist/tools/research/web-search.d.ts +75 -0
- package/dist/tools/research/web-search.js +112 -0
- package/dist/tools/sandbox/list-available-packages.d.ts +7 -4
- package/dist/tools/sandbox/list-available-packages.js +12 -4
- package/dist/tools/sandbox/list-available-refs.d.ts +21 -0
- package/dist/tools/sandbox/list-available-refs.js +60 -3
- package/dist/tools/sandbox/report-blocker.js +5 -1
- package/dist/tools/terminal-text.d.ts +11 -0
- package/dist/tools/terminal-text.js +19 -0
- package/dist/tools/workspace/decision-record.d.ts +81 -0
- package/dist/tools/workspace/decision-record.js +117 -0
- package/dist/tools/workspace/edit-file.d.ts +3 -1
- package/dist/tools/workspace/edit-file.js +20 -2
- package/dist/tools/workspace/execute-command.d.ts +17 -0
- package/dist/tools/workspace/execute-command.js +28 -1
- package/dist/tools/workspace/index.d.ts +1 -0
- package/dist/tools/workspace/index.js +1 -0
- package/dist/tools/workspace/mutator.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +62 -21
- package/dist/workflows/execute-analysis.js +307 -79
- package/dist/workflows/plan/graph.d.ts +16 -0
- package/dist/workflows/plan/graph.js +36 -0
- package/dist/workflows/sandbox-step.d.ts +12 -0
- package/dist/workflows/sandbox-step.js +14 -12
- package/package.json +5 -1
package/dist/state/index.js
CHANGED
|
@@ -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,
|
|
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, countRunsByAnalysis, queryRunsByThread, queryRunIdsByPlan, queryNewestPlanRun, } 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
|
+
};
|
package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.d.ts
ADDED
|
@@ -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;
|
package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js
ADDED
|
@@ -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
|
}
|
package/dist/state/plans.d.ts
CHANGED
|
@@ -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>;
|
package/dist/state/plans.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/state/runs.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface InsertRunInput {
|
|
|
21
21
|
planId?: string | null;
|
|
22
22
|
mandateJti?: string | null;
|
|
23
23
|
mandateExpiresAt?: string | null;
|
|
24
|
+
/** The start time as an ISO string. Without it, the row gets the clock of the host. */
|
|
25
|
+
startedAt?: string;
|
|
24
26
|
}
|
|
25
27
|
export interface RunReservation {
|
|
26
28
|
readonly row: CortexRunRow;
|
|
@@ -64,6 +66,11 @@ export declare function reserveRunById(pool: Querier, input: InsertRunInput): Re
|
|
|
64
66
|
* most one match.
|
|
65
67
|
*/
|
|
66
68
|
export declare function queryActiveRun(pool: Querier, analysisId: string, planId: string | null): ResultAsync<CortexRunRow | null, DbError>;
|
|
69
|
+
/** The optional text filter of the run reads. */
|
|
70
|
+
export interface RunTextFilter {
|
|
71
|
+
/** A part of the run id or of the plan title, with no regard to case. Each character matches only itself. */
|
|
72
|
+
q?: string;
|
|
73
|
+
}
|
|
67
74
|
/**
|
|
68
75
|
* Every NON-TERMINAL run of an analysis, newest-first and deliberately unbounded.
|
|
69
76
|
*
|
|
@@ -77,28 +84,86 @@ export declare function queryActiveRun(pool: Querier, analysisId: string, planId
|
|
|
77
84
|
* analysis's live concurrency, not by its history, and a run leaves the set permanently when it
|
|
78
85
|
* finishes.
|
|
79
86
|
*
|
|
80
|
-
* The status set is the one {@link queryActiveRun} uses — non-terminal means
|
|
81
|
-
*
|
|
82
|
-
*
|
|
87
|
+
* The status set is the one {@link queryActiveRun} uses — non-terminal means `running`. Keep the two
|
|
88
|
+
* in step: a new non-terminal status added to one and not the other makes a live run invisible to
|
|
89
|
+
* exactly one of its readers.
|
|
90
|
+
*
|
|
91
|
+
* With `q`, only the runs that the text filter selects are members.
|
|
92
|
+
*/
|
|
93
|
+
export declare function queryActiveRunsByAnalysis(pool: Querier, analysisId: string, { q }?: RunTextFilter): ResultAsync<CortexRunRow[], DbError>;
|
|
94
|
+
/**
|
|
95
|
+
* How long a reserved run may wait for its workflow before {@link reconcileOrphanedRuns} fails it. A launch writes
|
|
96
|
+
* the workflow row seconds after the reservation, and the margin covers a slow authorization gate of a managed host.
|
|
83
97
|
*/
|
|
84
|
-
export declare
|
|
85
|
-
|
|
98
|
+
export declare const ORPHANED_RUN_GRACE_MS: number;
|
|
99
|
+
/**
|
|
100
|
+
* Fail each `running` run that started before the grace and that has no DBOS workflow row, and give the ids of
|
|
101
|
+
* the runs that it failed.
|
|
102
|
+
*
|
|
103
|
+
* A launch reserves the run row, then starts the workflow. A host that dies between the two, or a start that throws
|
|
104
|
+
* while the following status write also fails, leaves a `running` row that no workflow will ever settle. That row
|
|
105
|
+
* holds the active-plan slot, thus each later launch of the plan answers `started: false` for a run that never
|
|
106
|
+
* started. Recovery cannot help, because no workflow row exists to recover.
|
|
107
|
+
*
|
|
108
|
+
* Only a run with no workflow row at all is failed. A run whose workflow exists, also a terminal one, is the
|
|
109
|
+
* workflow's to settle: a failed mark could misname a run that completed. Call it after `DBOS.launch()`, thus a
|
|
110
|
+
* recovered workflow is visible to the predicate.
|
|
111
|
+
*/
|
|
112
|
+
export declare function reconcileOrphanedRuns(pool: Querier, opts?: {
|
|
113
|
+
now?: number;
|
|
114
|
+
graceMs?: number;
|
|
115
|
+
}): ResultAsync<string[], DbError>;
|
|
116
|
+
/**
|
|
117
|
+
* Write a status to a run row that is still `running`, and report whether the row changed. The first terminal write
|
|
118
|
+
* of a run wins: a row that already holds a terminal status keeps its status, its `completed_at`, and its error.
|
|
119
|
+
* The error text loses each NUL character, because Postgres rejects a NUL in text and the write must not fail for its own value.
|
|
120
|
+
*/
|
|
121
|
+
export declare function updateRunStatus(pool: Querier, runId: string, status: "running" | "completed" | "partial" | "failed" | "canceled" | "suspended_insufficient_funds", error?: string | null): ResultAsync<boolean, DbError>;
|
|
86
122
|
/**
|
|
87
123
|
* Conditionally transition an ACTIVE run to `canceled`, stamping `completed_at`
|
|
88
124
|
* and recording `reason` in `error` (the same column `collectAndComplete`
|
|
89
125
|
* stamps `external_cancel` into). Reports whether a row transitioned — false
|
|
90
126
|
* means the run was already terminal (e.g. it completed concurrently with an
|
|
91
|
-
* external cancel)
|
|
92
|
-
*
|
|
93
|
-
* `queryActiveRun` deduplicates on.
|
|
127
|
+
* external cancel). The guarded set is the active set `queryActiveRun`
|
|
128
|
+
* deduplicates on.
|
|
94
129
|
*/
|
|
95
130
|
export declare function markRunCanceledIfActive(pool: Querier, runId: string, reason: string): ResultAsync<boolean, DbError>;
|
|
96
131
|
export declare function promoteFailedToPartial(pool: Querier, runId: string): ResultAsync<boolean, DbError>;
|
|
97
132
|
export declare function queryRun(pool: Querier, runId: string): ResultAsync<CortexRunRow | null, DbError>;
|
|
98
|
-
|
|
133
|
+
/**
|
|
134
|
+
* One window of the runs of an analysis, newest start first. With `q`, the window holds only the runs that the text
|
|
135
|
+
* filter selects: the filter applies before `limit` and `offset`, and it does not change the order.
|
|
136
|
+
*/
|
|
137
|
+
export declare function queryRunsByAnalysis(pool: Querier, analysisId: string, { limit, offset, q }?: {
|
|
99
138
|
limit?: number;
|
|
100
139
|
offset?: number;
|
|
101
|
-
}): ResultAsync<CortexRunRow[], DbError>;
|
|
140
|
+
} & RunTextFilter): ResultAsync<CortexRunRow[], DbError>;
|
|
141
|
+
/**
|
|
142
|
+
* The count of the runs of an analysis, in one statement. With `q`, the count holds only the runs that the text filter
|
|
143
|
+
* selects, thus it agrees with the windows of {@link queryRunsByAnalysis} for the same `q`.
|
|
144
|
+
*/
|
|
145
|
+
export declare function countRunsByAnalysis(pool: Querier, analysisId: string, { q }?: RunTextFilter): ResultAsync<number, DbError>;
|
|
146
|
+
/**
|
|
147
|
+
* The run ids of one plan of one analysis, newest start first, at most `limit`.
|
|
148
|
+
* The run id breaks a tie of start times, highest first. A plan with no run gives
|
|
149
|
+
* an empty list.
|
|
150
|
+
*/
|
|
151
|
+
export declare function queryRunIdsByPlan(pool: Querier, opts: {
|
|
152
|
+
analysisId: string;
|
|
153
|
+
planId: string;
|
|
154
|
+
limit: number;
|
|
155
|
+
}): ResultAsync<string[], DbError>;
|
|
156
|
+
/**
|
|
157
|
+
* The newest run of one plan of one analysis, or `null` for a plan with no run. The order is the one of
|
|
158
|
+
* {@link queryRunIdsByPlan}: the latest start first, and the highest run id on a tie of start times.
|
|
159
|
+
*/
|
|
160
|
+
export declare function queryNewestPlanRun(pool: Querier, opts: {
|
|
161
|
+
analysisId: string;
|
|
162
|
+
planId: string;
|
|
163
|
+
}): ResultAsync<{
|
|
164
|
+
runId: string;
|
|
165
|
+
startedAt: string;
|
|
166
|
+
} | null, DbError>;
|
|
102
167
|
/**
|
|
103
168
|
* Read the analysis's non-terminal run activity without coupling it to a
|
|
104
169
|
* conversation thread. The total is counted before the detail limit so a
|
|
@@ -106,8 +171,7 @@ export declare function queryRunsByAnalysis(pool: Querier, analysisId: string, {
|
|
|
106
171
|
*/
|
|
107
172
|
export declare function queryNonTerminalRunsByAnalysis(pool: Querier, analysisId: string, limit?: number): ResultAsync<RunPage, DbError>;
|
|
108
173
|
/**
|
|
109
|
-
* Read one stable inspection page: running first, then
|
|
110
|
-
* terminal history. This is deliberately separate from
|
|
174
|
+
* Read one stable inspection page: running first, then each other run. This is deliberately separate from
|
|
111
175
|
* {@link queryRunsByAnalysis}, whose chronological order is an embedder/UI
|
|
112
176
|
* contract.
|
|
113
177
|
*/
|
|
@@ -115,10 +179,11 @@ export declare function queryRunsForInspection(pool: Querier, analysisId: string
|
|
|
115
179
|
limit?: number;
|
|
116
180
|
offset?: number;
|
|
117
181
|
}): ResultAsync<RunPage, DbError>;
|
|
118
|
-
|
|
182
|
+
/** The runs of one thread of an analysis, newest start first. With `q`, the text filter applies before the window. */
|
|
183
|
+
export declare function queryRunsByThread(pool: Querier, analysisId: string, threadId: string, { limit, offset, q }?: {
|
|
119
184
|
limit?: number;
|
|
120
185
|
offset?: number;
|
|
121
|
-
}): ResultAsync<CortexRunRow[], DbError>;
|
|
186
|
+
} & RunTextFilter): ResultAsync<CortexRunRow[], DbError>;
|
|
122
187
|
/**
|
|
123
188
|
* Persist the run mandate jti + expiry for a run. oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
|
|
124
189
|
* Called by `authorizeForRun` after a successful mint so the cancel
|
package/dist/state/runs.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* `DbError`. Every other write maps SQLSTATE 23505/23503/23502/23514 to
|
|
10
10
|
* `err(constraint_violation)` via `tryMutation`.
|
|
11
11
|
*/
|
|
12
|
+
import { stripNulCharacters } from "../input-sanitization.js";
|
|
12
13
|
import { tryMutation, tryQuery } from "../lib/db-result.js";
|
|
13
14
|
export class RunIdentityCollisionError extends Error {
|
|
14
15
|
runId;
|
|
@@ -44,7 +45,7 @@ export class RunDedupCollisionError extends Error {
|
|
|
44
45
|
* the err channel as a `DbError`.
|
|
45
46
|
*/
|
|
46
47
|
export function insertRun(pool, input) {
|
|
47
|
-
const now = new Date().toISOString();
|
|
48
|
+
const now = input.startedAt ?? new Date().toISOString();
|
|
48
49
|
return tryMutation("runs.insertRun", async () => {
|
|
49
50
|
await pool.query({
|
|
50
51
|
text: `INSERT INTO cortex_runs
|
|
@@ -75,7 +76,7 @@ export function insertRun(pool, input) {
|
|
|
75
76
|
}
|
|
76
77
|
/** Reserve a deterministic run id, treating same-analysis PK conflict as redelivery. */
|
|
77
78
|
export function reserveRunById(pool, input) {
|
|
78
|
-
const now = new Date().toISOString();
|
|
79
|
+
const now = input.startedAt ?? new Date().toISOString();
|
|
79
80
|
return tryMutation("runs.reserveRunById", async () => {
|
|
80
81
|
const result = await pool.query({
|
|
81
82
|
text: `INSERT INTO cortex_runs
|
|
@@ -132,13 +133,27 @@ export function queryActiveRun(pool, analysisId, planId) {
|
|
|
132
133
|
FROM cortex_runs
|
|
133
134
|
WHERE analysis_id = $1
|
|
134
135
|
AND plan_id IS NOT DISTINCT FROM $2
|
|
135
|
-
AND status
|
|
136
|
+
AND status = 'running'`,
|
|
136
137
|
values: [analysisId, planId],
|
|
137
138
|
});
|
|
138
139
|
const row = result.rows[0];
|
|
139
140
|
return row ? mapRunRow(row) : null;
|
|
140
141
|
});
|
|
141
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* The SQL condition of the text filter `q` for the bound placeholder `placeholder`. A run matches when its id holds the
|
|
145
|
+
* text, or when the `title` of the plan of the same analysis holds it. `strpos` is the test, because `LIKE` would turn
|
|
146
|
+
* `%` and `_` of the input into wildcards. The `analysis_id` condition keeps the plan of a different analysis out, the
|
|
147
|
+
* same scope as `loadPlan`.
|
|
148
|
+
*/
|
|
149
|
+
function runTextCondition(placeholder) {
|
|
150
|
+
return `(strpos(lower(run_id), lower(${placeholder}::text)) > 0
|
|
151
|
+
OR EXISTS (SELECT 1 FROM cortex_plans p
|
|
152
|
+
WHERE p.plan_id = cortex_runs.plan_id
|
|
153
|
+
AND p.analysis_id = cortex_runs.analysis_id
|
|
154
|
+
AND jsonb_typeof(p.plan -> 'title') = 'string'
|
|
155
|
+
AND strpos(lower(p.plan ->> 'title'), lower(${placeholder}::text)) > 0))`;
|
|
156
|
+
}
|
|
142
157
|
/**
|
|
143
158
|
* Every NON-TERMINAL run of an analysis, newest-first and deliberately unbounded.
|
|
144
159
|
*
|
|
@@ -152,35 +167,82 @@ export function queryActiveRun(pool, analysisId, planId) {
|
|
|
152
167
|
* analysis's live concurrency, not by its history, and a run leaves the set permanently when it
|
|
153
168
|
* finishes.
|
|
154
169
|
*
|
|
155
|
-
* The status set is the one {@link queryActiveRun} uses — non-terminal means
|
|
156
|
-
*
|
|
157
|
-
*
|
|
170
|
+
* The status set is the one {@link queryActiveRun} uses — non-terminal means `running`. Keep the two
|
|
171
|
+
* in step: a new non-terminal status added to one and not the other makes a live run invisible to
|
|
172
|
+
* exactly one of its readers.
|
|
173
|
+
*
|
|
174
|
+
* With `q`, only the runs that the text filter selects are members.
|
|
158
175
|
*/
|
|
159
|
-
export function queryActiveRunsByAnalysis(pool, analysisId) {
|
|
176
|
+
export function queryActiveRunsByAnalysis(pool, analysisId, { q } = {}) {
|
|
160
177
|
return tryQuery("runs.queryActiveRunsByAnalysis", async () => {
|
|
178
|
+
const values = [analysisId];
|
|
179
|
+
const text = q === undefined ? "" : `AND ${runTextCondition(`$${values.push(q)}`)}`;
|
|
161
180
|
const result = await pool.query({
|
|
162
181
|
text: `SELECT run_id, analysis_id, thread_id, workflow_name,
|
|
163
182
|
status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
|
|
164
183
|
mandate_jti, mandate_expires_at, plan_id -- oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
|
|
165
184
|
FROM cortex_runs
|
|
166
185
|
WHERE analysis_id = $1
|
|
167
|
-
AND status
|
|
186
|
+
AND status = 'running'
|
|
187
|
+
${text}
|
|
168
188
|
ORDER BY started_at DESC`,
|
|
169
|
-
values
|
|
189
|
+
values,
|
|
170
190
|
});
|
|
171
191
|
return result.rows.map(mapRunRow);
|
|
172
192
|
});
|
|
173
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* How long a reserved run may wait for its workflow before {@link reconcileOrphanedRuns} fails it. A launch writes
|
|
196
|
+
* the workflow row seconds after the reservation, and the margin covers a slow authorization gate of a managed host.
|
|
197
|
+
*/
|
|
198
|
+
export const ORPHANED_RUN_GRACE_MS = 5 * 60 * 1000;
|
|
199
|
+
/**
|
|
200
|
+
* Fail each `running` run that started before the grace and that has no DBOS workflow row, and give the ids of
|
|
201
|
+
* the runs that it failed.
|
|
202
|
+
*
|
|
203
|
+
* A launch reserves the run row, then starts the workflow. A host that dies between the two, or a start that throws
|
|
204
|
+
* while the following status write also fails, leaves a `running` row that no workflow will ever settle. That row
|
|
205
|
+
* holds the active-plan slot, thus each later launch of the plan answers `started: false` for a run that never
|
|
206
|
+
* started. Recovery cannot help, because no workflow row exists to recover.
|
|
207
|
+
*
|
|
208
|
+
* Only a run with no workflow row at all is failed. A run whose workflow exists, also a terminal one, is the
|
|
209
|
+
* workflow's to settle: a failed mark could misname a run that completed. Call it after `DBOS.launch()`, thus a
|
|
210
|
+
* recovered workflow is visible to the predicate.
|
|
211
|
+
*/
|
|
212
|
+
export function reconcileOrphanedRuns(pool, opts = {}) {
|
|
213
|
+
const now = opts.now ?? Date.now();
|
|
214
|
+
const before = new Date(now - (opts.graceMs ?? ORPHANED_RUN_GRACE_MS)).toISOString();
|
|
215
|
+
return tryMutation("runs.reconcileOrphanedRuns", async () => {
|
|
216
|
+
const result = await pool.query({
|
|
217
|
+
text: `UPDATE cortex_runs r
|
|
218
|
+
SET status = 'failed',
|
|
219
|
+
error = 'The workflow of the run never started.',
|
|
220
|
+
completed_at = $1
|
|
221
|
+
WHERE r.status = 'running'
|
|
222
|
+
AND r.started_at < $2
|
|
223
|
+
AND NOT EXISTS (SELECT 1 FROM dbos.workflow_status ws WHERE ws.workflow_uuid = r.run_id)
|
|
224
|
+
RETURNING r.run_id`,
|
|
225
|
+
values: [new Date(now).toISOString(), before],
|
|
226
|
+
});
|
|
227
|
+
return result.rows.map((row) => row.run_id);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Write a status to a run row that is still `running`, and report whether the row changed. The first terminal write
|
|
232
|
+
* of a run wins: a row that already holds a terminal status keeps its status, its `completed_at`, and its error.
|
|
233
|
+
* The error text loses each NUL character, because Postgres rejects a NUL in text and the write must not fail for its own value.
|
|
234
|
+
*/
|
|
174
235
|
export function updateRunStatus(pool, runId, status, error) {
|
|
175
236
|
const now = new Date().toISOString();
|
|
176
|
-
const isTerminal = status === "completed" || status === "partial" || status === "failed" || status === "canceled";
|
|
237
|
+
const isTerminal = status === "completed" || status === "partial" || status === "failed" || status === "canceled" || status === "suspended_insufficient_funds";
|
|
177
238
|
return tryMutation("runs.updateRunStatus", async () => {
|
|
178
|
-
await pool.query({
|
|
239
|
+
const result = await pool.query({
|
|
179
240
|
text: `UPDATE cortex_runs
|
|
180
241
|
SET status = $1, completed_at = $2, error = $3
|
|
181
|
-
WHERE run_id = $4`,
|
|
182
|
-
values: [status, isTerminal ? now : null, error
|
|
242
|
+
WHERE run_id = $4 AND status = 'running'`,
|
|
243
|
+
values: [status, isTerminal ? now : null, error === undefined || error === null ? null : stripNulCharacters(error), runId],
|
|
183
244
|
});
|
|
245
|
+
return (result.rowCount ?? 0) > 0;
|
|
184
246
|
});
|
|
185
247
|
}
|
|
186
248
|
/**
|
|
@@ -188,9 +250,8 @@ export function updateRunStatus(pool, runId, status, error) {
|
|
|
188
250
|
* and recording `reason` in `error` (the same column `collectAndComplete`
|
|
189
251
|
* stamps `external_cancel` into). Reports whether a row transitioned — false
|
|
190
252
|
* means the run was already terminal (e.g. it completed concurrently with an
|
|
191
|
-
* external cancel)
|
|
192
|
-
*
|
|
193
|
-
* `queryActiveRun` deduplicates on.
|
|
253
|
+
* external cancel). The guarded set is the active set `queryActiveRun`
|
|
254
|
+
* deduplicates on.
|
|
194
255
|
*/
|
|
195
256
|
export function markRunCanceledIfActive(pool, runId, reason) {
|
|
196
257
|
const now = new Date().toISOString();
|
|
@@ -198,7 +259,7 @@ export function markRunCanceledIfActive(pool, runId, reason) {
|
|
|
198
259
|
const result = await pool.query({
|
|
199
260
|
text: `UPDATE cortex_runs
|
|
200
261
|
SET status = 'canceled', completed_at = $2, error = $3
|
|
201
|
-
WHERE run_id = $1 AND status
|
|
262
|
+
WHERE run_id = $1 AND status = 'running'`,
|
|
202
263
|
values: [runId, now, reason],
|
|
203
264
|
});
|
|
204
265
|
return (result.rowCount ?? 0) > 0;
|
|
@@ -228,20 +289,79 @@ export function queryRun(pool, runId) {
|
|
|
228
289
|
return row ? mapRunRow(row) : null;
|
|
229
290
|
});
|
|
230
291
|
}
|
|
231
|
-
|
|
292
|
+
/**
|
|
293
|
+
* One window of the runs of an analysis, newest start first. With `q`, the window holds only the runs that the text
|
|
294
|
+
* filter selects: the filter applies before `limit` and `offset`, and it does not change the order.
|
|
295
|
+
*/
|
|
296
|
+
export function queryRunsByAnalysis(pool, analysisId, { limit = 50, offset = 0, q } = {}) {
|
|
232
297
|
return tryQuery("runs.queryRunsByAnalysis", async () => {
|
|
298
|
+
const values = [analysisId, limit, offset];
|
|
299
|
+
const text = q === undefined ? "" : `AND ${runTextCondition(`$${values.push(q)}`)}`;
|
|
233
300
|
const result = await pool.query({
|
|
234
301
|
text: `SELECT run_id, analysis_id, thread_id, workflow_name,
|
|
235
302
|
status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
|
|
236
303
|
mandate_jti, mandate_expires_at, plan_id -- oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
|
|
237
304
|
FROM cortex_runs
|
|
238
305
|
WHERE analysis_id = $1
|
|
306
|
+
${text}
|
|
239
307
|
ORDER BY started_at DESC LIMIT $2 OFFSET $3`,
|
|
240
|
-
values
|
|
308
|
+
values,
|
|
241
309
|
});
|
|
242
310
|
return result.rows.map(mapRunRow);
|
|
243
311
|
});
|
|
244
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* The count of the runs of an analysis, in one statement. With `q`, the count holds only the runs that the text filter
|
|
315
|
+
* selects, thus it agrees with the windows of {@link queryRunsByAnalysis} for the same `q`.
|
|
316
|
+
*/
|
|
317
|
+
export function countRunsByAnalysis(pool, analysisId, { q } = {}) {
|
|
318
|
+
return tryQuery("runs.countRunsByAnalysis", async () => {
|
|
319
|
+
const values = [analysisId];
|
|
320
|
+
const text = q === undefined ? "" : `AND ${runTextCondition(`$${values.push(q)}`)}`;
|
|
321
|
+
const result = await pool.query({
|
|
322
|
+
text: `SELECT COUNT(*)::int AS total
|
|
323
|
+
FROM cortex_runs
|
|
324
|
+
WHERE analysis_id = $1
|
|
325
|
+
${text}`,
|
|
326
|
+
values,
|
|
327
|
+
});
|
|
328
|
+
return Number(result.rows[0]?.total ?? 0);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* The run ids of one plan of one analysis, newest start first, at most `limit`.
|
|
333
|
+
* The run id breaks a tie of start times, highest first. A plan with no run gives
|
|
334
|
+
* an empty list.
|
|
335
|
+
*/
|
|
336
|
+
export function queryRunIdsByPlan(pool, opts) {
|
|
337
|
+
return tryQuery("runs.queryRunIdsByPlan", async () => {
|
|
338
|
+
const result = await pool.query({
|
|
339
|
+
text: `SELECT run_id FROM cortex_runs
|
|
340
|
+
WHERE analysis_id = $1 AND plan_id = $2
|
|
341
|
+
ORDER BY started_at DESC, run_id DESC
|
|
342
|
+
LIMIT $3`,
|
|
343
|
+
values: [opts.analysisId, opts.planId, opts.limit],
|
|
344
|
+
});
|
|
345
|
+
return result.rows.map((row) => row.run_id);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The newest run of one plan of one analysis, or `null` for a plan with no run. The order is the one of
|
|
350
|
+
* {@link queryRunIdsByPlan}: the latest start first, and the highest run id on a tie of start times.
|
|
351
|
+
*/
|
|
352
|
+
export function queryNewestPlanRun(pool, opts) {
|
|
353
|
+
return tryQuery("runs.queryNewestPlanRun", async () => {
|
|
354
|
+
const result = await pool.query({
|
|
355
|
+
text: `SELECT run_id, started_at FROM cortex_runs
|
|
356
|
+
WHERE analysis_id = $1 AND plan_id = $2
|
|
357
|
+
ORDER BY started_at DESC, run_id DESC
|
|
358
|
+
LIMIT 1`,
|
|
359
|
+
values: [opts.analysisId, opts.planId],
|
|
360
|
+
});
|
|
361
|
+
const row = result.rows[0];
|
|
362
|
+
return row ? { runId: row.run_id, startedAt: row.started_at } : null;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
245
365
|
/**
|
|
246
366
|
* Read the analysis's non-terminal run activity without coupling it to a
|
|
247
367
|
* conversation thread. The total is counted before the detail limit so a
|
|
@@ -253,7 +373,7 @@ export function queryNonTerminalRunsByAnalysis(pool, analysisId, limit = 20) {
|
|
|
253
373
|
text: `SELECT COUNT(*)::int AS total
|
|
254
374
|
FROM cortex_runs
|
|
255
375
|
WHERE analysis_id = $1
|
|
256
|
-
AND status
|
|
376
|
+
AND status = 'running'`,
|
|
257
377
|
values: [analysisId],
|
|
258
378
|
});
|
|
259
379
|
const rowsResult = await pool.query({
|
|
@@ -262,13 +382,8 @@ export function queryNonTerminalRunsByAnalysis(pool, analysisId, limit = 20) {
|
|
|
262
382
|
mandate_jti, mandate_expires_at, plan_id
|
|
263
383
|
FROM cortex_runs
|
|
264
384
|
WHERE analysis_id = $1
|
|
265
|
-
AND status
|
|
266
|
-
ORDER BY
|
|
267
|
-
WHEN 'running' THEN 0
|
|
268
|
-
WHEN 'suspended_insufficient_funds' THEN 1
|
|
269
|
-
ELSE 2
|
|
270
|
-
END,
|
|
271
|
-
started_at DESC,
|
|
385
|
+
AND status = 'running'
|
|
386
|
+
ORDER BY started_at DESC,
|
|
272
387
|
run_id DESC
|
|
273
388
|
LIMIT $2`,
|
|
274
389
|
values: [analysisId, limit],
|
|
@@ -280,8 +395,7 @@ export function queryNonTerminalRunsByAnalysis(pool, analysisId, limit = 20) {
|
|
|
280
395
|
});
|
|
281
396
|
}
|
|
282
397
|
/**
|
|
283
|
-
* Read one stable inspection page: running first, then
|
|
284
|
-
* terminal history. This is deliberately separate from
|
|
398
|
+
* Read one stable inspection page: running first, then each other run. This is deliberately separate from
|
|
285
399
|
* {@link queryRunsByAnalysis}, whose chronological order is an embedder/UI
|
|
286
400
|
* contract.
|
|
287
401
|
*/
|
|
@@ -299,8 +413,7 @@ export function queryRunsForInspection(pool, analysisId, { limit = 50, offset =
|
|
|
299
413
|
WHERE analysis_id = $1
|
|
300
414
|
ORDER BY CASE status
|
|
301
415
|
WHEN 'running' THEN 0
|
|
302
|
-
|
|
303
|
-
ELSE 2
|
|
416
|
+
ELSE 1
|
|
304
417
|
END,
|
|
305
418
|
started_at DESC,
|
|
306
419
|
run_id DESC
|
|
@@ -313,16 +426,20 @@ export function queryRunsForInspection(pool, analysisId, { limit = 50, offset =
|
|
|
313
426
|
};
|
|
314
427
|
});
|
|
315
428
|
}
|
|
316
|
-
|
|
429
|
+
/** The runs of one thread of an analysis, newest start first. With `q`, the text filter applies before the window. */
|
|
430
|
+
export function queryRunsByThread(pool, analysisId, threadId, { limit = 50, offset = 0, q } = {}) {
|
|
317
431
|
return tryQuery("runs.queryRunsByThread", async () => {
|
|
432
|
+
const values = [analysisId, threadId, limit, offset];
|
|
433
|
+
const text = q === undefined ? "" : `AND ${runTextCondition(`$${values.push(q)}`)}`;
|
|
318
434
|
const result = await pool.query({
|
|
319
435
|
text: `SELECT run_id, analysis_id, thread_id, workflow_name,
|
|
320
436
|
status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
|
|
321
437
|
mandate_jti, mandate_expires_at, plan_id -- oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
|
|
322
438
|
FROM cortex_runs
|
|
323
439
|
WHERE analysis_id = $1 AND thread_id = $2
|
|
440
|
+
${text}
|
|
324
441
|
ORDER BY started_at DESC LIMIT $3 OFFSET $4`,
|
|
325
|
-
values
|
|
442
|
+
values,
|
|
326
443
|
});
|
|
327
444
|
return result.rows.map(mapRunRow);
|
|
328
445
|
});
|