@hasna/workflows 0.1.2 → 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/README.md +1 -1
- package/dist/agent-tools.js +152 -54
- package/dist/cli/index.js +297 -107
- package/dist/index.js +193 -71
- package/dist/mcp/index.js +186 -64
- package/dist/serve/index.js +193 -71
- package/dist/service.js +190 -68
- package/hasna.contract.json +2 -2
- package/package.json +6 -5
- package/types/daemon.d.ts +3 -0
- package/types/graph.d.ts +7 -0
- package/types/paths.d.ts +51 -0
- package/types/session.d.ts +24 -0
- package/types/store.d.ts +5 -0
- package/types/types.d.ts +3 -1
package/dist/mcp/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 {
|
|
@@ -817,6 +826,18 @@ function validateGraph(graph) {
|
|
|
817
826
|
if (typeof graph.version !== "string" || graph.version.trim() === "") {
|
|
818
827
|
issue(issues, "version", "graph requires a version string");
|
|
819
828
|
}
|
|
829
|
+
if (graph.memoWatch !== undefined) {
|
|
830
|
+
if (!Array.isArray(graph.memoWatch) || graph.memoWatch.length === 0) {
|
|
831
|
+
issue(issues, "memoWatch", "memoWatch must be a non-empty array of file paths or globs");
|
|
832
|
+
} else {
|
|
833
|
+
for (const entry of graph.memoWatch) {
|
|
834
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
835
|
+
issue(issues, "memoWatch", "each memoWatch entry must be a non-empty path or glob string");
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
820
841
|
if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) {
|
|
821
842
|
issue(issues, "nodes", "graph requires at least one node");
|
|
822
843
|
return { ok: false, issues };
|
|
@@ -975,9 +996,6 @@ function validateNode(graph, node, issues) {
|
|
|
975
996
|
}
|
|
976
997
|
}
|
|
977
998
|
|
|
978
|
-
// src/daemon.ts
|
|
979
|
-
import { createHash as createHash2 } from "crypto";
|
|
980
|
-
|
|
981
999
|
// src/secrets.ts
|
|
982
1000
|
class SecretsGateError extends Error {
|
|
983
1001
|
constructor(message) {
|
|
@@ -1054,6 +1072,8 @@ function assertNoSecrets(value, what) {
|
|
|
1054
1072
|
|
|
1055
1073
|
// src/session.ts
|
|
1056
1074
|
import { createHash } from "crypto";
|
|
1075
|
+
import { readFileSync, statSync } from "fs";
|
|
1076
|
+
import { isAbsolute, join } from "path";
|
|
1057
1077
|
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
1058
1078
|
function repairTornRuns(store, wal, opts = {}) {
|
|
1059
1079
|
const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
@@ -1109,6 +1129,46 @@ function recordMemo(store, graphName, nodeId, input, output) {
|
|
|
1109
1129
|
const key = memoKey(graphName, nodeId, input);
|
|
1110
1130
|
store.memoPut(key, graphName, nodeId, inputHash(input), JSON.stringify(output));
|
|
1111
1131
|
}
|
|
1132
|
+
function memoWatchFingerprint(graph, dataDir) {
|
|
1133
|
+
if (!graph.memoWatch || graph.memoWatch.length === 0)
|
|
1134
|
+
return;
|
|
1135
|
+
const entries = [];
|
|
1136
|
+
for (const pattern of graph.memoWatch) {
|
|
1137
|
+
if (hasGlobMagic(pattern)) {
|
|
1138
|
+
const matches = [...new Bun.Glob(pattern).scanSync({ cwd: dataDir, onlyFiles: true })].sort();
|
|
1139
|
+
for (const match of matches) {
|
|
1140
|
+
const path2 = isAbsolute(match) ? match : join(dataDir, match);
|
|
1141
|
+
entries.push(fingerprintFile(pattern, path2));
|
|
1142
|
+
}
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
const path = isAbsolute(pattern) ? pattern : join(dataDir, pattern);
|
|
1146
|
+
entries.push(fingerprintFile(pattern, path));
|
|
1147
|
+
}
|
|
1148
|
+
return entries;
|
|
1149
|
+
}
|
|
1150
|
+
function hasGlobMagic(pattern) {
|
|
1151
|
+
return /[*?[\]{}]/.test(pattern);
|
|
1152
|
+
}
|
|
1153
|
+
function fingerprintFile(pattern, path) {
|
|
1154
|
+
try {
|
|
1155
|
+
const stat = statSync(path);
|
|
1156
|
+
if (!stat.isFile()) {
|
|
1157
|
+
return { pattern, path, missing: true, mtimeMs: null, size: null, sha256: null };
|
|
1158
|
+
}
|
|
1159
|
+
const content = readFileSync(path);
|
|
1160
|
+
return {
|
|
1161
|
+
pattern,
|
|
1162
|
+
path,
|
|
1163
|
+
missing: false,
|
|
1164
|
+
mtimeMs: stat.mtimeMs,
|
|
1165
|
+
size: content.byteLength,
|
|
1166
|
+
sha256: createHash("sha256").update(content).digest("hex")
|
|
1167
|
+
};
|
|
1168
|
+
} catch {
|
|
1169
|
+
return { pattern, path, missing: true, mtimeMs: null, size: null, sha256: null };
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1112
1172
|
function findRunByIdempotencyKey(store, key) {
|
|
1113
1173
|
const runs = store.listRuns({ limit: 1000 });
|
|
1114
1174
|
for (const run of runs) {
|
|
@@ -1383,10 +1443,13 @@ class WorkflowsDaemon {
|
|
|
1383
1443
|
}
|
|
1384
1444
|
async executeStep(run, graph, context, wf, node) {
|
|
1385
1445
|
const nodeId = node.id;
|
|
1386
|
-
const
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1446
|
+
const latest = this.store.getLatestRunNode(run.id, nodeId);
|
|
1447
|
+
const isLoopBody = graph.nodes.some((candidate) => candidate.type === "while" && candidate.body.includes(nodeId));
|
|
1448
|
+
const nodeRow = latest === undefined || isLoopBody && latest.status === "completed" ? this.store.createRunNode({ runId: run.id, nodeId, lane: node.lane ?? "claude" }) : latest;
|
|
1449
|
+
const memoWatch = node.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
|
|
1450
|
+
const memoSource = { command: node.command, prompt: node.prompt, context: stripRunState(context) };
|
|
1451
|
+
const memoInput = node.memo ? memoWatch !== undefined ? { ...memoSource, memoWatch } : memoSource : undefined;
|
|
1452
|
+
if (memoInput !== undefined) {
|
|
1390
1453
|
const hit = tryMemoHit(this.store, run.graphName, nodeId, memoInput);
|
|
1391
1454
|
if (hit) {
|
|
1392
1455
|
const cached = JSON.parse(hit.outputJson);
|
|
@@ -1415,9 +1478,9 @@ class WorkflowsDaemon {
|
|
|
1415
1478
|
}
|
|
1416
1479
|
const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
|
|
1417
1480
|
if (!result.ok) {
|
|
1418
|
-
const
|
|
1419
|
-
|
|
1420
|
-
|
|
1481
|
+
const shouldRetry = nodeRow.attempts < (node.maxRetries ?? 0);
|
|
1482
|
+
this.store.bumpAttemptsNode(nodeRow.id);
|
|
1483
|
+
if (shouldRetry) {
|
|
1421
1484
|
this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
|
|
1422
1485
|
return { kind: "advanced", nodeId };
|
|
1423
1486
|
}
|
|
@@ -1438,9 +1501,13 @@ class WorkflowsDaemon {
|
|
|
1438
1501
|
}
|
|
1439
1502
|
this.store.setRunNodeStatus(nodeRow.id, "completed", { exitCode: result.exitCode, output });
|
|
1440
1503
|
this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "completed", at: nowIso() });
|
|
1441
|
-
if (
|
|
1442
|
-
|
|
1443
|
-
|
|
1504
|
+
if (memoInput !== undefined) {
|
|
1505
|
+
const memoInputAfter = node.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
|
|
1506
|
+
const stable = memoWatch === undefined && memoInputAfter === undefined || memoWatch !== undefined && memoInputAfter !== undefined && JSON.stringify(memoWatch) === JSON.stringify(memoInputAfter);
|
|
1507
|
+
if (stable) {
|
|
1508
|
+
recordMemo(this.store, run.graphName, nodeId, memoInput, output);
|
|
1509
|
+
this.wal.append({ op: "memo_set", key: memoKey(run.graphName, nodeId, memoInput), at: nowIso() });
|
|
1510
|
+
}
|
|
1444
1511
|
}
|
|
1445
1512
|
this.recordStepResult(context, nodeId, output);
|
|
1446
1513
|
return this.afterNode(run, context, wf, node.next);
|
|
@@ -1535,6 +1602,7 @@ async function runGraphToCompletion(store, wal, graph, context, options = {}) {
|
|
|
1535
1602
|
isolatedRunId: options.isolatedRunId
|
|
1536
1603
|
});
|
|
1537
1604
|
const run = daemon.startRun(graph, context);
|
|
1605
|
+
options.onRunCreated?.(run);
|
|
1538
1606
|
daemon.isolateRun(run.id);
|
|
1539
1607
|
const maxCycles = options.maxCycles ?? 500;
|
|
1540
1608
|
for (let cycle = 0;cycle < maxCycles; cycle++) {
|
|
@@ -1555,17 +1623,41 @@ init_lanes();
|
|
|
1555
1623
|
// src/store.ts
|
|
1556
1624
|
import { Database } from "bun:sqlite";
|
|
1557
1625
|
import { mkdirSync } from "fs";
|
|
1558
|
-
import { join } from "path";
|
|
1626
|
+
import { join as join2 } from "path";
|
|
1559
1627
|
import { randomUUID } from "crypto";
|
|
1560
1628
|
function nowIso2() {
|
|
1561
1629
|
return new Date().toISOString();
|
|
1562
1630
|
}
|
|
1631
|
+
var BUSY_TIMEOUT_MS = 3000;
|
|
1632
|
+
var BUSY_RETRIES = 2;
|
|
1633
|
+
var BUSY_RETRY_SLEEP_MS = 50;
|
|
1634
|
+
function isBusyError(err) {
|
|
1635
|
+
if (!(err instanceof Error))
|
|
1636
|
+
return false;
|
|
1637
|
+
const code = String(err.code ?? "");
|
|
1638
|
+
return code.includes("BUSY") || /\bdatabase is locked\b/i.test(err.message);
|
|
1639
|
+
}
|
|
1640
|
+
function retryBusy(fn) {
|
|
1641
|
+
let last;
|
|
1642
|
+
for (let attempt = 0;attempt <= BUSY_RETRIES; attempt++) {
|
|
1643
|
+
try {
|
|
1644
|
+
return fn();
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
if (!isBusyError(err))
|
|
1647
|
+
throw err;
|
|
1648
|
+
last = err;
|
|
1649
|
+
Bun.sleepSync(BUSY_RETRY_SLEEP_MS);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
throw last;
|
|
1653
|
+
}
|
|
1563
1654
|
function openStore(dataDir) {
|
|
1564
1655
|
mkdirSync(dataDir, { recursive: true });
|
|
1565
|
-
const db = new Database(
|
|
1566
|
-
db.exec(
|
|
1567
|
-
db.exec("PRAGMA
|
|
1568
|
-
db.exec(
|
|
1656
|
+
const db = new Database(join2(dataDir, "workflows.db"));
|
|
1657
|
+
retryBusy(() => db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`));
|
|
1658
|
+
retryBusy(() => db.exec("PRAGMA journal_mode = WAL"));
|
|
1659
|
+
retryBusy(() => db.exec("PRAGMA foreign_keys = ON"));
|
|
1660
|
+
retryBusy(() => db.exec(`
|
|
1569
1661
|
CREATE TABLE IF NOT EXISTS runs (
|
|
1570
1662
|
id TEXT PRIMARY KEY,
|
|
1571
1663
|
graph_name TEXT NOT NULL,
|
|
@@ -1605,7 +1697,7 @@ function openStore(dataDir) {
|
|
|
1605
1697
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
1606
1698
|
CREATE INDEX IF NOT EXISTS idx_runs_created ON runs(created_at);
|
|
1607
1699
|
CREATE INDEX IF NOT EXISTS idx_run_nodes_run ON run_nodes(run_id);
|
|
1608
|
-
`);
|
|
1700
|
+
`));
|
|
1609
1701
|
const rowToRun = (r) => ({
|
|
1610
1702
|
id: String(r.id),
|
|
1611
1703
|
graphName: String(r.graph_name),
|
|
@@ -1643,31 +1735,33 @@ function openStore(dataDir) {
|
|
|
1643
1735
|
createdAt: String(r.created_at)
|
|
1644
1736
|
});
|
|
1645
1737
|
return {
|
|
1738
|
+
dataDir,
|
|
1646
1739
|
close() {
|
|
1647
1740
|
db.close();
|
|
1648
1741
|
},
|
|
1649
1742
|
listTables() {
|
|
1650
|
-
const rows = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all();
|
|
1743
|
+
const rows = retryBusy(() => db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all());
|
|
1651
1744
|
return rows.map((r) => r.name);
|
|
1652
1745
|
},
|
|
1653
1746
|
createRun(input) {
|
|
1654
1747
|
const id = randomUUID();
|
|
1655
1748
|
const now = nowIso2();
|
|
1656
|
-
db.query(`INSERT INTO runs (id, graph_name, graph_version, status, attempts, context_json, created_at, updated_at)
|
|
1657
|
-
|
|
1749
|
+
retryBusy(() => db.query(`INSERT INTO runs (id, graph_name, graph_version, status, attempts, context_json, created_at, updated_at)
|
|
1750
|
+
VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)`).run(id, input.graphName, input.graphVersion, JSON.stringify(input.context ?? {}), now, now));
|
|
1658
1751
|
return this.getRun(id);
|
|
1659
1752
|
},
|
|
1660
1753
|
getRun(id) {
|
|
1661
|
-
const row = db.query(`SELECT * FROM runs WHERE id = ?`).get(id);
|
|
1754
|
+
const row = retryBusy(() => db.query(`SELECT * FROM runs WHERE id = ?`).get(id));
|
|
1662
1755
|
return row ? rowToRun(row) : undefined;
|
|
1663
1756
|
},
|
|
1664
1757
|
listRuns(opts = {}) {
|
|
1665
1758
|
const limit = Math.min(opts.limit ?? 100, 1000);
|
|
1666
1759
|
if (opts.status) {
|
|
1667
|
-
const
|
|
1760
|
+
const status = opts.status;
|
|
1761
|
+
const rows2 = retryBusy(() => db.query(`SELECT * FROM runs WHERE status = ? ORDER BY created_at DESC, id DESC LIMIT ?`).all(status, limit));
|
|
1668
1762
|
return rows2.map((r) => rowToRun(r));
|
|
1669
1763
|
}
|
|
1670
|
-
const rows = db.query(`SELECT * FROM runs ORDER BY created_at DESC, id DESC LIMIT ?`).all(limit);
|
|
1764
|
+
const rows = retryBusy(() => db.query(`SELECT * FROM runs ORDER BY created_at DESC, id DESC LIMIT ?`).all(limit));
|
|
1671
1765
|
return rows.map((r) => rowToRun(r));
|
|
1672
1766
|
},
|
|
1673
1767
|
setRunStatus(id, status, patch = {}) {
|
|
@@ -1678,28 +1772,32 @@ function openStore(dataDir) {
|
|
|
1678
1772
|
const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing?.startedAt === null ? now : existing?.startedAt ?? null;
|
|
1679
1773
|
const terminal = status === "completed" || status === "failed" || status === "cancelled" || status === "interrupted";
|
|
1680
1774
|
const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing?.finishedAt === null ? now : existing?.finishedAt ?? null;
|
|
1681
|
-
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);
|
|
1775
|
+
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));
|
|
1682
1776
|
},
|
|
1683
1777
|
bumpAttempts(id) {
|
|
1684
|
-
db.query(`UPDATE runs SET attempts = attempts + 1, updated_at = ? WHERE id = ?`).run(nowIso2(), id);
|
|
1778
|
+
retryBusy(() => db.query(`UPDATE runs SET attempts = attempts + 1, updated_at = ? WHERE id = ?`).run(nowIso2(), id));
|
|
1685
1779
|
},
|
|
1686
1780
|
setRunContext(id, context) {
|
|
1687
|
-
db.query(`UPDATE runs SET context_json = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(context), nowIso2(), id);
|
|
1781
|
+
retryBusy(() => db.query(`UPDATE runs SET context_json = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(context), nowIso2(), id));
|
|
1688
1782
|
},
|
|
1689
1783
|
createRunNode(input) {
|
|
1690
1784
|
const id = randomUUID();
|
|
1691
|
-
db.query(`INSERT INTO run_nodes (id, run_id, node_id, status, attempts, lane)
|
|
1692
|
-
|
|
1785
|
+
retryBusy(() => db.query(`INSERT INTO run_nodes (id, run_id, node_id, status, attempts, lane)
|
|
1786
|
+
VALUES (?, ?, ?, 'pending', 0, ?)`).run(id, input.runId, input.nodeId, input.lane ?? null));
|
|
1693
1787
|
return this.getRunNode(id);
|
|
1694
1788
|
},
|
|
1695
1789
|
getRunNode(id) {
|
|
1696
|
-
const row = db.query(`SELECT * FROM run_nodes WHERE id = ?`).get(id);
|
|
1790
|
+
const row = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE id = ?`).get(id));
|
|
1697
1791
|
return row ? rowToNode(row) : undefined;
|
|
1698
1792
|
},
|
|
1699
1793
|
listRunNodes(runId) {
|
|
1700
|
-
const rows = db.query(`SELECT * FROM run_nodes WHERE run_id = ? ORDER BY rowid ASC`).all(runId);
|
|
1794
|
+
const rows = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE run_id = ? ORDER BY rowid ASC`).all(runId));
|
|
1701
1795
|
return rows.map((r) => rowToNode(r));
|
|
1702
1796
|
},
|
|
1797
|
+
getLatestRunNode(runId, nodeId) {
|
|
1798
|
+
const row = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE run_id = ? AND node_id = ? ORDER BY rowid DESC LIMIT 1`).get(runId, nodeId));
|
|
1799
|
+
return row ? rowToNode(row) : undefined;
|
|
1800
|
+
},
|
|
1703
1801
|
setRunNodeStatus(id, status, patch = {}) {
|
|
1704
1802
|
const now = nowIso2();
|
|
1705
1803
|
const existing = this.getRunNode(id);
|
|
@@ -1711,44 +1809,44 @@ function openStore(dataDir) {
|
|
|
1711
1809
|
const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing.startedAt === null ? now : existing.startedAt;
|
|
1712
1810
|
const terminal = status === "completed" || status === "failed" || status === "skipped";
|
|
1713
1811
|
const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing.finishedAt === null ? now : existing.finishedAt;
|
|
1714
|
-
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);
|
|
1812
|
+
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));
|
|
1715
1813
|
},
|
|
1716
1814
|
bumpAttemptsNode(id) {
|
|
1717
|
-
db.query(`UPDATE run_nodes SET attempts = attempts + 1 WHERE id = ?`).run(id);
|
|
1815
|
+
retryBusy(() => db.query(`UPDATE run_nodes SET attempts = attempts + 1 WHERE id = ?`).run(id));
|
|
1718
1816
|
},
|
|
1719
1817
|
memoPut(key, graphName, nodeId, inputHash2, outputJson) {
|
|
1720
|
-
db.query(`INSERT INTO memos (key, graph_name, node_id, input_hash, output_json, hit_count, created_at)
|
|
1721
|
-
|
|
1722
|
-
|
|
1818
|
+
retryBusy(() => db.query(`INSERT INTO memos (key, graph_name, node_id, input_hash, output_json, hit_count, created_at)
|
|
1819
|
+
VALUES (?, ?, ?, ?, ?, 0, ?)
|
|
1820
|
+
ON CONFLICT(key) DO UPDATE SET output_json = excluded.output_json`).run(key, graphName, nodeId, inputHash2, outputJson, nowIso2()));
|
|
1723
1821
|
},
|
|
1724
1822
|
memoGet(key) {
|
|
1725
|
-
const row = db.query(`SELECT * FROM memos WHERE key = ?`).get(key);
|
|
1823
|
+
const row = retryBusy(() => db.query(`SELECT * FROM memos WHERE key = ?`).get(key));
|
|
1726
1824
|
return row ? rowToMemo(row) : undefined;
|
|
1727
1825
|
},
|
|
1728
1826
|
memoHit(key) {
|
|
1729
|
-
db.query(`UPDATE memos SET hit_count = hit_count + 1 WHERE key = ?`).run(key);
|
|
1827
|
+
retryBusy(() => db.query(`UPDATE memos SET hit_count = hit_count + 1 WHERE key = ?`).run(key));
|
|
1730
1828
|
},
|
|
1731
1829
|
memoList() {
|
|
1732
|
-
const rows = db.query(`SELECT * FROM memos ORDER BY created_at DESC LIMIT 1000`).all();
|
|
1830
|
+
const rows = retryBusy(() => db.query(`SELECT * FROM memos ORDER BY created_at DESC LIMIT 1000`).all());
|
|
1733
1831
|
return rows.map((r) => rowToMemo(r));
|
|
1734
1832
|
},
|
|
1735
1833
|
memoClear() {
|
|
1736
|
-
db.exec(`DELETE FROM memos`);
|
|
1834
|
+
retryBusy(() => db.exec(`DELETE FROM memos`));
|
|
1737
1835
|
}
|
|
1738
1836
|
};
|
|
1739
1837
|
}
|
|
1740
1838
|
|
|
1741
1839
|
// src/wal.ts
|
|
1742
|
-
import { createHash as
|
|
1743
|
-
import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
|
|
1744
|
-
import { join as
|
|
1840
|
+
import { createHash as createHash2 } from "crypto";
|
|
1841
|
+
import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, truncateSync, writeFileSync } from "fs";
|
|
1842
|
+
import { join as join3 } from "path";
|
|
1745
1843
|
var WAL_FILE_NAME = "session.wal";
|
|
1746
1844
|
var SESSIONS_DIR_NAME = "sessions";
|
|
1747
1845
|
var MAX_WAL_BYTES = 256 * 1024 * 1024;
|
|
1748
1846
|
function checksumOf(op) {
|
|
1749
1847
|
if (op === undefined || op === null)
|
|
1750
1848
|
return "";
|
|
1751
|
-
return
|
|
1849
|
+
return createHash2("sha256").update(JSON.stringify(op)).digest("hex");
|
|
1752
1850
|
}
|
|
1753
1851
|
|
|
1754
1852
|
class SessionWAL {
|
|
@@ -1757,9 +1855,9 @@ class SessionWAL {
|
|
|
1757
1855
|
this.filePath = filePath;
|
|
1758
1856
|
}
|
|
1759
1857
|
static open(dataDir) {
|
|
1760
|
-
mkdirSync2(
|
|
1761
|
-
const canonical =
|
|
1762
|
-
const legacy =
|
|
1858
|
+
mkdirSync2(join3(dataDir, SESSIONS_DIR_NAME), { recursive: true });
|
|
1859
|
+
const canonical = join3(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
|
|
1860
|
+
const legacy = join3(dataDir, WAL_FILE_NAME);
|
|
1763
1861
|
if (existsSync(legacy) && !existsSync(canonical)) {
|
|
1764
1862
|
try {
|
|
1765
1863
|
renameSync(legacy, canonical);
|
|
@@ -1791,7 +1889,7 @@ class SessionWAL {
|
|
|
1791
1889
|
let repaired = false;
|
|
1792
1890
|
let content;
|
|
1793
1891
|
try {
|
|
1794
|
-
content =
|
|
1892
|
+
content = readFileSync2(this.filePath, "utf8");
|
|
1795
1893
|
} catch {
|
|
1796
1894
|
return { entries, torn, repaired, liveClaims: () => new Map };
|
|
1797
1895
|
}
|
|
@@ -1974,13 +2072,10 @@ async function callWorkflowTool(service, name, args) {
|
|
|
1974
2072
|
throw new Error(`Unknown tool: ${name}`);
|
|
1975
2073
|
}
|
|
1976
2074
|
}
|
|
1977
|
-
|
|
1978
|
-
// src/service.ts
|
|
1979
|
-
import { join as join3 } from "path";
|
|
1980
2075
|
// package.json
|
|
1981
2076
|
var package_default = {
|
|
1982
2077
|
name: "@hasna/workflows",
|
|
1983
|
-
version: "0.1.
|
|
2078
|
+
version: "0.1.4",
|
|
1984
2079
|
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",
|
|
1985
2080
|
type: "module",
|
|
1986
2081
|
main: "./dist/index.js",
|
|
@@ -2050,20 +2145,48 @@ var package_default = {
|
|
|
2050
2145
|
author: "Hasna",
|
|
2051
2146
|
license: "Apache-2.0",
|
|
2052
2147
|
dependencies: {
|
|
2053
|
-
"@anthropic-ai/claude-agent-sdk": "
|
|
2148
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.234",
|
|
2054
2149
|
"@cursor/sdk": "^1.0.28",
|
|
2055
|
-
"@
|
|
2150
|
+
"@hasna/paths": "0.1.0",
|
|
2151
|
+
"@openai/codex-sdk": "0.147.0",
|
|
2056
2152
|
commander: "^13.1.0"
|
|
2057
2153
|
},
|
|
2058
2154
|
devDependencies: {
|
|
2059
|
-
"@hasna/contracts": "0.14.
|
|
2060
|
-
"@types/bun": "
|
|
2155
|
+
"@hasna/contracts": "0.14.2",
|
|
2156
|
+
"@types/bun": "1.3.14",
|
|
2061
2157
|
"@types/pg": "^8.15.6",
|
|
2062
2158
|
pg: "^8.16.3",
|
|
2063
2159
|
typescript: "^5.7.3"
|
|
2064
2160
|
}
|
|
2065
2161
|
};
|
|
2066
2162
|
|
|
2163
|
+
// src/paths.ts
|
|
2164
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2165
|
+
import { homedir } from "os";
|
|
2166
|
+
import { join as join4 } from "path";
|
|
2167
|
+
import { dataDir } from "@hasna/paths";
|
|
2168
|
+
var APP = "workflows";
|
|
2169
|
+
var STORE_DB_FILE = "workflows.db";
|
|
2170
|
+
function getHomeDir(env = process.env) {
|
|
2171
|
+
return env["HOME"] || env["USERPROFILE"] || homedir();
|
|
2172
|
+
}
|
|
2173
|
+
function getLegacyDataDir(env = process.env) {
|
|
2174
|
+
return join4(getHomeDir(env), ".hasna", APP);
|
|
2175
|
+
}
|
|
2176
|
+
function getResolverDataDir(env = process.env) {
|
|
2177
|
+
return dataDir({ app: APP, home: getHomeDir(env), env });
|
|
2178
|
+
}
|
|
2179
|
+
function adoptResolverDataDir(resolved, env = process.env) {
|
|
2180
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
2181
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
2182
|
+
return true;
|
|
2183
|
+
return existsSync2(join4(resolved, STORE_DB_FILE));
|
|
2184
|
+
}
|
|
2185
|
+
function getEffectiveDataDir(env = process.env) {
|
|
2186
|
+
const resolved = getResolverDataDir(env);
|
|
2187
|
+
return adoptResolverDataDir(resolved, env) ? resolved : getLegacyDataDir(env);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2067
2190
|
// src/service.ts
|
|
2068
2191
|
var DEFAULT_PORT = 8790;
|
|
2069
2192
|
var DEFAULT_HOST = "127.0.0.1";
|
|
@@ -2086,11 +2209,10 @@ function envInt(names, fallback) {
|
|
|
2086
2209
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
2087
2210
|
}
|
|
2088
2211
|
function resolveWorkflowsConfig(overrides = {}) {
|
|
2089
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? ".";
|
|
2090
2212
|
return {
|
|
2091
2213
|
port: overrides.port ?? envInt(["HASNA_WORKFLOWS_PORT", "WORKFLOWS_PORT"], DEFAULT_PORT),
|
|
2092
2214
|
host: overrides.host ?? envString(["HASNA_WORKFLOWS_HOST", "WORKFLOWS_HOST"]) ?? DEFAULT_HOST,
|
|
2093
|
-
dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ??
|
|
2215
|
+
dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? getEffectiveDataDir(),
|
|
2094
2216
|
apiUrl: overrides.apiUrl ?? envString(["WORKFLOWS_API_URL"]),
|
|
2095
2217
|
apiKey: overrides.apiKey ?? envString(["WORKFLOWS_API_KEY"])
|
|
2096
2218
|
};
|