@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
@@ -243,6 +243,7 @@ function translateSql(sql) {
243
243
  let translated = sql.replace(/\?/g, () => `$${++parameterIndex}`);
244
244
  const ISO_FMT = `'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'`;
245
245
  translated = translated.replace(/datetime\s*\(\s*'now'\s*\)/gi, `to_char(now() AT TIME ZONE 'UTC', ${ISO_FMT})`);
246
+ 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})`);
246
247
  translated = translated.replace(/datetime\s*\(\s*'now'\s*,\s*'(-?\d+)\s+(minutes?|hours?|days?|seconds?)'\s*\)/gi, (_match, amount, unit) => {
247
248
  const parsed = parseInt(String(amount), 10);
248
249
  const absolute = Math.abs(parsed);
@@ -524,8 +525,13 @@ var init_storage = __esm(() => {
524
525
  data;
525
526
  closed = false;
526
527
  lastError = null;
528
+ generation = 0;
527
529
  static DATA_BYTES = 128 * 1024 * 1024;
528
- static QUERY_TIMEOUT_MS = 60000;
530
+ static queryTimeoutMs() {
531
+ const raw = process.env["MEMENTOS_PGSYNC_QUERY_TIMEOUT_MS"]?.trim();
532
+ const parsed = raw ? Number(raw) : Number.NaN;
533
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 60000;
534
+ }
529
535
  static resolveWorkerPath() {
530
536
  const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
531
537
  const here = fileURLToPath(new URL(".", import.meta.url));
@@ -540,12 +546,12 @@ var init_storage = __esm(() => {
540
546
  }
541
547
  return candidates[0];
542
548
  }
543
- constructor(connectionString) {
544
- const control = new SharedArrayBuffer(8);
549
+ constructor(connectionString, workerPath) {
550
+ const control = new SharedArrayBuffer(12);
545
551
  const dataSab = new SharedArrayBuffer(PgSyncPool.DATA_BYTES);
546
552
  this.status = new Int32Array(control);
547
553
  this.data = new Uint8Array(dataSab);
548
- this.worker = new Worker(PgSyncPool.resolveWorkerPath(), {
554
+ this.worker = new Worker(workerPath ?? PgSyncPool.resolveWorkerPath(), {
549
555
  workerData: {
550
556
  dsn: stripSslParams(connectionString),
551
557
  ssl: sslConfigFor(connectionString),
@@ -563,21 +569,35 @@ var init_storage = __esm(() => {
563
569
  throw new Error("PgSyncPool is closed");
564
570
  if (this.lastError)
565
571
  throw this.lastError;
572
+ const timeoutMs = PgSyncPool.queryTimeoutMs();
573
+ const gen = ++this.generation;
566
574
  Atomics.store(this.status, 0, 0);
567
- this.worker.postMessage({ sql, params });
568
- const waitResult = Atomics.wait(this.status, 0, 0, PgSyncPool.QUERY_TIMEOUT_MS);
569
- const code = Atomics.load(this.status, 0);
570
- if (code === 0 || waitResult === "timed-out") {
571
- if (this.lastError)
572
- throw this.lastError;
573
- throw new Error("PostgreSQL query timed out after 60s");
574
- }
575
- const len = Atomics.load(this.status, 1);
576
- const payload = JSON.parse(new TextDecoder().decode(this.data.subarray(0, len)));
577
- if (code === 2) {
578
- throw new Error(payload.message ?? "PostgreSQL error");
575
+ Atomics.store(this.status, 2, 0);
576
+ this.worker.postMessage({ sql, params, gen });
577
+ const deadline = Date.now() + timeoutMs;
578
+ for (;; ) {
579
+ const remaining = deadline - Date.now();
580
+ const responding = Atomics.load(this.status, 0);
581
+ if (responding === gen) {
582
+ const code = Atomics.load(this.status, 2);
583
+ const len = Atomics.load(this.status, 1);
584
+ const payload = JSON.parse(new TextDecoder().decode(this.data.subarray(0, len)));
585
+ if (code === 2) {
586
+ throw new Error(payload.message ?? "PostgreSQL error");
587
+ }
588
+ return payload;
589
+ }
590
+ if (responding !== 0) {
591
+ Atomics.compareExchange(this.status, 0, responding, 0);
592
+ continue;
593
+ }
594
+ if (remaining <= 0) {
595
+ if (this.lastError)
596
+ throw this.lastError;
597
+ throw new Error(`PostgreSQL query timed out after ${timeoutMs}ms`);
598
+ }
599
+ Atomics.wait(this.status, 0, 0, remaining);
579
600
  }
580
- return payload;
581
601
  }
582
602
  end() {
583
603
  if (this.closed)
@@ -2704,6 +2724,7 @@ __export(exports_memories, {
2704
2724
  updateMemory: () => updateMemory,
2705
2725
  touchMemory: () => touchMemory,
2706
2726
  semanticSearch: () => semanticSearch,
2727
+ reservedAgentIdViolation: () => reservedAgentIdViolation,
2707
2728
  parseMemoryRow: () => parseMemoryRow,
2708
2729
  listMemoryHistoryPage: () => listMemoryHistoryPage,
2709
2730
  listMemoryHistory: () => listMemoryHistory,
@@ -2775,7 +2796,20 @@ function parseMemoryRow(row) {
2775
2796
  accessed_at: row["accessed_at"] || null
2776
2797
  };
2777
2798
  }
2799
+ function reservedAgentIdViolation(agentId) {
2800
+ if (!agentId)
2801
+ return null;
2802
+ const normalized = agentId.trim().toLowerCase();
2803
+ if (RESERVED_AGENT_IDS.has(normalized)) {
2804
+ 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.`;
2805
+ }
2806
+ return null;
2807
+ }
2778
2808
  function createMemory(input, dedupeMode = "merge", db) {
2809
+ const reservedViolation = reservedAgentIdViolation(input.agent_id);
2810
+ if (reservedViolation) {
2811
+ throw new Error(reservedViolation);
2812
+ }
2779
2813
  if (!db && isApiMode()) {
2780
2814
  const { status, data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
2781
2815
  if (!data || !data.id) {
@@ -2825,7 +2859,8 @@ function createMemory(input, dedupeMode = "merge", db) {
2825
2859
  importance = ?, metadata = ?, expires_at = ?,
2826
2860
  when_to_use = ?,
2827
2861
  pinned = COALESCE(pinned, 0),
2828
- version = version + 1, updated_at = ?
2862
+ version = version + 1, updated_at = ?,
2863
+ updated_by_agent = ?
2829
2864
  WHERE id = ?`, [
2830
2865
  safeValue,
2831
2866
  input.category || "knowledge",
@@ -2836,6 +2871,7 @@ function createMemory(input, dedupeMode = "merge", db) {
2836
2871
  expiresAt,
2837
2872
  input.when_to_use || null,
2838
2873
  timestamp,
2874
+ input.agent_id || null,
2839
2875
  existing.id
2840
2876
  ]);
2841
2877
  d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
@@ -2940,6 +2976,12 @@ function bulkUpsertMemories(memories, db) {
2940
2976
  errors.push(`Rejected "${key}": ${formatEnumViolation(violation)}`);
2941
2977
  continue;
2942
2978
  }
2979
+ const agentViolation = reservedAgentIdViolation(typeof mem["agent_id"] === "string" ? mem["agent_id"] : undefined);
2980
+ if (agentViolation) {
2981
+ rejected++;
2982
+ errors.push(`Rejected "${key}": ${agentViolation}`);
2983
+ continue;
2984
+ }
2943
2985
  const timestamp = now();
2944
2986
  let tags = [];
2945
2987
  const rawTags = mem["tags"];
@@ -3218,6 +3260,11 @@ function listMemoriesPage(filter, db) {
3218
3260
  agent_id: f.agent_id,
3219
3261
  project_id: f.project_id,
3220
3262
  session_id: f.session_id,
3263
+ machine_id: f.machine_id,
3264
+ visible_to_machine_id: f.visible_to_machine_id,
3265
+ search: f.search,
3266
+ source: f.source,
3267
+ flag: f.flag,
3221
3268
  namespace: f.namespace,
3222
3269
  as_of: f.as_of,
3223
3270
  limit: f.limit,
@@ -3497,20 +3544,29 @@ function updateMemory(id, input, db) {
3497
3544
  sets.push("when_to_use = ?");
3498
3545
  params.push(input.when_to_use ?? null);
3499
3546
  }
3547
+ if (input.updated_by_agent !== undefined) {
3548
+ sets.push("updated_by_agent = ?");
3549
+ params.push(input.updated_by_agent ?? null);
3550
+ }
3500
3551
  if (input.tags !== undefined) {
3501
3552
  sets.push("tags = ?");
3502
3553
  params.push(JSON.stringify(input.tags));
3503
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memoryId]);
3504
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
3505
- for (const tag of input.tags) {
3506
- insertTag.run(memoryId, tag);
3507
- }
3508
3554
  }
3509
3555
  params.push(memoryId);
3510
- const result = d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
3511
- if (result.changes === 0) {
3512
- 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.`);
3513
- }
3556
+ d.transaction(() => {
3557
+ const res = d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
3558
+ if (res.changes === 0) {
3559
+ 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.`);
3560
+ }
3561
+ if (input.tags !== undefined) {
3562
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memoryId]);
3563
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
3564
+ for (const tag of input.tags) {
3565
+ insertTag.run(memoryId, tag);
3566
+ }
3567
+ }
3568
+ return res;
3569
+ });
3514
3570
  const updated = getMemory(memoryId, d);
3515
3571
  if (input.value !== undefined) {
3516
3572
  try {
@@ -3681,7 +3737,7 @@ async function semanticSearch(queryText, options = {}, db) {
3681
3737
  scored.sort((a, b) => b.score - a.score);
3682
3738
  return scored.slice(0, limit);
3683
3739
  }
3684
- var RECALL_PROMOTE_THRESHOLD = 3;
3740
+ var RESERVED_AGENT_IDS, RECALL_PROMOTE_THRESHOLD = 3;
3685
3741
  var init_memories = __esm(() => {
3686
3742
  init_types();
3687
3743
  init_database();
@@ -3691,6 +3747,12 @@ var init_memories = __esm(() => {
3691
3747
  init_poisoning();
3692
3748
  init_entity_memories();
3693
3749
  init_api_mode();
3750
+ RESERVED_AGENT_IDS = new Set([
3751
+ "agent-a",
3752
+ "agent-x",
3753
+ "agent-z",
3754
+ "nonexistent-agent"
3755
+ ]);
3694
3756
  });
3695
3757
 
3696
3758
  // src/db/entities.ts
@@ -4273,7 +4335,7 @@ function buildFilterConditions(filter) {
4273
4335
  const conditions = [];
4274
4336
  const params = [];
4275
4337
  conditions.push("m.status = 'active'");
4276
- conditions.push("(m.expires_at IS NULL OR m.expires_at >= datetime('now'))");
4338
+ conditions.push("(m.expires_at IS NULL OR m.expires_at >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))");
4277
4339
  if (!filter)
4278
4340
  return { conditions, params };
4279
4341
  if (filter.scope) {
@@ -6309,7 +6371,7 @@ async function detectContradiction(newKey, newValue, options = {}, db) {
6309
6371
  conditions.push("project_id = ?");
6310
6372
  params.push(project_id);
6311
6373
  }
6312
- conditions.push("(valid_until IS NULL OR valid_until > datetime('now'))");
6374
+ conditions.push("(valid_until IS NULL OR valid_until > strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))");
6313
6375
  const sql = `SELECT * FROM memories WHERE ${conditions.join(" AND ")} ORDER BY importance DESC LIMIT 10`;
6314
6376
  const rows = d.query(sql).all(...params);
6315
6377
  if (rows.length === 0) {
@@ -19986,7 +20048,7 @@ class JSONSchemaGenerator {
19986
20048
  if (val === undefined) {
19987
20049
  if (this.unrepresentable === "throw") {
19988
20050
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
19989
- }
20051
+ } else {}
19990
20052
  } else if (typeof val === "bigint") {
19991
20053
  if (this.unrepresentable === "throw") {
19992
20054
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -41636,7 +41698,7 @@ var require_tracestate_impl = __commonJS((exports) => {
41636
41698
  const value = listMember.slice(i + 1, part.length);
41637
41699
  if ((0, tracestate_validators_1.validateKey)(key) && (0, tracestate_validators_1.validateValue)(value)) {
41638
41700
  agg.set(key, value);
41639
- }
41701
+ } else {}
41640
41702
  }
41641
41703
  return agg;
41642
41704
  }, new Map);
@@ -55465,6 +55527,8 @@ init_hooks();
55465
55527
  // src/db/webhook_hooks.ts
55466
55528
  init_database();
55467
55529
  init_api_mode();
55530
+ import { isIP } from "net";
55531
+ import { lookup as dnsLookup } from "dns/promises";
55468
55532
  function parseRow(row) {
55469
55533
  return {
55470
55534
  id: row["id"],
@@ -55481,7 +55545,154 @@ function parseRow(row) {
55481
55545
  failureCount: row["failure_count"]
55482
55546
  };
55483
55547
  }
55484
- function createWebhookHook(input, db) {
55548
+ function isBlockedIpv4(parts) {
55549
+ const a = parts[0];
55550
+ const b = parts[1];
55551
+ if (a === 0)
55552
+ return true;
55553
+ if (a === 127)
55554
+ return true;
55555
+ if (a === 169 && b === 254)
55556
+ return true;
55557
+ if (a === 10)
55558
+ return true;
55559
+ if (a === 172 && b >= 16 && b <= 31)
55560
+ return true;
55561
+ if (a === 192 && b === 168)
55562
+ return true;
55563
+ return false;
55564
+ }
55565
+ function isBlockedIpv6(bytes) {
55566
+ const mapped = bytes.slice(0, 10).every((b) => b === 0) && bytes[10] === 255 && bytes[11] === 255;
55567
+ if (mapped)
55568
+ return isBlockedIpv4(bytes.slice(12, 16));
55569
+ if (bytes.every((b) => b === 0))
55570
+ return true;
55571
+ if (bytes.slice(0, 15).every((b) => b === 0) && bytes[15] === 1)
55572
+ return true;
55573
+ if ((bytes[0] & 254) === 252)
55574
+ return true;
55575
+ if (bytes[0] === 254 && (bytes[1] & 192) === 128)
55576
+ return true;
55577
+ return false;
55578
+ }
55579
+ function quadToGroups(quad) {
55580
+ const nums = quad.split(".").map((p) => Number(p));
55581
+ const [a, b, c, d] = nums;
55582
+ const valid = [a, b, c, d].every((n) => n !== undefined && Number.isInteger(n) && n >= 0 && n <= 255);
55583
+ if (!valid)
55584
+ return null;
55585
+ return [a << 8 | b, c << 8 | d];
55586
+ }
55587
+ function parseIpv6Bytes(host) {
55588
+ const groups = host.split("::");
55589
+ if (groups.length > 2)
55590
+ return null;
55591
+ const headRaw = groups[0] ?? "";
55592
+ const tailRaw = groups[1] ?? "";
55593
+ const head = headRaw === "" ? [] : headRaw.split(":");
55594
+ const tail = tailRaw === "" ? [] : tailRaw.split(":");
55595
+ const headNums = [];
55596
+ for (const g of head) {
55597
+ if (!/^[0-9a-f]{1,4}$/i.test(g))
55598
+ return null;
55599
+ headNums.push(parseInt(g, 16));
55600
+ }
55601
+ const tailNums = [];
55602
+ for (const g of tail) {
55603
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(g)) {
55604
+ const quads = quadToGroups(g);
55605
+ if (!quads)
55606
+ return null;
55607
+ tailNums.push(...quads);
55608
+ } else if (/^[0-9a-f]{1,4}$/i.test(g)) {
55609
+ tailNums.push(parseInt(g, 16));
55610
+ } else {
55611
+ return null;
55612
+ }
55613
+ }
55614
+ const hasCompression = groups.length === 2;
55615
+ if (!hasCompression && headNums.length !== 8)
55616
+ return null;
55617
+ if (hasCompression && headNums.length + tailNums.length >= 8)
55618
+ return null;
55619
+ const zeros = 8 - headNums.length - tailNums.length;
55620
+ const all = [...headNums, ...new Array(zeros).fill(0), ...tailNums];
55621
+ const bytes = [];
55622
+ for (const n of all) {
55623
+ bytes.push(n >> 8 & 255, n & 255);
55624
+ }
55625
+ return bytes;
55626
+ }
55627
+ var BLOCKED_TARGET_MESSAGE = "Invalid webhook handler URL \u2014 loopback, link-local, and private network targets are not allowed";
55628
+ function defaultResolveHost(hostname2) {
55629
+ return dnsLookup(hostname2, { all: true, verbatim: true });
55630
+ }
55631
+ function assertResolvedAddressPublic(address, url) {
55632
+ const version = isIP(address);
55633
+ if (version === 4) {
55634
+ if (isBlockedIpv4(address.split(".").map((p) => Number(p)))) {
55635
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
55636
+ }
55637
+ } else if (version === 6) {
55638
+ const bytes = parseIpv6Bytes(address);
55639
+ if (!bytes || isBlockedIpv6(bytes)) {
55640
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
55641
+ }
55642
+ } else {
55643
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
55644
+ }
55645
+ }
55646
+ async function validateWebhookHandlerUrl(url, opts) {
55647
+ const resolveHost = opts?.lookup ?? defaultResolveHost;
55648
+ let parsed;
55649
+ try {
55650
+ parsed = new URL(url);
55651
+ } catch {
55652
+ throw new Error(`Invalid webhook handler URL "${url}" \u2014 must be a valid http(s) URL`);
55653
+ }
55654
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
55655
+ throw new Error(`Invalid webhook handler URL "${url}" \u2014 only http and https are allowed`);
55656
+ }
55657
+ if (parsed.username || parsed.password) {
55658
+ throw new Error(`Invalid webhook handler URL "${url}" \u2014 embedded credentials are not allowed`);
55659
+ }
55660
+ const host = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
55661
+ if (host === "localhost" || host.endsWith(".localhost")) {
55662
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
55663
+ }
55664
+ const version = isIP(host);
55665
+ if (version === 4 || version === 6) {
55666
+ if (version === 4) {
55667
+ if (isBlockedIpv4(host.split(".").map((p) => Number(p)))) {
55668
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
55669
+ }
55670
+ } else {
55671
+ const bytes = parseIpv6Bytes(host);
55672
+ if (!bytes || isBlockedIpv6(bytes)) {
55673
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
55674
+ }
55675
+ }
55676
+ return;
55677
+ }
55678
+ if (/^[0-9]+(\.[0-9]+)*$/.test(host) || /^0x[0-9a-f]+$/i.test(host) || host.includes("%")) {
55679
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
55680
+ }
55681
+ let addrs;
55682
+ try {
55683
+ addrs = await resolveHost(host);
55684
+ } catch {
55685
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
55686
+ }
55687
+ if (addrs.length === 0) {
55688
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
55689
+ }
55690
+ for (const { address } of addrs) {
55691
+ assertResolvedAddressPublic(address, url);
55692
+ }
55693
+ }
55694
+ async function createWebhookHook(input, db, opts) {
55695
+ await validateWebhookHandlerUrl(input.handlerUrl, opts);
55485
55696
  if (!db && isApiMode()) {
55486
55697
  const { data } = apiJson("POST", "/webhooks", {
55487
55698
  type: input.type,
@@ -55765,13 +55976,19 @@ hookRegistry.register({
55765
55976
  }
55766
55977
  });
55767
55978
  var _webhooksLoaded = false;
55768
- function loadWebhooksFromDb() {
55979
+ async function loadWebhooksFromDb() {
55769
55980
  if (_webhooksLoaded)
55770
55981
  return;
55771
55982
  _webhooksLoaded = true;
55772
55983
  try {
55773
55984
  const webhooks = listWebhookHooks({ enabled: true });
55774
55985
  for (const wh of webhooks) {
55986
+ try {
55987
+ await validateWebhookHandlerUrl(wh.handlerUrl);
55988
+ } catch (err) {
55989
+ console.error(`[hooks] Skipping webhook ${wh.id} (${wh.type}): ${err instanceof Error ? err.message : String(err)}`);
55990
+ continue;
55991
+ }
55775
55992
  hookRegistry.register({
55776
55993
  type: wh.type,
55777
55994
  blocking: wh.blocking,
@@ -55789,9 +56006,10 @@ function loadWebhooksFromDb() {
55789
56006
  console.error("[hooks] Failed to load webhooks from DB:", err);
55790
56007
  }
55791
56008
  }
55792
- function makeWebhookHandler(webhookId, url) {
56009
+ function makeWebhookHandler(webhookId, url, opts) {
55793
56010
  return async (context) => {
55794
56011
  try {
56012
+ await validateWebhookHandlerUrl(url, opts);
55795
56013
  const res = await fetch(url, {
55796
56014
  method: "POST",
55797
56015
  headers: { "Content-Type": "application/json" },
@@ -55804,9 +56022,9 @@ function makeWebhookHandler(webhookId, url) {
55804
56022
  }
55805
56023
  };
55806
56024
  }
55807
- function reloadWebhooks() {
56025
+ async function reloadWebhooks() {
55808
56026
  _webhooksLoaded = false;
55809
- loadWebhooksFromDb();
56027
+ await loadWebhooksFromDb();
55810
56028
  }
55811
56029
 
55812
56030
  // src/lib/session-queue.ts
@@ -55943,6 +56161,18 @@ function getNextPendingJob(db) {
55943
56161
  return null;
55944
56162
  return parseJobRow(row);
55945
56163
  }
56164
+ function claimSessionJob(id, db) {
56165
+ const d = db || getDatabase();
56166
+ const startedAt = now();
56167
+ const result = d.run("UPDATE session_memory_jobs SET status = 'processing', started_at = ? WHERE id = ? AND status = 'pending'", [startedAt, id]);
56168
+ return result.changes;
56169
+ }
56170
+ function recoverStaleProcessingJobs(maxAgeMs, db) {
56171
+ const d = db || getDatabase();
56172
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
56173
+ const result = d.run("UPDATE session_memory_jobs SET status = 'pending', started_at = NULL WHERE status = 'processing' AND started_at < ?", [cutoff]);
56174
+ return result.changes;
56175
+ }
55946
56176
 
55947
56177
  // src/lib/session-processor.ts
55948
56178
  init_memories();
@@ -56445,7 +56675,11 @@ async function processSessionJob(jobId, db) {
56445
56675
  return result;
56446
56676
  }
56447
56677
  try {
56448
- updateSessionJob(jobId, { status: "processing", started_at: new Date().toISOString() }, db);
56678
+ const changes = claimSessionJob(jobId, db);
56679
+ if (changes === 0) {
56680
+ result.errors.push(`Job already claimed or not pending: ${jobId}`);
56681
+ return result;
56682
+ }
56449
56683
  } catch (e) {
56450
56684
  result.errors.push(`Failed to mark job as processing: ${String(e)}`);
56451
56685
  return result;
@@ -56553,6 +56787,9 @@ function startSessionQueueWorker() {
56553
56787
  return;
56554
56788
  _workerStarted = true;
56555
56789
  setInterval(() => {
56790
+ try {
56791
+ recoverStaleProcessingJobs(30 * 60 * 1000);
56792
+ } catch {}
56556
56793
  _processNext();
56557
56794
  }, 5000);
56558
56795
  }
@@ -56824,7 +57061,7 @@ function getTaskStats(db, filter) {
56824
57061
  const priorityRows = db.query(`SELECT priority, COUNT(*) as c FROM tasks ${where} GROUP BY priority`).all(...params);
56825
57062
  for (const row of priorityRows)
56826
57063
  byPriority[row.priority] = row.c;
56827
- const overdue = db.query(`SELECT COUNT(*) as c FROM tasks ${where} AND status != 'completed' AND status != 'cancelled' AND due_at IS NOT NULL AND due_at < datetime('now')`).get(...params).c;
57064
+ const overdue = db.query(`SELECT COUNT(*) as c FROM tasks ${where} AND status != 'completed' AND status != 'cancelled' AND due_at IS NOT NULL AND due_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`).get(...params).c;
56828
57065
  return {
56829
57066
  total,
56830
57067
  by_status: {
@@ -56955,6 +57192,255 @@ async function _tick() {
56955
57192
 
56956
57193
  // src/server/index.ts
56957
57194
  init_storage();
57195
+
57196
+ // src/db/analytics.ts
57197
+ init_database();
57198
+ init_api_mode();
57199
+ function getMemoryStats(db) {
57200
+ if (!db && isApiMode()) {
57201
+ const { data } = apiJson("GET", "/memories/stats");
57202
+ return normalizeStats(data);
57203
+ }
57204
+ const d = db || getDatabase();
57205
+ const total = d.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active'").get().c;
57206
+ const byScope = d.query("SELECT scope, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY scope").all();
57207
+ const byCategory = d.query("SELECT category, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY category").all();
57208
+ const byStatus = d.query("SELECT status, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY status").all();
57209
+ const pinnedCount = d.query("SELECT COUNT(*) as c FROM memories WHERE pinned = 1 AND status = 'active'").get().c;
57210
+ const expiredCount = d.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired'").get().c;
57211
+ const expiresAtCount = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL").get().c;
57212
+ const nowIso = new Date().toISOString();
57213
+ 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;
57214
+ const stats = {
57215
+ total,
57216
+ by_scope: { global: 0, shared: 0, private: 0, working: 0 },
57217
+ by_category: { preference: 0, fact: 0, knowledge: 0, history: 0, procedural: 0, resource: 0 },
57218
+ by_status: { active: 0, archived: 0, expired: 0 },
57219
+ by_agent: {},
57220
+ pinned_count: pinnedCount,
57221
+ expired_count: expiredCount,
57222
+ expires_at_count: expiresAtCount,
57223
+ expired_due_count: expiredDueCount
57224
+ };
57225
+ for (const row of byScope)
57226
+ if (row.scope in stats.by_scope)
57227
+ stats.by_scope[row.scope] = row.c;
57228
+ for (const row of byCategory)
57229
+ if (row.category in stats.by_category)
57230
+ stats.by_category[row.category] = row.c;
57231
+ for (const row of byStatus) {
57232
+ if (row.status in stats.by_status) {
57233
+ stats.by_status[row.status] = row.c;
57234
+ }
57235
+ }
57236
+ const byAgent = d.query("SELECT agent_id, COUNT(*) as c FROM memories WHERE status = 'active' AND agent_id IS NOT NULL GROUP BY agent_id").all();
57237
+ for (const row of byAgent)
57238
+ stats.by_agent[row.agent_id] = row.c;
57239
+ return stats;
57240
+ }
57241
+ function normalizeStats(data) {
57242
+ return {
57243
+ total: data?.total ?? 0,
57244
+ by_scope: { global: 0, shared: 0, private: 0, working: 0, ...data?.by_scope ?? {} },
57245
+ by_category: {
57246
+ preference: 0,
57247
+ fact: 0,
57248
+ knowledge: 0,
57249
+ history: 0,
57250
+ procedural: 0,
57251
+ resource: 0,
57252
+ ...data?.by_category ?? {}
57253
+ },
57254
+ by_status: { active: 0, archived: 0, expired: 0, ...data?.by_status ?? {} },
57255
+ by_agent: data?.by_agent ?? {},
57256
+ pinned_count: data?.pinned_count ?? 0,
57257
+ expired_count: data?.expired_count ?? 0,
57258
+ expires_at_count: data?.expires_at_count ?? 0,
57259
+ expired_due_count: data?.expired_due_count ?? 0
57260
+ };
57261
+ }
57262
+ function getMemoryActivity(filter = {}, db) {
57263
+ const days = Math.min(filter.days || 30, 365);
57264
+ if (!db && isApiMode()) {
57265
+ const q = toQuery({ days, scope: filter.scope, agent_id: filter.agent_id, project_id: filter.project_id });
57266
+ const { data } = apiJson("GET", `/activity${q}`);
57267
+ return { activity: data?.activity ?? [], days: data?.days ?? days, total: data?.total ?? 0 };
57268
+ }
57269
+ const d = db || getDatabase();
57270
+ const conditions = ["status = 'active'"];
57271
+ const params = [];
57272
+ if (filter.scope) {
57273
+ conditions.push("scope = ?");
57274
+ params.push(filter.scope);
57275
+ }
57276
+ if (filter.agent_id) {
57277
+ conditions.push("agent_id = ?");
57278
+ params.push(filter.agent_id);
57279
+ }
57280
+ if (filter.project_id) {
57281
+ conditions.push("project_id = ?");
57282
+ params.push(filter.project_id);
57283
+ }
57284
+ const where = conditions.map((c) => `AND ${c}`).join(" ");
57285
+ const cutoffDate = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
57286
+ params.push(cutoffDate);
57287
+ const rows = d.query(`
57288
+ SELECT
57289
+ date(created_at) AS date,
57290
+ COUNT(*) AS memories_created,
57291
+ SUM(CASE WHEN scope = 'global' THEN 1 ELSE 0 END) AS global_count,
57292
+ SUM(CASE WHEN scope = 'shared' THEN 1 ELSE 0 END) AS shared_count,
57293
+ SUM(CASE WHEN scope = 'private' THEN 1 ELSE 0 END) AS private_count,
57294
+ AVG(importance) AS avg_importance
57295
+ FROM memories
57296
+ WHERE date(created_at) >= ? ${where}
57297
+ GROUP BY date(created_at)
57298
+ ORDER BY date ASC
57299
+ `).all(...params);
57300
+ return { activity: rows, days, total: rows.reduce((s, r) => s + r.memories_created, 0) };
57301
+ }
57302
+ function getMemoryReport(filter = {}, db) {
57303
+ const days = Math.min(filter.days || 7, 365);
57304
+ if (!db && isApiMode()) {
57305
+ const q = toQuery({ days, project_id: filter.project_id, agent_id: filter.agent_id });
57306
+ const { data } = apiJson("GET", `/report${q}`);
57307
+ return {
57308
+ total: data?.total ?? 0,
57309
+ pinned: data?.pinned ?? 0,
57310
+ days: data?.days ?? days,
57311
+ recent: data?.recent ?? { total: 0, activity: [] },
57312
+ by_scope: data?.by_scope ?? {},
57313
+ by_category: data?.by_category ?? {},
57314
+ top_memories: data?.top_memories ?? [],
57315
+ top_agents: data?.top_agents ?? []
57316
+ };
57317
+ }
57318
+ const d = db || getDatabase();
57319
+ const cutoffDate = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
57320
+ const scopedCond = [
57321
+ filter.project_id ? "AND project_id = ?" : "",
57322
+ filter.agent_id ? "AND agent_id = ?" : ""
57323
+ ].filter(Boolean).join(" ");
57324
+ const scopedParams = [
57325
+ ...filter.project_id ? [filter.project_id] : [],
57326
+ ...filter.agent_id ? [filter.agent_id] : []
57327
+ ];
57328
+ const recentParams = [cutoffDate, ...scopedParams];
57329
+ const total = d.query(`SELECT COUNT(*) as c FROM memories WHERE status = 'active' ${scopedCond}`).get(...scopedParams).c;
57330
+ const pinned = d.query(`SELECT COUNT(*) as c FROM memories WHERE status = 'active' AND pinned = 1 ${scopedCond}`).get(...scopedParams).c;
57331
+ const actRows = d.query(`
57332
+ SELECT date(created_at) AS date, COUNT(*) AS memories_created
57333
+ FROM memories WHERE status = 'active' AND date(created_at) >= ? ${scopedCond}
57334
+ GROUP BY date(created_at) ORDER BY date(created_at) ASC
57335
+ `).all(...recentParams);
57336
+ const recentTotal = actRows.reduce((s, r) => s + r.memories_created, 0);
57337
+ const byScopeRows = d.query(`SELECT scope, COUNT(*) as c FROM memories WHERE status = 'active' ${scopedCond} GROUP BY scope`).all(...scopedParams);
57338
+ const byCatRows = d.query(`SELECT category, COUNT(*) as c FROM memories WHERE status = 'active' ${scopedCond} GROUP BY category`).all(...scopedParams);
57339
+ const topMems = d.query(`SELECT id, key, value, importance, scope, category FROM memories WHERE status = 'active' ${scopedCond} ORDER BY importance DESC, access_count DESC LIMIT 5`).all(...scopedParams);
57340
+ const topAgents = d.query(`SELECT agent_id, COUNT(*) as c FROM memories WHERE status = 'active' AND agent_id IS NOT NULL ${scopedCond} GROUP BY agent_id ORDER BY c DESC LIMIT 5`).all(...scopedParams);
57341
+ return {
57342
+ total,
57343
+ pinned,
57344
+ days,
57345
+ recent: { total: recentTotal, activity: actRows },
57346
+ by_scope: Object.fromEntries(byScopeRows.map((r) => [r.scope, r.c])),
57347
+ by_category: Object.fromEntries(byCatRows.map((r) => [r.category, r.c])),
57348
+ top_memories: topMems,
57349
+ top_agents: topAgents
57350
+ };
57351
+ }
57352
+ function getStaleMemoriesPage(filter = {}, db) {
57353
+ const days = Math.min(filter.days || 30, 365);
57354
+ const limit = filter.limit ?? 20;
57355
+ const offset = filter.offset ?? 0;
57356
+ if (!db && isApiMode()) {
57357
+ const q = toQuery({ days, project_id: filter.project_id, agent_id: filter.agent_id, pinned: filter.pinned, limit, offset });
57358
+ const { data } = apiJson("GET", `/memories/stale${q}`);
57359
+ const rows2 = data?.memories ?? [];
57360
+ return {
57361
+ rows: rows2,
57362
+ total: data?.total ?? rows2.length,
57363
+ has_more: data?.has_more,
57364
+ next_cursor: data?.next_cursor ?? null
57365
+ };
57366
+ }
57367
+ const d = db || getDatabase();
57368
+ const cutoffDate = new Date(Date.now() - days * 86400000).toISOString();
57369
+ const conds = ["status = 'active'", "(accessed_at IS NULL OR accessed_at < ?)"];
57370
+ const params = [cutoffDate];
57371
+ if (filter.pinned !== undefined) {
57372
+ conds.push("pinned = ?");
57373
+ params.push(filter.pinned ? 1 : 0);
57374
+ } else {
57375
+ conds.push("pinned = 0");
57376
+ }
57377
+ if (filter.project_id) {
57378
+ conds.push("project_id = ?");
57379
+ params.push(filter.project_id);
57380
+ }
57381
+ if (filter.agent_id) {
57382
+ conds.push("agent_id = ?");
57383
+ params.push(filter.agent_id);
57384
+ }
57385
+ const where = conds.join(" AND ");
57386
+ const countRow = d.query(`SELECT COUNT(*) AS c FROM memories WHERE ${where}`).get(...params);
57387
+ let sql = `SELECT id, key, value, importance, scope, category, accessed_at, access_count, created_at FROM memories WHERE ${where} ORDER BY COALESCE(accessed_at, created_at) ASC LIMIT ?`;
57388
+ params.push(limit);
57389
+ if (offset) {
57390
+ sql += " OFFSET ?";
57391
+ params.push(offset);
57392
+ }
57393
+ const rows = d.query(sql).all(...params);
57394
+ const hasMore = rows.length === limit;
57395
+ return {
57396
+ rows,
57397
+ total: countRow?.c ?? 0,
57398
+ has_more: hasMore,
57399
+ next_cursor: hasMore ? offset + rows.length : null
57400
+ };
57401
+ }
57402
+ function getMemoryHealth(filter = {}, db) {
57403
+ const staleDays = filter.stale_days ?? 30;
57404
+ const forgottenDays = filter.forgotten_days ?? 60;
57405
+ const limit = filter.limit ?? 10;
57406
+ if (!db && isApiMode()) {
57407
+ const q = toQuery({
57408
+ stale_days: staleDays,
57409
+ forgotten_days: forgottenDays,
57410
+ project_id: filter.project_id,
57411
+ agent_id: filter.agent_id,
57412
+ limit
57413
+ });
57414
+ const { data } = apiJson("GET", `/memories/health${q}`);
57415
+ return { stale: data?.stale ?? [], forgotten: data?.forgotten ?? [], dupes: data?.dupes ?? [] };
57416
+ }
57417
+ const d = db || getDatabase();
57418
+ const extraWhere = [
57419
+ ...filter.project_id ? ["project_id = ?"] : [],
57420
+ ...filter.agent_id ? ["agent_id = ?"] : []
57421
+ ].join(" AND ");
57422
+ const scopeParams = [
57423
+ ...filter.project_id ? [filter.project_id] : [],
57424
+ ...filter.agent_id ? [filter.agent_id] : []
57425
+ ];
57426
+ const staleCutoff = new Date(Date.now() - staleDays * 86400000).toISOString();
57427
+ const forgottenCutoff = new Date(Date.now() - forgottenDays * 86400000).toISOString();
57428
+ const base = `status = 'active' AND pinned = 0${extraWhere ? " AND " + extraWhere : ""}`;
57429
+ const stale = d.prepare(`SELECT id, key, value, importance, scope, created_at FROM memories
57430
+ WHERE ${base} AND access_count = 0 AND created_at < ?
57431
+ ORDER BY created_at ASC LIMIT ?`).all(...scopeParams, staleCutoff, limit);
57432
+ const forgotten = d.prepare(`SELECT id, key, value, importance, scope, accessed_at FROM memories
57433
+ WHERE ${base} AND importance >= 7
57434
+ AND (accessed_at IS NULL OR accessed_at < ?)
57435
+ ORDER BY importance DESC, COALESCE(accessed_at, created_at) ASC LIMIT ?`).all(...scopeParams, forgottenCutoff, limit);
57436
+ const dupes = d.prepare(`SELECT key, COUNT(*) as cnt, MAX(updated_at) as latest, MIN(created_at) as oldest
57437
+ FROM memories WHERE ${base}
57438
+ GROUP BY key HAVING COUNT(*) > 1
57439
+ ORDER BY cnt DESC LIMIT ?`).all(...scopeParams, limit);
57440
+ return { stale, forgotten, dupes };
57441
+ }
57442
+
57443
+ // src/server/index.ts
56958
57444
  init_router();
56959
57445
 
56960
57446
  // src/server/helpers.ts
@@ -57226,6 +57712,16 @@ addRoute("GET", "/api/memories", (_req, url) => {
57226
57712
  filter.project_id = q["project_id"];
57227
57713
  if (q["session_id"])
57228
57714
  filter.session_id = q["session_id"];
57715
+ if (q["machine_id"])
57716
+ filter.machine_id = q["machine_id"];
57717
+ if (q["visible_to_machine_id"])
57718
+ filter.visible_to_machine_id = q["visible_to_machine_id"];
57719
+ if (q["search"])
57720
+ filter.search = q["search"];
57721
+ if (q["source"])
57722
+ filter.source = q["source"].includes(",") ? q["source"].split(",") : q["source"];
57723
+ if (q["flag"])
57724
+ filter.flag = q["flag"];
57229
57725
  if (q["namespace"])
57230
57726
  filter.namespace = q["namespace"];
57231
57727
  if (q["status"])
@@ -57356,242 +57852,6 @@ addRoute("DELETE", "/api/memories/:id", (_req, _url, params) => {
57356
57852
 
57357
57853
  // src/server/routes/memories-stats.ts
57358
57854
  init_database();
57359
-
57360
- // src/db/analytics.ts
57361
- init_database();
57362
- init_api_mode();
57363
- function getMemoryStats(db) {
57364
- if (!db && isApiMode()) {
57365
- const { data } = apiJson("GET", "/memories/stats");
57366
- return normalizeStats(data);
57367
- }
57368
- const d = db || getDatabase();
57369
- const total = d.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active'").get().c;
57370
- const byScope = d.query("SELECT scope, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY scope").all();
57371
- const byCategory = d.query("SELECT category, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY category").all();
57372
- const byStatus = d.query("SELECT status, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY status").all();
57373
- const pinnedCount = d.query("SELECT COUNT(*) as c FROM memories WHERE pinned = 1 AND status = 'active'").get().c;
57374
- 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;
57375
- const stats = {
57376
- total,
57377
- by_scope: { global: 0, shared: 0, private: 0, working: 0 },
57378
- by_category: { preference: 0, fact: 0, knowledge: 0, history: 0, procedural: 0, resource: 0 },
57379
- by_status: { active: 0, archived: 0, expired: 0 },
57380
- by_agent: {},
57381
- pinned_count: pinnedCount,
57382
- expired_count: expiredCount
57383
- };
57384
- for (const row of byScope)
57385
- if (row.scope in stats.by_scope)
57386
- stats.by_scope[row.scope] = row.c;
57387
- for (const row of byCategory)
57388
- if (row.category in stats.by_category)
57389
- stats.by_category[row.category] = row.c;
57390
- for (const row of byStatus) {
57391
- if (row.status in stats.by_status) {
57392
- stats.by_status[row.status] = row.c;
57393
- }
57394
- }
57395
- const byAgent = d.query("SELECT agent_id, COUNT(*) as c FROM memories WHERE status = 'active' AND agent_id IS NOT NULL GROUP BY agent_id").all();
57396
- for (const row of byAgent)
57397
- stats.by_agent[row.agent_id] = row.c;
57398
- return stats;
57399
- }
57400
- function normalizeStats(data) {
57401
- return {
57402
- total: data?.total ?? 0,
57403
- by_scope: { global: 0, shared: 0, private: 0, working: 0, ...data?.by_scope ?? {} },
57404
- by_category: {
57405
- preference: 0,
57406
- fact: 0,
57407
- knowledge: 0,
57408
- history: 0,
57409
- procedural: 0,
57410
- resource: 0,
57411
- ...data?.by_category ?? {}
57412
- },
57413
- by_status: { active: 0, archived: 0, expired: 0, ...data?.by_status ?? {} },
57414
- by_agent: data?.by_agent ?? {},
57415
- pinned_count: data?.pinned_count ?? 0,
57416
- expired_count: data?.expired_count ?? 0
57417
- };
57418
- }
57419
- function getMemoryActivity(filter = {}, db) {
57420
- const days = Math.min(filter.days || 30, 365);
57421
- if (!db && isApiMode()) {
57422
- const q = toQuery({ days, scope: filter.scope, agent_id: filter.agent_id, project_id: filter.project_id });
57423
- const { data } = apiJson("GET", `/activity${q}`);
57424
- return { activity: data?.activity ?? [], days: data?.days ?? days, total: data?.total ?? 0 };
57425
- }
57426
- const d = db || getDatabase();
57427
- const conditions = ["status = 'active'"];
57428
- const params = [];
57429
- if (filter.scope) {
57430
- conditions.push("scope = ?");
57431
- params.push(filter.scope);
57432
- }
57433
- if (filter.agent_id) {
57434
- conditions.push("agent_id = ?");
57435
- params.push(filter.agent_id);
57436
- }
57437
- if (filter.project_id) {
57438
- conditions.push("project_id = ?");
57439
- params.push(filter.project_id);
57440
- }
57441
- const where = conditions.map((c) => `AND ${c}`).join(" ");
57442
- const cutoffDate = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
57443
- params.push(cutoffDate);
57444
- const rows = d.query(`
57445
- SELECT
57446
- date(created_at) AS date,
57447
- COUNT(*) AS memories_created,
57448
- SUM(CASE WHEN scope = 'global' THEN 1 ELSE 0 END) AS global_count,
57449
- SUM(CASE WHEN scope = 'shared' THEN 1 ELSE 0 END) AS shared_count,
57450
- SUM(CASE WHEN scope = 'private' THEN 1 ELSE 0 END) AS private_count,
57451
- AVG(importance) AS avg_importance
57452
- FROM memories
57453
- WHERE date(created_at) >= ? ${where}
57454
- GROUP BY date(created_at)
57455
- ORDER BY date ASC
57456
- `).all(...params);
57457
- return { activity: rows, days, total: rows.reduce((s, r) => s + r.memories_created, 0) };
57458
- }
57459
- function getMemoryReport(filter = {}, db) {
57460
- const days = Math.min(filter.days || 7, 365);
57461
- if (!db && isApiMode()) {
57462
- const q = toQuery({ days, project_id: filter.project_id, agent_id: filter.agent_id });
57463
- const { data } = apiJson("GET", `/report${q}`);
57464
- return {
57465
- total: data?.total ?? 0,
57466
- pinned: data?.pinned ?? 0,
57467
- days: data?.days ?? days,
57468
- recent: data?.recent ?? { total: 0, activity: [] },
57469
- by_scope: data?.by_scope ?? {},
57470
- by_category: data?.by_category ?? {},
57471
- top_memories: data?.top_memories ?? [],
57472
- top_agents: data?.top_agents ?? []
57473
- };
57474
- }
57475
- const d = db || getDatabase();
57476
- const cutoffDate = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
57477
- const scopedCond = [
57478
- filter.project_id ? "AND project_id = ?" : "",
57479
- filter.agent_id ? "AND agent_id = ?" : ""
57480
- ].filter(Boolean).join(" ");
57481
- const scopedParams = [
57482
- ...filter.project_id ? [filter.project_id] : [],
57483
- ...filter.agent_id ? [filter.agent_id] : []
57484
- ];
57485
- const recentParams = [cutoffDate, ...scopedParams];
57486
- const total = d.query(`SELECT COUNT(*) as c FROM memories WHERE status = 'active' ${scopedCond}`).get(...scopedParams).c;
57487
- const pinned = d.query(`SELECT COUNT(*) as c FROM memories WHERE status = 'active' AND pinned = 1 ${scopedCond}`).get(...scopedParams).c;
57488
- const actRows = d.query(`
57489
- SELECT date(created_at) AS date, COUNT(*) AS memories_created
57490
- FROM memories WHERE status = 'active' AND date(created_at) >= ? ${scopedCond}
57491
- GROUP BY date(created_at) ORDER BY date(created_at) ASC
57492
- `).all(...recentParams);
57493
- const recentTotal = actRows.reduce((s, r) => s + r.memories_created, 0);
57494
- const byScopeRows = d.query(`SELECT scope, COUNT(*) as c FROM memories WHERE status = 'active' ${scopedCond} GROUP BY scope`).all(...scopedParams);
57495
- const byCatRows = d.query(`SELECT category, COUNT(*) as c FROM memories WHERE status = 'active' ${scopedCond} GROUP BY category`).all(...scopedParams);
57496
- const topMems = d.query(`SELECT id, key, value, importance, scope, category FROM memories WHERE status = 'active' ${scopedCond} ORDER BY importance DESC, access_count DESC LIMIT 5`).all(...scopedParams);
57497
- const topAgents = d.query(`SELECT agent_id, COUNT(*) as c FROM memories WHERE status = 'active' AND agent_id IS NOT NULL ${scopedCond} GROUP BY agent_id ORDER BY c DESC LIMIT 5`).all(...scopedParams);
57498
- return {
57499
- total,
57500
- pinned,
57501
- days,
57502
- recent: { total: recentTotal, activity: actRows },
57503
- by_scope: Object.fromEntries(byScopeRows.map((r) => [r.scope, r.c])),
57504
- by_category: Object.fromEntries(byCatRows.map((r) => [r.category, r.c])),
57505
- top_memories: topMems,
57506
- top_agents: topAgents
57507
- };
57508
- }
57509
- function getStaleMemoriesPage(filter = {}, db) {
57510
- const days = Math.min(filter.days || 30, 365);
57511
- const limit = filter.limit ?? 20;
57512
- const offset = filter.offset ?? 0;
57513
- if (!db && isApiMode()) {
57514
- const q = toQuery({ days, project_id: filter.project_id, agent_id: filter.agent_id, limit, offset });
57515
- const { data } = apiJson("GET", `/memories/stale${q}`);
57516
- const rows2 = data?.memories ?? [];
57517
- return {
57518
- rows: rows2,
57519
- total: data?.total ?? rows2.length,
57520
- has_more: data?.has_more,
57521
- next_cursor: data?.next_cursor ?? null
57522
- };
57523
- }
57524
- const d = db || getDatabase();
57525
- const cutoffDate = new Date(Date.now() - days * 86400000).toISOString();
57526
- const conds = ["status = 'active'", "(accessed_at IS NULL OR accessed_at < ?)", "pinned = 0"];
57527
- const params = [cutoffDate];
57528
- if (filter.project_id) {
57529
- conds.push("project_id = ?");
57530
- params.push(filter.project_id);
57531
- }
57532
- if (filter.agent_id) {
57533
- conds.push("agent_id = ?");
57534
- params.push(filter.agent_id);
57535
- }
57536
- const where = conds.join(" AND ");
57537
- const countRow = d.query(`SELECT COUNT(*) AS c FROM memories WHERE ${where}`).get(...params);
57538
- let sql = `SELECT id, key, value, importance, scope, category, accessed_at, access_count, created_at FROM memories WHERE ${where} ORDER BY COALESCE(accessed_at, created_at) ASC LIMIT ?`;
57539
- params.push(limit);
57540
- if (offset) {
57541
- sql += " OFFSET ?";
57542
- params.push(offset);
57543
- }
57544
- const rows = d.query(sql).all(...params);
57545
- const hasMore = rows.length === limit;
57546
- return {
57547
- rows,
57548
- total: countRow?.c ?? 0,
57549
- has_more: hasMore,
57550
- next_cursor: hasMore ? offset + rows.length : null
57551
- };
57552
- }
57553
- function getMemoryHealth(filter = {}, db) {
57554
- const staleDays = filter.stale_days ?? 30;
57555
- const forgottenDays = filter.forgotten_days ?? 60;
57556
- const limit = filter.limit ?? 10;
57557
- if (!db && isApiMode()) {
57558
- const q = toQuery({
57559
- stale_days: staleDays,
57560
- forgotten_days: forgottenDays,
57561
- project_id: filter.project_id,
57562
- agent_id: filter.agent_id,
57563
- limit
57564
- });
57565
- const { data } = apiJson("GET", `/memories/health${q}`);
57566
- return { stale: data?.stale ?? [], forgotten: data?.forgotten ?? [], dupes: data?.dupes ?? [] };
57567
- }
57568
- const d = db || getDatabase();
57569
- const extraWhere = [
57570
- ...filter.project_id ? ["project_id = ?"] : [],
57571
- ...filter.agent_id ? ["agent_id = ?"] : []
57572
- ].join(" AND ");
57573
- const scopeParams = [
57574
- ...filter.project_id ? [filter.project_id] : [],
57575
- ...filter.agent_id ? [filter.agent_id] : []
57576
- ];
57577
- const staleCutoff = new Date(Date.now() - staleDays * 86400000).toISOString();
57578
- const forgottenCutoff = new Date(Date.now() - forgottenDays * 86400000).toISOString();
57579
- const base = `status = 'active' AND pinned = 0${extraWhere ? " AND " + extraWhere : ""}`;
57580
- const stale = d.prepare(`SELECT id, key, value, importance, scope, created_at FROM memories
57581
- WHERE ${base} AND access_count = 0 AND created_at < ?
57582
- ORDER BY created_at ASC LIMIT ?`).all(...scopeParams, staleCutoff, limit);
57583
- const forgotten = d.prepare(`SELECT id, key, value, importance, scope, accessed_at FROM memories
57584
- WHERE ${base} AND importance >= 7
57585
- AND (accessed_at IS NULL OR accessed_at < ?)
57586
- ORDER BY importance DESC, COALESCE(accessed_at, created_at) ASC LIMIT ?`).all(...scopeParams, forgottenCutoff, limit);
57587
- const dupes = d.prepare(`SELECT key, COUNT(*) as cnt, MAX(updated_at) as latest, MIN(created_at) as oldest
57588
- FROM memories WHERE ${base}
57589
- GROUP BY key HAVING COUNT(*) > 1
57590
- ORDER BY cnt DESC LIMIT ?`).all(...scopeParams, limit);
57591
- return { stale, forgotten, dupes };
57592
- }
57593
-
57594
- // src/server/routes/memories-stats.ts
57595
57855
  init_memories();
57596
57856
  init_router();
57597
57857
  addRoute("GET", "/api/memories/stats", () => {
@@ -57635,8 +57895,10 @@ addRoute("GET", "/api/memories/stale", (_req, url) => {
57635
57895
  const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 1000) : 20;
57636
57896
  const parsedOffset = Number(q["offset"]);
57637
57897
  const offset = Number.isInteger(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0;
57898
+ const pinned = q["pinned"] === "1" || q["pinned"] === "true" ? true : q["pinned"] === "0" || q["pinned"] === "false" ? false : undefined;
57638
57899
  const page = getStaleMemoriesPage({
57639
57900
  days,
57901
+ pinned,
57640
57902
  project_id: q["project_id"],
57641
57903
  agent_id: q["agent_id"],
57642
57904
  limit,
@@ -57708,7 +57970,7 @@ function hasFts5Table2(db) {
57708
57970
  function buildScopeFilter(opts) {
57709
57971
  const conditions = [
57710
57972
  "m.status = 'active'",
57711
- "(m.expires_at IS NULL OR m.expires_at >= datetime('now'))",
57973
+ "(m.expires_at IS NULL OR m.expires_at >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))",
57712
57974
  "m.category IN ('preference', 'fact', 'knowledge')"
57713
57975
  ];
57714
57976
  const params = [];
@@ -57976,7 +58238,7 @@ async function runTemporalAgent(db, query, opts) {
57976
58238
  const maxResults = opts.max_results ?? 20;
57977
58239
  const limit = maxResults * 3;
57978
58240
  const conditions = [
57979
- "(m.expires_at IS NULL OR m.expires_at >= datetime('now'))"
58241
+ "(m.expires_at IS NULL OR m.expires_at >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
57980
58242
  ];
57981
58243
  const params = [];
57982
58244
  if (opts.project_id) {
@@ -59810,7 +60072,7 @@ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive",
59810
60072
  }
59811
60073
  const d = db || getDatabase();
59812
60074
  cleanExpiredLocks(d);
59813
- 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);
60075
+ 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);
59814
60076
  if (ownLock) {
59815
60077
  const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
59816
60078
  d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
@@ -59820,7 +60082,7 @@ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive",
59820
60082
  return parseLockRow({ ...ownLock, expires_at: newExpiry });
59821
60083
  }
59822
60084
  if (lockType === "exclusive") {
59823
- 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);
60085
+ 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);
59824
60086
  if (existing) {
59825
60087
  return null;
59826
60088
  }
@@ -59865,7 +60127,7 @@ function checkLock(resourceType, resourceId, lockType, db) {
59865
60127
  }
59866
60128
  const d = db || getDatabase();
59867
60129
  cleanExpiredLocks(d);
59868
- 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')";
60130
+ 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')";
59869
60131
  const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
59870
60132
  return rows.map(parseLockRow);
59871
60133
  }
@@ -59876,7 +60138,7 @@ function listAgentLocks(agentId, db) {
59876
60138
  }
59877
60139
  const d = db || getDatabase();
59878
60140
  cleanExpiredLocks(d);
59879
- const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
60141
+ 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);
59880
60142
  return rows.map(parseLockRow);
59881
60143
  }
59882
60144
  function cleanExpiredLocks(db) {
@@ -59885,7 +60147,7 @@ function cleanExpiredLocks(db) {
59885
60147
  return data?.cleaned ?? 0;
59886
60148
  }
59887
60149
  const d = db || getDatabase();
59888
- const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
60150
+ const result = d.run("DELETE FROM resource_locks WHERE expires_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')");
59889
60151
  return result.changes;
59890
60152
  }
59891
60153
 
@@ -62511,17 +62773,21 @@ function registerSystemHookRoutes() {
62511
62773
  if (!body.type || !body.handler_url) {
62512
62774
  return errorResponse("type and handler_url are required", 400);
62513
62775
  }
62514
- const wh = createWebhookHook({
62515
- type: body.type,
62516
- handlerUrl: body.handler_url,
62517
- priority: body.priority,
62518
- blocking: body.blocking,
62519
- agentId: body.agent_id,
62520
- projectId: body.project_id,
62521
- description: body.description
62522
- });
62523
- reloadWebhooks();
62524
- return json(wh, 201);
62776
+ try {
62777
+ const wh = await createWebhookHook({
62778
+ type: body.type,
62779
+ handlerUrl: body.handler_url,
62780
+ priority: body.priority,
62781
+ blocking: body.blocking,
62782
+ agentId: body.agent_id,
62783
+ projectId: body.project_id,
62784
+ description: body.description
62785
+ });
62786
+ await reloadWebhooks();
62787
+ return json(wh, 201);
62788
+ } catch (err) {
62789
+ return errorResponse(err instanceof Error ? err.message : String(err), 400);
62790
+ }
62525
62791
  });
62526
62792
  addRoute("GET", "/api/webhooks/:id", (_req, _url, params) => {
62527
62793
  const wh = getWebhookHook(params["id"]);
@@ -62538,7 +62804,7 @@ function registerSystemHookRoutes() {
62538
62804
  });
62539
62805
  if (!updated)
62540
62806
  return errorResponse("Webhook not found", 404);
62541
- reloadWebhooks();
62807
+ await reloadWebhooks();
62542
62808
  return json(updated);
62543
62809
  });
62544
62810
  addRoute("DELETE", "/api/webhooks/:id", (_req, _url, params) => {
@@ -64976,13 +65242,11 @@ function startServer(port) {
64976
65242
  const profile = getActiveProfile();
64977
65243
  try {
64978
65244
  const db = getDatabase();
64979
- const total = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active'").get().c;
64980
- const expired = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired' OR (expires_at IS NOT NULL AND expires_at < datetime('now'))").get().c;
64981
- const pinned = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active' AND pinned = 1").get().c;
65245
+ const stats = getMemoryStats(db);
64982
65246
  const agents = db.query("SELECT COUNT(*) as c FROM agents").get().c;
64983
65247
  const projects = db.query("SELECT COUNT(*) as c FROM projects").get().c;
64984
- const status = expired > 50 ? "warn" : "ok";
64985
- return json({ status, version: pkgVersion(), backend, profile: profile ?? "default", db_path: getDbPath(), hostname: hostname3, memories: { total, expired, pinned }, agents, projects });
65248
+ const status = stats.expired_due_count > 50 ? "warn" : "ok";
65249
+ return json({ status, version: pkgVersion(), backend, profile: profile ?? "default", db_path: getDbPath(), hostname: hostname3, memories: { total: stats.total, expired: stats.expired_count, expired_due: stats.expired_due_count, pinned: stats.pinned_count }, agents, projects });
64986
65250
  } catch (e) {
64987
65251
  return json({ status: "error", version: pkgVersion(), backend, error: e instanceof Error ? e.message : String(e) }, 503);
64988
65252
  }