@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/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 = statSync(`${entry}/${binary}`);
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?.status ?? "finished";
301
+ const status = run.status;
302
302
  const ok = status === "finished";
303
- return { ok, exitCode: ok ? 0 : 1, output: run?.result ?? "", durationMs };
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
- return run;
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 existing = this.store.listRunNodes(run.id).find((n) => n.nodeId === nodeId);
1387
- const nodeRow = existing ?? this.store.createRunNode({ runId: run.id, nodeId, lane: node.lane ?? "claude" });
1388
- if (node.memo) {
1389
- const memoInput = { command: node.command, prompt: node.prompt, context: stripRunState(context) };
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 (node.memo) {
1442
- recordMemo(this.store, run.graphName, nodeId, { command: node.command, prompt: node.prompt, context: stripRunState(context) }, output);
1443
- this.wal.append({ op: "memo_set", key: `${run.graphName}:${nodeId}:${createHash2("sha256").update(JSON.stringify({ command: node.command, prompt: node.prompt, context: stripRunState(context) })).digest("hex")}`, at: nowIso() });
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(join(dataDir, "workflows.db"));
1566
- db.exec("PRAGMA journal_mode = WAL");
1567
- db.exec("PRAGMA foreign_keys = ON");
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
- VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)`).run(id, input.graphName, input.graphVersion, JSON.stringify(input.context ?? {}), now, now);
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 rows2 = db.query(`SELECT * FROM runs WHERE status = ? ORDER BY created_at DESC, id DESC LIMIT ?`).all(opts.status, limit);
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
- VALUES (?, ?, ?, 'pending', 0, ?)`).run(id, input.runId, input.nodeId, input.lane ?? null);
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
- VALUES (?, ?, ?, ?, ?, 0, ?)
1722
- ON CONFLICT(key) DO UPDATE SET output_json = excluded.output_json`).run(key, graphName, nodeId, inputHash2, outputJson, nowIso2());
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 createHash3 } from "crypto";
1743
- import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
1744
- import { join as join2 } from "path";
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 createHash3("sha256").update(JSON.stringify(op)).digest("hex");
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(join2(dataDir, SESSIONS_DIR_NAME), { recursive: true });
1761
- const canonical = join2(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1762
- const legacy = join2(dataDir, WAL_FILE_NAME);
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 = readFileSync(this.filePath, "utf8");
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.3",
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.1",
2058
- "@types/bun": "latest",
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 join3 } from "path";
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 join3(getHomeDir(env), ".hasna", APP);
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(join3(resolved, STORE_DB_FILE));
2183
+ return existsSync2(join4(resolved, STORE_DB_FILE));
2086
2184
  }
2087
2185
  function getEffectiveDataDir(env = process.env) {
2088
2186
  const resolved = getResolverDataDir(env);