stageflow 0.1.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.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/agent/activity.d.ts +76 -0
- package/dist/agent/activity.js +89 -0
- package/dist/agent/activityObserver.d.ts +17 -0
- package/dist/agent/activityObserver.js +66 -0
- package/dist/agent/cursorExtension.d.ts +12 -0
- package/dist/agent/cursorExtension.js +88 -0
- package/dist/agent/cursorProvider.d.ts +14 -0
- package/dist/agent/cursorProvider.js +139 -0
- package/dist/agent/fakeAgent.d.ts +29 -0
- package/dist/agent/fakeAgent.js +295 -0
- package/dist/agent/oauthSessionManager.d.ts +33 -0
- package/dist/agent/oauthSessionManager.js +214 -0
- package/dist/agent/piAdapter.d.ts +128 -0
- package/dist/agent/piAdapter.js +967 -0
- package/dist/agent/port.d.ts +63 -0
- package/dist/agent/port.js +46 -0
- package/dist/agent/providerAuth.d.ts +101 -0
- package/dist/agent/providerAuth.js +282 -0
- package/dist/agent/providerSupport.d.ts +28 -0
- package/dist/agent/providerSupport.js +19 -0
- package/dist/cli/providersCommand.d.ts +11 -0
- package/dist/cli/providersCommand.js +275 -0
- package/dist/cli/terminalAuthInteraction.d.ts +8 -0
- package/dist/cli/terminalAuthInteraction.js +65 -0
- package/dist/cli/terminalSecret.d.ts +2 -0
- package/dist/cli/terminalSecret.js +67 -0
- package/dist/cli/validateCommand.d.ts +11 -0
- package/dist/cli/validateCommand.js +81 -0
- package/dist/cli/validateOutput.d.ts +8 -0
- package/dist/cli/validateOutput.js +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +273 -0
- package/dist/config/createPipeline.d.ts +31 -0
- package/dist/config/createPipeline.js +255 -0
- package/dist/config/createStage.d.ts +25 -0
- package/dist/config/createStage.js +181 -0
- package/dist/config/listConfig.d.ts +33 -0
- package/dist/config/listConfig.js +195 -0
- package/dist/config/listExtensions.d.ts +26 -0
- package/dist/config/listExtensions.js +50 -0
- package/dist/config/listSkills.d.ts +29 -0
- package/dist/config/listSkills.js +55 -0
- package/dist/config/loadOutcome.d.ts +27 -0
- package/dist/config/loadOutcome.js +6 -0
- package/dist/config/loadPipeline.d.ts +25 -0
- package/dist/config/loadPipeline.js +196 -0
- package/dist/config/loadStage.d.ts +4 -0
- package/dist/config/loadStage.js +127 -0
- package/dist/config/loadTask.d.ts +7 -0
- package/dist/config/loadTask.js +69 -0
- package/dist/config/readYamlObject.d.ts +1 -0
- package/dist/config/readYamlObject.js +5 -0
- package/dist/config/resolvePipelineDag.d.ts +12 -0
- package/dist/config/resolvePipelineDag.js +282 -0
- package/dist/config/validateCatalog.d.ts +36 -0
- package/dist/config/validateCatalog.js +322 -0
- package/dist/envelope/check.d.ts +3 -0
- package/dist/envelope/check.js +52 -0
- package/dist/envelope/payloadSchema.d.ts +10 -0
- package/dist/envelope/payloadSchema.js +91 -0
- package/dist/hitl/qaTrail.d.ts +31 -0
- package/dist/hitl/qaTrail.js +65 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +7 -0
- package/dist/mcp/projectRun.d.ts +37 -0
- package/dist/mcp/projectRun.js +57 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +46 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +26 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +96 -0
- package/dist/package-meta.d.ts +1 -0
- package/dist/package-meta.js +1 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -0
- package/dist/prompt/priorEnvelope.js +5 -0
- package/dist/runstore/catalog.d.ts +8 -0
- package/dist/runstore/catalog.js +16 -0
- package/dist/runstore/createStore.d.ts +15 -0
- package/dist/runstore/createStore.js +23 -0
- package/dist/runstore/disk/DiskRunStore.d.ts +30 -0
- package/dist/runstore/disk/DiskRunStore.js +238 -0
- package/dist/runstore/disk/catalogHelpers.d.ts +11 -0
- package/dist/runstore/disk/catalogHelpers.js +102 -0
- package/dist/runstore/layout.d.ts +22 -0
- package/dist/runstore/layout.js +58 -0
- package/dist/runstore/paths.d.ts +7 -0
- package/dist/runstore/paths.js +60 -0
- package/dist/runstore/pipelineDagSnapshot.d.ts +5 -0
- package/dist/runstore/pipelineDagSnapshot.js +43 -0
- package/dist/runstore/port.d.ts +146 -0
- package/dist/runstore/port.js +28 -0
- package/dist/runstore/runProjection.d.ts +6 -0
- package/dist/runstore/runProjection.js +102 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +48 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +460 -0
- package/dist/runstore/sqlite/migrateFromDisk.d.ts +5 -0
- package/dist/runstore/sqlite/migrateFromDisk.js +108 -0
- package/dist/runstore/sqlite/schema.d.ts +1 -0
- package/dist/runstore/sqlite/schema.js +59 -0
- package/dist/runstore/stageExecution.d.ts +13 -0
- package/dist/runstore/stageExecution.js +22 -0
- package/dist/runstore/stageSnapshot.d.ts +2 -0
- package/dist/runstore/stageSnapshot.js +46 -0
- package/dist/runstore/syntheticStageSnapshot.d.ts +2 -0
- package/dist/runstore/syntheticStageSnapshot.js +10 -0
- package/dist/runstore/trackProjection.d.ts +7 -0
- package/dist/runstore/trackProjection.js +94 -0
- package/dist/runstore/workspaceLayout.d.ts +31 -0
- package/dist/runstore/workspaceLayout.js +172 -0
- package/dist/runtime/answerResume.d.ts +16 -0
- package/dist/runtime/answerResume.js +22 -0
- package/dist/runtime/credentialBinding.d.ts +16 -0
- package/dist/runtime/credentialBinding.js +80 -0
- package/dist/runtime/dagTraversal.d.ts +3 -0
- package/dist/runtime/dagTraversal.js +26 -0
- package/dist/runtime/envelopeRouting.d.ts +20 -0
- package/dist/runtime/envelopeRouting.js +59 -0
- package/dist/runtime/hitlSeams.d.ts +38 -0
- package/dist/runtime/hitlSeams.js +2 -0
- package/dist/runtime/pipelineRunner.d.ts +77 -0
- package/dist/runtime/pipelineRunner.js +141 -0
- package/dist/runtime/pipelineScheduler.d.ts +90 -0
- package/dist/runtime/pipelineScheduler.js +494 -0
- package/dist/runtime/pipelineValidationError.d.ts +6 -0
- package/dist/runtime/pipelineValidationError.js +8 -0
- package/dist/runtime/resumeReconstruct.d.ts +33 -0
- package/dist/runtime/resumeReconstruct.js +144 -0
- package/dist/runtime/runManager.d.ts +129 -0
- package/dist/runtime/runManager.js +818 -0
- package/dist/runtime/runRetryCoordinator.d.ts +85 -0
- package/dist/runtime/runRetryCoordinator.js +372 -0
- package/dist/runtime/settingsFile.d.ts +17 -0
- package/dist/runtime/settingsFile.js +88 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +40 -0
- package/dist/runtime/stageAttemptBootstrap.js +82 -0
- package/dist/runtime/stageAttemptContext.d.ts +11 -0
- package/dist/runtime/stageAttemptContext.js +26 -0
- package/dist/runtime/stageConcurrency.d.ts +10 -0
- package/dist/runtime/stageConcurrency.js +51 -0
- package/dist/runtime/stageHitl.d.ts +95 -0
- package/dist/runtime/stageHitl.js +198 -0
- package/dist/runtime/stageProcessLauncher.d.ts +45 -0
- package/dist/runtime/stageProcessLauncher.js +199 -0
- package/dist/runtime/stageRecovery.d.ts +10 -0
- package/dist/runtime/stageRecovery.js +43 -0
- package/dist/runtime/stageRoots.d.ts +22 -0
- package/dist/runtime/stageRoots.js +93 -0
- package/dist/runtime/stageRunner.d.ts +59 -0
- package/dist/runtime/stageRunner.js +221 -0
- package/dist/runtime/stageWorker.d.ts +6 -0
- package/dist/runtime/stageWorker.js +109 -0
- package/dist/runtime/stageWorkerProtocol.d.ts +27 -0
- package/dist/runtime/stageWorkerProtocol.js +16 -0
- package/dist/runtime/taskInput.d.ts +15 -0
- package/dist/runtime/taskInput.js +38 -0
- package/dist/server/http.d.ts +31 -0
- package/dist/server/http.js +561 -0
- package/dist/server/providerRoutes.d.ts +15 -0
- package/dist/server/providerRoutes.js +186 -0
- package/dist/tools/askOperator.d.ts +185 -0
- package/dist/tools/askOperator.js +409 -0
- package/dist/tools/emitStageEnvelope.d.ts +42 -0
- package/dist/tools/emitStageEnvelope.js +61 -0
- package/dist/tools/writeStageArtifact.d.ts +26 -0
- package/dist/tools/writeStageArtifact.js +103 -0
- package/dist/types/envelope.d.ts +12 -0
- package/dist/types/envelope.js +6 -0
- package/dist/types/pipeline.d.ts +25 -0
- package/dist/types/pipeline.js +1 -0
- package/dist/types/stage.d.ts +12 -0
- package/dist/types/stage.js +6 -0
- package/dist/types/task.d.ts +7 -0
- package/dist/types/task.js +1 -0
- package/dist/ui/assets/index-Cry0Tpfx.js +118 -0
- package/dist/ui/assets/index-DCsDopak.css +1 -0
- package/dist/ui/index.html +21 -0
- package/package.json +72 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { projectRunDetail, projectRunSummary, orderStageSnapshots } from "../runProjection.js";
|
|
5
|
+
import { buildStageSnapshotFromStore } from "../stageSnapshot.js";
|
|
6
|
+
import { parsePipelineDagSnapshot } from "../pipelineDagSnapshot.js";
|
|
7
|
+
import { newRunId, runWorkspaceDir } from "../paths.js";
|
|
8
|
+
import { attemptAgentDir, attemptArtifactsDir, stageDir, } from "../workspaceLayout.js";
|
|
9
|
+
import { importDiskRunsIfEmpty } from "./migrateFromDisk.js";
|
|
10
|
+
import { SCHEMA_SQL } from "./schema.js";
|
|
11
|
+
const ensuredStageDirs = new Set();
|
|
12
|
+
const DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5000;
|
|
13
|
+
function readSqliteBusyTimeoutMs() {
|
|
14
|
+
const raw = process.env.STAGEFLOW_SQLITE_BUSY_TIMEOUT_MS;
|
|
15
|
+
if (raw === undefined || raw.trim() === "") {
|
|
16
|
+
return DEFAULT_SQLITE_BUSY_TIMEOUT_MS;
|
|
17
|
+
}
|
|
18
|
+
const parsed = Number.parseInt(raw, 10);
|
|
19
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
20
|
+
return DEFAULT_SQLITE_BUSY_TIMEOUT_MS;
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
function ensureCheckoutRootColumn(db) {
|
|
25
|
+
const cols = db.prepare(`PRAGMA table_info(runs)`).all();
|
|
26
|
+
if (!cols.some((c) => c.name === "checkout_root")) {
|
|
27
|
+
db.exec(`ALTER TABLE runs ADD COLUMN checkout_root TEXT`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function ensurePipelineDagColumn(db) {
|
|
31
|
+
const cols = db.prepare(`PRAGMA table_info(runs)`).all();
|
|
32
|
+
if (!cols.some((c) => c.name === "pipeline_dag_json")) {
|
|
33
|
+
db.exec(`ALTER TABLE runs ADD COLUMN pipeline_dag_json TEXT`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function ensureStageExecutionsTable(db) {
|
|
37
|
+
db.exec(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS stage_executions (
|
|
39
|
+
run_id TEXT NOT NULL,
|
|
40
|
+
stage_id TEXT NOT NULL,
|
|
41
|
+
attempt INTEGER NOT NULL,
|
|
42
|
+
status TEXT NOT NULL,
|
|
43
|
+
started_at TEXT,
|
|
44
|
+
finished_at TEXT,
|
|
45
|
+
envelope_json TEXT,
|
|
46
|
+
PRIMARY KEY (run_id, stage_id, attempt),
|
|
47
|
+
FOREIGN KEY (run_id) REFERENCES runs(run_id)
|
|
48
|
+
);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_stage_executions_run_stage
|
|
50
|
+
ON stage_executions (run_id, stage_id, attempt);
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
function ensureStageEventsAttemptColumn(db) {
|
|
54
|
+
const cols = db
|
|
55
|
+
.prepare(`PRAGMA table_info(stage_events)`)
|
|
56
|
+
.all();
|
|
57
|
+
if (!cols.some((c) => c.name === "attempt")) {
|
|
58
|
+
db.exec(`ALTER TABLE stage_events ADD COLUMN attempt INTEGER NOT NULL DEFAULT 1`);
|
|
59
|
+
db.exec(`
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_stage_events_run_stage_attempt_at
|
|
61
|
+
ON stage_events (run_id, stage_id, attempt, at);
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function executionFromRow(row) {
|
|
66
|
+
return {
|
|
67
|
+
run_id: row.run_id,
|
|
68
|
+
stage_id: row.stage_id,
|
|
69
|
+
attempt: row.attempt,
|
|
70
|
+
status: row.status,
|
|
71
|
+
...(row.started_at != null ? { started_at: row.started_at } : {}),
|
|
72
|
+
...(row.finished_at != null ? { finished_at: row.finished_at } : {}),
|
|
73
|
+
envelope: row.envelope_json
|
|
74
|
+
? JSON.parse(row.envelope_json)
|
|
75
|
+
: null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export class SqliteRunStore {
|
|
79
|
+
storeRoot;
|
|
80
|
+
db;
|
|
81
|
+
migratePromise;
|
|
82
|
+
constructor(storeRoot) {
|
|
83
|
+
this.storeRoot = storeRoot;
|
|
84
|
+
const dbPath = path.join(storeRoot, "state.db");
|
|
85
|
+
this.db = new Database(dbPath);
|
|
86
|
+
this.db.pragma("journal_mode = WAL");
|
|
87
|
+
this.db.pragma(`busy_timeout = ${readSqliteBusyTimeoutMs()}`);
|
|
88
|
+
this.db.exec(SCHEMA_SQL);
|
|
89
|
+
ensureCheckoutRootColumn(this.db);
|
|
90
|
+
ensurePipelineDagColumn(this.db);
|
|
91
|
+
ensureStageExecutionsTable(this.db);
|
|
92
|
+
ensureStageEventsAttemptColumn(this.db);
|
|
93
|
+
this.migratePromise = importDiskRunsIfEmpty(this.db, storeRoot).then(() => undefined);
|
|
94
|
+
}
|
|
95
|
+
/** Await one-shot disk import before catalog reads (create paths also wait). */
|
|
96
|
+
async ready() {
|
|
97
|
+
await this.migratePromise;
|
|
98
|
+
}
|
|
99
|
+
getWorkspaceDir(runId) {
|
|
100
|
+
return runWorkspaceDir(this.storeRoot, runId);
|
|
101
|
+
}
|
|
102
|
+
async createRun(input) {
|
|
103
|
+
await this.ready();
|
|
104
|
+
const runId = newRunId();
|
|
105
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
106
|
+
await mkdir(path.join(workspaceDir, "stages"), { recursive: true });
|
|
107
|
+
const now = new Date().toISOString();
|
|
108
|
+
this.db
|
|
109
|
+
.prepare(`INSERT INTO runs
|
|
110
|
+
(run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json)
|
|
111
|
+
VALUES
|
|
112
|
+
(@run_id, @pipeline_id, @task_id, @task_yaml, @status, @created_at, @updated_at, @checkout_root, @pipeline_dag_json)`)
|
|
113
|
+
.run({
|
|
114
|
+
run_id: runId,
|
|
115
|
+
pipeline_id: input.pipelineId,
|
|
116
|
+
task_id: input.taskId ?? null,
|
|
117
|
+
task_yaml: input.taskYaml,
|
|
118
|
+
status: "running",
|
|
119
|
+
created_at: now,
|
|
120
|
+
updated_at: now,
|
|
121
|
+
checkout_root: input.checkoutRoot ?? null,
|
|
122
|
+
pipeline_dag_json: input.pipelineDag
|
|
123
|
+
? JSON.stringify(input.pipelineDag)
|
|
124
|
+
: null,
|
|
125
|
+
});
|
|
126
|
+
return { runId, workspaceDir };
|
|
127
|
+
}
|
|
128
|
+
async updateRunStatus(runId, status) {
|
|
129
|
+
await this.ready();
|
|
130
|
+
const result = this.db
|
|
131
|
+
.prepare(`UPDATE runs SET status = @status, updated_at = @updated_at WHERE run_id = @run_id`)
|
|
132
|
+
.run({
|
|
133
|
+
run_id: runId,
|
|
134
|
+
status,
|
|
135
|
+
updated_at: new Date().toISOString(),
|
|
136
|
+
});
|
|
137
|
+
if (result.changes === 0) {
|
|
138
|
+
throw new Error(`Run not found: ${runId}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async readRunMeta(runId) {
|
|
142
|
+
await this.ready();
|
|
143
|
+
return this.runMetaFromRow(this.getRunRow(runId));
|
|
144
|
+
}
|
|
145
|
+
async readTaskYaml(runId) {
|
|
146
|
+
await this.ready();
|
|
147
|
+
return this.getRunRow(runId).task_yaml;
|
|
148
|
+
}
|
|
149
|
+
async ensureStageWorkspace(runId, stageId) {
|
|
150
|
+
await this.ready();
|
|
151
|
+
this.getRunRow(runId);
|
|
152
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
153
|
+
const dir = stageDir(workspaceDir, stageId);
|
|
154
|
+
if (!ensuredStageDirs.has(dir)) {
|
|
155
|
+
await mkdir(dir, { recursive: true });
|
|
156
|
+
ensuredStageDirs.add(dir);
|
|
157
|
+
}
|
|
158
|
+
this.db
|
|
159
|
+
.prepare(`INSERT OR IGNORE INTO stages (run_id, stage_id, status)
|
|
160
|
+
VALUES (@run_id, @stage_id, @status)`)
|
|
161
|
+
.run({ run_id: runId, stage_id: stageId, status: "pending" });
|
|
162
|
+
}
|
|
163
|
+
async ensureAttemptWorkspace(runId, stageId, attempt) {
|
|
164
|
+
await this.ready();
|
|
165
|
+
await this.ensureStageWorkspace(runId, stageId);
|
|
166
|
+
this.getRunRow(runId);
|
|
167
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
168
|
+
const key = `${runId}:${stageId}:${attempt}`;
|
|
169
|
+
if (!ensuredStageDirs.has(key)) {
|
|
170
|
+
await mkdir(attemptArtifactsDir(workspaceDir, stageId, attempt), {
|
|
171
|
+
recursive: true,
|
|
172
|
+
});
|
|
173
|
+
await mkdir(attemptAgentDir(workspaceDir, stageId, attempt), {
|
|
174
|
+
recursive: true,
|
|
175
|
+
});
|
|
176
|
+
ensuredStageDirs.add(key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async createStageExecution(runId, stageId) {
|
|
180
|
+
await this.ready();
|
|
181
|
+
this.getRunRow(runId);
|
|
182
|
+
return this.db.transaction(() => {
|
|
183
|
+
const maxRow = this.db
|
|
184
|
+
.prepare(`SELECT MAX(attempt) AS max_attempt FROM stage_executions
|
|
185
|
+
WHERE run_id = ? AND stage_id = ?`)
|
|
186
|
+
.get(runId, stageId);
|
|
187
|
+
const attempt = (maxRow?.max_attempt ?? 0) + 1;
|
|
188
|
+
this.db
|
|
189
|
+
.prepare(`INSERT INTO stage_executions (run_id, stage_id, attempt, status)
|
|
190
|
+
VALUES (@run_id, @stage_id, @attempt, @status)`)
|
|
191
|
+
.run({
|
|
192
|
+
run_id: runId,
|
|
193
|
+
stage_id: stageId,
|
|
194
|
+
attempt,
|
|
195
|
+
status: "pending",
|
|
196
|
+
});
|
|
197
|
+
const row = this.db
|
|
198
|
+
.prepare(`SELECT run_id, stage_id, attempt, status, started_at, finished_at, envelope_json
|
|
199
|
+
FROM stage_executions
|
|
200
|
+
WHERE run_id = ? AND stage_id = ? AND attempt = ?`)
|
|
201
|
+
.get(runId, stageId, attempt);
|
|
202
|
+
return executionFromRow(row);
|
|
203
|
+
})();
|
|
204
|
+
}
|
|
205
|
+
async listStageExecutions(runId, stageId) {
|
|
206
|
+
await this.ready();
|
|
207
|
+
const rows = this.db
|
|
208
|
+
.prepare(`SELECT run_id, stage_id, attempt, status, started_at, finished_at, envelope_json
|
|
209
|
+
FROM stage_executions
|
|
210
|
+
WHERE run_id = ? AND stage_id = ?
|
|
211
|
+
ORDER BY attempt ASC`)
|
|
212
|
+
.all(runId, stageId);
|
|
213
|
+
return rows.map(executionFromRow);
|
|
214
|
+
}
|
|
215
|
+
async getLatestStageExecution(runId, stageId) {
|
|
216
|
+
await this.ready();
|
|
217
|
+
const row = this.db
|
|
218
|
+
.prepare(`SELECT run_id, stage_id, attempt, status, started_at, finished_at, envelope_json
|
|
219
|
+
FROM stage_executions
|
|
220
|
+
WHERE run_id = ? AND stage_id = ?
|
|
221
|
+
ORDER BY attempt DESC
|
|
222
|
+
LIMIT 1`)
|
|
223
|
+
.get(runId, stageId);
|
|
224
|
+
return row ? executionFromRow(row) : null;
|
|
225
|
+
}
|
|
226
|
+
async countStageAttempts(runId, stageId) {
|
|
227
|
+
await this.ready();
|
|
228
|
+
const row = this.db
|
|
229
|
+
.prepare(`SELECT COUNT(*) AS n FROM stage_executions
|
|
230
|
+
WHERE run_id = ? AND stage_id = ?`)
|
|
231
|
+
.get(runId, stageId);
|
|
232
|
+
return row.n;
|
|
233
|
+
}
|
|
234
|
+
async getStageExecution(runId, stageId, attempt) {
|
|
235
|
+
await this.ready();
|
|
236
|
+
const row = this.db
|
|
237
|
+
.prepare(`SELECT run_id, stage_id, attempt, status, started_at, finished_at, envelope_json
|
|
238
|
+
FROM stage_executions
|
|
239
|
+
WHERE run_id = ? AND stage_id = ? AND attempt = ?`)
|
|
240
|
+
.get(runId, stageId, attempt);
|
|
241
|
+
if (!row) {
|
|
242
|
+
throw new Error(`Stage execution not found: ${runId}/${stageId} attempt ${attempt}`);
|
|
243
|
+
}
|
|
244
|
+
return executionFromRow(row);
|
|
245
|
+
}
|
|
246
|
+
async updateStageExecution(runId, stageId, attempt, patch) {
|
|
247
|
+
await this.ready();
|
|
248
|
+
const sets = [];
|
|
249
|
+
const params = {
|
|
250
|
+
run_id: runId,
|
|
251
|
+
stage_id: stageId,
|
|
252
|
+
attempt,
|
|
253
|
+
};
|
|
254
|
+
if (patch.status !== undefined) {
|
|
255
|
+
sets.push("status = @status");
|
|
256
|
+
params.status = patch.status;
|
|
257
|
+
}
|
|
258
|
+
if (patch.started_at !== undefined) {
|
|
259
|
+
sets.push("started_at = @started_at");
|
|
260
|
+
params.started_at = patch.started_at;
|
|
261
|
+
}
|
|
262
|
+
if (patch.finished_at !== undefined) {
|
|
263
|
+
sets.push("finished_at = @finished_at");
|
|
264
|
+
params.finished_at = patch.finished_at;
|
|
265
|
+
}
|
|
266
|
+
if (patch.envelope !== undefined) {
|
|
267
|
+
sets.push("envelope_json = @envelope_json");
|
|
268
|
+
params.envelope_json =
|
|
269
|
+
patch.envelope != null ? JSON.stringify(patch.envelope) : null;
|
|
270
|
+
}
|
|
271
|
+
if (sets.length === 0)
|
|
272
|
+
return;
|
|
273
|
+
const result = this.db
|
|
274
|
+
.prepare(`UPDATE stage_executions SET ${sets.join(", ")}
|
|
275
|
+
WHERE run_id = @run_id AND stage_id = @stage_id AND attempt = @attempt`)
|
|
276
|
+
.run(params);
|
|
277
|
+
if (result.changes === 0) {
|
|
278
|
+
throw new Error(`Stage execution not found: ${runId}/${stageId} attempt ${attempt}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async writeEnvelope(runId, stageId, envelope, options) {
|
|
282
|
+
const attempt = options?.attempt ?? 1;
|
|
283
|
+
await this.ensureAttemptWorkspace(runId, stageId, attempt);
|
|
284
|
+
this.db
|
|
285
|
+
.prepare(`INSERT INTO stages
|
|
286
|
+
(run_id, stage_id, status, summary, envelope_json)
|
|
287
|
+
VALUES
|
|
288
|
+
(@run_id, @stage_id, @status, @summary, @envelope_json)
|
|
289
|
+
ON CONFLICT(run_id, stage_id) DO UPDATE SET
|
|
290
|
+
summary = excluded.summary,
|
|
291
|
+
envelope_json = excluded.envelope_json`)
|
|
292
|
+
.run({
|
|
293
|
+
run_id: runId,
|
|
294
|
+
stage_id: stageId,
|
|
295
|
+
status: envelope.status === "success" ? "succeeded" : "failed",
|
|
296
|
+
summary: envelope.summary,
|
|
297
|
+
envelope_json: JSON.stringify(envelope),
|
|
298
|
+
});
|
|
299
|
+
await this.updateStageExecution(runId, stageId, attempt, { envelope });
|
|
300
|
+
}
|
|
301
|
+
async readEnvelope(runId, stageId) {
|
|
302
|
+
await this.ready();
|
|
303
|
+
const execution = await this.getLatestStageExecution(runId, stageId);
|
|
304
|
+
if (execution?.envelope != null) {
|
|
305
|
+
return execution.envelope;
|
|
306
|
+
}
|
|
307
|
+
const fromStages = this.readEnvelopeFromDb(runId, stageId);
|
|
308
|
+
if (fromStages != null) {
|
|
309
|
+
return fromStages;
|
|
310
|
+
}
|
|
311
|
+
throw new Error(`Envelope not found: ${runId}/${stageId}`);
|
|
312
|
+
}
|
|
313
|
+
readEnvelopeFromDb(runId, stageId) {
|
|
314
|
+
const row = this.db
|
|
315
|
+
.prepare(`SELECT envelope_json FROM stages WHERE run_id = ? AND stage_id = ?`)
|
|
316
|
+
.get(runId, stageId);
|
|
317
|
+
if (row?.envelope_json == null) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return JSON.parse(row.envelope_json);
|
|
321
|
+
}
|
|
322
|
+
async appendStageEvent(runId, stageId, event, options) {
|
|
323
|
+
const attempt = options?.attempt ?? 1;
|
|
324
|
+
await this.ensureAttemptWorkspace(runId, stageId, attempt);
|
|
325
|
+
const at = new Date().toISOString();
|
|
326
|
+
const eventName = event.event;
|
|
327
|
+
const rest = { ...event };
|
|
328
|
+
delete rest.event;
|
|
329
|
+
delete rest.at;
|
|
330
|
+
this.db
|
|
331
|
+
.prepare(`INSERT INTO stage_events (run_id, stage_id, attempt, at, event, payload_json)
|
|
332
|
+
VALUES (@run_id, @stage_id, @attempt, @at, @event, @payload_json)`)
|
|
333
|
+
.run({
|
|
334
|
+
run_id: runId,
|
|
335
|
+
stage_id: stageId,
|
|
336
|
+
attempt,
|
|
337
|
+
at,
|
|
338
|
+
event: eventName,
|
|
339
|
+
payload_json: Object.keys(rest).length > 0 ? JSON.stringify(rest) : null,
|
|
340
|
+
});
|
|
341
|
+
this.updateStageStatusDenorm(runId, stageId, eventName, at);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* @remarks Intentional denormalization: mirrors lifecycle events into
|
|
345
|
+
* `stages.status` for legacy compatibility. This column is not the
|
|
346
|
+
* authoritative source for snapshot projection; status is event-derived
|
|
347
|
+
* in `buildStageSnapshotFromStore`.
|
|
348
|
+
*/
|
|
349
|
+
updateStageStatusDenorm(runId, stageId, eventName, at) {
|
|
350
|
+
if (eventName === "started" || eventName === "resumed") {
|
|
351
|
+
this.db
|
|
352
|
+
.prepare(`UPDATE stages SET status = 'running', started_at = COALESCE(started_at, @at)
|
|
353
|
+
WHERE run_id = @run_id AND stage_id = @stage_id`)
|
|
354
|
+
.run({ run_id: runId, stage_id: stageId, at });
|
|
355
|
+
}
|
|
356
|
+
else if (eventName === "waiting_for_input") {
|
|
357
|
+
this.db
|
|
358
|
+
.prepare(`UPDATE stages SET status = 'waiting_for_input'
|
|
359
|
+
WHERE run_id = @run_id AND stage_id = @stage_id`)
|
|
360
|
+
.run({ run_id: runId, stage_id: stageId });
|
|
361
|
+
}
|
|
362
|
+
else if (eventName === "succeeded" || eventName === "failed") {
|
|
363
|
+
this.db
|
|
364
|
+
.prepare(`UPDATE stages SET status = @status, finished_at = @at
|
|
365
|
+
WHERE run_id = @run_id AND stage_id = @stage_id`)
|
|
366
|
+
.run({
|
|
367
|
+
run_id: runId,
|
|
368
|
+
stage_id: stageId,
|
|
369
|
+
status: eventName,
|
|
370
|
+
at,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async listStageEvents(runId, stageId, attempt) {
|
|
375
|
+
await this.ready();
|
|
376
|
+
return this.loadEvents(runId, stageId, attempt);
|
|
377
|
+
}
|
|
378
|
+
async listRuns() {
|
|
379
|
+
await this.ready();
|
|
380
|
+
const rows = this.db
|
|
381
|
+
.prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json
|
|
382
|
+
FROM runs ORDER BY created_at DESC`)
|
|
383
|
+
.all();
|
|
384
|
+
const summaries = [];
|
|
385
|
+
for (const row of rows) {
|
|
386
|
+
const dagSnapshot = this.readPipelineDagSnapshotFromRow(row);
|
|
387
|
+
const stages = await this.loadStageSnapshots(row.run_id, dagSnapshot?.stage_ids);
|
|
388
|
+
summaries.push(projectRunSummary(this.runMetaFromRow(row), stages));
|
|
389
|
+
}
|
|
390
|
+
return summaries;
|
|
391
|
+
}
|
|
392
|
+
async readRun(runId) {
|
|
393
|
+
await this.ready();
|
|
394
|
+
const row = this.getRunRow(runId);
|
|
395
|
+
const dagSnapshot = this.readPipelineDagSnapshotFromRow(row);
|
|
396
|
+
const stages = await this.loadStageSnapshots(runId, dagSnapshot?.stage_ids);
|
|
397
|
+
return projectRunDetail(this.runMetaFromRow(row), stages, row.task_yaml, dagSnapshot);
|
|
398
|
+
}
|
|
399
|
+
readPipelineDagSnapshotFromRow(row) {
|
|
400
|
+
if (!row.pipeline_dag_json)
|
|
401
|
+
return null;
|
|
402
|
+
return parsePipelineDagSnapshot(JSON.parse(row.pipeline_dag_json));
|
|
403
|
+
}
|
|
404
|
+
runMetaFromRow(row) {
|
|
405
|
+
return {
|
|
406
|
+
run_id: row.run_id,
|
|
407
|
+
pipeline_id: row.pipeline_id,
|
|
408
|
+
created_at: row.created_at,
|
|
409
|
+
status: row.status,
|
|
410
|
+
task_id: row.task_id ?? undefined,
|
|
411
|
+
updated_at: row.updated_at,
|
|
412
|
+
...(row.checkout_root != null ? { checkout_root: row.checkout_root } : {}),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
getRunRow(runId) {
|
|
416
|
+
const row = this.db
|
|
417
|
+
.prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json
|
|
418
|
+
FROM runs WHERE run_id = ?`)
|
|
419
|
+
.get(runId);
|
|
420
|
+
if (!row)
|
|
421
|
+
throw new Error(`Run not found: ${runId}`);
|
|
422
|
+
return row;
|
|
423
|
+
}
|
|
424
|
+
loadEvents(runId, stageId, attempt) {
|
|
425
|
+
const rows = attempt
|
|
426
|
+
? this.db
|
|
427
|
+
.prepare(`SELECT at, event, payload_json FROM stage_events
|
|
428
|
+
WHERE run_id = ? AND stage_id = ? AND attempt = ?
|
|
429
|
+
ORDER BY id ASC`)
|
|
430
|
+
.all(runId, stageId, attempt)
|
|
431
|
+
: this.db
|
|
432
|
+
.prepare(`SELECT at, event, payload_json FROM stage_events
|
|
433
|
+
WHERE run_id = ? AND stage_id = ?
|
|
434
|
+
ORDER BY id ASC`)
|
|
435
|
+
.all(runId, stageId);
|
|
436
|
+
return rows.map((row) => {
|
|
437
|
+
const payload = row.payload_json
|
|
438
|
+
? JSON.parse(row.payload_json)
|
|
439
|
+
: {};
|
|
440
|
+
return { event: row.event, at: row.at, ...payload };
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async loadStageSnapshots(runId, stageIds) {
|
|
444
|
+
const workspaceDir = this.getWorkspaceDir(runId);
|
|
445
|
+
const rows = this.db
|
|
446
|
+
.prepare(`SELECT stage_id FROM stages WHERE run_id = ? ORDER BY stage_id ASC`)
|
|
447
|
+
.all(runId);
|
|
448
|
+
const ids = stageIds?.length
|
|
449
|
+
? stageIds
|
|
450
|
+
: rows.map((row) => row.stage_id);
|
|
451
|
+
const snapshots = await Promise.all(ids.map((stageId) => buildStageSnapshotFromStore(this, runId, stageId, workspaceDir)));
|
|
452
|
+
if (stageIds?.length) {
|
|
453
|
+
return orderStageSnapshots(stageIds, snapshots);
|
|
454
|
+
}
|
|
455
|
+
return snapshots;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
export function openSqliteRunStore(storeRoot) {
|
|
459
|
+
return new SqliteRunStore(storeRoot);
|
|
460
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildRunDetailFromDisk, fileExists, readRunMetaFromDisk, } from "../disk/catalogHelpers.js";
|
|
4
|
+
import { runsDir } from "../paths.js";
|
|
5
|
+
/**
|
|
6
|
+
* One-shot import of legacy disk run folders into SQLite when the DB has no runs yet.
|
|
7
|
+
*/
|
|
8
|
+
export async function importDiskRunsIfEmpty(db, storeRoot) {
|
|
9
|
+
const count = db.prepare("SELECT COUNT(*) AS n FROM runs").get().n;
|
|
10
|
+
if (count > 0)
|
|
11
|
+
return 0;
|
|
12
|
+
const root = runsDir(storeRoot);
|
|
13
|
+
if (!(await fileExists(root)))
|
|
14
|
+
return 0;
|
|
15
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
16
|
+
let imported = 0;
|
|
17
|
+
const insertRun = db.prepare(`
|
|
18
|
+
INSERT OR IGNORE INTO runs
|
|
19
|
+
(run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root)
|
|
20
|
+
VALUES
|
|
21
|
+
(@run_id, @pipeline_id, @task_id, @task_yaml, @status, @created_at, @updated_at, @checkout_root)
|
|
22
|
+
`);
|
|
23
|
+
const upsertStage = db.prepare(`
|
|
24
|
+
INSERT INTO stages
|
|
25
|
+
(run_id, stage_id, status, summary, envelope_json, started_at, finished_at)
|
|
26
|
+
VALUES
|
|
27
|
+
(@run_id, @stage_id, @status, @summary, @envelope_json, @started_at, @finished_at)
|
|
28
|
+
ON CONFLICT(run_id, stage_id) DO UPDATE SET
|
|
29
|
+
status = excluded.status,
|
|
30
|
+
summary = excluded.summary,
|
|
31
|
+
envelope_json = excluded.envelope_json,
|
|
32
|
+
started_at = excluded.started_at,
|
|
33
|
+
finished_at = excluded.finished_at
|
|
34
|
+
`);
|
|
35
|
+
const insertEvent = db.prepare(`
|
|
36
|
+
INSERT INTO stage_events (run_id, stage_id, attempt, at, event, payload_json)
|
|
37
|
+
VALUES (@run_id, @stage_id, @attempt, @at, @event, @payload_json)
|
|
38
|
+
`);
|
|
39
|
+
const insertExecution = db.prepare(`
|
|
40
|
+
INSERT INTO stage_executions
|
|
41
|
+
(run_id, stage_id, attempt, status, started_at, finished_at, envelope_json)
|
|
42
|
+
VALUES
|
|
43
|
+
(@run_id, @stage_id, @attempt, @status, @started_at, @finished_at, @envelope_json)
|
|
44
|
+
`);
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (!entry.isDirectory())
|
|
47
|
+
continue;
|
|
48
|
+
const workspaceDir = path.join(root, entry.name);
|
|
49
|
+
try {
|
|
50
|
+
if (!(await fileExists(path.join(workspaceDir, "meta.json"))))
|
|
51
|
+
continue;
|
|
52
|
+
const detail = await buildRunDetailFromDisk(workspaceDir);
|
|
53
|
+
const meta = await readRunMetaFromDisk(workspaceDir);
|
|
54
|
+
db.transaction(() => {
|
|
55
|
+
insertRun.run({
|
|
56
|
+
run_id: detail.run_id,
|
|
57
|
+
pipeline_id: detail.pipeline_id,
|
|
58
|
+
task_id: detail.task_id ?? null,
|
|
59
|
+
task_yaml: detail.task_yaml,
|
|
60
|
+
status: detail.status,
|
|
61
|
+
created_at: detail.created_at,
|
|
62
|
+
updated_at: detail.updated_at ?? meta.updated_at ?? detail.created_at,
|
|
63
|
+
checkout_root: meta.checkout_root ?? null,
|
|
64
|
+
});
|
|
65
|
+
for (const stage of detail.stages) {
|
|
66
|
+
const started = stage.events.find((e) => e.event === "started")?.at ?? null;
|
|
67
|
+
const finished = [...stage.events]
|
|
68
|
+
.reverse()
|
|
69
|
+
.find((e) => e.event === "succeeded" || e.event === "failed")?.at ?? null;
|
|
70
|
+
upsertStage.run({
|
|
71
|
+
run_id: detail.run_id,
|
|
72
|
+
stage_id: stage.stage_id,
|
|
73
|
+
status: stage.status,
|
|
74
|
+
summary: stage.envelope?.summary ?? null,
|
|
75
|
+
envelope_json: stage.envelope ? JSON.stringify(stage.envelope) : null,
|
|
76
|
+
started_at: started,
|
|
77
|
+
finished_at: finished,
|
|
78
|
+
});
|
|
79
|
+
insertExecution.run({
|
|
80
|
+
run_id: detail.run_id,
|
|
81
|
+
stage_id: stage.stage_id,
|
|
82
|
+
attempt: 1,
|
|
83
|
+
status: stage.status,
|
|
84
|
+
started_at: started,
|
|
85
|
+
finished_at: finished,
|
|
86
|
+
envelope_json: stage.envelope ? JSON.stringify(stage.envelope) : null,
|
|
87
|
+
});
|
|
88
|
+
for (const ev of stage.events) {
|
|
89
|
+
const { event, at, ...rest } = ev;
|
|
90
|
+
insertEvent.run({
|
|
91
|
+
run_id: detail.run_id,
|
|
92
|
+
stage_id: stage.stage_id,
|
|
93
|
+
attempt: 1,
|
|
94
|
+
at: at ?? new Date().toISOString(),
|
|
95
|
+
event,
|
|
96
|
+
payload_json: Object.keys(rest).length > 0 ? JSON.stringify(rest) : null,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})();
|
|
101
|
+
imported += 1;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// skip malformed run folders
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return imported;
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS runs (\n run_id TEXT PRIMARY KEY,\n pipeline_id TEXT NOT NULL,\n task_id TEXT,\n task_yaml TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n checkout_root TEXT,\n pipeline_dag_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS stages (\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n status TEXT,\n summary TEXT,\n envelope_json TEXT,\n started_at TEXT,\n finished_at TEXT,\n PRIMARY KEY (run_id, stage_id),\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS stage_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n at TEXT NOT NULL,\n event TEXT NOT NULL,\n payload_json TEXT,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_runs_status_created\n ON runs (status, created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_runs_pipeline_created\n ON runs (pipeline_id, created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_stage_events_run_stage_at\n ON stage_events (run_id, stage_id, at);\n\nCREATE TABLE IF NOT EXISTS stage_executions (\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n attempt INTEGER NOT NULL,\n status TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n envelope_json TEXT,\n PRIMARY KEY (run_id, stage_id, attempt),\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_stage_executions_run_stage\n ON stage_executions (run_id, stage_id, attempt);\n";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const SCHEMA_SQL = `
|
|
2
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
3
|
+
run_id TEXT PRIMARY KEY,
|
|
4
|
+
pipeline_id TEXT NOT NULL,
|
|
5
|
+
task_id TEXT,
|
|
6
|
+
task_yaml TEXT NOT NULL,
|
|
7
|
+
status TEXT NOT NULL,
|
|
8
|
+
created_at TEXT NOT NULL,
|
|
9
|
+
updated_at TEXT NOT NULL,
|
|
10
|
+
checkout_root TEXT,
|
|
11
|
+
pipeline_dag_json TEXT
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS stages (
|
|
15
|
+
run_id TEXT NOT NULL,
|
|
16
|
+
stage_id TEXT NOT NULL,
|
|
17
|
+
status TEXT,
|
|
18
|
+
summary TEXT,
|
|
19
|
+
envelope_json TEXT,
|
|
20
|
+
started_at TEXT,
|
|
21
|
+
finished_at TEXT,
|
|
22
|
+
PRIMARY KEY (run_id, stage_id),
|
|
23
|
+
FOREIGN KEY (run_id) REFERENCES runs(run_id)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS stage_events (
|
|
27
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
run_id TEXT NOT NULL,
|
|
29
|
+
stage_id TEXT NOT NULL,
|
|
30
|
+
at TEXT NOT NULL,
|
|
31
|
+
event TEXT NOT NULL,
|
|
32
|
+
payload_json TEXT,
|
|
33
|
+
FOREIGN KEY (run_id) REFERENCES runs(run_id)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status_created
|
|
37
|
+
ON runs (status, created_at DESC);
|
|
38
|
+
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_runs_pipeline_created
|
|
40
|
+
ON runs (pipeline_id, created_at DESC);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_stage_events_run_stage_at
|
|
43
|
+
ON stage_events (run_id, stage_id, at);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS stage_executions (
|
|
46
|
+
run_id TEXT NOT NULL,
|
|
47
|
+
stage_id TEXT NOT NULL,
|
|
48
|
+
attempt INTEGER NOT NULL,
|
|
49
|
+
status TEXT NOT NULL,
|
|
50
|
+
started_at TEXT,
|
|
51
|
+
finished_at TEXT,
|
|
52
|
+
envelope_json TEXT,
|
|
53
|
+
PRIMARY KEY (run_id, stage_id, attempt),
|
|
54
|
+
FOREIGN KEY (run_id) REFERENCES runs(run_id)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_stage_executions_run_stage
|
|
58
|
+
ON stage_executions (run_id, stage_id, attempt);
|
|
59
|
+
`;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StageEnvelope } from "../types/envelope.js";
|
|
2
|
+
import type { StageLogEvent, StageSnapshot } from "./port.js";
|
|
3
|
+
export type StageExecutionPatch = {
|
|
4
|
+
status?: StageSnapshot["status"];
|
|
5
|
+
started_at?: string;
|
|
6
|
+
finished_at?: string;
|
|
7
|
+
envelope?: StageEnvelope | null;
|
|
8
|
+
};
|
|
9
|
+
export declare function executionStatusFromEvents(events: StageLogEvent[]): StageSnapshot["status"];
|
|
10
|
+
export declare function deriveExecutionPatchFromEvent(event: StageLogEvent, current: Pick<StageSnapshot, "status"> & {
|
|
11
|
+
started_at?: string;
|
|
12
|
+
finished_at?: string;
|
|
13
|
+
}): StageExecutionPatch;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { stageStatusFromEvents } from "./port.js";
|
|
2
|
+
export function executionStatusFromEvents(events) {
|
|
3
|
+
return stageStatusFromEvents(events);
|
|
4
|
+
}
|
|
5
|
+
export function deriveExecutionPatchFromEvent(event, current) {
|
|
6
|
+
const patch = {};
|
|
7
|
+
const eventName = event.event;
|
|
8
|
+
const at = event.at ?? new Date().toISOString();
|
|
9
|
+
if (eventName === "started" || eventName === "resumed") {
|
|
10
|
+
patch.status = "running";
|
|
11
|
+
if (!current.started_at)
|
|
12
|
+
patch.started_at = at;
|
|
13
|
+
}
|
|
14
|
+
else if (eventName === "waiting_for_input") {
|
|
15
|
+
patch.status = "waiting_for_input";
|
|
16
|
+
}
|
|
17
|
+
else if (eventName === "succeeded" || eventName === "failed") {
|
|
18
|
+
patch.status = eventName;
|
|
19
|
+
patch.finished_at = at;
|
|
20
|
+
}
|
|
21
|
+
return patch;
|
|
22
|
+
}
|