stageflow 0.19.0 → 0.25.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 (120) hide show
  1. package/README.md +16 -3
  2. package/dist/a2a/configDiscovery.d.ts +9 -0
  3. package/dist/a2a/configDiscovery.js +18 -0
  4. package/dist/a2a/contracts.d.ts +93 -0
  5. package/dist/a2a/contracts.js +163 -0
  6. package/dist/a2a/limits.d.ts +14 -0
  7. package/dist/a2a/limits.js +29 -0
  8. package/dist/a2a/registry.d.ts +44 -0
  9. package/dist/a2a/registry.js +159 -0
  10. package/dist/a2a/server.d.ts +29 -0
  11. package/dist/a2a/server.js +341 -0
  12. package/dist/a2a/service.d.ts +97 -0
  13. package/dist/a2a/service.js +496 -0
  14. package/dist/a2a/store.d.ts +106 -0
  15. package/dist/a2a/store.js +245 -0
  16. package/dist/agent/activity.d.ts +5 -0
  17. package/dist/agent/activityObserver.d.ts +3 -0
  18. package/dist/agent/activityObserver.js +6 -1
  19. package/dist/agent/piAdapter.js +7 -3
  20. package/dist/agent/port.d.ts +2 -0
  21. package/dist/agent/providerInspect.d.ts +19 -0
  22. package/dist/agent/providerInspect.js +35 -0
  23. package/dist/agent/streamLogRedact.d.ts +2 -0
  24. package/dist/agent/streamLogRedact.js +13 -0
  25. package/dist/cli/a2aCommand.d.ts +8 -0
  26. package/dist/cli/a2aCommand.js +131 -0
  27. package/dist/cli/artifactCommand.js +2 -2
  28. package/dist/cli/envelopeCommand.js +2 -2
  29. package/dist/cli/exportRunCommand.js +2 -2
  30. package/dist/cli/hostClient.d.ts +43 -0
  31. package/dist/cli/hostClient.js +182 -0
  32. package/dist/cli/providersCommand.js +5 -3
  33. package/dist/cli/runCommand.d.ts +6 -5
  34. package/dist/cli/runCommand.js +23 -31
  35. package/dist/cli/runOutput.d.ts +1 -1
  36. package/dist/cli/runStageCommand.d.ts +33 -0
  37. package/dist/cli/runStageCommand.js +407 -0
  38. package/dist/cli/runsCommand.d.ts +3 -7
  39. package/dist/cli/runsCommand.js +33 -94
  40. package/dist/cli.js +20 -5
  41. package/dist/config/describePipeline.d.ts +26 -0
  42. package/dist/config/describePipeline.js +37 -0
  43. package/dist/config/loadPipeline.d.ts +13 -1
  44. package/dist/config/loadPipeline.js +34 -4
  45. package/dist/config/mergePipelineIncludes.d.ts +11 -0
  46. package/dist/config/mergePipelineIncludes.js +75 -0
  47. package/dist/config/validateCatalog.d.ts +2 -2
  48. package/dist/config/validateCatalog.js +24 -17
  49. package/dist/mcp/catalogTools.js +285 -52
  50. package/dist/mcp/controlTools.js +77 -19
  51. package/dist/mcp/deps.d.ts +3 -0
  52. package/dist/mcp/projectMcpTools.d.ts +3 -0
  53. package/dist/mcp/projectMcpTools.js +25 -0
  54. package/dist/mcp/providerTools.d.ts +3 -0
  55. package/dist/mcp/providerTools.js +17 -0
  56. package/dist/mcp/readArtifact.d.ts +9 -0
  57. package/dist/mcp/readArtifact.js +11 -0
  58. package/dist/mcp/server.js +9 -1
  59. package/dist/mcp/tailStreamLog.d.ts +8 -0
  60. package/dist/mcp/tailStreamLog.js +28 -0
  61. package/dist/mcp/toolResults.d.ts +17 -0
  62. package/dist/mcp/toolResults.js +15 -0
  63. package/dist/mcp/tools.js +4 -0
  64. package/dist/mcp/waitRun.js +4 -4
  65. package/dist/mcp/waitingGates.d.ts +1 -0
  66. package/dist/mcp/waitingGates.js +3 -2
  67. package/dist/package-meta.d.ts +1 -1
  68. package/dist/package-meta.js +1 -1
  69. package/dist/projection/projectRun.d.ts +3 -0
  70. package/dist/projection/projectRun.js +7 -0
  71. package/dist/runstore/createStore.d.ts +10 -0
  72. package/dist/runstore/createStore.js +13 -4
  73. package/dist/runstore/port.d.ts +17 -2
  74. package/dist/runstore/sqlite/SqliteRunStore.d.ts +11 -1
  75. package/dist/runstore/sqlite/SqliteRunStore.js +81 -26
  76. package/dist/runstore/sqlite/schema.d.ts +1 -1
  77. package/dist/runstore/sqlite/schema.js +6 -0
  78. package/dist/runstore/submission.d.ts +11 -0
  79. package/dist/runstore/submission.js +7 -0
  80. package/dist/runstore/workspaceLayout.d.ts +1 -0
  81. package/dist/runstore/workspaceLayout.js +3 -0
  82. package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
  83. package/dist/runtime/feedbackLoopCoordinator.js +50 -20
  84. package/dist/runtime/feedbackLoopDecision.js +33 -1
  85. package/dist/runtime/pipelineRunner.d.ts +5 -3
  86. package/dist/runtime/pipelineRunner.js +6 -1
  87. package/dist/runtime/pipelineScheduler.js +32 -15
  88. package/dist/runtime/reloadRunCatalog.js +7 -1
  89. package/dist/runtime/resolveEnvelopeRef.d.ts +30 -0
  90. package/dist/runtime/resolveEnvelopeRef.js +79 -0
  91. package/dist/runtime/resumeReconstruct.js +2 -2
  92. package/dist/runtime/runManager.d.ts +24 -2
  93. package/dist/runtime/runManager.js +83 -19
  94. package/dist/runtime/settingsFile.d.ts +2 -0
  95. package/dist/runtime/settingsFile.js +19 -0
  96. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
  97. package/dist/runtime/stageAttemptBootstrap.js +8 -2
  98. package/dist/runtime/stageStreamLog.d.ts +17 -0
  99. package/dist/runtime/stageStreamLog.js +89 -0
  100. package/dist/runtime/stageWorker.js +2 -1
  101. package/dist/server/bootstrap.d.ts +2 -0
  102. package/dist/server/bootstrap.js +22 -4
  103. package/dist/server/createHttpHost.js +6 -0
  104. package/dist/server/ensureGlobalService.d.ts +35 -0
  105. package/dist/server/ensureGlobalService.js +138 -0
  106. package/dist/server/http.d.ts +20 -1
  107. package/dist/server/http.js +450 -402
  108. package/dist/server/mcpHost.js +7 -8
  109. package/dist/server/operatorResults.d.ts +10 -0
  110. package/dist/server/operatorResults.js +24 -0
  111. package/dist/server/providerRoutes.d.ts +0 -6
  112. package/dist/server/providerRoutes.js +10 -18
  113. package/dist/types/pipeline.d.ts +8 -0
  114. package/dist/ui/assets/{index-DMVdl1qB.js → index-Da5EHU6M.js} +5 -5
  115. package/dist/ui/index.html +1 -1
  116. package/package.json +2 -1
  117. package/skills/stageflow/references/control-surface.md +2 -2
  118. package/skills/stageflow-run/SKILL.md +27 -9
  119. package/skills/stageflow-run/references/mcp-call.md +1 -1
  120. package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import { inspectProviderReadiness, mapProviderAuthError, } from "../agent/providerInspect.js";
3
+ import { textResult } from "./toolResults.js";
4
+ export function registerProviderTools(server, deps) {
5
+ server.registerTool("list_providers", {
6
+ description: "List login-capable model providers with auth readiness and a Pi-home detect summary. Read-only; does not log in or return secrets.",
7
+ inputSchema: z.object({}),
8
+ }, async () => {
9
+ try {
10
+ return textResult(await inspectProviderReadiness(deps.cwd, deps.providerAuthContext));
11
+ }
12
+ catch (err) {
13
+ const mapped = mapProviderAuthError(err);
14
+ return textResult({ error: mapped.body.error, status: mapped.status }, true);
15
+ }
16
+ });
17
+ }
@@ -1,4 +1,13 @@
1
1
  import type { RunStore } from "../runstore/port.js";
2
2
  export declare function artifactMediaType(relativePath: string): string | undefined;
3
+ export type ClassifiedArtifactContent = {
4
+ kind: "image";
5
+ mimeType: string;
6
+ } | {
7
+ kind: "utf8";
8
+ } | {
9
+ kind: "binary";
10
+ };
11
+ export declare function classifyArtifactContent(relativePath: string, bytes: Buffer): ClassifiedArtifactContent;
3
12
  export declare function readRunArtifact(store: RunStore, runId: string, relativePath: string): Promise<string>;
4
13
  export declare function readRunArtifactBytes(store: RunStore, runId: string, relativePath: string): Promise<Buffer>;
@@ -1,3 +1,4 @@
1
+ import { isUtf8 } from "node:buffer";
1
2
  import { readFile, realpath } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { isInsideDir } from "../runstore/workspaceLayout.js";
@@ -28,6 +29,16 @@ export function artifactMediaType(relativePath) {
28
29
  return undefined;
29
30
  }
30
31
  }
32
+ export function classifyArtifactContent(relativePath, bytes) {
33
+ const mimeType = artifactMediaType(relativePath);
34
+ if (mimeType !== undefined) {
35
+ return { kind: "image", mimeType };
36
+ }
37
+ if (isUtf8(bytes)) {
38
+ return { kind: "utf8" };
39
+ }
40
+ return { kind: "binary" };
41
+ }
31
42
  async function resolveRunArtifactFile(store, runId, relativePath) {
32
43
  assertSafeRunId(runId);
33
44
  if (typeof relativePath !== "string" || relativePath.trim().length === 0) {
@@ -34,7 +34,15 @@ function sessionIdFromRequest(req) {
34
34
  return undefined;
35
35
  }
36
36
  function toolDepsFrom(deps) {
37
- return { manager: deps.manager, store: deps.store, cwd: deps.cwd };
37
+ return {
38
+ manager: deps.manager,
39
+ store: deps.store,
40
+ cwd: deps.cwd,
41
+ ...(deps.providerAuthContext !== undefined
42
+ ? { providerAuthContext: deps.providerAuthContext }
43
+ : {}),
44
+ ...(deps.projectRoot !== undefined ? { projectRoot: deps.projectRoot } : {}),
45
+ };
38
46
  }
39
47
  function attachBusNotifications(server, bus) {
40
48
  return bus.on((event) => {
@@ -0,0 +1,8 @@
1
+ export type StreamLogTail = {
2
+ text: string;
3
+ nextOffset: number;
4
+ truncated?: boolean;
5
+ earliestOffset?: number;
6
+ };
7
+ /** Read new content from a stage's stream.log since a caller's last cursor. */
8
+ export declare function readStreamLogTail(streamLogPath: string, sinceOffset: number | undefined): Promise<StreamLogTail>;
@@ -0,0 +1,28 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parseStreamLogHeader } from "../runtime/stageStreamLog.js";
3
+ /** Read new content from a stage's stream.log since a caller's last cursor. */
4
+ export async function readStreamLogTail(streamLogPath, sinceOffset) {
5
+ let raw;
6
+ try {
7
+ raw = await readFile(streamLogPath);
8
+ }
9
+ catch {
10
+ return { text: "", nextOffset: 0 };
11
+ }
12
+ const { base, headerLength } = parseStreamLogHeader(raw);
13
+ const contentBytes = raw.subarray(headerLength);
14
+ const eof = base + contentBytes.length;
15
+ if (sinceOffset === undefined || sinceOffset < base) {
16
+ const truncated = sinceOffset !== undefined && sinceOffset < base;
17
+ return {
18
+ text: contentBytes.toString("utf8"),
19
+ nextOffset: eof,
20
+ ...(truncated ? { truncated: true, earliestOffset: base } : {}),
21
+ };
22
+ }
23
+ if (sinceOffset >= eof) {
24
+ return { text: "", nextOffset: eof };
25
+ }
26
+ const slice = contentBytes.subarray(sinceOffset - base);
27
+ return { text: slice.toString("utf8"), nextOffset: eof };
28
+ }
@@ -5,3 +5,20 @@ export declare function textResult(data: unknown, isError?: boolean): {
5
5
  }[];
6
6
  isError?: boolean | undefined;
7
7
  };
8
+ export declare function imageResult(mimeType: string, bytes: Buffer, identity: {
9
+ runId: string;
10
+ path: string;
11
+ mimeType: string;
12
+ }): {
13
+ content: ({
14
+ type: "image";
15
+ mimeType: string;
16
+ data: string;
17
+ text?: undefined;
18
+ } | {
19
+ mimeType?: undefined;
20
+ data?: undefined;
21
+ type: "text";
22
+ text: string;
23
+ })[];
24
+ };
@@ -4,3 +4,18 @@ export function textResult(data, isError = false) {
4
4
  ...(isError ? { isError: true } : {}),
5
5
  };
6
6
  }
7
+ export function imageResult(mimeType, bytes, identity) {
8
+ return {
9
+ content: [
10
+ {
11
+ type: "image",
12
+ mimeType,
13
+ data: bytes.toString("base64"),
14
+ },
15
+ {
16
+ type: "text",
17
+ text: JSON.stringify(identity, null, 2),
18
+ },
19
+ ],
20
+ };
21
+ }
package/dist/mcp/tools.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import { z } from "zod";
2
2
  import { registerCatalogTools } from "./catalogTools.js";
3
3
  import { registerControlTools } from "./controlTools.js";
4
+ import { registerProjectMcpTools } from "./projectMcpTools.js";
5
+ import { registerProviderTools } from "./providerTools.js";
4
6
  import { textResult } from "./toolResults.js";
5
7
  import { DEFAULT_TIMEOUT_MS, waitRun } from "./waitRun.js";
6
8
  export function registerMcpTools(server, deps) {
7
9
  const { store } = deps;
8
10
  registerCatalogTools(server, deps);
9
11
  registerControlTools(server, deps);
12
+ registerProviderTools(server, deps);
13
+ registerProjectMcpTools(server, deps);
10
14
  server.registerTool("wait_run", {
11
15
  description: "Long-poll until a run is waiting for HITL input and/or terminal (succeeded/failed), or until timeout_ms. Default until=any wakes on waiting OR terminal. Abort cancels only this wait (not the run). Optional notifications/progress when the client supplies _meta.progressToken. Compose: wait_run → answer_gate → wait_run.",
12
16
  inputSchema: z.object({
@@ -1,3 +1,4 @@
1
+ import { mapStoreLookupError } from "../server/operatorResults.js";
1
2
  import { projectRunForMcp } from "./projectRun.js";
2
3
  export const DEFAULT_TIMEOUT_MS = 60_000;
3
4
  export const MAX_TIMEOUT_MS = 240_000;
@@ -90,12 +91,11 @@ export async function waitRun(opts) {
90
91
  detail = await opts.store.readRun(opts.runId);
91
92
  }
92
93
  catch (err) {
93
- const message = err instanceof Error ? err.message : String(err);
94
- const notFound = /not found|no such|unknown run/i.test(message);
94
+ const mapped = mapStoreLookupError(err, { policy: "run" });
95
95
  return {
96
96
  ok: false,
97
- error: message,
98
- status: notFound ? 404 : 500,
97
+ error: mapped.error,
98
+ status: mapped.status,
99
99
  };
100
100
  }
101
101
  const run = projectRunForMcp(detail);
@@ -9,5 +9,6 @@ export declare function projectWaitingGate(opts: {
9
9
  }): Record<string, unknown>;
10
10
  export declare function projectWaitingGates(store: RunStore, opts?: {
11
11
  runId?: string;
12
+ projectRoot?: string;
12
13
  }): Promise<Array<Record<string, unknown>>>;
13
14
  export {};
@@ -83,8 +83,9 @@ export async function projectWaitingGates(store, opts) {
83
83
  return projectGatesForRun(detail, detail);
84
84
  }
85
85
  const summaries = await store.listRuns();
86
- const waiting = summaries.filter((r) => (r.waiting_stage_ids !== undefined && r.waiting_stage_ids.length > 0) ||
87
- r.waiting_stage_id !== undefined);
86
+ const waiting = summaries.filter((r) => (opts?.projectRoot === undefined || r.project_root === opts.projectRoot) &&
87
+ ((r.waiting_stage_ids !== undefined && r.waiting_stage_ids.length > 0) ||
88
+ r.waiting_stage_id !== undefined));
88
89
  const perRun = await Promise.all(waiting.map(async (summary) => {
89
90
  let detail;
90
91
  try {
@@ -1,2 +1,2 @@
1
1
  export declare const PACKAGE_NAME = "stageflow";
2
- export declare const PACKAGE_VERSION = "0.19.0";
2
+ export declare const PACKAGE_VERSION = "0.25.0";
@@ -1,2 +1,2 @@
1
1
  export const PACKAGE_NAME = "stageflow";
2
- export const PACKAGE_VERSION = "0.19.0";
2
+ export const PACKAGE_VERSION = "0.25.0";
@@ -16,6 +16,8 @@ export type StageProjection = {
16
16
  artifacts: string[];
17
17
  last_at?: string;
18
18
  pending_prompt?: AskOperatorPrompt;
19
+ definition_id?: string;
20
+ cost_usd?: number;
19
21
  };
20
22
  export type RunProjection = {
21
23
  run_id: string;
@@ -40,6 +42,7 @@ export type RunProjection = {
40
42
  failed_reason?: RunSummary["failed_reason"];
41
43
  active_feedback_loop?: FeedbackLoopRecord;
42
44
  feedback_loops: FeedbackLoopHistory[];
45
+ total_cost_usd?: number;
43
46
  };
44
47
  export declare function projectRun(detail: RunDetail): RunProjection;
45
48
  export { projectRun as projectRunForMcp };
@@ -44,6 +44,9 @@ export function projectRun(detail) {
44
44
  ...(detail.active_feedback_loop !== undefined
45
45
  ? { active_feedback_loop: detail.active_feedback_loop }
46
46
  : {}),
47
+ ...(detail.total_cost_usd !== undefined
48
+ ? { total_cost_usd: detail.total_cost_usd }
49
+ : {}),
47
50
  feedback_loops: detail.feedback_loops ?? [],
48
51
  stages: detail.stages.map((stage) => ({
49
52
  stage_id: stage.stage_id,
@@ -69,6 +72,10 @@ export function projectRun(detail) {
69
72
  ...(stage.pending_prompt
70
73
  ? { pending_prompt: stage.pending_prompt }
71
74
  : {}),
75
+ ...(stage.definition_id !== undefined
76
+ ? { definition_id: stage.definition_id }
77
+ : {}),
78
+ ...(stage.cost_usd !== undefined ? { cost_usd: stage.cost_usd } : {}),
72
79
  })),
73
80
  };
74
81
  }
@@ -1,3 +1,4 @@
1
+ import type Database from "better-sqlite3";
1
2
  import type { RunStore } from "./port.js";
2
3
  export type RunStoreKind = "sqlite";
3
4
  export type RunStoreConfig = {
@@ -7,6 +8,15 @@ export type RunStoreConfig = {
7
8
  kind?: RunStoreKind | "disk";
8
9
  };
9
10
  export declare const DISK_STORE_REJECTED = "SF_STORE=disk is no longer a live RunStore. SQLite is the only adapter. Disk-era .stageflow/runs trees import automatically when the SQLite store is empty.";
11
+ /**
12
+ * Composition-root factory: hands back the concrete SQLite connection alongside the `RunStore`,
13
+ * for the one caller wiring a second, colocated store (A2A's tables) into the same `state.db`
14
+ * file. Ordinary call sites use `createRunStore` below and never see this.
15
+ */
16
+ export declare function createRunStoreWithConnection(config: RunStoreConfig): {
17
+ store: RunStore;
18
+ connection: Database.Database;
19
+ };
10
20
  /**
11
21
  * Factory at the application edge. Call sites should receive the returned `RunStore`
12
22
  * and must not branch on backend kind.
@@ -11,13 +11,22 @@ function resolveKind(kind) {
11
11
  return "sqlite";
12
12
  }
13
13
  /**
14
- * Factory at the application edge. Call sites should receive the returned `RunStore`
15
- * and must not branch on backend kind.
14
+ * Composition-root factory: hands back the concrete SQLite connection alongside the `RunStore`,
15
+ * for the one caller wiring a second, colocated store (A2A's tables) into the same `state.db`
16
+ * file. Ordinary call sites use `createRunStore` below and never see this.
16
17
  */
17
- export function createRunStore(config) {
18
+ export function createRunStoreWithConnection(config) {
18
19
  resolveKind(config.kind);
19
20
  migrateLegacyStoreRoot(config.rootDir);
20
21
  const storeRoot = storeRootFor(config.rootDir);
21
22
  mkdirSync(storeRoot, { recursive: true });
22
- return new SqliteRunStore(storeRoot);
23
+ const store = new SqliteRunStore(storeRoot);
24
+ return { store, connection: store.connection };
25
+ }
26
+ /**
27
+ * Factory at the application edge. Call sites should receive the returned `RunStore`
28
+ * and must not branch on backend kind.
29
+ */
30
+ export function createRunStore(config) {
31
+ return createRunStoreWithConnection(config).store;
23
32
  }
@@ -1,7 +1,8 @@
1
1
  import type { StageLogLine } from "../agent/activity.js";
2
+ import type { RunSubmission, RunSubmissionRecord } from "./submission.js";
2
3
  import type { AskOperatorPrompt } from "../tools/askOperator.js";
3
4
  import type { StageEnvelope } from "../types/envelope.js";
4
- import type { FeedbackLoopConfig, ResolvedPipelineDag } from "../types/pipeline.js";
5
+ import type { FeedbackLoopConfig, InlinePipelineDefinition, ResolvedPipelineDag } from "../types/pipeline.js";
5
6
  import type { CompletionCheck } from "../types/completion.js";
6
7
  import type { StageGateKind } from "../types/stage.js";
7
8
  import type { StageUsage } from "../types/usage.js";
@@ -51,6 +52,14 @@ export type RunMeta = {
51
52
  pipeline_path?: string;
52
53
  task_path?: string;
53
54
  project_root?: string;
55
+ /**
56
+ * The raw pipeline body for a run started from an inline pipeline
57
+ * (no pipeline_path). Lets a fresh stage-worker process — which has no
58
+ * memory of the original in-process call — reconstruct the pipeline
59
+ * without needing a catalog file, mirroring how task_yaml already lets
60
+ * task reconstruction work without a task_path.
61
+ */
62
+ inline_pipeline?: InlinePipelineDefinition;
54
63
  };
55
64
  export type CreatedRun = {
56
65
  runId: string;
@@ -275,6 +284,7 @@ export type FeedbackLoopHistory = {
275
284
  fork_generations: ForkGenerationRecord[];
276
285
  };
277
286
  export type CreateRunInput = {
287
+ submission?: RunSubmission;
278
288
  pipelineId: string;
279
289
  taskYaml: string;
280
290
  taskId?: string;
@@ -286,6 +296,8 @@ export type CreateRunInput = {
286
296
  pipelinePath?: string;
287
297
  taskPath?: string;
288
298
  projectRoot?: string;
299
+ /** Raw inline pipeline body, persisted only when pipelinePath is absent. */
300
+ inlinePipeline?: InlinePipelineDefinition;
289
301
  };
290
302
  export type ListRunsFilter = {
291
303
  status?: RunStatus;
@@ -301,6 +313,7 @@ export type ListRunsFilter = {
301
313
  * run id is all you have (catalog / rehydrate).
302
314
  */
303
315
  export interface RunStore {
316
+ getRunBySubmission(key: string): Promise<RunSubmissionRecord | null>;
304
317
  createRun(input: CreateRunInput): Promise<CreatedRun>;
305
318
  updateRunStatus(runId: string, status: RunStatus): Promise<void>;
306
319
  readRunMeta(runId: string): Promise<RunMeta>;
@@ -327,12 +340,14 @@ export interface RunStore {
327
340
  writeEnvelope(runId: string, stageId: string, envelope: StageEnvelope, options?: {
328
341
  attempt?: number;
329
342
  }): Promise<void>;
330
- readEnvelope(runId: string, stageId: string): Promise<StageEnvelope>;
343
+ readEnvelope(runId: string, stageId: string, attempt?: number): Promise<StageEnvelope>;
331
344
  appendStageEvent(runId: string, stageId: string, event: StageLogLine, options?: {
332
345
  attempt?: number;
333
346
  }): Promise<void>;
334
347
  listStageEvents(runId: string, stageId: string, attempt?: number): Promise<StageLogEvent[]>;
335
348
  listRuns(filter?: ListRunsFilter): Promise<RunSummary[]>;
349
+ /** Every distinct non-empty project_root recorded across all runs. */
350
+ listProjectRoots(): Promise<string[]>;
336
351
  readRun(runId: string): Promise<RunDetail>;
337
352
  updatePipelineDag(runId: string, dag: RunPipelineDagSnapshot): Promise<void>;
338
353
  createFeedbackLoop(runId: string, input: CreateFeedbackLoopInput): Promise<FeedbackLoopRecord>;
@@ -1,6 +1,8 @@
1
+ import Database from "better-sqlite3";
1
2
  import type { StageEnvelope } from "../../types/envelope.js";
2
3
  import type { StageLogLine } from "../../agent/activity.js";
3
4
  import { type CreateFeedbackLoopInput, type CreateFeedbackReplayInput, type CreateFeedbackReplayStagePassInput, type CreateForkGenerationInput, type CreateRunInput, type CreatedRun, type FeedbackLoopPatch, type FeedbackLoopRecord, type FeedbackLoopState, type FeedbackReplayPatch, type FeedbackReplayRecord, type FeedbackReplayStagePassPatch, type FeedbackReplayStagePassRecord, type ForkGenerationPatch, type ForkGenerationRecord, type ListRunsFilter, type RunDetail, type RunMeta, type RunPipelineDagSnapshot, type RunStatus, type RunStore, type RunSummary, type StageExecution, type StageExecutionPatch, type StageLogEvent, type VerificationCheckResult, type VerificationCheckResultPatch } from "../port.js";
5
+ import { type RunSubmissionRecord } from "../submission.js";
4
6
  export declare class SqliteRunStore implements RunStore {
5
7
  private readonly storeRoot;
6
8
  private readonly db;
@@ -8,8 +10,15 @@ export declare class SqliteRunStore implements RunStore {
8
10
  constructor(storeRoot: string);
9
11
  /** Await one-shot disk import before catalog reads (create paths also wait). */
10
12
  ready(): Promise<void>;
13
+ /**
14
+ * The connection this store owns, for the one caller allowed to share it: the composition root
15
+ * wiring the A2A tables into the same `state.db` file. Not part of the `RunStore` interface.
16
+ */
17
+ get connection(): Database.Database;
11
18
  getWorkspaceDir(runId: string): string;
12
19
  createRun(input: CreateRunInput): Promise<CreatedRun>;
20
+ private readSubmission;
21
+ getRunBySubmission(key: string): Promise<RunSubmissionRecord | null>;
13
22
  updateRunStatus(runId: string, status: RunStatus): Promise<void>;
14
23
  updatePipelineDag(runId: string, dag: RunPipelineDagSnapshot): Promise<void>;
15
24
  readRunMeta(runId: string): Promise<RunMeta>;
@@ -29,7 +38,7 @@ export declare class SqliteRunStore implements RunStore {
29
38
  writeEnvelope(runId: string, stageId: string, envelope: StageEnvelope, options?: {
30
39
  attempt?: number;
31
40
  }): Promise<void>;
32
- readEnvelope(runId: string, stageId: string): Promise<StageEnvelope>;
41
+ readEnvelope(runId: string, stageId: string, attempt?: number): Promise<StageEnvelope>;
33
42
  private readEnvelopeFromDb;
34
43
  appendStageEvent(runId: string, stageId: string, event: StageLogLine, options?: {
35
44
  attempt?: number;
@@ -43,6 +52,7 @@ export declare class SqliteRunStore implements RunStore {
43
52
  private updateStageStatusDenorm;
44
53
  listStageEvents(runId: string, stageId: string, attempt?: number): Promise<StageLogEvent[]>;
45
54
  listRuns(filter?: ListRunsFilter): Promise<RunSummary[]>;
55
+ listProjectRoots(): Promise<string[]>;
46
56
  readRun(runId: string): Promise<RunDetail>;
47
57
  createFeedbackLoop(runId: string, input: CreateFeedbackLoopInput): Promise<FeedbackLoopRecord>;
48
58
  getFeedbackLoop(runId: string, loopId: string): Promise<FeedbackLoopRecord>;
@@ -9,6 +9,7 @@ import { newRunId, runWorkspaceDir } from "../paths.js";
9
9
  import { attemptAgentDir, attemptArtifactsDir, stageDir, } from "../workspaceLayout.js";
10
10
  import { importDiskRunsIfEmpty } from "./migrateFromDisk.js";
11
11
  import { SCHEMA_SQL } from "./schema.js";
12
+ import { RunSubmissionExistsError } from "../submission.js";
12
13
  const ensuredStageDirs = new Set();
13
14
  const DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5000;
14
15
  function readSqliteBusyTimeoutMs() {
@@ -52,6 +53,12 @@ function ensureRunLocatorColumns(db) {
52
53
  }
53
54
  }
54
55
  }
56
+ function ensureInlinePipelineColumn(db) {
57
+ const cols = db.prepare(`PRAGMA table_info(runs)`).all();
58
+ if (!cols.some((c) => c.name === "inline_pipeline_json")) {
59
+ db.exec(`ALTER TABLE runs ADD COLUMN inline_pipeline_json TEXT`);
60
+ }
61
+ }
55
62
  function ensureStageExecutionsTable(db) {
56
63
  db.exec(`
57
64
  CREATE TABLE IF NOT EXISTS stage_executions (
@@ -304,6 +311,7 @@ export class SqliteRunStore {
304
311
  ensureCiIdentityColumns(this.db);
305
312
  ensurePipelineDagColumn(this.db);
306
313
  ensureRunLocatorColumns(this.db);
314
+ ensureInlinePipelineColumn(this.db);
307
315
  ensureStageExecutionsTable(this.db);
308
316
  ensureStageExecutionVerificationOutcomeColumn(this.db);
309
317
  ensureStageExecutionCostColumns(this.db);
@@ -319,6 +327,13 @@ export class SqliteRunStore {
319
327
  async ready() {
320
328
  await this.migratePromise;
321
329
  }
330
+ /**
331
+ * The connection this store owns, for the one caller allowed to share it: the composition root
332
+ * wiring the A2A tables into the same `state.db` file. Not part of the `RunStore` interface.
333
+ */
334
+ get connection() {
335
+ return this.db;
336
+ }
322
337
  getWorkspaceDir(runId) {
323
338
  return runWorkspaceDir(this.storeRoot, runId);
324
339
  }
@@ -337,32 +352,55 @@ export class SqliteRunStore {
337
352
  const projectRoot = input.projectRoot
338
353
  ? normalizeCatalogPath(input.projectRoot)
339
354
  : null;
340
- this.db
341
- .prepare(`INSERT INTO runs
342
- (run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root)
355
+ this.db.transaction(() => {
356
+ if (input.submission) {
357
+ const existing = this.readSubmission(input.submission.key);
358
+ if (existing)
359
+ throw new RunSubmissionExistsError(existing);
360
+ }
361
+ this.db
362
+ .prepare(`INSERT INTO runs
363
+ (run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, inline_pipeline_json)
343
364
  VALUES
344
- (@run_id, @pipeline_id, @task_id, @task_yaml, @status, @created_at, @updated_at, @checkout_root, @pipeline_dag_json, @git_sha, @ci_pr_url, @ci_job_url, @pipeline_path, @task_path, @project_root)`)
345
- .run({
346
- run_id: runId,
347
- pipeline_id: input.pipelineId,
348
- task_id: input.taskId ?? null,
349
- task_yaml: input.taskYaml,
350
- status: "running",
351
- created_at: now,
352
- updated_at: now,
353
- checkout_root: input.checkoutRoot ?? null,
354
- pipeline_dag_json: input.pipelineDag
355
- ? JSON.stringify(input.pipelineDag)
356
- : null,
357
- git_sha: input.gitSha ?? null,
358
- ci_pr_url: input.ciPrUrl ?? null,
359
- ci_job_url: input.ciJobUrl ?? null,
360
- pipeline_path: pipelinePath,
361
- task_path: taskPath,
362
- project_root: projectRoot,
363
- });
365
+ (@run_id, @pipeline_id, @task_id, @task_yaml, @status, @created_at, @updated_at, @checkout_root, @pipeline_dag_json, @git_sha, @ci_pr_url, @ci_job_url, @pipeline_path, @task_path, @project_root, @inline_pipeline_json)`)
366
+ .run({
367
+ run_id: runId,
368
+ pipeline_id: input.pipelineId,
369
+ task_id: input.taskId ?? null,
370
+ task_yaml: input.taskYaml,
371
+ status: "running",
372
+ created_at: now,
373
+ updated_at: now,
374
+ checkout_root: input.checkoutRoot ?? null,
375
+ pipeline_dag_json: input.pipelineDag
376
+ ? JSON.stringify(input.pipelineDag)
377
+ : null,
378
+ git_sha: input.gitSha ?? null,
379
+ ci_pr_url: input.ciPrUrl ?? null,
380
+ ci_job_url: input.ciJobUrl ?? null,
381
+ pipeline_path: pipelinePath,
382
+ task_path: taskPath,
383
+ project_root: projectRoot,
384
+ inline_pipeline_json: input.inlinePipeline
385
+ ? JSON.stringify(input.inlinePipeline)
386
+ : null,
387
+ });
388
+ if (input.submission) {
389
+ this.db.prepare("INSERT INTO run_submissions (submission_key, request_hash, run_id) VALUES (?, ?, ?)")
390
+ .run(input.submission.key, input.submission.requestHash, runId);
391
+ }
392
+ })();
364
393
  return { runId, workspaceDir };
365
394
  }
395
+ readSubmission(key) {
396
+ const row = this.db.prepare("SELECT submission_key AS key, request_hash AS requestHash, run_id AS runId FROM run_submissions WHERE submission_key = ?")
397
+ .get(key);
398
+ return row ?? null;
399
+ }
400
+ async getRunBySubmission(key) {
401
+ await this.ready();
402
+ return this.readSubmission(key);
403
+ }
366
404
  async updateRunStatus(runId, status) {
367
405
  await this.ready();
368
406
  const result = this.db
@@ -606,8 +644,15 @@ export class SqliteRunStore {
606
644
  });
607
645
  await this.updateStageExecution(runId, stageId, attempt, { envelope });
608
646
  }
609
- async readEnvelope(runId, stageId) {
647
+ async readEnvelope(runId, stageId, attempt) {
610
648
  await this.ready();
649
+ if (attempt !== undefined) {
650
+ const execution = await this.getStageExecution(runId, stageId, attempt);
651
+ if (execution.envelope != null) {
652
+ return execution.envelope;
653
+ }
654
+ throw new Error(`Envelope not found: ${runId}/${stageId}`);
655
+ }
611
656
  const execution = await this.getLatestStageExecution(runId, stageId);
612
657
  if (execution?.envelope != null) {
613
658
  return execution.envelope;
@@ -703,7 +748,7 @@ export class SqliteRunStore {
703
748
  }
704
749
  const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
705
750
  const rows = this.db
706
- .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root
751
+ .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, inline_pipeline_json
707
752
  FROM runs ${where} ORDER BY created_at DESC`)
708
753
  .all(...params);
709
754
  const summaries = [];
@@ -717,6 +762,13 @@ export class SqliteRunStore {
717
762
  }
718
763
  return summaries;
719
764
  }
765
+ async listProjectRoots() {
766
+ await this.ready();
767
+ const rows = this.db
768
+ .prepare(`SELECT DISTINCT project_root FROM runs WHERE project_root IS NOT NULL AND project_root != ''`)
769
+ .all();
770
+ return rows.map((row) => row.project_root);
771
+ }
720
772
  async readRun(runId) {
721
773
  await this.ready();
722
774
  const row = this.getRunRow(runId);
@@ -1125,11 +1177,14 @@ export class SqliteRunStore {
1125
1177
  ...(row.task_path != null ? { task_path: row.task_path } : {}),
1126
1178
  ...(row.project_root != null ? { project_root: row.project_root } : {}),
1127
1179
  ...(pipeline_dag ? { pipeline_dag } : {}),
1180
+ ...(row.inline_pipeline_json != null
1181
+ ? { inline_pipeline: JSON.parse(row.inline_pipeline_json) }
1182
+ : {}),
1128
1183
  };
1129
1184
  }
1130
1185
  getRunRow(runId) {
1131
1186
  const row = this.db
1132
- .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root
1187
+ .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, inline_pipeline_json
1133
1188
  FROM runs WHERE run_id = ?`)
1134
1189
  .get(runId);
1135
1190
  if (!row)
@@ -1 +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 git_sha TEXT,\n ci_pr_url TEXT,\n ci_job_url TEXT,\n pipeline_path TEXT,\n task_path TEXT,\n project_root 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 verification_outcome TEXT NOT NULL DEFAULT 'not_run',\n started_at TEXT,\n finished_at TEXT,\n envelope_json TEXT,\n cost_usd REAL,\n usage_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\nCREATE TABLE IF NOT EXISTS verification_check_results (\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n attempt INTEGER NOT NULL,\n check_id TEXT NOT NULL,\n check_type TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n evidence_json TEXT,\n PRIMARY KEY (run_id, stage_id, attempt, check_id),\n FOREIGN KEY (run_id, stage_id, attempt)\n REFERENCES stage_executions(run_id, stage_id, attempt)\n);\n\nCREATE INDEX IF NOT EXISTS idx_verification_check_results_execution\n ON verification_check_results (run_id, stage_id, attempt, check_id);\n\nCREATE TABLE IF NOT EXISTS feedback_loops (\n run_id TEXT NOT NULL,\n loop_id TEXT NOT NULL,\n source_stage_id TEXT NOT NULL,\n source_attempt INTEGER NOT NULL,\n policy_json TEXT NOT NULL,\n state TEXT NOT NULL,\n current_replay_id TEXT,\n current_replay_number INTEGER,\n deferred_send_back_json TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, loop_id),\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_feedback_loops_run_state\n ON feedback_loops (run_id, state, created_at);\n\nCREATE TABLE IF NOT EXISTS feedback_replays (\n run_id TEXT NOT NULL,\n replay_id TEXT NOT NULL,\n loop_id TEXT NOT NULL,\n source_stage_id TEXT NOT NULL,\n source_attempt INTEGER NOT NULL,\n target_stage_id TEXT NOT NULL,\n replay_number INTEGER NOT NULL,\n max_replays INTEGER NOT NULL,\n replay_session TEXT NOT NULL,\n route_stage_ids_json TEXT NOT NULL,\n feedback_envelope_json TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, replay_id),\n FOREIGN KEY (run_id, loop_id) REFERENCES feedback_loops(run_id, loop_id)\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_feedback_replays_loop_number\n ON feedback_replays (run_id, loop_id, replay_number);\nCREATE INDEX IF NOT EXISTS idx_feedback_replays_run_loop\n ON feedback_replays (run_id, loop_id, created_at);\n\nCREATE TABLE IF NOT EXISTS feedback_replay_stage_passes (\n run_id TEXT NOT NULL,\n replay_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n stage_attempt INTEGER NOT NULL,\n session_origin_attempt INTEGER,\n session_mode TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n emitted_envelope_json TEXT,\n PRIMARY KEY (run_id, replay_id, stage_id),\n FOREIGN KEY (run_id, replay_id) REFERENCES feedback_replays(run_id, replay_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_feedback_replay_stage_passes_replay\n ON feedback_replay_stage_passes (run_id, replay_id, stage_id);\n\nCREATE TABLE IF NOT EXISTS fork_generations (\n run_id TEXT NOT NULL,\n generation_id TEXT NOT NULL,\n replay_id TEXT,\n fork_parent_stage_id TEXT NOT NULL,\n generation_number INTEGER NOT NULL,\n clone_stage_ids_json TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, generation_id),\n FOREIGN KEY (run_id, replay_id) REFERENCES feedback_replays(run_id, replay_id)\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_fork_generations_parent_number\n ON fork_generations (run_id, fork_parent_stage_id, generation_number);\nCREATE INDEX IF NOT EXISTS idx_fork_generations_run_replay\n ON fork_generations (run_id, replay_id, created_at);\n";
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 git_sha TEXT,\n ci_pr_url TEXT,\n ci_job_url TEXT,\n pipeline_path TEXT,\n task_path TEXT,\n project_root TEXT\n);\n\nCREATE TABLE IF NOT EXISTS run_submissions (\n submission_key TEXT PRIMARY KEY,\n request_hash TEXT NOT NULL,\n run_id TEXT NOT NULL UNIQUE REFERENCES runs(run_id)\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 verification_outcome TEXT NOT NULL DEFAULT 'not_run',\n started_at TEXT,\n finished_at TEXT,\n envelope_json TEXT,\n cost_usd REAL,\n usage_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\nCREATE TABLE IF NOT EXISTS verification_check_results (\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n attempt INTEGER NOT NULL,\n check_id TEXT NOT NULL,\n check_type TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n evidence_json TEXT,\n PRIMARY KEY (run_id, stage_id, attempt, check_id),\n FOREIGN KEY (run_id, stage_id, attempt)\n REFERENCES stage_executions(run_id, stage_id, attempt)\n);\n\nCREATE INDEX IF NOT EXISTS idx_verification_check_results_execution\n ON verification_check_results (run_id, stage_id, attempt, check_id);\n\nCREATE TABLE IF NOT EXISTS feedback_loops (\n run_id TEXT NOT NULL,\n loop_id TEXT NOT NULL,\n source_stage_id TEXT NOT NULL,\n source_attempt INTEGER NOT NULL,\n policy_json TEXT NOT NULL,\n state TEXT NOT NULL,\n current_replay_id TEXT,\n current_replay_number INTEGER,\n deferred_send_back_json TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, loop_id),\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_feedback_loops_run_state\n ON feedback_loops (run_id, state, created_at);\n\nCREATE TABLE IF NOT EXISTS feedback_replays (\n run_id TEXT NOT NULL,\n replay_id TEXT NOT NULL,\n loop_id TEXT NOT NULL,\n source_stage_id TEXT NOT NULL,\n source_attempt INTEGER NOT NULL,\n target_stage_id TEXT NOT NULL,\n replay_number INTEGER NOT NULL,\n max_replays INTEGER NOT NULL,\n replay_session TEXT NOT NULL,\n route_stage_ids_json TEXT NOT NULL,\n feedback_envelope_json TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, replay_id),\n FOREIGN KEY (run_id, loop_id) REFERENCES feedback_loops(run_id, loop_id)\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_feedback_replays_loop_number\n ON feedback_replays (run_id, loop_id, replay_number);\nCREATE INDEX IF NOT EXISTS idx_feedback_replays_run_loop\n ON feedback_replays (run_id, loop_id, created_at);\n\nCREATE TABLE IF NOT EXISTS feedback_replay_stage_passes (\n run_id TEXT NOT NULL,\n replay_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n stage_attempt INTEGER NOT NULL,\n session_origin_attempt INTEGER,\n session_mode TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n emitted_envelope_json TEXT,\n PRIMARY KEY (run_id, replay_id, stage_id),\n FOREIGN KEY (run_id, replay_id) REFERENCES feedback_replays(run_id, replay_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_feedback_replay_stage_passes_replay\n ON feedback_replay_stage_passes (run_id, replay_id, stage_id);\n\nCREATE TABLE IF NOT EXISTS fork_generations (\n run_id TEXT NOT NULL,\n generation_id TEXT NOT NULL,\n replay_id TEXT,\n fork_parent_stage_id TEXT NOT NULL,\n generation_number INTEGER NOT NULL,\n clone_stage_ids_json TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, generation_id),\n FOREIGN KEY (run_id, replay_id) REFERENCES feedback_replays(run_id, replay_id)\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_fork_generations_parent_number\n ON fork_generations (run_id, fork_parent_stage_id, generation_number);\nCREATE INDEX IF NOT EXISTS idx_fork_generations_run_replay\n ON fork_generations (run_id, replay_id, created_at);\n";
@@ -17,6 +17,12 @@ CREATE TABLE IF NOT EXISTS runs (
17
17
  project_root TEXT
18
18
  );
19
19
 
20
+ CREATE TABLE IF NOT EXISTS run_submissions (
21
+ submission_key TEXT PRIMARY KEY,
22
+ request_hash TEXT NOT NULL,
23
+ run_id TEXT NOT NULL UNIQUE REFERENCES runs(run_id)
24
+ );
25
+
20
26
  CREATE TABLE IF NOT EXISTS stages (
21
27
  run_id TEXT NOT NULL,
22
28
  stage_id TEXT NOT NULL,
@@ -0,0 +1,11 @@
1
+ export type RunSubmission = {
2
+ key: string;
3
+ requestHash: string;
4
+ };
5
+ export type RunSubmissionRecord = RunSubmission & {
6
+ runId: string;
7
+ };
8
+ export declare class RunSubmissionExistsError extends Error {
9
+ readonly submission: RunSubmissionRecord;
10
+ constructor(submission: RunSubmissionRecord);
11
+ }