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