@hasna/mementos 0.14.85 → 0.14.87

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.
Files changed (55) hide show
  1. package/bun.lock +63 -4
  2. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts +2 -0
  3. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts.map +1 -0
  4. package/dist/cli/commands/info-stale.d.ts.map +1 -1
  5. package/dist/cli/commands/io-restore.d.ts.map +1 -1
  6. package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
  7. package/dist/cli/commands/memory-cmd-list.d.ts.map +1 -1
  8. package/dist/cli/commands/memory-cmd-remove.d.ts.map +1 -1
  9. package/dist/cli/index.js +623 -208
  10. package/dist/db/__fixtures__/list-filter-capture-server.d.ts +2 -0
  11. package/dist/db/__fixtures__/list-filter-capture-server.d.ts.map +1 -0
  12. package/dist/db/__fixtures__/list-filter-client-runner.d.ts +2 -0
  13. package/dist/db/__fixtures__/list-filter-client-runner.d.ts.map +1 -0
  14. package/dist/db/agents.d.ts +14 -0
  15. package/dist/db/agents.d.ts.map +1 -1
  16. package/dist/db/analytics.d.ts +4 -0
  17. package/dist/db/analytics.d.ts.map +1 -1
  18. package/dist/db/memories.d.ts +6 -0
  19. package/dist/db/memories.d.ts.map +1 -1
  20. package/dist/db/session-jobs.d.ts +18 -0
  21. package/dist/db/session-jobs.d.ts.map +1 -1
  22. package/dist/db/webhook_hooks.d.ts +24 -1
  23. package/dist/db/webhook_hooks.d.ts.map +1 -1
  24. package/dist/diagnostics/historical-project-registration-receipt.js +36 -17
  25. package/dist/index.js +141 -52
  26. package/dist/lib/built-in-hooks.d.ts +22 -2
  27. package/dist/lib/built-in-hooks.d.ts.map +1 -1
  28. package/dist/lib/file-deps.d.ts +1 -1
  29. package/dist/lib/open-sessions-connector.d.ts +6 -6
  30. package/dist/lib/open-sessions-connector.d.ts.map +1 -1
  31. package/dist/lib/redact.d.ts +16 -0
  32. package/dist/lib/redact.d.ts.map +1 -1
  33. package/dist/lib/session-processor.d.ts.map +1 -1
  34. package/dist/lib/session-queue.d.ts.map +1 -1
  35. package/dist/lib/storage-sync.d.ts.map +1 -1
  36. package/dist/mcp/index.js +348 -70
  37. package/dist/mcp/tools/memory-lifecycle.d.ts.map +1 -1
  38. package/dist/pg-sync-worker.js +7 -5
  39. package/dist/project-registration.js +108 -36
  40. package/dist/sdk/index.d.ts +9 -0
  41. package/dist/sdk/index.d.ts.map +1 -1
  42. package/dist/sdk/index.js +1 -0
  43. package/dist/server/index.d.ts.map +1 -1
  44. package/dist/server/index.js +563 -299
  45. package/dist/server/routes/system-hooks.d.ts.map +1 -1
  46. package/dist/storage.d.ts +16 -2
  47. package/dist/storage.d.ts.map +1 -1
  48. package/dist/storage.js +63 -26
  49. package/dist/test-support/pg-sync-stub-worker.d.ts +2 -0
  50. package/dist/test-support/pg-sync-stub-worker.d.ts.map +1 -0
  51. package/dist/types/hooks.d.ts +1 -1
  52. package/dist/types/index.d.ts +10 -0
  53. package/dist/types/index.d.ts.map +1 -1
  54. package/hasna.contract.json +5 -4
  55. package/package.json +4 -3
package/dist/mcp/index.js CHANGED
@@ -320,6 +320,7 @@ function translateSql(sql) {
320
320
  let translated = sql.replace(/\?/g, () => `$${++parameterIndex}`);
321
321
  const ISO_FMT = `'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'`;
322
322
  translated = translated.replace(/datetime\s*\(\s*'now'\s*\)/gi, `to_char(now() AT TIME ZONE 'UTC', ${ISO_FMT})`);
323
+ translated = translated.replace(/strftime\s*\(\s*'%Y-%m-%dT%H:%M:%fZ'\s*,\s*'now'\s*\)/gi, `to_char(now() AT TIME ZONE 'UTC', ${ISO_FMT})`);
323
324
  translated = translated.replace(/datetime\s*\(\s*'now'\s*,\s*'(-?\d+)\s+(minutes?|hours?|days?|seconds?)'\s*\)/gi, (_match, amount, unit) => {
324
325
  const parsed = parseInt(String(amount), 10);
325
326
  const absolute = Math.abs(parsed);
@@ -804,16 +805,24 @@ function transferRows(target, table, rows, options) {
804
805
  const conflictColumn = options.conflictColumn ?? "updated_at";
805
806
  let written = 0;
806
807
  let skipped = 0;
808
+ let maxSyncedAt = null;
807
809
  const errors = [];
810
+ const bumpMaxSyncedAt = (row) => {
811
+ const value = row[conflictColumn];
812
+ if (typeof value === "string" && (maxSyncedAt === null || value > maxSyncedAt)) {
813
+ maxSyncedAt = value;
814
+ }
815
+ };
808
816
  if (rows.length === 0) {
809
- return { written, skipped, errors };
817
+ return { written, skipped, errors, maxSyncedAt };
810
818
  }
811
819
  const columns = Object.keys(rows[0] ?? {});
812
820
  if (!columns.includes(primaryKey)) {
813
821
  return {
814
822
  written,
815
823
  skipped,
816
- errors: [`Table "${table}" has no "${primaryKey}" column; skipping`]
824
+ errors: [`Table "${table}" has no "${primaryKey}" column; skipping`],
825
+ maxSyncedAt
817
826
  };
818
827
  }
819
828
  const hasConflictColumn = columns.includes(conflictColumn);
@@ -826,6 +835,7 @@ function transferRows(target, table, rows, options) {
826
835
  const incomingTime = Date.parse(String(row[conflictColumn]));
827
836
  if (Number.isFinite(existingTime) && Number.isFinite(incomingTime) && existingTime >= incomingTime) {
828
837
  skipped++;
838
+ bumpMaxSyncedAt(row);
829
839
  continue;
830
840
  }
831
841
  }
@@ -838,11 +848,12 @@ function transferRows(target, table, rows, options) {
838
848
  target.run(`INSERT INTO "${table}" (${columnList}) VALUES (${placeholders})`, ...columns.map((column) => row[column]));
839
849
  }
840
850
  written++;
851
+ bumpMaxSyncedAt(row);
841
852
  } catch (error) {
842
853
  errors.push(`Row ${String(row[primaryKey] ?? "unknown")}: ${error instanceof Error ? error.message : String(error)}`);
843
854
  }
844
855
  }
845
- return { written, skipped, errors };
856
+ return { written, skipped, errors, maxSyncedAt };
846
857
  }
847
858
  function incrementalSyncPush(local, remote, tables, options = {}) {
848
859
  return runIncrementalSync("push", local, remote, local, tables, options);
@@ -880,22 +891,29 @@ function runIncrementalSync(direction, source, target, metaDb, tables, options)
880
891
  rows = source.all(`SELECT * FROM "${table}"`);
881
892
  stat.first_sync = true;
882
893
  }
894
+ let maxSyncedAt = null;
883
895
  for (let offset = 0;offset < rows.length; offset += batchSize) {
884
896
  const batch = rows.slice(offset, offset + batchSize);
885
897
  const result = transferRows(target, table, batch, options);
886
898
  stat.synced_rows += result.written;
887
899
  stat.skipped_rows += result.skipped;
888
900
  stat.errors.push(...result.errors);
901
+ if (result.maxSyncedAt !== null && (maxSyncedAt === null || result.maxSyncedAt > maxSyncedAt)) {
902
+ maxSyncedAt = result.maxSyncedAt;
903
+ }
889
904
  }
890
905
  if (rows.length === 0) {
891
906
  stat.skipped_rows = stat.total_rows;
892
907
  }
893
- upsertSyncMeta(metaDb, {
894
- table_name: table,
895
- last_synced_at: new Date().toISOString(),
896
- last_synced_row_count: stat.synced_rows,
897
- direction
898
- });
908
+ const nextCursor = maxSyncedAt ?? meta?.last_synced_at ?? null;
909
+ if (rows.length > 0 && stat.errors.length === 0 && nextCursor !== null) {
910
+ upsertSyncMeta(metaDb, {
911
+ table_name: table,
912
+ last_synced_at: nextCursor,
913
+ last_synced_row_count: stat.synced_rows,
914
+ direction
915
+ });
916
+ }
899
917
  } catch (error) {
900
918
  stat.errors.push(`Table "${table}": ${error instanceof Error ? error.message : String(error)}`);
901
919
  }
@@ -923,8 +941,13 @@ var init_storage = __esm(() => {
923
941
  data;
924
942
  closed = false;
925
943
  lastError = null;
944
+ generation = 0;
926
945
  static DATA_BYTES = 128 * 1024 * 1024;
927
- static QUERY_TIMEOUT_MS = 60000;
946
+ static queryTimeoutMs() {
947
+ const raw = process.env["MEMENTOS_PGSYNC_QUERY_TIMEOUT_MS"]?.trim();
948
+ const parsed = raw ? Number(raw) : Number.NaN;
949
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 60000;
950
+ }
928
951
  static resolveWorkerPath() {
929
952
  const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
930
953
  const here = fileURLToPath(new URL(".", import.meta.url));
@@ -939,12 +962,12 @@ var init_storage = __esm(() => {
939
962
  }
940
963
  return candidates[0];
941
964
  }
942
- constructor(connectionString) {
943
- const control = new SharedArrayBuffer(8);
965
+ constructor(connectionString, workerPath) {
966
+ const control = new SharedArrayBuffer(12);
944
967
  const dataSab = new SharedArrayBuffer(PgSyncPool.DATA_BYTES);
945
968
  this.status = new Int32Array(control);
946
969
  this.data = new Uint8Array(dataSab);
947
- this.worker = new Worker(PgSyncPool.resolveWorkerPath(), {
970
+ this.worker = new Worker(workerPath ?? PgSyncPool.resolveWorkerPath(), {
948
971
  workerData: {
949
972
  dsn: stripSslParams(connectionString),
950
973
  ssl: sslConfigFor(connectionString),
@@ -962,21 +985,35 @@ var init_storage = __esm(() => {
962
985
  throw new Error("PgSyncPool is closed");
963
986
  if (this.lastError)
964
987
  throw this.lastError;
988
+ const timeoutMs = PgSyncPool.queryTimeoutMs();
989
+ const gen = ++this.generation;
965
990
  Atomics.store(this.status, 0, 0);
966
- this.worker.postMessage({ sql, params });
967
- const waitResult = Atomics.wait(this.status, 0, 0, PgSyncPool.QUERY_TIMEOUT_MS);
968
- const code = Atomics.load(this.status, 0);
969
- if (code === 0 || waitResult === "timed-out") {
970
- if (this.lastError)
971
- throw this.lastError;
972
- throw new Error("PostgreSQL query timed out after 60s");
973
- }
974
- const len = Atomics.load(this.status, 1);
975
- const payload = JSON.parse(new TextDecoder().decode(this.data.subarray(0, len)));
976
- if (code === 2) {
977
- throw new Error(payload.message ?? "PostgreSQL error");
991
+ Atomics.store(this.status, 2, 0);
992
+ this.worker.postMessage({ sql, params, gen });
993
+ const deadline = Date.now() + timeoutMs;
994
+ for (;; ) {
995
+ const remaining = deadline - Date.now();
996
+ const responding = Atomics.load(this.status, 0);
997
+ if (responding === gen) {
998
+ const code = Atomics.load(this.status, 2);
999
+ const len = Atomics.load(this.status, 1);
1000
+ const payload = JSON.parse(new TextDecoder().decode(this.data.subarray(0, len)));
1001
+ if (code === 2) {
1002
+ throw new Error(payload.message ?? "PostgreSQL error");
1003
+ }
1004
+ return payload;
1005
+ }
1006
+ if (responding !== 0) {
1007
+ Atomics.compareExchange(this.status, 0, responding, 0);
1008
+ continue;
1009
+ }
1010
+ if (remaining <= 0) {
1011
+ if (this.lastError)
1012
+ throw this.lastError;
1013
+ throw new Error(`PostgreSQL query timed out after ${timeoutMs}ms`);
1014
+ }
1015
+ Atomics.wait(this.status, 0, 0, remaining);
978
1016
  }
979
- return payload;
980
1017
  }
981
1018
  end() {
982
1019
  if (this.closed)
@@ -3252,6 +3289,7 @@ __export(exports_memories, {
3252
3289
  updateMemory: () => updateMemory,
3253
3290
  touchMemory: () => touchMemory,
3254
3291
  semanticSearch: () => semanticSearch,
3292
+ reservedAgentIdViolation: () => reservedAgentIdViolation,
3255
3293
  parseMemoryRow: () => parseMemoryRow,
3256
3294
  listMemoryHistoryPage: () => listMemoryHistoryPage,
3257
3295
  listMemoryHistory: () => listMemoryHistory,
@@ -3323,7 +3361,20 @@ function parseMemoryRow(row) {
3323
3361
  accessed_at: row["accessed_at"] || null
3324
3362
  };
3325
3363
  }
3364
+ function reservedAgentIdViolation(agentId) {
3365
+ if (!agentId)
3366
+ return null;
3367
+ const normalized = agentId.trim().toLowerCase();
3368
+ if (RESERVED_AGENT_IDS.has(normalized)) {
3369
+ return `Reserved placeholder agent id "${agentId}" cannot own a memory. ` + `Refusing to write: test harnesses must not write memories under placeholder agent ` + `identities. Register a real agent (mementos register-agent <name>) and pass its id.`;
3370
+ }
3371
+ return null;
3372
+ }
3326
3373
  function createMemory(input, dedupeMode = "merge", db) {
3374
+ const reservedViolation = reservedAgentIdViolation(input.agent_id);
3375
+ if (reservedViolation) {
3376
+ throw new Error(reservedViolation);
3377
+ }
3327
3378
  if (!db && isApiMode()) {
3328
3379
  const { status, data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
3329
3380
  if (!data || !data.id) {
@@ -3373,7 +3424,8 @@ function createMemory(input, dedupeMode = "merge", db) {
3373
3424
  importance = ?, metadata = ?, expires_at = ?,
3374
3425
  when_to_use = ?,
3375
3426
  pinned = COALESCE(pinned, 0),
3376
- version = version + 1, updated_at = ?
3427
+ version = version + 1, updated_at = ?,
3428
+ updated_by_agent = ?
3377
3429
  WHERE id = ?`, [
3378
3430
  safeValue,
3379
3431
  input.category || "knowledge",
@@ -3384,6 +3436,7 @@ function createMemory(input, dedupeMode = "merge", db) {
3384
3436
  expiresAt,
3385
3437
  input.when_to_use || null,
3386
3438
  timestamp,
3439
+ input.agent_id || null,
3387
3440
  existing.id
3388
3441
  ]);
3389
3442
  d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
@@ -3488,6 +3541,12 @@ function bulkUpsertMemories(memories, db) {
3488
3541
  errors.push(`Rejected "${key}": ${formatEnumViolation(violation)}`);
3489
3542
  continue;
3490
3543
  }
3544
+ const agentViolation = reservedAgentIdViolation(typeof mem["agent_id"] === "string" ? mem["agent_id"] : undefined);
3545
+ if (agentViolation) {
3546
+ rejected++;
3547
+ errors.push(`Rejected "${key}": ${agentViolation}`);
3548
+ continue;
3549
+ }
3491
3550
  const timestamp = now();
3492
3551
  let tags = [];
3493
3552
  const rawTags = mem["tags"];
@@ -3766,6 +3825,11 @@ function listMemoriesPage(filter, db) {
3766
3825
  agent_id: f.agent_id,
3767
3826
  project_id: f.project_id,
3768
3827
  session_id: f.session_id,
3828
+ machine_id: f.machine_id,
3829
+ visible_to_machine_id: f.visible_to_machine_id,
3830
+ search: f.search,
3831
+ source: f.source,
3832
+ flag: f.flag,
3769
3833
  namespace: f.namespace,
3770
3834
  as_of: f.as_of,
3771
3835
  limit: f.limit,
@@ -4045,20 +4109,29 @@ function updateMemory(id, input, db) {
4045
4109
  sets.push("when_to_use = ?");
4046
4110
  params.push(input.when_to_use ?? null);
4047
4111
  }
4112
+ if (input.updated_by_agent !== undefined) {
4113
+ sets.push("updated_by_agent = ?");
4114
+ params.push(input.updated_by_agent ?? null);
4115
+ }
4048
4116
  if (input.tags !== undefined) {
4049
4117
  sets.push("tags = ?");
4050
4118
  params.push(JSON.stringify(input.tags));
4051
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memoryId]);
4052
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
4053
- for (const tag of input.tags) {
4054
- insertTag.run(memoryId, tag);
4055
- }
4056
4119
  }
4057
4120
  params.push(memoryId);
4058
- const result = d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
4059
- if (result.changes === 0) {
4060
- throw new Error(`Update affected no rows for memory ${memoryId}: the record was read but not written. ` + `This is a bug in @hasna/mementos, not a bad argument \u2014 please report it.`);
4061
- }
4121
+ d.transaction(() => {
4122
+ const res = d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
4123
+ if (res.changes === 0) {
4124
+ throw new Error(`Update affected no rows for memory ${memoryId}: the record was read but not written. ` + `This is a bug in @hasna/mementos, not a bad argument \u2014 please report it.`);
4125
+ }
4126
+ if (input.tags !== undefined) {
4127
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memoryId]);
4128
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
4129
+ for (const tag of input.tags) {
4130
+ insertTag.run(memoryId, tag);
4131
+ }
4132
+ }
4133
+ return res;
4134
+ });
4062
4135
  const updated = getMemory(memoryId, d);
4063
4136
  if (input.value !== undefined) {
4064
4137
  try {
@@ -4229,7 +4302,7 @@ async function semanticSearch(queryText, options = {}, db) {
4229
4302
  scored.sort((a, b) => b.score - a.score);
4230
4303
  return scored.slice(0, limit);
4231
4304
  }
4232
- var RECALL_PROMOTE_THRESHOLD = 3;
4305
+ var RESERVED_AGENT_IDS, RECALL_PROMOTE_THRESHOLD = 3;
4233
4306
  var init_memories = __esm(() => {
4234
4307
  init_types();
4235
4308
  init_database();
@@ -4239,6 +4312,12 @@ var init_memories = __esm(() => {
4239
4312
  init_poisoning();
4240
4313
  init_entity_memories();
4241
4314
  init_api_mode();
4315
+ RESERVED_AGENT_IDS = new Set([
4316
+ "agent-a",
4317
+ "agent-x",
4318
+ "agent-z",
4319
+ "nonexistent-agent"
4320
+ ]);
4242
4321
  });
4243
4322
 
4244
4323
  // src/db/machines.ts
@@ -4333,6 +4412,8 @@ var init_machines = __esm(() => {
4333
4412
  });
4334
4413
 
4335
4414
  // src/db/webhook_hooks.ts
4415
+ import { isIP } from "net";
4416
+ import { lookup as dnsLookup } from "dns/promises";
4336
4417
  function parseRow(row) {
4337
4418
  return {
4338
4419
  id: row["id"],
@@ -4349,7 +4430,153 @@ function parseRow(row) {
4349
4430
  failureCount: row["failure_count"]
4350
4431
  };
4351
4432
  }
4352
- function createWebhookHook(input, db) {
4433
+ function isBlockedIpv4(parts) {
4434
+ const a = parts[0];
4435
+ const b = parts[1];
4436
+ if (a === 0)
4437
+ return true;
4438
+ if (a === 127)
4439
+ return true;
4440
+ if (a === 169 && b === 254)
4441
+ return true;
4442
+ if (a === 10)
4443
+ return true;
4444
+ if (a === 172 && b >= 16 && b <= 31)
4445
+ return true;
4446
+ if (a === 192 && b === 168)
4447
+ return true;
4448
+ return false;
4449
+ }
4450
+ function isBlockedIpv6(bytes) {
4451
+ const mapped = bytes.slice(0, 10).every((b) => b === 0) && bytes[10] === 255 && bytes[11] === 255;
4452
+ if (mapped)
4453
+ return isBlockedIpv4(bytes.slice(12, 16));
4454
+ if (bytes.every((b) => b === 0))
4455
+ return true;
4456
+ if (bytes.slice(0, 15).every((b) => b === 0) && bytes[15] === 1)
4457
+ return true;
4458
+ if ((bytes[0] & 254) === 252)
4459
+ return true;
4460
+ if (bytes[0] === 254 && (bytes[1] & 192) === 128)
4461
+ return true;
4462
+ return false;
4463
+ }
4464
+ function quadToGroups(quad) {
4465
+ const nums = quad.split(".").map((p) => Number(p));
4466
+ const [a, b, c, d] = nums;
4467
+ const valid = [a, b, c, d].every((n) => n !== undefined && Number.isInteger(n) && n >= 0 && n <= 255);
4468
+ if (!valid)
4469
+ return null;
4470
+ return [a << 8 | b, c << 8 | d];
4471
+ }
4472
+ function parseIpv6Bytes(host) {
4473
+ const groups = host.split("::");
4474
+ if (groups.length > 2)
4475
+ return null;
4476
+ const headRaw = groups[0] ?? "";
4477
+ const tailRaw = groups[1] ?? "";
4478
+ const head = headRaw === "" ? [] : headRaw.split(":");
4479
+ const tail = tailRaw === "" ? [] : tailRaw.split(":");
4480
+ const headNums = [];
4481
+ for (const g of head) {
4482
+ if (!/^[0-9a-f]{1,4}$/i.test(g))
4483
+ return null;
4484
+ headNums.push(parseInt(g, 16));
4485
+ }
4486
+ const tailNums = [];
4487
+ for (const g of tail) {
4488
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(g)) {
4489
+ const quads = quadToGroups(g);
4490
+ if (!quads)
4491
+ return null;
4492
+ tailNums.push(...quads);
4493
+ } else if (/^[0-9a-f]{1,4}$/i.test(g)) {
4494
+ tailNums.push(parseInt(g, 16));
4495
+ } else {
4496
+ return null;
4497
+ }
4498
+ }
4499
+ const hasCompression = groups.length === 2;
4500
+ if (!hasCompression && headNums.length !== 8)
4501
+ return null;
4502
+ if (hasCompression && headNums.length + tailNums.length >= 8)
4503
+ return null;
4504
+ const zeros = 8 - headNums.length - tailNums.length;
4505
+ const all = [...headNums, ...new Array(zeros).fill(0), ...tailNums];
4506
+ const bytes = [];
4507
+ for (const n of all) {
4508
+ bytes.push(n >> 8 & 255, n & 255);
4509
+ }
4510
+ return bytes;
4511
+ }
4512
+ function defaultResolveHost(hostname2) {
4513
+ return dnsLookup(hostname2, { all: true, verbatim: true });
4514
+ }
4515
+ function assertResolvedAddressPublic(address, url) {
4516
+ const version = isIP(address);
4517
+ if (version === 4) {
4518
+ if (isBlockedIpv4(address.split(".").map((p) => Number(p)))) {
4519
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
4520
+ }
4521
+ } else if (version === 6) {
4522
+ const bytes = parseIpv6Bytes(address);
4523
+ if (!bytes || isBlockedIpv6(bytes)) {
4524
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
4525
+ }
4526
+ } else {
4527
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
4528
+ }
4529
+ }
4530
+ async function validateWebhookHandlerUrl(url, opts) {
4531
+ const resolveHost = opts?.lookup ?? defaultResolveHost;
4532
+ let parsed;
4533
+ try {
4534
+ parsed = new URL(url);
4535
+ } catch {
4536
+ throw new Error(`Invalid webhook handler URL "${url}" \u2014 must be a valid http(s) URL`);
4537
+ }
4538
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
4539
+ throw new Error(`Invalid webhook handler URL "${url}" \u2014 only http and https are allowed`);
4540
+ }
4541
+ if (parsed.username || parsed.password) {
4542
+ throw new Error(`Invalid webhook handler URL "${url}" \u2014 embedded credentials are not allowed`);
4543
+ }
4544
+ const host = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
4545
+ if (host === "localhost" || host.endsWith(".localhost")) {
4546
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
4547
+ }
4548
+ const version = isIP(host);
4549
+ if (version === 4 || version === 6) {
4550
+ if (version === 4) {
4551
+ if (isBlockedIpv4(host.split(".").map((p) => Number(p)))) {
4552
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
4553
+ }
4554
+ } else {
4555
+ const bytes = parseIpv6Bytes(host);
4556
+ if (!bytes || isBlockedIpv6(bytes)) {
4557
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
4558
+ }
4559
+ }
4560
+ return;
4561
+ }
4562
+ if (/^[0-9]+(\.[0-9]+)*$/.test(host) || /^0x[0-9a-f]+$/i.test(host) || host.includes("%")) {
4563
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
4564
+ }
4565
+ let addrs;
4566
+ try {
4567
+ addrs = await resolveHost(host);
4568
+ } catch {
4569
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
4570
+ }
4571
+ if (addrs.length === 0) {
4572
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
4573
+ }
4574
+ for (const { address } of addrs) {
4575
+ assertResolvedAddressPublic(address, url);
4576
+ }
4577
+ }
4578
+ async function createWebhookHook(input, db, opts) {
4579
+ await validateWebhookHandlerUrl(input.handlerUrl, opts);
4353
4580
  if (!db && isApiMode()) {
4354
4581
  const { data } = apiJson("POST", "/webhooks", {
4355
4582
  type: input.type,
@@ -4467,6 +4694,7 @@ function recordWebhookInvocation(id, success, db) {
4467
4694
  d.run("UPDATE webhook_hooks SET invocation_count = invocation_count + 1, failure_count = failure_count + 1 WHERE id = ?", [id]);
4468
4695
  }
4469
4696
  }
4697
+ var BLOCKED_TARGET_MESSAGE = "Invalid webhook handler URL \u2014 loopback, link-local, and private network targets are not allowed";
4470
4698
  var init_webhook_hooks = __esm(() => {
4471
4699
  init_database();
4472
4700
  init_api_mode();
@@ -5116,7 +5344,7 @@ function buildFilterConditions(filter) {
5116
5344
  const conditions = [];
5117
5345
  const params = [];
5118
5346
  conditions.push("m.status = 'active'");
5119
- conditions.push("(m.expires_at IS NULL OR m.expires_at >= datetime('now'))");
5347
+ conditions.push("(m.expires_at IS NULL OR m.expires_at >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))");
5120
5348
  if (!filter)
5121
5349
  return { conditions, params };
5122
5350
  if (filter.scope) {
@@ -7125,7 +7353,7 @@ async function detectContradiction(newKey, newValue, options = {}, db) {
7125
7353
  conditions.push("project_id = ?");
7126
7354
  params.push(project_id);
7127
7355
  }
7128
- conditions.push("(valid_until IS NULL OR valid_until > datetime('now'))");
7356
+ conditions.push("(valid_until IS NULL OR valid_until > strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))");
7129
7357
  const sql = `SELECT * FROM memories WHERE ${conditions.join(" AND ")} ORDER BY importance DESC LIMIT 10`;
7130
7358
  const rows = d.query(sql).all(...params);
7131
7359
  if (rows.length === 0) {
@@ -7172,6 +7400,7 @@ var init_contradiction = __esm(() => {
7172
7400
  var exports_built_in_hooks = {};
7173
7401
  __export(exports_built_in_hooks, {
7174
7402
  reloadWebhooks: () => reloadWebhooks,
7403
+ makeWebhookHandler: () => makeWebhookHandler,
7175
7404
  loadWebhooksFromDb: () => loadWebhooksFromDb
7176
7405
  });
7177
7406
  async function getAutoMemory() {
@@ -7181,13 +7410,19 @@ async function getAutoMemory() {
7181
7410
  }
7182
7411
  return _processConversationTurn;
7183
7412
  }
7184
- function loadWebhooksFromDb() {
7413
+ async function loadWebhooksFromDb() {
7185
7414
  if (_webhooksLoaded)
7186
7415
  return;
7187
7416
  _webhooksLoaded = true;
7188
7417
  try {
7189
7418
  const webhooks = listWebhookHooks({ enabled: true });
7190
7419
  for (const wh of webhooks) {
7420
+ try {
7421
+ await validateWebhookHandlerUrl(wh.handlerUrl);
7422
+ } catch (err) {
7423
+ console.error(`[hooks] Skipping webhook ${wh.id} (${wh.type}): ${err instanceof Error ? err.message : String(err)}`);
7424
+ continue;
7425
+ }
7191
7426
  hookRegistry.register({
7192
7427
  type: wh.type,
7193
7428
  blocking: wh.blocking,
@@ -7205,9 +7440,10 @@ function loadWebhooksFromDb() {
7205
7440
  console.error("[hooks] Failed to load webhooks from DB:", err);
7206
7441
  }
7207
7442
  }
7208
- function makeWebhookHandler(webhookId, url) {
7443
+ function makeWebhookHandler(webhookId, url, opts) {
7209
7444
  return async (context) => {
7210
7445
  try {
7446
+ await validateWebhookHandlerUrl(url, opts);
7211
7447
  const res = await fetch(url, {
7212
7448
  method: "POST",
7213
7449
  headers: { "Content-Type": "application/json" },
@@ -7220,9 +7456,9 @@ function makeWebhookHandler(webhookId, url) {
7220
7456
  }
7221
7457
  };
7222
7458
  }
7223
- function reloadWebhooks() {
7459
+ async function reloadWebhooks() {
7224
7460
  _webhooksLoaded = false;
7225
- loadWebhooksFromDb();
7461
+ await loadWebhooksFromDb();
7226
7462
  }
7227
7463
  var _processConversationTurn = null, _webhooksLoaded = false;
7228
7464
  var init_built_in_hooks = __esm(() => {
@@ -23314,7 +23550,7 @@ class JSONSchemaGenerator {
23314
23550
  if (val === undefined) {
23315
23551
  if (this.unrepresentable === "throw") {
23316
23552
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
23317
- }
23553
+ } else {}
23318
23554
  } else if (typeof val === "bigint") {
23319
23555
  if (this.unrepresentable === "throw") {
23320
23556
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -44964,7 +45200,7 @@ var require_tracestate_impl = __commonJS((exports) => {
44964
45200
  const value = listMember.slice(i + 1, part.length);
44965
45201
  if ((0, tracestate_validators_1.validateKey)(key) && (0, tracestate_validators_1.validateValue)(value)) {
44966
45202
  agg.set(key, value);
44967
- }
45203
+ } else {}
44968
45204
  }
44969
45205
  return agg;
44970
45206
  }, new Map);
@@ -60192,7 +60428,10 @@ function getMemoryStats(db) {
60192
60428
  const byCategory = d.query("SELECT category, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY category").all();
60193
60429
  const byStatus = d.query("SELECT status, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY status").all();
60194
60430
  const pinnedCount = d.query("SELECT COUNT(*) as c FROM memories WHERE pinned = 1 AND status = 'active'").get().c;
60195
- const expiredCount = d.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired' OR (expires_at IS NOT NULL AND expires_at < datetime('now'))").get().c;
60431
+ const expiredCount = d.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired'").get().c;
60432
+ const expiresAtCount = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL").get().c;
60433
+ const nowIso = new Date().toISOString();
60434
+ const expiredDueCount = d.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired' OR (expires_at IS NOT NULL AND expires_at < ?)").get(nowIso).c;
60196
60435
  const stats = {
60197
60436
  total,
60198
60437
  by_scope: { global: 0, shared: 0, private: 0, working: 0 },
@@ -60200,7 +60439,9 @@ function getMemoryStats(db) {
60200
60439
  by_status: { active: 0, archived: 0, expired: 0 },
60201
60440
  by_agent: {},
60202
60441
  pinned_count: pinnedCount,
60203
- expired_count: expiredCount
60442
+ expired_count: expiredCount,
60443
+ expires_at_count: expiresAtCount,
60444
+ expired_due_count: expiredDueCount
60204
60445
  };
60205
60446
  for (const row of byScope)
60206
60447
  if (row.scope in stats.by_scope)
@@ -60234,7 +60475,9 @@ function normalizeStats(data) {
60234
60475
  by_status: { active: 0, archived: 0, expired: 0, ...data?.by_status ?? {} },
60235
60476
  by_agent: data?.by_agent ?? {},
60236
60477
  pinned_count: data?.pinned_count ?? 0,
60237
- expired_count: data?.expired_count ?? 0
60478
+ expired_count: data?.expired_count ?? 0,
60479
+ expires_at_count: data?.expires_at_count ?? 0,
60480
+ expired_due_count: data?.expired_due_count ?? 0
60238
60481
  };
60239
60482
  }
60240
60483
  function getMemoryActivity(filter = {}, db) {
@@ -60332,7 +60575,7 @@ function getStaleMemoriesPage(filter = {}, db) {
60332
60575
  const limit = filter.limit ?? 20;
60333
60576
  const offset = filter.offset ?? 0;
60334
60577
  if (!db && isApiMode()) {
60335
- const q = toQuery({ days, project_id: filter.project_id, agent_id: filter.agent_id, limit, offset });
60578
+ const q = toQuery({ days, project_id: filter.project_id, agent_id: filter.agent_id, pinned: filter.pinned, limit, offset });
60336
60579
  const { data } = apiJson("GET", `/memories/stale${q}`);
60337
60580
  const rows2 = data?.memories ?? [];
60338
60581
  return {
@@ -60344,8 +60587,14 @@ function getStaleMemoriesPage(filter = {}, db) {
60344
60587
  }
60345
60588
  const d = db || getDatabase();
60346
60589
  const cutoffDate = new Date(Date.now() - days * 86400000).toISOString();
60347
- const conds = ["status = 'active'", "(accessed_at IS NULL OR accessed_at < ?)", "pinned = 0"];
60590
+ const conds = ["status = 'active'", "(accessed_at IS NULL OR accessed_at < ?)"];
60348
60591
  const params = [cutoffDate];
60592
+ if (filter.pinned !== undefined) {
60593
+ conds.push("pinned = ?");
60594
+ params.push(filter.pinned ? 1 : 0);
60595
+ } else {
60596
+ conds.push("pinned = 0");
60597
+ }
60349
60598
  if (filter.project_id) {
60350
60599
  conds.push("project_id = ?");
60351
60600
  params.push(filter.project_id);
@@ -60543,7 +60792,7 @@ function hasFts5Table2(db) {
60543
60792
  function buildScopeFilter(opts) {
60544
60793
  const conditions = [
60545
60794
  "m.status = 'active'",
60546
- "(m.expires_at IS NULL OR m.expires_at >= datetime('now'))",
60795
+ "(m.expires_at IS NULL OR m.expires_at >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))",
60547
60796
  "m.category IN ('preference', 'fact', 'knowledge')"
60548
60797
  ];
60549
60798
  const params = [];
@@ -60811,7 +61060,7 @@ async function runTemporalAgent(db, query, opts) {
60811
61060
  const maxResults = opts.max_results ?? 20;
60812
61061
  const limit = maxResults * 3;
60813
61062
  const conditions = [
60814
- "(m.expires_at IS NULL OR m.expires_at >= datetime('now'))"
61063
+ "(m.expires_at IS NULL OR m.expires_at >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
60815
61064
  ];
60816
61065
  const params = [];
60817
61066
  if (opts.project_id) {
@@ -61529,6 +61778,7 @@ function registerMemoryLifecycleTools(server) {
61529
61778
  });
61530
61779
  server.tool("memory_stale", "Find memories not accessed recently. Useful for cleanup or review.", {
61531
61780
  days: exports_external.coerce.number().optional(),
61781
+ pinned: exports_external.boolean().optional().describe("Review stale pinned memories (default excludes pinned)"),
61532
61782
  project_id: exports_external.string().optional(),
61533
61783
  agent_id: exports_external.string().optional(),
61534
61784
  limit: exports_external.coerce.number().optional()
@@ -61537,6 +61787,7 @@ function registerMemoryLifecycleTools(server) {
61537
61787
  const days = args.days || 30;
61538
61788
  const rows = getStaleMemories({
61539
61789
  days,
61790
+ pinned: args.pinned,
61540
61791
  project_id: args.project_id,
61541
61792
  agent_id: args.agent_id,
61542
61793
  limit: args.limit || 20
@@ -63213,6 +63464,7 @@ function syncMemoriesTable(source, target, local, direction, currentMachineId) {
63213
63464
  const countResult = source.get(`SELECT COUNT(*) as cnt FROM "${MEMORY_TABLE}"`);
63214
63465
  stat.total_rows = countResult?.cnt ?? 0;
63215
63466
  const rows = since ? source.all(`SELECT * FROM "${MEMORY_TABLE}" WHERE updated_at > ?`, since) : source.all(`SELECT * FROM "${MEMORY_TABLE}"`);
63467
+ const erroredRowIds = new Set;
63216
63468
  for (const row of rows) {
63217
63469
  try {
63218
63470
  const sourceMachine = sourceMachineRef(row, currentMachineId);
@@ -63271,18 +63523,34 @@ function syncMemoriesTable(source, target, local, direction, currentMachineId) {
63271
63523
  clearMemoryEmbedding(winnerDb, String(winner["id"]));
63272
63524
  stat.synced_rows++;
63273
63525
  } catch (error) {
63526
+ erroredRowIds.add(String(row["id"] ?? "unknown"));
63274
63527
  stat.errors.push(`Memory ${String(row["id"] ?? "unknown")}: ${error instanceof Error ? error.message : String(error)}`);
63275
63528
  }
63276
63529
  }
63277
63530
  if (rows.length === 0) {
63278
63531
  stat.skipped_rows = stat.total_rows;
63279
63532
  }
63280
- upsertMemorySyncMeta(local, {
63281
- table_name: MEMORY_TABLE,
63282
- direction,
63283
- last_synced_at: new Date().toISOString(),
63284
- last_synced_row_count: stat.synced_rows
63285
- });
63533
+ if (rows.length > 0 && stat.errors.length === 0) {
63534
+ let maxSyncedAt = null;
63535
+ for (const row of rows) {
63536
+ if (erroredRowIds.has(String(row["id"] ?? "unknown"))) {
63537
+ continue;
63538
+ }
63539
+ const value = row["updated_at"];
63540
+ if (typeof value === "string" && (maxSyncedAt === null || value > maxSyncedAt)) {
63541
+ maxSyncedAt = value;
63542
+ }
63543
+ }
63544
+ const nextCursor = maxSyncedAt ?? syncMeta?.last_synced_at ?? null;
63545
+ if (nextCursor !== null) {
63546
+ upsertMemorySyncMeta(local, {
63547
+ table_name: MEMORY_TABLE,
63548
+ direction,
63549
+ last_synced_at: nextCursor,
63550
+ last_synced_row_count: stat.synced_rows
63551
+ });
63552
+ }
63553
+ }
63286
63554
  } catch (error) {
63287
63555
  stat.errors.push(error instanceof Error ? error.message : String(error));
63288
63556
  }
@@ -63683,7 +63951,7 @@ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive",
63683
63951
  }
63684
63952
  const d = db || getDatabase();
63685
63953
  cleanExpiredLocks(d);
63686
- const ownLock = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND agent_id = ? AND lock_type = ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId, lockType);
63954
+ const ownLock = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND agent_id = ? AND lock_type = ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')").get(resourceType, resourceId, agentId, lockType);
63687
63955
  if (ownLock) {
63688
63956
  const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
63689
63957
  d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
@@ -63693,7 +63961,7 @@ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive",
63693
63961
  return parseLockRow({ ...ownLock, expires_at: newExpiry });
63694
63962
  }
63695
63963
  if (lockType === "exclusive") {
63696
- const existing = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = 'exclusive' AND agent_id != ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId);
63964
+ const existing = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = 'exclusive' AND agent_id != ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')").get(resourceType, resourceId, agentId);
63697
63965
  if (existing) {
63698
63966
  return null;
63699
63967
  }
@@ -63729,7 +63997,7 @@ function checkLock(resourceType, resourceId, lockType, db) {
63729
63997
  }
63730
63998
  const d = db || getDatabase();
63731
63999
  cleanExpiredLocks(d);
63732
- const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
64000
+ const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
63733
64001
  const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
63734
64002
  return rows.map(parseLockRow);
63735
64003
  }
@@ -63740,14 +64008,14 @@ function listAgentLocks(agentId, db) {
63740
64008
  }
63741
64009
  const d = db || getDatabase();
63742
64010
  cleanExpiredLocks(d);
63743
- const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
64011
+ const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now') ORDER BY locked_at DESC").all(agentId);
63744
64012
  return rows.map(parseLockRow);
63745
64013
  }
63746
64014
  function cleanExpiredLocksWithInfo(db) {
63747
64015
  const d = db || getDatabase();
63748
- const expired = d.query("SELECT id, resource_type, resource_id, agent_id, lock_type FROM resource_locks WHERE expires_at <= datetime('now')").all();
64016
+ const expired = d.query("SELECT id, resource_type, resource_id, agent_id, lock_type FROM resource_locks WHERE expires_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')").all();
63749
64017
  if (expired.length > 0) {
63750
- d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
64018
+ d.run("DELETE FROM resource_locks WHERE expires_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')");
63751
64019
  }
63752
64020
  return expired;
63753
64021
  }
@@ -63757,7 +64025,7 @@ function cleanExpiredLocks(db) {
63757
64025
  return data?.cleaned ?? 0;
63758
64026
  }
63759
64027
  const d = db || getDatabase();
63760
- const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
64028
+ const result = d.run("DELETE FROM resource_locks WHERE expires_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')");
63761
64029
  return result.changes;
63762
64030
  }
63763
64031
 
@@ -64096,7 +64364,7 @@ ${lines.join(`
64096
64364
  }, async (args) => {
64097
64365
  try {
64098
64366
  const { reloadWebhooks: reloadWebhooks2 } = await Promise.resolve().then(() => (init_built_in_hooks(), exports_built_in_hooks));
64099
- const wh = createWebhookHook({
64367
+ const wh = await createWebhookHook({
64100
64368
  type: args.type,
64101
64369
  handlerUrl: args.handler_url,
64102
64370
  priority: args.priority,
@@ -64105,7 +64373,7 @@ ${lines.join(`
64105
64373
  projectId: args.project_id,
64106
64374
  description: args.description
64107
64375
  });
64108
- reloadWebhooks2();
64376
+ await reloadWebhooks2();
64109
64377
  return { content: [{ type: "text", text: JSON.stringify(wh, null, 2) }] };
64110
64378
  } catch (e) {
64111
64379
  return { content: [{ type: "text", text: formatError6(e) }], isError: true };
@@ -65547,6 +65815,12 @@ function getNextPendingJob(db) {
65547
65815
  return null;
65548
65816
  return parseJobRow(row);
65549
65817
  }
65818
+ function claimSessionJob(id, db) {
65819
+ const d = db || getDatabase();
65820
+ const startedAt = now();
65821
+ const result = d.run("UPDATE session_memory_jobs SET status = 'processing', started_at = ? WHERE id = ? AND status = 'pending'", [startedAt, id]);
65822
+ return result.changes;
65823
+ }
65550
65824
 
65551
65825
  // src/lib/session-queue.ts
65552
65826
  init_database();
@@ -65890,7 +66164,11 @@ async function processSessionJob(jobId, db) {
65890
66164
  return result;
65891
66165
  }
65892
66166
  try {
65893
- updateSessionJob(jobId, { status: "processing", started_at: new Date().toISOString() }, db);
66167
+ const changes = claimSessionJob(jobId, db);
66168
+ if (changes === 0) {
66169
+ result.errors.push(`Job already claimed or not pending: ${jobId}`);
66170
+ return result;
66171
+ }
65894
66172
  } catch (e) {
65895
66173
  result.errors.push(`Failed to mark job as processing: ${String(e)}`);
65896
66174
  return result;
@@ -68348,7 +68626,7 @@ async function prepareMcpRuntime() {
68348
68626
  }
68349
68627
  } catch {}
68350
68628
  await ensureRestServerRunning();
68351
- loadWebhooksFromDb();
68629
+ await loadWebhooksFromDb();
68352
68630
  }
68353
68631
  async function main() {
68354
68632
  if (hasFlag("--help", "-h")) {