@hasna/workflows 0.1.3 → 0.1.4
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/agent-tools.js +149 -51
- package/dist/cli/index.js +259 -93
- package/dist/index.js +158 -60
- package/dist/mcp/index.js +155 -57
- package/dist/serve/index.js +158 -60
- package/dist/service.js +155 -57
- package/hasna.contract.json +1 -1
- package/package.json +3 -3
- package/types/daemon.d.ts +3 -0
- package/types/graph.d.ts +7 -0
- package/types/session.d.ts +24 -0
- package/types/store.d.ts +5 -0
package/dist/serve/index.js
CHANGED
|
@@ -18,12 +18,12 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
18
18
|
var __require = import.meta.require;
|
|
19
19
|
|
|
20
20
|
// src/lanes/types.ts
|
|
21
|
-
import { statSync } from "fs";
|
|
21
|
+
import { statSync as statSync2 } from "fs";
|
|
22
22
|
function binaryOnPath(binary) {
|
|
23
23
|
const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
24
24
|
for (const entry of pathEntries) {
|
|
25
25
|
try {
|
|
26
|
-
const stat =
|
|
26
|
+
const stat = statSync2(`${entry}/${binary}`);
|
|
27
27
|
if (stat.isFile())
|
|
28
28
|
return true;
|
|
29
29
|
} catch {}
|
|
@@ -298,10 +298,12 @@ function createCursorAdapter(deps = {}) {
|
|
|
298
298
|
try {
|
|
299
299
|
const run = await withTimeout(runLocalAgent(sdk, job), timeoutMs, `cursor lane (${job.prompt.slice(0, 60)})`);
|
|
300
300
|
const durationMs = Date.now() - started;
|
|
301
|
-
const status = run
|
|
301
|
+
const status = run.status;
|
|
302
302
|
const ok = status === "finished";
|
|
303
|
-
return { ok, exitCode: ok ? 0 : 1, output: run
|
|
303
|
+
return { ok, exitCode: ok ? 0 : 1, output: run.result, durationMs };
|
|
304
304
|
} catch (err) {
|
|
305
|
+
if (err instanceof LaneAdapterShapeError)
|
|
306
|
+
throw err;
|
|
305
307
|
const durationMs = Date.now() - started;
|
|
306
308
|
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
|
|
307
309
|
}
|
|
@@ -322,7 +324,14 @@ async function runLocalAgent(sdk, job) {
|
|
|
322
324
|
if (!run || typeof run !== "object") {
|
|
323
325
|
throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
|
|
324
326
|
}
|
|
325
|
-
|
|
327
|
+
const candidate = run;
|
|
328
|
+
if (typeof candidate.status !== "string" || !["finished", "error", "cancelled"].includes(candidate.status)) {
|
|
329
|
+
throw new LaneAdapterShapeError(`@cursor/sdk Agent.send() returned a Run without a terminal status (got ${JSON.stringify(candidate.status)}; expected finished|error|cancelled)`);
|
|
330
|
+
}
|
|
331
|
+
if (typeof candidate.result !== "string") {
|
|
332
|
+
throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
|
|
333
|
+
}
|
|
334
|
+
return { status: candidate.status, result: candidate.result };
|
|
326
335
|
}
|
|
327
336
|
function runCursorCli(job, cliPath, started, timeoutMs) {
|
|
328
337
|
try {
|
|
@@ -443,7 +452,7 @@ var init_lanes = __esm(() => {
|
|
|
443
452
|
// package.json
|
|
444
453
|
var package_default = {
|
|
445
454
|
name: "@hasna/workflows",
|
|
446
|
-
version: "0.1.
|
|
455
|
+
version: "0.1.4",
|
|
447
456
|
description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
|
|
448
457
|
type: "module",
|
|
449
458
|
main: "./dist/index.js",
|
|
@@ -520,17 +529,14 @@ var package_default = {
|
|
|
520
529
|
commander: "^13.1.0"
|
|
521
530
|
},
|
|
522
531
|
devDependencies: {
|
|
523
|
-
"@hasna/contracts": "0.14.
|
|
524
|
-
"@types/bun": "
|
|
532
|
+
"@hasna/contracts": "0.14.2",
|
|
533
|
+
"@types/bun": "1.3.14",
|
|
525
534
|
"@types/pg": "^8.15.6",
|
|
526
535
|
pg: "^8.16.3",
|
|
527
536
|
typescript: "^5.7.3"
|
|
528
537
|
}
|
|
529
538
|
};
|
|
530
539
|
|
|
531
|
-
// src/daemon.ts
|
|
532
|
-
import { createHash as createHash2 } from "crypto";
|
|
533
|
-
|
|
534
540
|
// src/expr.ts
|
|
535
541
|
class ExprSyntaxError extends Error {
|
|
536
542
|
constructor(message) {
|
|
@@ -904,6 +910,18 @@ function validateGraph(graph) {
|
|
|
904
910
|
if (typeof graph.version !== "string" || graph.version.trim() === "") {
|
|
905
911
|
issue(issues, "version", "graph requires a version string");
|
|
906
912
|
}
|
|
913
|
+
if (graph.memoWatch !== undefined) {
|
|
914
|
+
if (!Array.isArray(graph.memoWatch) || graph.memoWatch.length === 0) {
|
|
915
|
+
issue(issues, "memoWatch", "memoWatch must be a non-empty array of file paths or globs");
|
|
916
|
+
} else {
|
|
917
|
+
for (const entry of graph.memoWatch) {
|
|
918
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
919
|
+
issue(issues, "memoWatch", "each memoWatch entry must be a non-empty path or glob string");
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
907
925
|
if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) {
|
|
908
926
|
issue(issues, "nodes", "graph requires at least one node");
|
|
909
927
|
return { ok: false, issues };
|
|
@@ -1138,6 +1156,8 @@ function assertNoSecrets(value, what) {
|
|
|
1138
1156
|
|
|
1139
1157
|
// src/session.ts
|
|
1140
1158
|
import { createHash } from "crypto";
|
|
1159
|
+
import { readFileSync, statSync } from "fs";
|
|
1160
|
+
import { isAbsolute, join } from "path";
|
|
1141
1161
|
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
1142
1162
|
function repairTornRuns(store, wal, opts = {}) {
|
|
1143
1163
|
const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
@@ -1193,6 +1213,46 @@ function recordMemo(store, graphName, nodeId, input, output) {
|
|
|
1193
1213
|
const key = memoKey(graphName, nodeId, input);
|
|
1194
1214
|
store.memoPut(key, graphName, nodeId, inputHash(input), JSON.stringify(output));
|
|
1195
1215
|
}
|
|
1216
|
+
function memoWatchFingerprint(graph, dataDir) {
|
|
1217
|
+
if (!graph.memoWatch || graph.memoWatch.length === 0)
|
|
1218
|
+
return;
|
|
1219
|
+
const entries = [];
|
|
1220
|
+
for (const pattern of graph.memoWatch) {
|
|
1221
|
+
if (hasGlobMagic(pattern)) {
|
|
1222
|
+
const matches = [...new Bun.Glob(pattern).scanSync({ cwd: dataDir, onlyFiles: true })].sort();
|
|
1223
|
+
for (const match of matches) {
|
|
1224
|
+
const path2 = isAbsolute(match) ? match : join(dataDir, match);
|
|
1225
|
+
entries.push(fingerprintFile(pattern, path2));
|
|
1226
|
+
}
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
const path = isAbsolute(pattern) ? pattern : join(dataDir, pattern);
|
|
1230
|
+
entries.push(fingerprintFile(pattern, path));
|
|
1231
|
+
}
|
|
1232
|
+
return entries;
|
|
1233
|
+
}
|
|
1234
|
+
function hasGlobMagic(pattern) {
|
|
1235
|
+
return /[*?[\]{}]/.test(pattern);
|
|
1236
|
+
}
|
|
1237
|
+
function fingerprintFile(pattern, path) {
|
|
1238
|
+
try {
|
|
1239
|
+
const stat = statSync(path);
|
|
1240
|
+
if (!stat.isFile()) {
|
|
1241
|
+
return { pattern, path, missing: true, mtimeMs: null, size: null, sha256: null };
|
|
1242
|
+
}
|
|
1243
|
+
const content = readFileSync(path);
|
|
1244
|
+
return {
|
|
1245
|
+
pattern,
|
|
1246
|
+
path,
|
|
1247
|
+
missing: false,
|
|
1248
|
+
mtimeMs: stat.mtimeMs,
|
|
1249
|
+
size: content.byteLength,
|
|
1250
|
+
sha256: createHash("sha256").update(content).digest("hex")
|
|
1251
|
+
};
|
|
1252
|
+
} catch {
|
|
1253
|
+
return { pattern, path, missing: true, mtimeMs: null, size: null, sha256: null };
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1196
1256
|
function findRunByIdempotencyKey(store, key) {
|
|
1197
1257
|
const runs = store.listRuns({ limit: 1000 });
|
|
1198
1258
|
for (const run of runs) {
|
|
@@ -1467,10 +1527,13 @@ class WorkflowsDaemon {
|
|
|
1467
1527
|
}
|
|
1468
1528
|
async executeStep(run, graph, context, wf, node) {
|
|
1469
1529
|
const nodeId = node.id;
|
|
1470
|
-
const
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1530
|
+
const latest = this.store.getLatestRunNode(run.id, nodeId);
|
|
1531
|
+
const isLoopBody = graph.nodes.some((candidate) => candidate.type === "while" && candidate.body.includes(nodeId));
|
|
1532
|
+
const nodeRow = latest === undefined || isLoopBody && latest.status === "completed" ? this.store.createRunNode({ runId: run.id, nodeId, lane: node.lane ?? "claude" }) : latest;
|
|
1533
|
+
const memoWatch = node.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
|
|
1534
|
+
const memoSource = { command: node.command, prompt: node.prompt, context: stripRunState(context) };
|
|
1535
|
+
const memoInput = node.memo ? memoWatch !== undefined ? { ...memoSource, memoWatch } : memoSource : undefined;
|
|
1536
|
+
if (memoInput !== undefined) {
|
|
1474
1537
|
const hit = tryMemoHit(this.store, run.graphName, nodeId, memoInput);
|
|
1475
1538
|
if (hit) {
|
|
1476
1539
|
const cached = JSON.parse(hit.outputJson);
|
|
@@ -1522,9 +1585,13 @@ class WorkflowsDaemon {
|
|
|
1522
1585
|
}
|
|
1523
1586
|
this.store.setRunNodeStatus(nodeRow.id, "completed", { exitCode: result.exitCode, output });
|
|
1524
1587
|
this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "completed", at: nowIso() });
|
|
1525
|
-
if (
|
|
1526
|
-
|
|
1527
|
-
|
|
1588
|
+
if (memoInput !== undefined) {
|
|
1589
|
+
const memoInputAfter = node.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
|
|
1590
|
+
const stable = memoWatch === undefined && memoInputAfter === undefined || memoWatch !== undefined && memoInputAfter !== undefined && JSON.stringify(memoWatch) === JSON.stringify(memoInputAfter);
|
|
1591
|
+
if (stable) {
|
|
1592
|
+
recordMemo(this.store, run.graphName, nodeId, memoInput, output);
|
|
1593
|
+
this.wal.append({ op: "memo_set", key: memoKey(run.graphName, nodeId, memoInput), at: nowIso() });
|
|
1594
|
+
}
|
|
1528
1595
|
}
|
|
1529
1596
|
this.recordStepResult(context, nodeId, output);
|
|
1530
1597
|
return this.afterNode(run, context, wf, node.next);
|
|
@@ -1619,6 +1686,7 @@ async function runGraphToCompletion(store, wal, graph, context, options = {}) {
|
|
|
1619
1686
|
isolatedRunId: options.isolatedRunId
|
|
1620
1687
|
});
|
|
1621
1688
|
const run = daemon.startRun(graph, context);
|
|
1689
|
+
options.onRunCreated?.(run);
|
|
1622
1690
|
daemon.isolateRun(run.id);
|
|
1623
1691
|
const maxCycles = options.maxCycles ?? 500;
|
|
1624
1692
|
for (let cycle = 0;cycle < maxCycles; cycle++) {
|
|
@@ -1636,7 +1704,7 @@ async function runGraphToCompletion(store, wal, graph, context, options = {}) {
|
|
|
1636
1704
|
// src/paths.ts
|
|
1637
1705
|
import { existsSync } from "fs";
|
|
1638
1706
|
import { homedir } from "os";
|
|
1639
|
-
import { join } from "path";
|
|
1707
|
+
import { join as join2 } from "path";
|
|
1640
1708
|
import { dataDir } from "@hasna/paths";
|
|
1641
1709
|
var APP = "workflows";
|
|
1642
1710
|
var STORE_DB_FILE = "workflows.db";
|
|
@@ -1644,7 +1712,7 @@ function getHomeDir(env = process.env) {
|
|
|
1644
1712
|
return env["HOME"] || env["USERPROFILE"] || homedir();
|
|
1645
1713
|
}
|
|
1646
1714
|
function getLegacyDataDir(env = process.env) {
|
|
1647
|
-
return
|
|
1715
|
+
return join2(getHomeDir(env), ".hasna", APP);
|
|
1648
1716
|
}
|
|
1649
1717
|
function getResolverDataDir(env = process.env) {
|
|
1650
1718
|
return dataDir({ app: APP, home: getHomeDir(env), env });
|
|
@@ -1653,7 +1721,7 @@ function adoptResolverDataDir(resolved, env = process.env) {
|
|
|
1653
1721
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
1654
1722
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
1655
1723
|
return true;
|
|
1656
|
-
return existsSync(
|
|
1724
|
+
return existsSync(join2(resolved, STORE_DB_FILE));
|
|
1657
1725
|
}
|
|
1658
1726
|
function getEffectiveDataDir(env = process.env) {
|
|
1659
1727
|
const resolved = getResolverDataDir(env);
|
|
@@ -1663,17 +1731,41 @@ function getEffectiveDataDir(env = process.env) {
|
|
|
1663
1731
|
// src/store.ts
|
|
1664
1732
|
import { Database } from "bun:sqlite";
|
|
1665
1733
|
import { mkdirSync } from "fs";
|
|
1666
|
-
import { join as
|
|
1734
|
+
import { join as join3 } from "path";
|
|
1667
1735
|
import { randomUUID } from "crypto";
|
|
1668
1736
|
function nowIso2() {
|
|
1669
1737
|
return new Date().toISOString();
|
|
1670
1738
|
}
|
|
1739
|
+
var BUSY_TIMEOUT_MS = 3000;
|
|
1740
|
+
var BUSY_RETRIES = 2;
|
|
1741
|
+
var BUSY_RETRY_SLEEP_MS = 50;
|
|
1742
|
+
function isBusyError(err) {
|
|
1743
|
+
if (!(err instanceof Error))
|
|
1744
|
+
return false;
|
|
1745
|
+
const code = String(err.code ?? "");
|
|
1746
|
+
return code.includes("BUSY") || /\bdatabase is locked\b/i.test(err.message);
|
|
1747
|
+
}
|
|
1748
|
+
function retryBusy(fn) {
|
|
1749
|
+
let last;
|
|
1750
|
+
for (let attempt = 0;attempt <= BUSY_RETRIES; attempt++) {
|
|
1751
|
+
try {
|
|
1752
|
+
return fn();
|
|
1753
|
+
} catch (err) {
|
|
1754
|
+
if (!isBusyError(err))
|
|
1755
|
+
throw err;
|
|
1756
|
+
last = err;
|
|
1757
|
+
Bun.sleepSync(BUSY_RETRY_SLEEP_MS);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
throw last;
|
|
1761
|
+
}
|
|
1671
1762
|
function openStore(dataDir2) {
|
|
1672
1763
|
mkdirSync(dataDir2, { recursive: true });
|
|
1673
|
-
const db = new Database(
|
|
1674
|
-
db.exec(
|
|
1675
|
-
db.exec("PRAGMA
|
|
1676
|
-
db.exec(
|
|
1764
|
+
const db = new Database(join3(dataDir2, "workflows.db"));
|
|
1765
|
+
retryBusy(() => db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`));
|
|
1766
|
+
retryBusy(() => db.exec("PRAGMA journal_mode = WAL"));
|
|
1767
|
+
retryBusy(() => db.exec("PRAGMA foreign_keys = ON"));
|
|
1768
|
+
retryBusy(() => db.exec(`
|
|
1677
1769
|
CREATE TABLE IF NOT EXISTS runs (
|
|
1678
1770
|
id TEXT PRIMARY KEY,
|
|
1679
1771
|
graph_name TEXT NOT NULL,
|
|
@@ -1713,7 +1805,7 @@ function openStore(dataDir2) {
|
|
|
1713
1805
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
1714
1806
|
CREATE INDEX IF NOT EXISTS idx_runs_created ON runs(created_at);
|
|
1715
1807
|
CREATE INDEX IF NOT EXISTS idx_run_nodes_run ON run_nodes(run_id);
|
|
1716
|
-
`);
|
|
1808
|
+
`));
|
|
1717
1809
|
const rowToRun = (r) => ({
|
|
1718
1810
|
id: String(r.id),
|
|
1719
1811
|
graphName: String(r.graph_name),
|
|
@@ -1751,31 +1843,33 @@ function openStore(dataDir2) {
|
|
|
1751
1843
|
createdAt: String(r.created_at)
|
|
1752
1844
|
});
|
|
1753
1845
|
return {
|
|
1846
|
+
dataDir: dataDir2,
|
|
1754
1847
|
close() {
|
|
1755
1848
|
db.close();
|
|
1756
1849
|
},
|
|
1757
1850
|
listTables() {
|
|
1758
|
-
const rows = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all();
|
|
1851
|
+
const rows = retryBusy(() => db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all());
|
|
1759
1852
|
return rows.map((r) => r.name);
|
|
1760
1853
|
},
|
|
1761
1854
|
createRun(input) {
|
|
1762
1855
|
const id = randomUUID();
|
|
1763
1856
|
const now = nowIso2();
|
|
1764
|
-
db.query(`INSERT INTO runs (id, graph_name, graph_version, status, attempts, context_json, created_at, updated_at)
|
|
1765
|
-
|
|
1857
|
+
retryBusy(() => db.query(`INSERT INTO runs (id, graph_name, graph_version, status, attempts, context_json, created_at, updated_at)
|
|
1858
|
+
VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)`).run(id, input.graphName, input.graphVersion, JSON.stringify(input.context ?? {}), now, now));
|
|
1766
1859
|
return this.getRun(id);
|
|
1767
1860
|
},
|
|
1768
1861
|
getRun(id) {
|
|
1769
|
-
const row = db.query(`SELECT * FROM runs WHERE id = ?`).get(id);
|
|
1862
|
+
const row = retryBusy(() => db.query(`SELECT * FROM runs WHERE id = ?`).get(id));
|
|
1770
1863
|
return row ? rowToRun(row) : undefined;
|
|
1771
1864
|
},
|
|
1772
1865
|
listRuns(opts = {}) {
|
|
1773
1866
|
const limit = Math.min(opts.limit ?? 100, 1000);
|
|
1774
1867
|
if (opts.status) {
|
|
1775
|
-
const
|
|
1868
|
+
const status = opts.status;
|
|
1869
|
+
const rows2 = retryBusy(() => db.query(`SELECT * FROM runs WHERE status = ? ORDER BY created_at DESC, id DESC LIMIT ?`).all(status, limit));
|
|
1776
1870
|
return rows2.map((r) => rowToRun(r));
|
|
1777
1871
|
}
|
|
1778
|
-
const rows = db.query(`SELECT * FROM runs ORDER BY created_at DESC, id DESC LIMIT ?`).all(limit);
|
|
1872
|
+
const rows = retryBusy(() => db.query(`SELECT * FROM runs ORDER BY created_at DESC, id DESC LIMIT ?`).all(limit));
|
|
1779
1873
|
return rows.map((r) => rowToRun(r));
|
|
1780
1874
|
},
|
|
1781
1875
|
setRunStatus(id, status, patch = {}) {
|
|
@@ -1786,28 +1880,32 @@ function openStore(dataDir2) {
|
|
|
1786
1880
|
const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing?.startedAt === null ? now : existing?.startedAt ?? null;
|
|
1787
1881
|
const terminal = status === "completed" || status === "failed" || status === "cancelled" || status === "interrupted";
|
|
1788
1882
|
const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing?.finishedAt === null ? now : existing?.finishedAt ?? null;
|
|
1789
|
-
db.query(`UPDATE runs SET status = ?, result_json = ?, error = ?, started_at = ?, finished_at = ?, updated_at = ? WHERE id = ?`).run(status, resultJson, error, startedAt, finishedAt, now, id);
|
|
1883
|
+
retryBusy(() => db.query(`UPDATE runs SET status = ?, result_json = ?, error = ?, started_at = ?, finished_at = ?, updated_at = ? WHERE id = ?`).run(status, resultJson, error, startedAt, finishedAt, now, id));
|
|
1790
1884
|
},
|
|
1791
1885
|
bumpAttempts(id) {
|
|
1792
|
-
db.query(`UPDATE runs SET attempts = attempts + 1, updated_at = ? WHERE id = ?`).run(nowIso2(), id);
|
|
1886
|
+
retryBusy(() => db.query(`UPDATE runs SET attempts = attempts + 1, updated_at = ? WHERE id = ?`).run(nowIso2(), id));
|
|
1793
1887
|
},
|
|
1794
1888
|
setRunContext(id, context) {
|
|
1795
|
-
db.query(`UPDATE runs SET context_json = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(context), nowIso2(), id);
|
|
1889
|
+
retryBusy(() => db.query(`UPDATE runs SET context_json = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(context), nowIso2(), id));
|
|
1796
1890
|
},
|
|
1797
1891
|
createRunNode(input) {
|
|
1798
1892
|
const id = randomUUID();
|
|
1799
|
-
db.query(`INSERT INTO run_nodes (id, run_id, node_id, status, attempts, lane)
|
|
1800
|
-
|
|
1893
|
+
retryBusy(() => db.query(`INSERT INTO run_nodes (id, run_id, node_id, status, attempts, lane)
|
|
1894
|
+
VALUES (?, ?, ?, 'pending', 0, ?)`).run(id, input.runId, input.nodeId, input.lane ?? null));
|
|
1801
1895
|
return this.getRunNode(id);
|
|
1802
1896
|
},
|
|
1803
1897
|
getRunNode(id) {
|
|
1804
|
-
const row = db.query(`SELECT * FROM run_nodes WHERE id = ?`).get(id);
|
|
1898
|
+
const row = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE id = ?`).get(id));
|
|
1805
1899
|
return row ? rowToNode(row) : undefined;
|
|
1806
1900
|
},
|
|
1807
1901
|
listRunNodes(runId) {
|
|
1808
|
-
const rows = db.query(`SELECT * FROM run_nodes WHERE run_id = ? ORDER BY rowid ASC`).all(runId);
|
|
1902
|
+
const rows = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE run_id = ? ORDER BY rowid ASC`).all(runId));
|
|
1809
1903
|
return rows.map((r) => rowToNode(r));
|
|
1810
1904
|
},
|
|
1905
|
+
getLatestRunNode(runId, nodeId) {
|
|
1906
|
+
const row = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE run_id = ? AND node_id = ? ORDER BY rowid DESC LIMIT 1`).get(runId, nodeId));
|
|
1907
|
+
return row ? rowToNode(row) : undefined;
|
|
1908
|
+
},
|
|
1811
1909
|
setRunNodeStatus(id, status, patch = {}) {
|
|
1812
1910
|
const now = nowIso2();
|
|
1813
1911
|
const existing = this.getRunNode(id);
|
|
@@ -1819,44 +1917,44 @@ function openStore(dataDir2) {
|
|
|
1819
1917
|
const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing.startedAt === null ? now : existing.startedAt;
|
|
1820
1918
|
const terminal = status === "completed" || status === "failed" || status === "skipped";
|
|
1821
1919
|
const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing.finishedAt === null ? now : existing.finishedAt;
|
|
1822
|
-
db.query(`UPDATE run_nodes SET status = ?, exit_code = ?, output_json = ?, error = ?, started_at = ?, finished_at = ? WHERE id = ?`).run(status, exitCode, outputJson, error, startedAt, finishedAt, id);
|
|
1920
|
+
retryBusy(() => db.query(`UPDATE run_nodes SET status = ?, exit_code = ?, output_json = ?, error = ?, started_at = ?, finished_at = ? WHERE id = ?`).run(status, exitCode, outputJson, error, startedAt, finishedAt, id));
|
|
1823
1921
|
},
|
|
1824
1922
|
bumpAttemptsNode(id) {
|
|
1825
|
-
db.query(`UPDATE run_nodes SET attempts = attempts + 1 WHERE id = ?`).run(id);
|
|
1923
|
+
retryBusy(() => db.query(`UPDATE run_nodes SET attempts = attempts + 1 WHERE id = ?`).run(id));
|
|
1826
1924
|
},
|
|
1827
1925
|
memoPut(key, graphName, nodeId, inputHash2, outputJson) {
|
|
1828
|
-
db.query(`INSERT INTO memos (key, graph_name, node_id, input_hash, output_json, hit_count, created_at)
|
|
1829
|
-
|
|
1830
|
-
|
|
1926
|
+
retryBusy(() => db.query(`INSERT INTO memos (key, graph_name, node_id, input_hash, output_json, hit_count, created_at)
|
|
1927
|
+
VALUES (?, ?, ?, ?, ?, 0, ?)
|
|
1928
|
+
ON CONFLICT(key) DO UPDATE SET output_json = excluded.output_json`).run(key, graphName, nodeId, inputHash2, outputJson, nowIso2()));
|
|
1831
1929
|
},
|
|
1832
1930
|
memoGet(key) {
|
|
1833
|
-
const row = db.query(`SELECT * FROM memos WHERE key = ?`).get(key);
|
|
1931
|
+
const row = retryBusy(() => db.query(`SELECT * FROM memos WHERE key = ?`).get(key));
|
|
1834
1932
|
return row ? rowToMemo(row) : undefined;
|
|
1835
1933
|
},
|
|
1836
1934
|
memoHit(key) {
|
|
1837
|
-
db.query(`UPDATE memos SET hit_count = hit_count + 1 WHERE key = ?`).run(key);
|
|
1935
|
+
retryBusy(() => db.query(`UPDATE memos SET hit_count = hit_count + 1 WHERE key = ?`).run(key));
|
|
1838
1936
|
},
|
|
1839
1937
|
memoList() {
|
|
1840
|
-
const rows = db.query(`SELECT * FROM memos ORDER BY created_at DESC LIMIT 1000`).all();
|
|
1938
|
+
const rows = retryBusy(() => db.query(`SELECT * FROM memos ORDER BY created_at DESC LIMIT 1000`).all());
|
|
1841
1939
|
return rows.map((r) => rowToMemo(r));
|
|
1842
1940
|
},
|
|
1843
1941
|
memoClear() {
|
|
1844
|
-
db.exec(`DELETE FROM memos`);
|
|
1942
|
+
retryBusy(() => db.exec(`DELETE FROM memos`));
|
|
1845
1943
|
}
|
|
1846
1944
|
};
|
|
1847
1945
|
}
|
|
1848
1946
|
|
|
1849
1947
|
// src/wal.ts
|
|
1850
|
-
import { createHash as
|
|
1851
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
|
|
1852
|
-
import { join as
|
|
1948
|
+
import { createHash as createHash2 } from "crypto";
|
|
1949
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, truncateSync, writeFileSync } from "fs";
|
|
1950
|
+
import { join as join4 } from "path";
|
|
1853
1951
|
var WAL_FILE_NAME = "session.wal";
|
|
1854
1952
|
var SESSIONS_DIR_NAME = "sessions";
|
|
1855
1953
|
var MAX_WAL_BYTES = 256 * 1024 * 1024;
|
|
1856
1954
|
function checksumOf(op) {
|
|
1857
1955
|
if (op === undefined || op === null)
|
|
1858
1956
|
return "";
|
|
1859
|
-
return
|
|
1957
|
+
return createHash2("sha256").update(JSON.stringify(op)).digest("hex");
|
|
1860
1958
|
}
|
|
1861
1959
|
|
|
1862
1960
|
class SessionWAL {
|
|
@@ -1865,9 +1963,9 @@ class SessionWAL {
|
|
|
1865
1963
|
this.filePath = filePath;
|
|
1866
1964
|
}
|
|
1867
1965
|
static open(dataDir2) {
|
|
1868
|
-
mkdirSync2(
|
|
1869
|
-
const canonical =
|
|
1870
|
-
const legacy =
|
|
1966
|
+
mkdirSync2(join4(dataDir2, SESSIONS_DIR_NAME), { recursive: true });
|
|
1967
|
+
const canonical = join4(dataDir2, SESSIONS_DIR_NAME, WAL_FILE_NAME);
|
|
1968
|
+
const legacy = join4(dataDir2, WAL_FILE_NAME);
|
|
1871
1969
|
if (existsSync2(legacy) && !existsSync2(canonical)) {
|
|
1872
1970
|
try {
|
|
1873
1971
|
renameSync(legacy, canonical);
|
|
@@ -1899,7 +1997,7 @@ class SessionWAL {
|
|
|
1899
1997
|
let repaired = false;
|
|
1900
1998
|
let content;
|
|
1901
1999
|
try {
|
|
1902
|
-
content =
|
|
2000
|
+
content = readFileSync2(this.filePath, "utf8");
|
|
1903
2001
|
} catch {
|
|
1904
2002
|
return { entries, torn, repaired, liveClaims: () => new Map };
|
|
1905
2003
|
}
|
|
@@ -2079,7 +2177,7 @@ function createWorkflowsService(config = {}) {
|
|
|
2079
2177
|
}
|
|
2080
2178
|
|
|
2081
2179
|
// src/handlers.ts
|
|
2082
|
-
import { createHash as
|
|
2180
|
+
import { createHash as createHash3, timingSafeEqual } from "crypto";
|
|
2083
2181
|
function jsonResponse(body, status = 200) {
|
|
2084
2182
|
return new Response(JSON.stringify(body), {
|
|
2085
2183
|
status,
|
|
@@ -2135,8 +2233,8 @@ function bearerMatches(authHeader, expected) {
|
|
|
2135
2233
|
const supplied = authHeader.slice("Bearer ".length);
|
|
2136
2234
|
if (supplied.length === 0)
|
|
2137
2235
|
return false;
|
|
2138
|
-
const a =
|
|
2139
|
-
const b =
|
|
2236
|
+
const a = createHash3("sha256").update(supplied).digest();
|
|
2237
|
+
const b = createHash3("sha256").update(expected).digest();
|
|
2140
2238
|
return timingSafeEqual(a, b);
|
|
2141
2239
|
}
|
|
2142
2240
|
async function handleTrigger(service, req) {
|