@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/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);
|
|
@@ -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
|
}
|
|
@@ -1977,7 +2075,7 @@ async function callWorkflowTool(service, name, args) {
|
|
|
1977
2075
|
// package.json
|
|
1978
2076
|
var package_default = {
|
|
1979
2077
|
name: "@hasna/workflows",
|
|
1980
|
-
version: "0.1.
|
|
2078
|
+
version: "0.1.4",
|
|
1981
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",
|
|
1982
2080
|
type: "module",
|
|
1983
2081
|
main: "./dist/index.js",
|
|
@@ -2054,8 +2152,8 @@ var package_default = {
|
|
|
2054
2152
|
commander: "^13.1.0"
|
|
2055
2153
|
},
|
|
2056
2154
|
devDependencies: {
|
|
2057
|
-
"@hasna/contracts": "0.14.
|
|
2058
|
-
"@types/bun": "
|
|
2155
|
+
"@hasna/contracts": "0.14.2",
|
|
2156
|
+
"@types/bun": "1.3.14",
|
|
2059
2157
|
"@types/pg": "^8.15.6",
|
|
2060
2158
|
pg: "^8.16.3",
|
|
2061
2159
|
typescript: "^5.7.3"
|
|
@@ -2065,7 +2163,7 @@ var package_default = {
|
|
|
2065
2163
|
// src/paths.ts
|
|
2066
2164
|
import { existsSync as existsSync2 } from "fs";
|
|
2067
2165
|
import { homedir } from "os";
|
|
2068
|
-
import { join as
|
|
2166
|
+
import { join as join4 } from "path";
|
|
2069
2167
|
import { dataDir } from "@hasna/paths";
|
|
2070
2168
|
var APP = "workflows";
|
|
2071
2169
|
var STORE_DB_FILE = "workflows.db";
|
|
@@ -2073,7 +2171,7 @@ function getHomeDir(env = process.env) {
|
|
|
2073
2171
|
return env["HOME"] || env["USERPROFILE"] || homedir();
|
|
2074
2172
|
}
|
|
2075
2173
|
function getLegacyDataDir(env = process.env) {
|
|
2076
|
-
return
|
|
2174
|
+
return join4(getHomeDir(env), ".hasna", APP);
|
|
2077
2175
|
}
|
|
2078
2176
|
function getResolverDataDir(env = process.env) {
|
|
2079
2177
|
return dataDir({ app: APP, home: getHomeDir(env), env });
|
|
@@ -2082,7 +2180,7 @@ function adoptResolverDataDir(resolved, env = process.env) {
|
|
|
2082
2180
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
2083
2181
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
2084
2182
|
return true;
|
|
2085
|
-
return existsSync2(
|
|
2183
|
+
return existsSync2(join4(resolved, STORE_DB_FILE));
|
|
2086
2184
|
}
|
|
2087
2185
|
function getEffectiveDataDir(env = process.env) {
|
|
2088
2186
|
const resolved = getResolverDataDir(env);
|