@blokjs/runner 0.2.2 → 0.6.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/dist/Blok.js +32 -3
- package/dist/Blok.js.map +1 -1
- package/dist/Configuration.d.ts +59 -5
- package/dist/Configuration.js +366 -96
- package/dist/Configuration.js.map +1 -1
- package/dist/ForEachNode.d.ts +59 -0
- package/dist/ForEachNode.js +522 -0
- package/dist/ForEachNode.js.map +1 -0
- package/dist/LoopMaxIterationsError.d.ts +11 -0
- package/dist/LoopMaxIterationsError.js +18 -0
- package/dist/LoopMaxIterationsError.js.map +1 -0
- package/dist/LoopNode.d.ts +36 -0
- package/dist/LoopNode.js +182 -0
- package/dist/LoopNode.js.map +1 -0
- package/dist/PayloadTooLargeError.d.ts +19 -0
- package/dist/PayloadTooLargeError.js +29 -0
- package/dist/PayloadTooLargeError.js.map +1 -0
- package/dist/RunCancelledError.d.ts +17 -0
- package/dist/RunCancelledError.js +25 -0
- package/dist/RunCancelledError.js.map +1 -0
- package/dist/Runner.d.ts +11 -1
- package/dist/Runner.js +9 -2
- package/dist/Runner.js.map +1 -1
- package/dist/RunnerSteps.js +648 -44
- package/dist/RunnerSteps.js.map +1 -1
- package/dist/RuntimeAdapterNode.d.ts +2 -1
- package/dist/RuntimeAdapterNode.js +2 -2
- package/dist/RuntimeAdapterNode.js.map +1 -1
- package/dist/RuntimeRegistry.d.ts +23 -2
- package/dist/RuntimeRegistry.js +31 -2
- package/dist/RuntimeRegistry.js.map +1 -1
- package/dist/SubworkflowNode.d.ts +181 -0
- package/dist/SubworkflowNode.js +479 -0
- package/dist/SubworkflowNode.js.map +1 -0
- package/dist/SwitchNode.d.ts +37 -0
- package/dist/SwitchNode.js +153 -0
- package/dist/SwitchNode.js.map +1 -0
- package/dist/TriggerBase.d.ts +178 -0
- package/dist/TriggerBase.js +1032 -5
- package/dist/TriggerBase.js.map +1 -1
- package/dist/TryCatchNode.d.ts +32 -0
- package/dist/TryCatchNode.js +207 -0
- package/dist/TryCatchNode.js.map +1 -0
- package/dist/WaitDispatchRequest.d.ts +38 -0
- package/dist/WaitDispatchRequest.js +13 -0
- package/dist/WaitDispatchRequest.js.map +1 -0
- package/dist/WaitNode.d.ts +23 -0
- package/dist/WaitNode.js +26 -0
- package/dist/WaitNode.js.map +1 -0
- package/dist/adapters/grpc/GrpcCodec.js +2 -2
- package/dist/adapters/grpc/GrpcRuntimeAdapter.d.ts +6 -4
- package/dist/adapters/grpc/GrpcRuntimeAdapter.js +6 -4
- package/dist/adapters/grpc/GrpcRuntimeAdapter.js.map +1 -1
- package/dist/adapters/grpc/types.d.ts +7 -5
- package/dist/adapters/grpc/types.js.map +1 -1
- package/dist/adapters/transport.d.ts +12 -41
- package/dist/adapters/transport.js +21 -70
- package/dist/adapters/transport.js.map +1 -1
- package/dist/cache/NodeResultCache.js +7 -0
- package/dist/cache/NodeResultCache.js.map +1 -1
- package/dist/concurrency/ConcurrencyBackend.d.ts +61 -0
- package/dist/concurrency/ConcurrencyBackend.js +20 -0
- package/dist/concurrency/ConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/ConcurrencyLimitError.d.ts +37 -0
- package/dist/concurrency/ConcurrencyLimitError.js +16 -0
- package/dist/concurrency/ConcurrencyLimitError.js.map +1 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.d.ts +64 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.js +310 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/QueueExpiredError.d.ts +40 -0
- package/dist/concurrency/QueueExpiredError.js +15 -0
- package/dist/concurrency/QueueExpiredError.js.map +1 -0
- package/dist/concurrency/RedisConcurrencyBackend.d.ts +64 -0
- package/dist/concurrency/RedisConcurrencyBackend.js +374 -0
- package/dist/concurrency/RedisConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/createConcurrencyBackend.d.ts +24 -0
- package/dist/concurrency/createConcurrencyBackend.js +38 -0
- package/dist/concurrency/createConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/readConcurrencyConfig.d.ts +60 -0
- package/dist/concurrency/readConcurrencyConfig.js +60 -0
- package/dist/concurrency/readConcurrencyConfig.js.map +1 -0
- package/dist/defineNode.d.ts +8 -0
- package/dist/defineNode.js +25 -5
- package/dist/defineNode.js.map +1 -1
- package/dist/graphql/GraphQLSchemaGenerator.js +1 -1
- package/dist/graphql/GraphQLSchemaGenerator.js.map +1 -1
- package/dist/idempotency/resolveIdempotencyKey.d.ts +20 -0
- package/dist/idempotency/resolveIdempotencyKey.js +37 -0
- package/dist/idempotency/resolveIdempotencyKey.js.map +1 -0
- package/dist/index.d.ts +30 -6
- package/dist/index.js +55 -6
- package/dist/index.js.map +1 -1
- package/dist/marketplace/RuntimeCatalog.d.ts +6 -0
- package/dist/marketplace/RuntimeCatalog.js.map +1 -1
- package/dist/marketplace/RuntimeDiscovery.d.ts +2 -2
- package/dist/marketplace/RuntimeDiscovery.js +18 -6
- package/dist/marketplace/RuntimeDiscovery.js.map +1 -1
- package/dist/monitoring/ConcurrencyMetrics.d.ts +82 -0
- package/dist/monitoring/ConcurrencyMetrics.js +139 -0
- package/dist/monitoring/ConcurrencyMetrics.js.map +1 -0
- package/dist/monitoring/ForEachWaitMetrics.d.ts +22 -0
- package/dist/monitoring/ForEachWaitMetrics.js +36 -0
- package/dist/monitoring/ForEachWaitMetrics.js.map +1 -0
- package/dist/monitoring/JanitorMetrics.d.ts +27 -0
- package/dist/monitoring/JanitorMetrics.js +48 -0
- package/dist/monitoring/JanitorMetrics.js.map +1 -0
- package/dist/openapi/OpenAPIGenerator.js +7 -2
- package/dist/openapi/OpenAPIGenerator.js.map +1 -1
- package/dist/runtime/PrimitiveStack.d.ts +64 -0
- package/dist/runtime/PrimitiveStack.js +92 -0
- package/dist/runtime/PrimitiveStack.js.map +1 -0
- package/dist/scheduling/DebounceBackend.d.ts +108 -0
- package/dist/scheduling/DebounceBackend.js +23 -0
- package/dist/scheduling/DebounceBackend.js.map +1 -0
- package/dist/scheduling/DebounceCoordinator.d.ts +141 -0
- package/dist/scheduling/DebounceCoordinator.js +362 -0
- package/dist/scheduling/DebounceCoordinator.js.map +1 -0
- package/dist/scheduling/DeferredDispatchSignal.d.ts +50 -0
- package/dist/scheduling/DeferredDispatchSignal.js +14 -0
- package/dist/scheduling/DeferredDispatchSignal.js.map +1 -0
- package/dist/scheduling/DeferredRunScheduler.d.ts +96 -0
- package/dist/scheduling/DeferredRunScheduler.js +256 -0
- package/dist/scheduling/DeferredRunScheduler.js.map +1 -0
- package/dist/scheduling/NatsKvDebounceBackend.d.ts +53 -0
- package/dist/scheduling/NatsKvDebounceBackend.js +334 -0
- package/dist/scheduling/NatsKvDebounceBackend.js.map +1 -0
- package/dist/scheduling/RedisDebounceBackend.d.ts +49 -0
- package/dist/scheduling/RedisDebounceBackend.js +356 -0
- package/dist/scheduling/RedisDebounceBackend.js.map +1 -0
- package/dist/scheduling/createDebounceBackend.d.ts +25 -0
- package/dist/scheduling/createDebounceBackend.js +39 -0
- package/dist/scheduling/createDebounceBackend.js.map +1 -0
- package/dist/scheduling/readSchedulingConfig.d.ts +24 -0
- package/dist/scheduling/readSchedulingConfig.js +52 -0
- package/dist/scheduling/readSchedulingConfig.js.map +1 -0
- package/dist/security/AuditLogger.js +1 -1
- package/dist/security/AuditLogger.js.map +1 -1
- package/dist/security/AuthMiddleware.d.ts +19 -20
- package/dist/security/AuthMiddleware.js +35 -20
- package/dist/security/AuthMiddleware.js.map +1 -1
- package/dist/security/OAuthProvider.js +2 -2
- package/dist/security/OAuthProvider.js.map +1 -1
- package/dist/security/SecretManager.js +14 -13
- package/dist/security/SecretManager.js.map +1 -1
- package/dist/security/index.d.ts +3 -1
- package/dist/security/index.js +3 -1
- package/dist/security/index.js.map +1 -1
- package/dist/testing/TestHarness.d.ts +27 -12
- package/dist/testing/TestHarness.js +19 -3
- package/dist/testing/TestHarness.js.map +1 -1
- package/dist/testing/WorkflowTestRunner.js +0 -7
- package/dist/testing/WorkflowTestRunner.js.map +1 -1
- package/dist/timeouts/StepTimeoutError.d.ts +22 -0
- package/dist/timeouts/StepTimeoutError.js +31 -0
- package/dist/timeouts/StepTimeoutError.js.map +1 -0
- package/dist/tracing/InMemoryRunStore.d.ts +41 -1
- package/dist/tracing/InMemoryRunStore.js +239 -0
- package/dist/tracing/InMemoryRunStore.js.map +1 -1
- package/dist/tracing/Janitor.d.ts +70 -0
- package/dist/tracing/Janitor.js +150 -0
- package/dist/tracing/Janitor.js.map +1 -0
- package/dist/tracing/PostgresRunStore.d.ts +57 -1
- package/dist/tracing/PostgresRunStore.js +711 -6
- package/dist/tracing/PostgresRunStore.js.map +1 -1
- package/dist/tracing/RoutingDiagnostics.d.ts +55 -0
- package/dist/tracing/RoutingDiagnostics.js +50 -0
- package/dist/tracing/RoutingDiagnostics.js.map +1 -0
- package/dist/tracing/RunStore.d.ts +181 -1
- package/dist/tracing/RunTracker.d.ts +244 -9
- package/dist/tracing/RunTracker.js +594 -1
- package/dist/tracing/RunTracker.js.map +1 -1
- package/dist/tracing/SqliteRunStore.d.ts +79 -2
- package/dist/tracing/SqliteRunStore.js +775 -16
- package/dist/tracing/SqliteRunStore.js.map +1 -1
- package/dist/tracing/TraceRouter.d.ts +20 -2
- package/dist/tracing/TraceRouter.js +612 -6
- package/dist/tracing/TraceRouter.js.map +1 -1
- package/dist/tracing/createStore.js +14 -3
- package/dist/tracing/createStore.js.map +1 -1
- package/dist/tracing/metadataFilter.d.ts +63 -0
- package/dist/tracing/metadataFilter.js +224 -0
- package/dist/tracing/metadataFilter.js.map +1 -0
- package/dist/tracing/sanitize.d.ts +11 -0
- package/dist/tracing/sanitize.js +29 -0
- package/dist/tracing/sanitize.js.map +1 -1
- package/dist/tracing/types.d.ts +672 -2
- package/dist/utils/createChildContext.d.ts +32 -0
- package/dist/utils/createChildContext.js +113 -0
- package/dist/utils/createChildContext.js.map +1 -0
- package/dist/utils/envAllowlist.d.ts +35 -0
- package/dist/utils/envAllowlist.js +113 -0
- package/dist/utils/envAllowlist.js.map +1 -0
- package/dist/version/RuntimeVersionValidator.d.ts +38 -0
- package/dist/version/RuntimeVersionValidator.js +121 -0
- package/dist/version/RuntimeVersionValidator.js.map +1 -0
- package/dist/visualization/WorkflowVisualizer.js +4 -4
- package/dist/visualization/WorkflowVisualizer.js.map +1 -1
- package/dist/workflow/PersistenceHelper.d.ts +18 -10
- package/dist/workflow/PersistenceHelper.js +35 -9
- package/dist/workflow/PersistenceHelper.js.map +1 -1
- package/dist/workflow/WorkflowNormalizer.d.ts +48 -42
- package/dist/workflow/WorkflowNormalizer.js +650 -18
- package/dist/workflow/WorkflowNormalizer.js.map +1 -1
- package/dist/workflow/WorkflowRegistry.d.ts +186 -0
- package/dist/workflow/WorkflowRegistry.js +202 -0
- package/dist/workflow/WorkflowRegistry.js.map +1 -0
- package/dist/workflow/sampleBody.d.ts +54 -0
- package/dist/workflow/sampleBody.js +320 -0
- package/dist/workflow/sampleBody.js.map +1 -0
- package/package.json +3 -8
- package/dist/adapters/HttpRuntimeAdapter.d.ts +0 -79
- package/dist/adapters/HttpRuntimeAdapter.js +0 -233
- package/dist/adapters/HttpRuntimeAdapter.js.map +0 -1
|
@@ -1,22 +1,63 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { isValidMetadataKey, normaliseMetadataFilters, translateFilterToSql } from "./metadataFilter";
|
|
2
3
|
const esmRequire = createRequire(import.meta.url);
|
|
4
|
+
function encodeNodeRunFlags(nodeRun) {
|
|
5
|
+
const flags = {};
|
|
6
|
+
if (nodeRun.wait !== undefined)
|
|
7
|
+
flags.wait = nodeRun.wait;
|
|
8
|
+
if (nodeRun.dispatch !== undefined)
|
|
9
|
+
flags.dispatch = nodeRun.dispatch;
|
|
10
|
+
if (nodeRun.subworkflowDepth !== undefined)
|
|
11
|
+
flags.subworkflowDepth = nodeRun.subworkflowDepth;
|
|
12
|
+
if (nodeRun.middleware !== undefined)
|
|
13
|
+
flags.middleware = nodeRun.middleware;
|
|
14
|
+
if (nodeRun.iterationIndex !== undefined)
|
|
15
|
+
flags.iterationIndex = nodeRun.iterationIndex;
|
|
16
|
+
const empty = Object.keys(flags).length === 0;
|
|
17
|
+
return empty ? null : JSON.stringify(flags);
|
|
18
|
+
}
|
|
19
|
+
function decodeNodeRunFlags(raw) {
|
|
20
|
+
if (!raw)
|
|
21
|
+
return {};
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (parsed && typeof parsed === "object")
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Corrupt JSON (concurrent write race, manual edit, etc.) — fall
|
|
29
|
+
// through to empty flags rather than failing the whole rowToNodeRun.
|
|
30
|
+
}
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
3
33
|
const isBun = "Bun" in globalThis;
|
|
4
34
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* optimal performance. Falls back to better-sqlite3 under Node.js.
|
|
9
|
-
*
|
|
10
|
-
* Provides persistent trace storage that survives process restarts.
|
|
11
|
-
* All operations are synchronous.
|
|
12
|
-
*
|
|
13
|
-
* Schema is auto-migrated on construction via a versioned migration system.
|
|
35
|
+
* F1 — read `BLOK_INDEXED_METADATA_KEYS=tier,region` from the env.
|
|
36
|
+
* Empty / unset = no F1 indexing. Exposed for test seams; production
|
|
37
|
+
* code reads through the `SqliteRunStore` constructor.
|
|
14
38
|
*/
|
|
39
|
+
export function readIndexedMetadataKeysFromEnv(env = process.env) {
|
|
40
|
+
const raw = env.BLOK_INDEXED_METADATA_KEYS;
|
|
41
|
+
if (typeof raw !== "string" || raw.length === 0)
|
|
42
|
+
return [];
|
|
43
|
+
return raw
|
|
44
|
+
.split(",")
|
|
45
|
+
.map((k) => k.trim())
|
|
46
|
+
.filter((k) => k.length > 0);
|
|
47
|
+
}
|
|
15
48
|
export class SqliteRunStore {
|
|
16
49
|
db;
|
|
17
50
|
// Prepared statements (lazy-initialized)
|
|
18
51
|
stmts = {};
|
|
19
|
-
|
|
52
|
+
/**
|
|
53
|
+
* F1 — the validated, frozen set of metadata keys that have an
|
|
54
|
+
* indexed generated column on `workflow_runs`. Read at query time
|
|
55
|
+
* to decide whether a filter's LHS is `metadata_<key>_idx`
|
|
56
|
+
* (indexed path) or `json_extract(metadata_json, '$.<key>')`
|
|
57
|
+
* (sequential path).
|
|
58
|
+
*/
|
|
59
|
+
indexedMetadataKeys;
|
|
60
|
+
constructor(dbPath = ".blok/trace.db", opts) {
|
|
20
61
|
if (isBun) {
|
|
21
62
|
// Use Bun's built-in SQLite (3-6x faster than better-sqlite3)
|
|
22
63
|
const bunMod = "bun:sqlite";
|
|
@@ -43,6 +84,50 @@ export class SqliteRunStore {
|
|
|
43
84
|
this.db.exec("PRAGMA synchronous = NORMAL");
|
|
44
85
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
45
86
|
this.migrate();
|
|
87
|
+
// F1 — resolve declared indexed keys (constructor option wins over
|
|
88
|
+
// env var) and ensure each one has a generated column + index.
|
|
89
|
+
// Runs AFTER `migrate()` so the base `workflow_runs` table exists.
|
|
90
|
+
const declared = opts?.indexedMetadataKeys ?? readIndexedMetadataKeysFromEnv();
|
|
91
|
+
this.indexedMetadataKeys = new Set(declared.filter((k) => isValidMetadataKey(k)));
|
|
92
|
+
this.ensureIndexedMetadataColumns(this.indexedMetadataKeys);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* F1 — for each declared key, ensure (a) the `metadata_<key>_idx`
|
|
96
|
+
* virtual generated column exists on `workflow_runs`, and (b) an
|
|
97
|
+
* index on that column exists. Both are idempotent — pre-existing
|
|
98
|
+
* columns are detected via `PRAGMA table_info` since SQLite doesn't
|
|
99
|
+
* support `ALTER TABLE ADD COLUMN IF NOT EXISTS`.
|
|
100
|
+
*
|
|
101
|
+
* Generated columns are `VIRTUAL` rather than `STORED` so they can
|
|
102
|
+
* be added to a non-empty table (STORED columns can't, per SQLite's
|
|
103
|
+
* `ALTER TABLE` rules). The query planner can still use a btree
|
|
104
|
+
* index built on a VIRTUAL column — that's the F1 win.
|
|
105
|
+
*/
|
|
106
|
+
ensureIndexedMetadataColumns(keys) {
|
|
107
|
+
if (keys.size === 0)
|
|
108
|
+
return;
|
|
109
|
+
// Use `table_xinfo` rather than `table_info` — virtual generated
|
|
110
|
+
// columns are HIDDEN columns and `PRAGMA table_info` deliberately
|
|
111
|
+
// omits them. `table_xinfo` returns hidden columns too (with
|
|
112
|
+
// `hidden=2` for virtual generated columns specifically), so the
|
|
113
|
+
// idempotency check actually works against a previously-added
|
|
114
|
+
// column on a re-opened db.
|
|
115
|
+
const existingColumns = new Set(this.db
|
|
116
|
+
.prepare("PRAGMA table_xinfo(workflow_runs)")
|
|
117
|
+
.all()
|
|
118
|
+
.map((r) => r.name));
|
|
119
|
+
for (const key of keys) {
|
|
120
|
+
const columnName = `metadata_${key}_idx`;
|
|
121
|
+
const indexName = `idx_workflow_runs_metadata_${key}`;
|
|
122
|
+
if (!existingColumns.has(columnName)) {
|
|
123
|
+
// TEXT type-hint is explicit so the planner has a stable
|
|
124
|
+
// expectation; the actual stored type still matches
|
|
125
|
+
// whatever `json_extract` returns at evaluation time
|
|
126
|
+
// (SQLite uses dynamic type affinity).
|
|
127
|
+
this.db.exec(`ALTER TABLE workflow_runs ADD COLUMN ${columnName} TEXT GENERATED ALWAYS AS (json_extract(metadata_json, '$.${key}')) VIRTUAL`);
|
|
128
|
+
}
|
|
129
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS ${indexName} ON workflow_runs(${columnName})`);
|
|
130
|
+
}
|
|
46
131
|
}
|
|
47
132
|
// === Schema Migration ===
|
|
48
133
|
migrate() {
|
|
@@ -159,6 +244,302 @@ export class SqliteRunStore {
|
|
|
159
244
|
CREATE INDEX IF NOT EXISTS idx_runs_environment ON workflow_runs(environment);
|
|
160
245
|
`,
|
|
161
246
|
},
|
|
247
|
+
{
|
|
248
|
+
// Tier 1 · idempotency caching. Adds:
|
|
249
|
+
// - `node_runs.cached_json` for cache-hit lineage on a node
|
|
250
|
+
// that short-circuited rather than ran.
|
|
251
|
+
// - `idempotency_cache` table for the cache backend keyed
|
|
252
|
+
// on (workflow_name, step_id, idempotency_key). Existing
|
|
253
|
+
// `node_runs` rows get NULL `cached_json` (a non-cached
|
|
254
|
+
// historical execution).
|
|
255
|
+
version: 4,
|
|
256
|
+
sql: `
|
|
257
|
+
ALTER TABLE node_runs ADD COLUMN cached_json TEXT;
|
|
258
|
+
|
|
259
|
+
CREATE TABLE IF NOT EXISTS idempotency_cache (
|
|
260
|
+
workflow_name TEXT NOT NULL,
|
|
261
|
+
step_id TEXT NOT NULL,
|
|
262
|
+
idempotency_key TEXT NOT NULL,
|
|
263
|
+
data_json TEXT NOT NULL,
|
|
264
|
+
cached_at INTEGER NOT NULL,
|
|
265
|
+
expires_at INTEGER,
|
|
266
|
+
source_run_id TEXT NOT NULL,
|
|
267
|
+
source_node_run_id TEXT NOT NULL,
|
|
268
|
+
PRIMARY KEY (workflow_name, step_id, idempotency_key)
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
CREATE INDEX IF NOT EXISTS idx_idem_expires ON idempotency_cache(expires_at);
|
|
272
|
+
`,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
// Tier 1 · retry loop + replay lineage. Adds:
|
|
276
|
+
// - `node_runs.attempts_json` for the per-attempt failure
|
|
277
|
+
// history before the node ultimately succeeded or was
|
|
278
|
+
// fail-noded (capped at MAX_STORED_ATTEMPTS by RunTracker).
|
|
279
|
+
// - `workflow_runs.replay_of` for the source run id when a
|
|
280
|
+
// run is started via `POST /__blok/runs/:id/replay`.
|
|
281
|
+
// Existing rows: NULL on both → "no retries; not a replay".
|
|
282
|
+
version: 5,
|
|
283
|
+
sql: `
|
|
284
|
+
ALTER TABLE node_runs ADD COLUMN attempts_json TEXT;
|
|
285
|
+
ALTER TABLE workflow_runs ADD COLUMN replay_of TEXT;
|
|
286
|
+
CREATE INDEX IF NOT EXISTS idx_runs_replay_of ON workflow_runs(replay_of);
|
|
287
|
+
`,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
// Tier 2 · sub-workflow lineage. Adds parent/child run linkage:
|
|
291
|
+
// - `workflow_runs.parent_run_id` — the parent run that
|
|
292
|
+
// invoked this run via a `subworkflow:` step.
|
|
293
|
+
// - `workflow_runs.parent_node_run_id` — the specific
|
|
294
|
+
// NodeRun within the parent that was the sub-workflow
|
|
295
|
+
// step (lets Studio jump to the exact invocation site).
|
|
296
|
+
// Index on parent_run_id for efficient `getRunsByParent` queries.
|
|
297
|
+
// Existing rows: NULL on both → "first-class triggered run".
|
|
298
|
+
version: 6,
|
|
299
|
+
sql: `
|
|
300
|
+
ALTER TABLE workflow_runs ADD COLUMN parent_run_id TEXT;
|
|
301
|
+
ALTER TABLE workflow_runs ADD COLUMN parent_node_run_id TEXT;
|
|
302
|
+
CREATE INDEX IF NOT EXISTS idx_runs_parent_run ON workflow_runs(parent_run_id);
|
|
303
|
+
`,
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
// Tier 2 #6 · concurrency keys. Adds a per-(workflow, key)
|
|
307
|
+
// in-flight slot table. Composite PK on (workflow_name,
|
|
308
|
+
// concurrency_key, run_id) lets a single run hold at most
|
|
309
|
+
// one slot per bucket while still permitting the
|
|
310
|
+
// `concurrencyLimit` runs to coexist within a bucket.
|
|
311
|
+
// `expires_at` is the lease upper bound — covers
|
|
312
|
+
// crash-safety when a process dies before releasing.
|
|
313
|
+
version: 7,
|
|
314
|
+
sql: `
|
|
315
|
+
CREATE TABLE IF NOT EXISTS concurrency_locks (
|
|
316
|
+
workflow_name TEXT NOT NULL,
|
|
317
|
+
concurrency_key TEXT NOT NULL,
|
|
318
|
+
run_id TEXT NOT NULL,
|
|
319
|
+
acquired_at INTEGER NOT NULL,
|
|
320
|
+
expires_at INTEGER NOT NULL,
|
|
321
|
+
PRIMARY KEY (workflow_name, concurrency_key, run_id)
|
|
322
|
+
);
|
|
323
|
+
CREATE INDEX IF NOT EXISTS idx_locks_expires ON concurrency_locks(expires_at);
|
|
324
|
+
CREATE INDEX IF NOT EXISTS idx_locks_workflow_key ON concurrency_locks(workflow_name, concurrency_key);
|
|
325
|
+
`,
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
// Tier 2 #5 + #7 · scheduling fields on workflow_runs:
|
|
329
|
+
// - `scheduled_at` — dispatch time (ms since epoch) for
|
|
330
|
+
// runs deferred via `trigger.delay`. NULL on immediate
|
|
331
|
+
// runs.
|
|
332
|
+
// - `expires_at` — TTL deadline (ms since epoch). When
|
|
333
|
+
// `now > expires_at` at dispatch time, the run is
|
|
334
|
+
// marked `expired` and skipped.
|
|
335
|
+
// - `debounce_key` — resolved key for runs that absorbed
|
|
336
|
+
// pings via `trigger.debounce`. NULL on non-debounced
|
|
337
|
+
// runs.
|
|
338
|
+
// - `debounce_mode` — `leading` | `trailing`.
|
|
339
|
+
// - `ping_count` — number of pings absorbed by this run.
|
|
340
|
+
// Indexes:
|
|
341
|
+
// - `idx_runs_scheduled_at` for "list scheduled runs"
|
|
342
|
+
// queries (Studio surface).
|
|
343
|
+
// - `idx_runs_debounce_key` for the debounce coordinator's
|
|
344
|
+
// "is there an active run for this key?" lookup
|
|
345
|
+
// (in-memory map is the hot path; SQLite is the
|
|
346
|
+
// fallback / restart-recovery).
|
|
347
|
+
// Existing rows: NULL on every new column. Backward-compat.
|
|
348
|
+
version: 8,
|
|
349
|
+
sql: `
|
|
350
|
+
ALTER TABLE workflow_runs ADD COLUMN scheduled_at INTEGER;
|
|
351
|
+
ALTER TABLE workflow_runs ADD COLUMN expires_at INTEGER;
|
|
352
|
+
ALTER TABLE workflow_runs ADD COLUMN debounce_key TEXT;
|
|
353
|
+
ALTER TABLE workflow_runs ADD COLUMN debounce_mode TEXT;
|
|
354
|
+
ALTER TABLE workflow_runs ADD COLUMN ping_count INTEGER;
|
|
355
|
+
CREATE INDEX IF NOT EXISTS idx_runs_scheduled_at ON workflow_runs(scheduled_at);
|
|
356
|
+
CREATE INDEX IF NOT EXISTS idx_runs_debounce_key ON workflow_runs(workflow_name, debounce_key);
|
|
357
|
+
`,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
// Tier 2 #5+#7 follow-up · durable scheduler.
|
|
361
|
+
//
|
|
362
|
+
// `scheduled_dispatches` persists the minimum payload needed
|
|
363
|
+
// to re-fire a deferred dispatch after a process crash. The
|
|
364
|
+
// `DeferredRunScheduler` writes a row when a dispatch is
|
|
365
|
+
// scheduled, deletes it when the dispatch fires or is
|
|
366
|
+
// cancelled. On boot, HttpTrigger.recoverDispatches scans
|
|
367
|
+
// the table, marks past-due+TTL-expired rows as `expired`,
|
|
368
|
+
// and re-registers timers for live dispatches.
|
|
369
|
+
//
|
|
370
|
+
// Workers don't need this — broker (BullMQ/NATS) already
|
|
371
|
+
// owns delayed delivery.
|
|
372
|
+
//
|
|
373
|
+
// Columns:
|
|
374
|
+
// - `run_id` — PK, FK-style reference to `workflow_runs.id`.
|
|
375
|
+
// - `workflow_name` — used by trigger boot recovery to
|
|
376
|
+
// filter rows it owns (when multiple HTTP triggers
|
|
377
|
+
// share the same store).
|
|
378
|
+
// - `trigger_type` — `"http"` for v1; future triggers
|
|
379
|
+
// can opt in by writing this column.
|
|
380
|
+
// - `scheduled_at` — ms since epoch when to fire.
|
|
381
|
+
// - `expires_at` — TTL deadline (NULL = no TTL).
|
|
382
|
+
// - `dispatch_status` — `"delayed"` | `"queued"` |
|
|
383
|
+
// `"debounced"`. Mirrors the run record's status.
|
|
384
|
+
// - `payload_json` — JSON-serialized minimal Context
|
|
385
|
+
// subset (HTTP: method, path, headers, body, params,
|
|
386
|
+
// query, workflow_path). Headers are pre-stripped of
|
|
387
|
+
// sensitive keys (authorization, cookie, x-api-key).
|
|
388
|
+
// - `created_at` — when the row was first written.
|
|
389
|
+
version: 9,
|
|
390
|
+
sql: `
|
|
391
|
+
CREATE TABLE IF NOT EXISTS scheduled_dispatches (
|
|
392
|
+
run_id TEXT PRIMARY KEY,
|
|
393
|
+
workflow_name TEXT NOT NULL,
|
|
394
|
+
trigger_type TEXT NOT NULL,
|
|
395
|
+
scheduled_at INTEGER NOT NULL,
|
|
396
|
+
expires_at INTEGER,
|
|
397
|
+
dispatch_status TEXT NOT NULL,
|
|
398
|
+
payload_json TEXT NOT NULL,
|
|
399
|
+
created_at INTEGER NOT NULL
|
|
400
|
+
);
|
|
401
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_dispatches_at ON scheduled_dispatches(scheduled_at);
|
|
402
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_dispatches_trigger ON scheduled_dispatches(trigger_type, workflow_name);
|
|
403
|
+
`,
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
// PR 4 — wait.for(duration) / wait.until(date) step primitive.
|
|
407
|
+
//
|
|
408
|
+
// On dispatchDeferred re-entry, the runner needs to know which
|
|
409
|
+
// steps already completed in the previous pass so it can skip
|
|
410
|
+
// past them. last_completed_step_index is the canonical
|
|
411
|
+
// resume cursor — runner increments after each non-wait
|
|
412
|
+
// step, then on re-entry skips steps with
|
|
413
|
+
// stepIndex <= last_completed_step_index. Default NULL (= no
|
|
414
|
+
// resume cursor; runner starts at step 0 as today).
|
|
415
|
+
version: 10,
|
|
416
|
+
sql: `
|
|
417
|
+
ALTER TABLE workflow_runs ADD COLUMN last_completed_step_index INTEGER;
|
|
418
|
+
`,
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
// v0.6 prerequisite for wait-inside-primitives Phase 2.
|
|
422
|
+
//
|
|
423
|
+
// state_snapshot is a JSON blob of `ctx.state` taken
|
|
424
|
+
// immediately before RunnerSteps throws WaitDispatchRequest.
|
|
425
|
+
// On dispatchDeferred re-entry — especially the cross-process
|
|
426
|
+
// recovery path where a fresh ctx is rebuilt from the
|
|
427
|
+
// persisted scheduled_dispatches row — TriggerBase.run reads
|
|
428
|
+
// this column and rehydrates ctx.state so subsequent steps
|
|
429
|
+
// see the same pre-wait state regardless of restart.
|
|
430
|
+
//
|
|
431
|
+
// Default NULL (= no snapshot; runner doesn't rehydrate,
|
|
432
|
+
// preserving exact pre-v0.6 behaviour for runs that never
|
|
433
|
+
// hit a wait).
|
|
434
|
+
version: 11,
|
|
435
|
+
sql: `
|
|
436
|
+
ALTER TABLE workflow_runs ADD COLUMN state_snapshot TEXT;
|
|
437
|
+
`,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
// v0.6 wait-inside-primitives Phase 2 — sequential forEach
|
|
441
|
+
// iteration cursor.
|
|
442
|
+
//
|
|
443
|
+
// iteration_context is a JSON blob written to the FOREACH'S
|
|
444
|
+
// NodeRun (not the inner-step NodeRun) when an inner step
|
|
445
|
+
// throws WaitDispatchRequest mid-iteration. Records:
|
|
446
|
+
// - `iteration`: which iteration of the forEach was in flight
|
|
447
|
+
// - `innerStepIndex`: which inner step within that iteration
|
|
448
|
+
// fired the wait
|
|
449
|
+
// - `completedResults`: the partial accumulator for
|
|
450
|
+
// iterations [0..iteration-1], so the post-resume final
|
|
451
|
+
// result array covers EVERY iteration
|
|
452
|
+
//
|
|
453
|
+
// Read on dispatchDeferred re-entry by TriggerBase.run, which
|
|
454
|
+
// rehydrates the column onto ctx as `_blokIterationResume`;
|
|
455
|
+
// ForEachNode picks it up to resume at the right iteration +
|
|
456
|
+
// inner step. Default NULL preserves pre-v0.6 behaviour for
|
|
457
|
+
// NodeRuns that never threw a wait (the vast majority).
|
|
458
|
+
//
|
|
459
|
+
// Phase 2 wires this for SEQUENTIAL forEach only. Future
|
|
460
|
+
// phases reuse the column with primitive-specific shapes
|
|
461
|
+
// (parallel forEach, loop, switch).
|
|
462
|
+
version: 12,
|
|
463
|
+
sql: `
|
|
464
|
+
ALTER TABLE node_runs ADD COLUMN iteration_context TEXT;
|
|
465
|
+
`,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
// Tier C #2 — cross-process scheduler coordination.
|
|
469
|
+
//
|
|
470
|
+
// Multi-process deployments sharing one PG / sqlite file risk
|
|
471
|
+
// double-firing the same dispatch when each process's
|
|
472
|
+
// `recoverDispatches()` independently registers timers. The
|
|
473
|
+
// claim columns let processes atomically take ownership of
|
|
474
|
+
// rows during recovery; the heartbeat keeps the claim fresh
|
|
475
|
+
// while the timer is registered; a dead holder's claim
|
|
476
|
+
// expires after `BLOK_SCHEDULER_CLAIM_LEASE_MS` and a
|
|
477
|
+
// surviving process can take over on its next recovery.
|
|
478
|
+
//
|
|
479
|
+
// Single-process sqlite deployments see no observable
|
|
480
|
+
// behavior change (the same process always wins the claim).
|
|
481
|
+
// Additive — pre-existing rows get NULL claim columns and
|
|
482
|
+
// become claimable on the next recovery.
|
|
483
|
+
version: 13,
|
|
484
|
+
sql: `
|
|
485
|
+
ALTER TABLE scheduled_dispatches ADD COLUMN claimed_by TEXT;
|
|
486
|
+
ALTER TABLE scheduled_dispatches ADD COLUMN claimed_at INTEGER;
|
|
487
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_dispatches_claim ON scheduled_dispatches(claimed_by, claimed_at);
|
|
488
|
+
`,
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
// E2 · server-side saved filters for the runs list. Replaces
|
|
492
|
+
// the prior `localStorage` impl in Studio so filter presets
|
|
493
|
+
// survive a fresh browser. `name` is UNIQUE — saving with an
|
|
494
|
+
// existing name overwrites the row (matches the localStorage
|
|
495
|
+
// "replace by name" semantics callers already rely on).
|
|
496
|
+
version: 14,
|
|
497
|
+
sql: `
|
|
498
|
+
CREATE TABLE IF NOT EXISTS trace_saved_filters (
|
|
499
|
+
id TEXT PRIMARY KEY,
|
|
500
|
+
name TEXT NOT NULL UNIQUE,
|
|
501
|
+
status TEXT NOT NULL DEFAULT '',
|
|
502
|
+
tags_input TEXT NOT NULL DEFAULT '',
|
|
503
|
+
metadata_input TEXT NOT NULL DEFAULT '',
|
|
504
|
+
created_at INTEGER NOT NULL,
|
|
505
|
+
updated_at INTEGER NOT NULL
|
|
506
|
+
);
|
|
507
|
+
CREATE INDEX IF NOT EXISTS idx_saved_filters_updated_at ON trace_saved_filters(updated_at);
|
|
508
|
+
`,
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
// v0.6 follow-up to #100 — recorded sample bodies for the
|
|
512
|
+
// Studio empty-state curl. Opt-in per HTTP trigger via
|
|
513
|
+
// `recordSample: true`. ONE row per workflow (PK on
|
|
514
|
+
// workflow_name); the trigger only writes the FIRST
|
|
515
|
+
// successful run's body so the operator-visible curl stays
|
|
516
|
+
// stable. Author overrides + static inference still apply
|
|
517
|
+
// when no row exists.
|
|
518
|
+
version: 15,
|
|
519
|
+
sql: `
|
|
520
|
+
CREATE TABLE IF NOT EXISTS trace_workflow_samples (
|
|
521
|
+
workflow_name TEXT PRIMARY KEY,
|
|
522
|
+
body_json TEXT NOT NULL,
|
|
523
|
+
source_run_id TEXT NOT NULL,
|
|
524
|
+
recorded_at INTEGER NOT NULL
|
|
525
|
+
);
|
|
526
|
+
`,
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
// v0.6 follow-up — bag column for Studio-visible step flags.
|
|
530
|
+
// Until now `wait`, `subworkflowDepth`, `middleware`,
|
|
531
|
+
// `iterationIndex` lived only on the in-memory NodeRun and
|
|
532
|
+
// silently dropped on sqlite/PG round-trip — the rail badges
|
|
533
|
+
// vanished on browser refresh or process restart. The new
|
|
534
|
+
// G2 `dispatch` field would have inherited the same gap.
|
|
535
|
+
// One opaque JSON column holds whichever of the five fields
|
|
536
|
+
// are set; pre-v16 rows get NULL (which decodes back to all
|
|
537
|
+
// fields undefined — matches the previous behaviour exactly).
|
|
538
|
+
version: 16,
|
|
539
|
+
sql: `
|
|
540
|
+
ALTER TABLE node_runs ADD COLUMN flags_json TEXT;
|
|
541
|
+
`,
|
|
542
|
+
},
|
|
162
543
|
];
|
|
163
544
|
const applyMigration = this.db.transaction((m) => {
|
|
164
545
|
this.db.exec(m.sql);
|
|
@@ -183,9 +564,12 @@ export class SqliteRunStore {
|
|
|
183
564
|
INSERT OR REPLACE INTO workflow_runs
|
|
184
565
|
(id, workflow_name, workflow_path, trigger_type, trigger_summary,
|
|
185
566
|
status, started_at, finished_at, duration_ms, error_json,
|
|
186
|
-
tags_json, metadata_json, node_count, completed_nodes, environment
|
|
187
|
-
|
|
188
|
-
|
|
567
|
+
tags_json, metadata_json, node_count, completed_nodes, environment, replay_of,
|
|
568
|
+
parent_run_id, parent_node_run_id,
|
|
569
|
+
scheduled_at, expires_at, debounce_key, debounce_mode, ping_count,
|
|
570
|
+
last_completed_step_index, state_snapshot)
|
|
571
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
572
|
+
`).run(run.id, run.workflowName, run.workflowPath, run.triggerType, run.triggerSummary, run.status, run.startedAt, run.finishedAt ?? null, run.durationMs ?? null, run.error ? JSON.stringify(run.error) : null, JSON.stringify(run.tags || []), run.metadata ? JSON.stringify(run.metadata) : null, run.nodeCount, run.completedNodes, run.environment ?? null, run.replayOf ?? null, run.parentRunId ?? null, run.parentNodeRunId ?? null, run.scheduledAt ?? null, run.expiresAt ?? null, run.debounceKey ?? null, run.debounceMode ?? null, run.pingCount ?? null, run.lastCompletedStepIndex ?? null, run.stateSnapshot ?? null);
|
|
189
573
|
}
|
|
190
574
|
updateRun(runId, updates) {
|
|
191
575
|
const setClauses = [];
|
|
@@ -218,6 +602,50 @@ export class SqliteRunStore {
|
|
|
218
602
|
setClauses.push("metadata_json = ?");
|
|
219
603
|
values.push(JSON.stringify(updates.metadata));
|
|
220
604
|
}
|
|
605
|
+
if (updates.replayOf !== undefined) {
|
|
606
|
+
setClauses.push("replay_of = ?");
|
|
607
|
+
values.push(updates.replayOf);
|
|
608
|
+
}
|
|
609
|
+
if (updates.parentRunId !== undefined) {
|
|
610
|
+
setClauses.push("parent_run_id = ?");
|
|
611
|
+
values.push(updates.parentRunId);
|
|
612
|
+
}
|
|
613
|
+
if (updates.parentNodeRunId !== undefined) {
|
|
614
|
+
setClauses.push("parent_node_run_id = ?");
|
|
615
|
+
values.push(updates.parentNodeRunId);
|
|
616
|
+
}
|
|
617
|
+
if (updates.scheduledAt !== undefined) {
|
|
618
|
+
setClauses.push("scheduled_at = ?");
|
|
619
|
+
values.push(updates.scheduledAt);
|
|
620
|
+
}
|
|
621
|
+
if (updates.expiresAt !== undefined) {
|
|
622
|
+
setClauses.push("expires_at = ?");
|
|
623
|
+
values.push(updates.expiresAt);
|
|
624
|
+
}
|
|
625
|
+
if (updates.debounceKey !== undefined) {
|
|
626
|
+
setClauses.push("debounce_key = ?");
|
|
627
|
+
values.push(updates.debounceKey);
|
|
628
|
+
}
|
|
629
|
+
if (updates.debounceMode !== undefined) {
|
|
630
|
+
setClauses.push("debounce_mode = ?");
|
|
631
|
+
values.push(updates.debounceMode);
|
|
632
|
+
}
|
|
633
|
+
if (updates.pingCount !== undefined) {
|
|
634
|
+
setClauses.push("ping_count = ?");
|
|
635
|
+
values.push(updates.pingCount);
|
|
636
|
+
}
|
|
637
|
+
if (updates.lastCompletedStepIndex !== undefined) {
|
|
638
|
+
setClauses.push("last_completed_step_index = ?");
|
|
639
|
+
values.push(updates.lastCompletedStepIndex);
|
|
640
|
+
}
|
|
641
|
+
if (updates.stateSnapshot !== undefined) {
|
|
642
|
+
setClauses.push("state_snapshot = ?");
|
|
643
|
+
values.push(updates.stateSnapshot);
|
|
644
|
+
}
|
|
645
|
+
if (updates.startedAt !== undefined) {
|
|
646
|
+
setClauses.push("started_at = ?");
|
|
647
|
+
values.push(updates.startedAt);
|
|
648
|
+
}
|
|
221
649
|
if (setClauses.length === 0)
|
|
222
650
|
return;
|
|
223
651
|
values.push(runId);
|
|
@@ -229,9 +657,10 @@ export class SqliteRunStore {
|
|
|
229
657
|
(id, run_id, node_name, node_type, runtime_kind,
|
|
230
658
|
status, started_at, finished_at, duration_ms,
|
|
231
659
|
inputs_json, outputs_json, error_json,
|
|
232
|
-
parent_node_id, depth, step_index, metrics_json
|
|
233
|
-
|
|
234
|
-
|
|
660
|
+
parent_node_id, depth, step_index, metrics_json, cached_json, attempts_json,
|
|
661
|
+
iteration_context, flags_json)
|
|
662
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
663
|
+
`).run(nodeRun.id, nodeRun.runId, nodeRun.nodeName, nodeRun.nodeType, nodeRun.runtimeKind ?? null, nodeRun.status, nodeRun.startedAt, nodeRun.finishedAt ?? null, nodeRun.durationMs ?? null, nodeRun.inputs !== undefined ? JSON.stringify(nodeRun.inputs) : null, nodeRun.outputs !== undefined ? JSON.stringify(nodeRun.outputs) : null, nodeRun.error ? JSON.stringify(nodeRun.error) : null, nodeRun.parentNodeId ?? null, nodeRun.depth, nodeRun.stepIndex, nodeRun.metrics ? JSON.stringify(nodeRun.metrics) : null, nodeRun.cached ? JSON.stringify(nodeRun.cached) : null, nodeRun.attempts ? JSON.stringify(nodeRun.attempts) : null, nodeRun.iterationContext ? JSON.stringify(nodeRun.iterationContext) : null, encodeNodeRunFlags(nodeRun));
|
|
235
664
|
}
|
|
236
665
|
updateNodeRun(nodeRunId, updates) {
|
|
237
666
|
const setClauses = [];
|
|
@@ -260,6 +689,18 @@ export class SqliteRunStore {
|
|
|
260
689
|
setClauses.push("metrics_json = ?");
|
|
261
690
|
values.push(JSON.stringify(updates.metrics));
|
|
262
691
|
}
|
|
692
|
+
if (updates.cached !== undefined) {
|
|
693
|
+
setClauses.push("cached_json = ?");
|
|
694
|
+
values.push(JSON.stringify(updates.cached));
|
|
695
|
+
}
|
|
696
|
+
if (updates.attempts !== undefined) {
|
|
697
|
+
setClauses.push("attempts_json = ?");
|
|
698
|
+
values.push(JSON.stringify(updates.attempts));
|
|
699
|
+
}
|
|
700
|
+
if (updates.iterationContext !== undefined) {
|
|
701
|
+
setClauses.push("iteration_context = ?");
|
|
702
|
+
values.push(JSON.stringify(updates.iterationContext));
|
|
703
|
+
}
|
|
263
704
|
if (setClauses.length === 0)
|
|
264
705
|
return;
|
|
265
706
|
values.push(nodeRunId);
|
|
@@ -293,6 +734,32 @@ export class SqliteRunStore {
|
|
|
293
734
|
conditions.push("status = ?");
|
|
294
735
|
params.push(opts.status);
|
|
295
736
|
}
|
|
737
|
+
// F2 (v0.5) — metadata filter with operator support
|
|
738
|
+
// (eq / ne / gt / gte / lt / lte / like / in / nin) via
|
|
739
|
+
// `translateFilterToSql`. F1 (v0.5) — declared indexed keys
|
|
740
|
+
// (`BLOK_INDEXED_METADATA_KEYS=tier,region`) reference
|
|
741
|
+
// `metadata_<key>_idx` (the generated column ensured at
|
|
742
|
+
// construction time) so the query planner uses the btree
|
|
743
|
+
// index instead of a sequential `json_extract` scan.
|
|
744
|
+
//
|
|
745
|
+
// `normaliseMetadataFilters` accepts both the legacy
|
|
746
|
+
// `Record<string, string>` shape (v0.4) AND the new
|
|
747
|
+
// `MetadataFilter[]` shape (v0.5) — keys outside
|
|
748
|
+
// `/^[a-zA-Z0-9_-]+$/` are silently dropped for JSON-path
|
|
749
|
+
// injection safety.
|
|
750
|
+
if (opts?.metadata) {
|
|
751
|
+
const filters = normaliseMetadataFilters(opts.metadata);
|
|
752
|
+
for (const f of filters) {
|
|
753
|
+
const lhs = this.indexedMetadataKeys.has(f.key)
|
|
754
|
+
? `metadata_${f.key}_idx`
|
|
755
|
+
: `json_extract(metadata_json, '$.${f.key}')`;
|
|
756
|
+
const translated = translateFilterToSql(f, lhs);
|
|
757
|
+
if (!translated)
|
|
758
|
+
continue;
|
|
759
|
+
conditions.push(translated.fragment);
|
|
760
|
+
params.push(...translated.params);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
296
763
|
const tags = opts?.tags;
|
|
297
764
|
if (tags && tags.length > 0) {
|
|
298
765
|
// For each tag, check that it exists in the JSON array
|
|
@@ -370,6 +837,10 @@ export class SqliteRunStore {
|
|
|
370
837
|
const row = this.stmt("getNodeRun", "SELECT * FROM node_runs WHERE id = ?").get(nodeRunId);
|
|
371
838
|
return row ? this.rowToNodeRun(row) : undefined;
|
|
372
839
|
}
|
|
840
|
+
getRunsByParent(parentRunId) {
|
|
841
|
+
const rows = this.stmt("getRunsByParent", "SELECT * FROM workflow_runs WHERE parent_run_id = ? ORDER BY started_at ASC").all(parentRunId);
|
|
842
|
+
return rows.map((r) => this.rowToRun(r));
|
|
843
|
+
}
|
|
373
844
|
getEvents(runId, since) {
|
|
374
845
|
if (since) {
|
|
375
846
|
return this.stmt("getEventsSince", "SELECT * FROM run_events WHERE run_id = ? AND timestamp > ? ORDER BY timestamp").all(runId, since).map((r) => this.rowToEvent(r));
|
|
@@ -600,6 +1071,84 @@ export class SqliteRunStore {
|
|
|
600
1071
|
values.push(dashboardId);
|
|
601
1072
|
this.db.prepare(`UPDATE dashboards SET ${setClauses.join(", ")} WHERE id = ?`).run(...values);
|
|
602
1073
|
}
|
|
1074
|
+
// === Saved filters (E2) ===
|
|
1075
|
+
upsertSavedFilter(filter) {
|
|
1076
|
+
// UNIQUE(name) lets us collapse "create or overwrite" into one
|
|
1077
|
+
// UPSERT — preserves the existing `id` + `created_at` when the
|
|
1078
|
+
// name matches, bumps `updated_at` to now. Matches the legacy
|
|
1079
|
+
// localStorage replace-by-name semantics callers depend on.
|
|
1080
|
+
const persisted = this.db.transaction((f) => {
|
|
1081
|
+
const existing = this.db.prepare("SELECT id, created_at FROM trace_saved_filters WHERE name = ?").get(f.name);
|
|
1082
|
+
const id = existing?.id ?? f.id;
|
|
1083
|
+
const createdAt = existing?.created_at ?? f.createdAt;
|
|
1084
|
+
const updatedAt = f.updatedAt;
|
|
1085
|
+
this.db
|
|
1086
|
+
.prepare(`
|
|
1087
|
+
INSERT INTO trace_saved_filters
|
|
1088
|
+
(id, name, status, tags_input, metadata_input, created_at, updated_at)
|
|
1089
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1090
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
1091
|
+
status = excluded.status,
|
|
1092
|
+
tags_input = excluded.tags_input,
|
|
1093
|
+
metadata_input = excluded.metadata_input,
|
|
1094
|
+
updated_at = excluded.updated_at
|
|
1095
|
+
`)
|
|
1096
|
+
.run(id, f.name, f.status, f.tagsInput, f.metadataInput, createdAt, updatedAt);
|
|
1097
|
+
return {
|
|
1098
|
+
id,
|
|
1099
|
+
name: f.name,
|
|
1100
|
+
status: f.status,
|
|
1101
|
+
tagsInput: f.tagsInput,
|
|
1102
|
+
metadataInput: f.metadataInput,
|
|
1103
|
+
createdAt,
|
|
1104
|
+
updatedAt,
|
|
1105
|
+
};
|
|
1106
|
+
})(filter);
|
|
1107
|
+
return persisted;
|
|
1108
|
+
}
|
|
1109
|
+
listSavedFilters() {
|
|
1110
|
+
const rows = this.db
|
|
1111
|
+
.prepare("SELECT * FROM trace_saved_filters ORDER BY updated_at DESC")
|
|
1112
|
+
.all();
|
|
1113
|
+
return rows.map((r) => rowToSavedFilter(r));
|
|
1114
|
+
}
|
|
1115
|
+
deleteSavedFilter(name) {
|
|
1116
|
+
const result = this.db.prepare("DELETE FROM trace_saved_filters WHERE name = ?").run(name);
|
|
1117
|
+
return result.changes > 0;
|
|
1118
|
+
}
|
|
1119
|
+
// === Sample-body recording (option C follow-up to #100) ===
|
|
1120
|
+
recordWorkflowSample(sample) {
|
|
1121
|
+
// First-record-wins. INSERT OR IGNORE skips the row when the
|
|
1122
|
+
// PRIMARY KEY (workflow_name) already exists; we then SELECT to
|
|
1123
|
+
// return whatever's actually persisted. Keeps the operator-
|
|
1124
|
+
// visible sample stable across re-runs.
|
|
1125
|
+
const insert = this.db.prepare("INSERT OR IGNORE INTO trace_workflow_samples (workflow_name, body_json, source_run_id, recorded_at) VALUES (?, ?, ?, ?)");
|
|
1126
|
+
insert.run(sample.workflowName, JSON.stringify(sample.body), sample.sourceRunId, sample.recordedAt);
|
|
1127
|
+
const existing = this.getWorkflowSample(sample.workflowName);
|
|
1128
|
+
return existing ?? sample;
|
|
1129
|
+
}
|
|
1130
|
+
getWorkflowSample(workflowName) {
|
|
1131
|
+
const row = this.db.prepare("SELECT * FROM trace_workflow_samples WHERE workflow_name = ?").get(workflowName);
|
|
1132
|
+
if (!row)
|
|
1133
|
+
return undefined;
|
|
1134
|
+
let body;
|
|
1135
|
+
try {
|
|
1136
|
+
body = JSON.parse(row.body_json);
|
|
1137
|
+
}
|
|
1138
|
+
catch {
|
|
1139
|
+
body = null;
|
|
1140
|
+
}
|
|
1141
|
+
return {
|
|
1142
|
+
workflowName: row.workflow_name,
|
|
1143
|
+
body,
|
|
1144
|
+
sourceRunId: row.source_run_id,
|
|
1145
|
+
recordedAt: row.recorded_at,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
deleteWorkflowSample(workflowName) {
|
|
1149
|
+
const result = this.db.prepare("DELETE FROM trace_workflow_samples WHERE workflow_name = ?").run(workflowName);
|
|
1150
|
+
return result.changes > 0;
|
|
1151
|
+
}
|
|
603
1152
|
// === Cleanup ===
|
|
604
1153
|
clearAll() {
|
|
605
1154
|
const count = this.db.prepare("SELECT COUNT(*) as c FROM workflow_runs").get()?.c ?? 0;
|
|
@@ -608,8 +1157,186 @@ export class SqliteRunStore {
|
|
|
608
1157
|
this.db.exec("DELETE FROM node_runs");
|
|
609
1158
|
this.db.exec("DELETE FROM workflow_runs");
|
|
610
1159
|
this.db.exec("DELETE FROM dashboards");
|
|
1160
|
+
this.db.exec("DELETE FROM idempotency_cache");
|
|
1161
|
+
this.db.exec("DELETE FROM concurrency_locks");
|
|
611
1162
|
return count;
|
|
612
1163
|
}
|
|
1164
|
+
// === Idempotency cache ===
|
|
1165
|
+
getIdempotencyCache(workflowName, stepId, key) {
|
|
1166
|
+
const row = this.stmt("getIdempotencyCache", "SELECT * FROM idempotency_cache WHERE workflow_name = ? AND step_id = ? AND idempotency_key = ?").get(workflowName, stepId, key);
|
|
1167
|
+
if (!row)
|
|
1168
|
+
return null;
|
|
1169
|
+
if (row.expires_at !== null && row.expires_at <= Date.now()) {
|
|
1170
|
+
// Lazy purge — remove the expired entry inline so subsequent
|
|
1171
|
+
// reads don't even pay for the row materialization.
|
|
1172
|
+
this.stmt("deleteExpiredIdempotency", "DELETE FROM idempotency_cache WHERE workflow_name = ? AND step_id = ? AND idempotency_key = ?").run(workflowName, stepId, key);
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
data: JSON.parse(row.data_json),
|
|
1177
|
+
cachedAt: row.cached_at,
|
|
1178
|
+
expiresAt: row.expires_at,
|
|
1179
|
+
sourceRunId: row.source_run_id,
|
|
1180
|
+
sourceNodeRunId: row.source_node_run_id,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
setIdempotencyCache(workflowName, stepId, key, entry) {
|
|
1184
|
+
this.stmt("setIdempotencyCache", `
|
|
1185
|
+
INSERT OR REPLACE INTO idempotency_cache
|
|
1186
|
+
(workflow_name, step_id, idempotency_key, data_json, cached_at, expires_at,
|
|
1187
|
+
source_run_id, source_node_run_id)
|
|
1188
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1189
|
+
`).run(workflowName, stepId, key, JSON.stringify(entry.data), entry.cachedAt, entry.expiresAt, entry.sourceRunId, entry.sourceNodeRunId);
|
|
1190
|
+
}
|
|
1191
|
+
purgeExpiredIdempotencyCache(now) {
|
|
1192
|
+
const result = this.stmt("purgeExpiredIdempotency", "DELETE FROM idempotency_cache WHERE expires_at IS NOT NULL AND expires_at <= ?").run(now);
|
|
1193
|
+
return result.changes;
|
|
1194
|
+
}
|
|
1195
|
+
// === Concurrency gating (Tier 2 #6) ===
|
|
1196
|
+
acquireConcurrencySlot(workflowName, concurrencyKey, concurrencyLimit, runId, leaseExpiresAt) {
|
|
1197
|
+
// Wrap the read-modify-write in a transaction so two concurrent
|
|
1198
|
+
// `acquireConcurrencySlot` calls can't both see N < limit and
|
|
1199
|
+
// both grant a slot. better-sqlite3 / bun:sqlite serialize within
|
|
1200
|
+
// a single connection; the transaction is the safety net for any
|
|
1201
|
+
// future multi-connection setup.
|
|
1202
|
+
const txn = this.db.transaction(() => {
|
|
1203
|
+
const now = Date.now();
|
|
1204
|
+
// Lazy-purge expired leases for THIS bucket so we don't deny
|
|
1205
|
+
// based on a slot held by a process that crashed mid-run.
|
|
1206
|
+
this.stmt("deleteExpiredLocksForBucket", "DELETE FROM concurrency_locks WHERE workflow_name = ? AND concurrency_key = ? AND expires_at <= ?").run(workflowName, concurrencyKey, now);
|
|
1207
|
+
// Idempotent re-acquire — if the same runId already holds a
|
|
1208
|
+
// slot, refresh its lease (UPSERT on PK) and report success
|
|
1209
|
+
// without growing the count.
|
|
1210
|
+
const existing = this.stmt("getLockForRun", "SELECT 1 FROM concurrency_locks WHERE workflow_name = ? AND concurrency_key = ? AND run_id = ?").get(workflowName, concurrencyKey, runId);
|
|
1211
|
+
if (existing) {
|
|
1212
|
+
this.stmt("refreshLockLease", "UPDATE concurrency_locks SET expires_at = ? WHERE workflow_name = ? AND concurrency_key = ? AND run_id = ?").run(leaseExpiresAt, workflowName, concurrencyKey, runId);
|
|
1213
|
+
const count = this.stmt("countLocksForBucket", "SELECT COUNT(*) as c FROM concurrency_locks WHERE workflow_name = ? AND concurrency_key = ?").get(workflowName, concurrencyKey)?.c ?? 0;
|
|
1214
|
+
return { acquired: true, currentInFlight: count };
|
|
1215
|
+
}
|
|
1216
|
+
const currentCount = this.stmt("countLocksForBucket", "SELECT COUNT(*) as c FROM concurrency_locks WHERE workflow_name = ? AND concurrency_key = ?").get(workflowName, concurrencyKey)?.c ?? 0;
|
|
1217
|
+
if (currentCount >= concurrencyLimit) {
|
|
1218
|
+
return { acquired: false, currentInFlight: currentCount };
|
|
1219
|
+
}
|
|
1220
|
+
this.stmt("insertLock", `INSERT INTO concurrency_locks
|
|
1221
|
+
(workflow_name, concurrency_key, run_id, acquired_at, expires_at)
|
|
1222
|
+
VALUES (?, ?, ?, ?, ?)`).run(workflowName, concurrencyKey, runId, now, leaseExpiresAt);
|
|
1223
|
+
return { acquired: true, currentInFlight: currentCount + 1 };
|
|
1224
|
+
});
|
|
1225
|
+
return txn();
|
|
1226
|
+
}
|
|
1227
|
+
releaseConcurrencySlot(workflowName, concurrencyKey, runId) {
|
|
1228
|
+
this.stmt("releaseLock", "DELETE FROM concurrency_locks WHERE workflow_name = ? AND concurrency_key = ? AND run_id = ?").run(workflowName, concurrencyKey, runId);
|
|
1229
|
+
}
|
|
1230
|
+
purgeExpiredConcurrencySlots(now) {
|
|
1231
|
+
const result = this.stmt("purgeExpiredLocks", "DELETE FROM concurrency_locks WHERE expires_at <= ?").run(now);
|
|
1232
|
+
return result.changes;
|
|
1233
|
+
}
|
|
1234
|
+
getConcurrencySnapshot(now) {
|
|
1235
|
+
const rows = this.db
|
|
1236
|
+
.prepare("SELECT workflow_name, concurrency_key, run_id, expires_at FROM concurrency_locks WHERE expires_at > ?")
|
|
1237
|
+
.all(now);
|
|
1238
|
+
const buckets = new Map();
|
|
1239
|
+
for (const r of rows) {
|
|
1240
|
+
const key = `${r.workflow_name}\x1f${r.concurrency_key}`;
|
|
1241
|
+
let bucket = buckets.get(key);
|
|
1242
|
+
if (!bucket) {
|
|
1243
|
+
bucket = { workflowName: r.workflow_name, concurrencyKey: r.concurrency_key, leases: [] };
|
|
1244
|
+
buckets.set(key, bucket);
|
|
1245
|
+
}
|
|
1246
|
+
bucket.leases.push({ runId: r.run_id, expiresAt: r.expires_at });
|
|
1247
|
+
}
|
|
1248
|
+
return Array.from(buckets.values());
|
|
1249
|
+
}
|
|
1250
|
+
// === Durable scheduling (Tier 2 #5+#7 follow-up) ===
|
|
1251
|
+
upsertScheduledDispatch(row) {
|
|
1252
|
+
// On CONFLICT we deliberately PRESERVE claimed_by + claimed_at —
|
|
1253
|
+
// debounce reset / queue re-defer updates the scheduling fields
|
|
1254
|
+
// but ownership stays with whoever currently holds the claim.
|
|
1255
|
+
this.stmt("upsertScheduledDispatch", `INSERT INTO scheduled_dispatches
|
|
1256
|
+
(run_id, workflow_name, trigger_type, scheduled_at, expires_at, dispatch_status, payload_json, created_at)
|
|
1257
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1258
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
1259
|
+
scheduled_at = excluded.scheduled_at,
|
|
1260
|
+
expires_at = excluded.expires_at,
|
|
1261
|
+
dispatch_status = excluded.dispatch_status,
|
|
1262
|
+
payload_json = excluded.payload_json`).run(row.runId, row.workflowName, row.triggerType, row.scheduledAt, row.expiresAt ?? null, row.dispatchStatus, JSON.stringify(row.payload ?? null), row.createdAt);
|
|
1263
|
+
}
|
|
1264
|
+
deleteScheduledDispatch(runId) {
|
|
1265
|
+
const result = this.stmt("deleteScheduledDispatch", "DELETE FROM scheduled_dispatches WHERE run_id = ?").run(runId);
|
|
1266
|
+
return result.changes > 0;
|
|
1267
|
+
}
|
|
1268
|
+
getScheduledDispatches(opts) {
|
|
1269
|
+
const triggerType = opts?.triggerType;
|
|
1270
|
+
const status = opts?.status;
|
|
1271
|
+
const clauses = [];
|
|
1272
|
+
const args = [];
|
|
1273
|
+
if (triggerType) {
|
|
1274
|
+
clauses.push("trigger_type = ?");
|
|
1275
|
+
args.push(triggerType);
|
|
1276
|
+
}
|
|
1277
|
+
if (status) {
|
|
1278
|
+
clauses.push("dispatch_status = ?");
|
|
1279
|
+
args.push(status);
|
|
1280
|
+
}
|
|
1281
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1282
|
+
const sql = `SELECT * FROM scheduled_dispatches ${where} ORDER BY scheduled_at ASC`;
|
|
1283
|
+
const rows = this.db.prepare(sql).all(...args);
|
|
1284
|
+
return rows.map((r) => this.rowToScheduledDispatch(r));
|
|
1285
|
+
}
|
|
1286
|
+
// === Tier C #2 — cross-process scheduler coordination ===
|
|
1287
|
+
claimDispatches(processId, leaseMs, now, opts) {
|
|
1288
|
+
// Atomic claim: UPDATE rows that are unclaimed OR whose lease has
|
|
1289
|
+
// expired, set the claim, return the affected rows.
|
|
1290
|
+
// SQLite supports `UPDATE ... RETURNING *` since v3.35.0 (2021);
|
|
1291
|
+
// better-sqlite3 ships a modern sqlite so this is safe.
|
|
1292
|
+
const triggerClause = opts?.triggerType ? "AND trigger_type = ?" : "";
|
|
1293
|
+
const sql = `
|
|
1294
|
+
UPDATE scheduled_dispatches
|
|
1295
|
+
SET claimed_by = ?, claimed_at = ?
|
|
1296
|
+
WHERE (claimed_by IS NULL OR claimed_at + ? < ?) ${triggerClause}
|
|
1297
|
+
RETURNING *
|
|
1298
|
+
`;
|
|
1299
|
+
const args = [processId, now, leaseMs, now];
|
|
1300
|
+
if (opts?.triggerType)
|
|
1301
|
+
args.push(opts.triggerType);
|
|
1302
|
+
const claimed = this.db.prepare(sql).all(...args);
|
|
1303
|
+
const out = claimed.map((r) => this.rowToScheduledDispatch(r));
|
|
1304
|
+
out.sort((a, b) => a.scheduledAt - b.scheduledAt);
|
|
1305
|
+
return out;
|
|
1306
|
+
}
|
|
1307
|
+
heartbeatClaims(processId, now) {
|
|
1308
|
+
const result = this.stmt("heartbeatClaims", "UPDATE scheduled_dispatches SET claimed_at = ? WHERE claimed_by = ?").run(now, processId);
|
|
1309
|
+
return result.changes;
|
|
1310
|
+
}
|
|
1311
|
+
releaseClaim(runId) {
|
|
1312
|
+
const result = this.stmt("releaseClaim", "UPDATE scheduled_dispatches SET claimed_by = NULL, claimed_at = NULL WHERE run_id = ? AND claimed_by IS NOT NULL").run(runId);
|
|
1313
|
+
return result.changes > 0;
|
|
1314
|
+
}
|
|
1315
|
+
rowToScheduledDispatch(r) {
|
|
1316
|
+
let parsedPayload = null;
|
|
1317
|
+
try {
|
|
1318
|
+
parsedPayload = JSON.parse(r.payload_json);
|
|
1319
|
+
}
|
|
1320
|
+
catch {
|
|
1321
|
+
parsedPayload = null;
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
runId: r.run_id,
|
|
1325
|
+
workflowName: r.workflow_name,
|
|
1326
|
+
triggerType: r.trigger_type,
|
|
1327
|
+
scheduledAt: r.scheduled_at,
|
|
1328
|
+
expiresAt: r.expires_at ?? undefined,
|
|
1329
|
+
dispatchStatus: r.dispatch_status,
|
|
1330
|
+
payload: parsedPayload,
|
|
1331
|
+
createdAt: r.created_at,
|
|
1332
|
+
claimedBy: r.claimed_by ?? undefined,
|
|
1333
|
+
claimedAt: r.claimed_at ?? undefined,
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
purgeExpiredScheduledDispatches(now) {
|
|
1337
|
+
const result = this.stmt("purgeExpiredScheduledDispatches", "DELETE FROM scheduled_dispatches WHERE expires_at IS NOT NULL AND expires_at < ?").run(now);
|
|
1338
|
+
return result.changes;
|
|
1339
|
+
}
|
|
613
1340
|
deleteRunsBefore(timestamp) {
|
|
614
1341
|
// Foreign key CASCADE handles child tables
|
|
615
1342
|
const result = this.db
|
|
@@ -658,9 +1385,20 @@ export class SqliteRunStore {
|
|
|
658
1385
|
// "production" so the EnvChip default-scope still surfaces
|
|
659
1386
|
// historical data without a backfill.
|
|
660
1387
|
environment: row.environment ?? "production",
|
|
1388
|
+
replayOf: row.replay_of ?? undefined,
|
|
1389
|
+
parentRunId: row.parent_run_id ?? undefined,
|
|
1390
|
+
parentNodeRunId: row.parent_node_run_id ?? undefined,
|
|
1391
|
+
scheduledAt: row.scheduled_at ?? undefined,
|
|
1392
|
+
expiresAt: row.expires_at ?? undefined,
|
|
1393
|
+
debounceKey: row.debounce_key ?? undefined,
|
|
1394
|
+
debounceMode: (row.debounce_mode ?? undefined),
|
|
1395
|
+
pingCount: row.ping_count ?? undefined,
|
|
1396
|
+
lastCompletedStepIndex: row.last_completed_step_index ?? undefined,
|
|
1397
|
+
stateSnapshot: row.state_snapshot ?? undefined,
|
|
661
1398
|
};
|
|
662
1399
|
}
|
|
663
1400
|
rowToNodeRun(row) {
|
|
1401
|
+
const flags = decodeNodeRunFlags(row.flags_json);
|
|
664
1402
|
return {
|
|
665
1403
|
id: row.id,
|
|
666
1404
|
runId: row.run_id,
|
|
@@ -678,6 +1416,16 @@ export class SqliteRunStore {
|
|
|
678
1416
|
depth: row.depth,
|
|
679
1417
|
stepIndex: row.step_index,
|
|
680
1418
|
metrics: row.metrics_json ? JSON.parse(row.metrics_json) : undefined,
|
|
1419
|
+
cached: row.cached_json ? JSON.parse(row.cached_json) : undefined,
|
|
1420
|
+
attempts: row.attempts_json ? JSON.parse(row.attempts_json) : undefined,
|
|
1421
|
+
iterationContext: row.iteration_context
|
|
1422
|
+
? JSON.parse(row.iteration_context)
|
|
1423
|
+
: undefined,
|
|
1424
|
+
wait: flags.wait,
|
|
1425
|
+
dispatch: flags.dispatch,
|
|
1426
|
+
subworkflowDepth: flags.subworkflowDepth,
|
|
1427
|
+
middleware: flags.middleware,
|
|
1428
|
+
iterationIndex: flags.iterationIndex,
|
|
681
1429
|
};
|
|
682
1430
|
}
|
|
683
1431
|
rowToEvent(row) {
|
|
@@ -716,4 +1464,15 @@ export class SqliteRunStore {
|
|
|
716
1464
|
};
|
|
717
1465
|
}
|
|
718
1466
|
}
|
|
1467
|
+
function rowToSavedFilter(row) {
|
|
1468
|
+
return {
|
|
1469
|
+
id: row.id,
|
|
1470
|
+
name: row.name,
|
|
1471
|
+
status: row.status,
|
|
1472
|
+
tagsInput: row.tags_input,
|
|
1473
|
+
metadataInput: row.metadata_input,
|
|
1474
|
+
createdAt: row.created_at,
|
|
1475
|
+
updatedAt: row.updated_at,
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
719
1478
|
//# sourceMappingURL=SqliteRunStore.js.map
|