@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.
- package/bun.lock +63 -4
- package/dist/cli/__fixtures__/io-restore-stub-server.d.ts +2 -0
- package/dist/cli/__fixtures__/io-restore-stub-server.d.ts.map +1 -0
- package/dist/cli/commands/info-stale.d.ts.map +1 -1
- package/dist/cli/commands/io-restore.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-list.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-remove.d.ts.map +1 -1
- package/dist/cli/index.js +623 -208
- package/dist/db/__fixtures__/list-filter-capture-server.d.ts +2 -0
- package/dist/db/__fixtures__/list-filter-capture-server.d.ts.map +1 -0
- package/dist/db/__fixtures__/list-filter-client-runner.d.ts +2 -0
- package/dist/db/__fixtures__/list-filter-client-runner.d.ts.map +1 -0
- package/dist/db/agents.d.ts +14 -0
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/analytics.d.ts +4 -0
- package/dist/db/analytics.d.ts.map +1 -1
- package/dist/db/memories.d.ts +6 -0
- package/dist/db/memories.d.ts.map +1 -1
- package/dist/db/session-jobs.d.ts +18 -0
- package/dist/db/session-jobs.d.ts.map +1 -1
- package/dist/db/webhook_hooks.d.ts +24 -1
- package/dist/db/webhook_hooks.d.ts.map +1 -1
- package/dist/diagnostics/historical-project-registration-receipt.js +36 -17
- package/dist/index.js +141 -52
- package/dist/lib/built-in-hooks.d.ts +22 -2
- package/dist/lib/built-in-hooks.d.ts.map +1 -1
- package/dist/lib/file-deps.d.ts +1 -1
- package/dist/lib/open-sessions-connector.d.ts +6 -6
- package/dist/lib/open-sessions-connector.d.ts.map +1 -1
- package/dist/lib/redact.d.ts +16 -0
- package/dist/lib/redact.d.ts.map +1 -1
- package/dist/lib/session-processor.d.ts.map +1 -1
- package/dist/lib/session-queue.d.ts.map +1 -1
- package/dist/lib/storage-sync.d.ts.map +1 -1
- package/dist/mcp/index.js +348 -70
- package/dist/mcp/tools/memory-lifecycle.d.ts.map +1 -1
- package/dist/pg-sync-worker.js +7 -5
- package/dist/project-registration.js +108 -36
- package/dist/sdk/index.d.ts +9 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +563 -299
- package/dist/server/routes/system-hooks.d.ts.map +1 -1
- package/dist/storage.d.ts +16 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +63 -26
- package/dist/test-support/pg-sync-stub-worker.d.ts +2 -0
- package/dist/test-support/pg-sync-stub-worker.d.ts.map +1 -0
- package/dist/types/hooks.d.ts +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/hasna.contract.json +5 -4
- package/package.json +4 -3
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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(
|
|
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.
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
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 >=
|
|
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 >
|
|
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
|
|
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
|
-
|
|
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 <
|
|
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 >=
|
|
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 >=
|
|
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 >
|
|
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 >
|
|
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 >
|
|
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 >
|
|
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 <=
|
|
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
|
-
|
|
62515
|
-
|
|
62516
|
-
|
|
62517
|
-
|
|
62518
|
-
|
|
62519
|
-
|
|
62520
|
-
|
|
62521
|
-
|
|
62522
|
-
|
|
62523
|
-
|
|
62524
|
-
|
|
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
|
|
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 =
|
|
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
|
}
|