@hasna/mementos 0.4.39 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2301 -1545
- package/dist/db/agents.d.ts +1 -1
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/locks.d.ts +52 -0
- package/dist/db/locks.d.ts.map +1 -0
- package/dist/db/memories.d.ts +1 -0
- package/dist/db/memories.d.ts.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1905 -1003
- package/dist/lib/auto-memory-queue.d.ts +46 -0
- package/dist/lib/auto-memory-queue.d.ts.map +1 -0
- package/dist/lib/auto-memory.d.ts +18 -0
- package/dist/lib/auto-memory.d.ts.map +1 -0
- package/dist/lib/dedup.d.ts +33 -0
- package/dist/lib/dedup.d.ts.map +1 -0
- package/dist/lib/focus.d.ts +58 -0
- package/dist/lib/focus.d.ts.map +1 -0
- package/dist/lib/memory-lock.d.ts +58 -0
- package/dist/lib/memory-lock.d.ts.map +1 -0
- package/dist/lib/providers/anthropic.d.ts +21 -0
- package/dist/lib/providers/anthropic.d.ts.map +1 -0
- package/dist/lib/providers/base.d.ts +96 -0
- package/dist/lib/providers/base.d.ts.map +1 -0
- package/dist/lib/providers/cerebras.d.ts +20 -0
- package/dist/lib/providers/cerebras.d.ts.map +1 -0
- package/dist/lib/providers/grok.d.ts +19 -0
- package/dist/lib/providers/grok.d.ts.map +1 -0
- package/dist/lib/providers/index.d.ts +7 -0
- package/dist/lib/providers/index.d.ts.map +1 -0
- package/dist/lib/providers/openai-compat.d.ts +18 -0
- package/dist/lib/providers/openai-compat.d.ts.map +1 -0
- package/dist/lib/providers/openai.d.ts +20 -0
- package/dist/lib/providers/openai.d.ts.map +1 -0
- package/dist/lib/providers/registry.d.ts +38 -0
- package/dist/lib/providers/registry.d.ts.map +1 -0
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/mcp/index.js +1917 -996
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1602 -894
- package/dist/types/index.d.ts +24 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/mcp/index.js
CHANGED
|
@@ -3989,6 +3989,24 @@ var coerce = {
|
|
|
3989
3989
|
};
|
|
3990
3990
|
var NEVER = INVALID;
|
|
3991
3991
|
// src/types/index.ts
|
|
3992
|
+
class AgentConflictError extends Error {
|
|
3993
|
+
conflict = true;
|
|
3994
|
+
existing_id;
|
|
3995
|
+
existing_name;
|
|
3996
|
+
last_seen_at;
|
|
3997
|
+
session_hint;
|
|
3998
|
+
working_dir;
|
|
3999
|
+
constructor(opts) {
|
|
4000
|
+
const msg = `Agent "${opts.existing_name}" is already active (session hint: ${opts.session_hint ?? "unknown"}, last seen ${opts.last_seen_at}). Wait 30 minutes or use a different name.`;
|
|
4001
|
+
super(msg);
|
|
4002
|
+
this.name = "AgentConflictError";
|
|
4003
|
+
this.existing_id = opts.existing_id;
|
|
4004
|
+
this.existing_name = opts.existing_name;
|
|
4005
|
+
this.last_seen_at = opts.last_seen_at;
|
|
4006
|
+
this.session_hint = opts.session_hint;
|
|
4007
|
+
this.working_dir = opts.working_dir ?? null;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
3992
4010
|
class EntityNotFoundError extends Error {
|
|
3993
4011
|
constructor(id) {
|
|
3994
4012
|
super(`Entity not found: ${id}`);
|
|
@@ -4283,6 +4301,33 @@ var MIGRATIONS = [
|
|
|
4283
4301
|
ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
|
|
4284
4302
|
CREATE INDEX IF NOT EXISTS idx_agents_active_project ON agents(active_project_id);
|
|
4285
4303
|
INSERT OR IGNORE INTO _migrations (id) VALUES (6);
|
|
4304
|
+
`,
|
|
4305
|
+
`
|
|
4306
|
+
ALTER TABLE agents ADD COLUMN session_id TEXT;
|
|
4307
|
+
CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id);
|
|
4308
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (7);
|
|
4309
|
+
`,
|
|
4310
|
+
`
|
|
4311
|
+
CREATE TABLE IF NOT EXISTS resource_locks (
|
|
4312
|
+
id TEXT PRIMARY KEY,
|
|
4313
|
+
resource_type TEXT NOT NULL CHECK(resource_type IN ('project', 'memory', 'entity', 'agent', 'connector')),
|
|
4314
|
+
resource_id TEXT NOT NULL,
|
|
4315
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
4316
|
+
lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
|
|
4317
|
+
locked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4318
|
+
expires_at TEXT NOT NULL
|
|
4319
|
+
);
|
|
4320
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
|
|
4321
|
+
ON resource_locks(resource_type, resource_id)
|
|
4322
|
+
WHERE lock_type = 'exclusive';
|
|
4323
|
+
CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
|
|
4324
|
+
CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
|
|
4325
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (8);
|
|
4326
|
+
`,
|
|
4327
|
+
`
|
|
4328
|
+
ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
|
|
4329
|
+
CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
|
|
4330
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (9);
|
|
4286
4331
|
`
|
|
4287
4332
|
];
|
|
4288
4333
|
var _db = null;
|
|
@@ -4364,1073 +4409,990 @@ function redactSecrets(text) {
|
|
|
4364
4409
|
return result;
|
|
4365
4410
|
}
|
|
4366
4411
|
|
|
4367
|
-
// src/db/
|
|
4368
|
-
function
|
|
4412
|
+
// src/db/entity-memories.ts
|
|
4413
|
+
function parseEntityMemoryRow(row) {
|
|
4369
4414
|
return {
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
4375
|
-
active_project_id: row["active_project_id"] || null,
|
|
4376
|
-
created_at: row["created_at"],
|
|
4377
|
-
last_seen_at: row["last_seen_at"]
|
|
4415
|
+
entity_id: row["entity_id"],
|
|
4416
|
+
memory_id: row["memory_id"],
|
|
4417
|
+
role: row["role"],
|
|
4418
|
+
created_at: row["created_at"]
|
|
4378
4419
|
};
|
|
4379
4420
|
}
|
|
4380
|
-
function
|
|
4421
|
+
function linkEntityToMemory(entityId, memoryId, role = "context", db) {
|
|
4381
4422
|
const d = db || getDatabase();
|
|
4382
4423
|
const timestamp = now();
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [
|
|
4388
|
-
timestamp,
|
|
4389
|
-
existingId
|
|
4390
|
-
]);
|
|
4391
|
-
if (description) {
|
|
4392
|
-
d.run("UPDATE agents SET description = ? WHERE id = ?", [
|
|
4393
|
-
description,
|
|
4394
|
-
existingId
|
|
4395
|
-
]);
|
|
4396
|
-
}
|
|
4397
|
-
if (role) {
|
|
4398
|
-
d.run("UPDATE agents SET role = ? WHERE id = ?", [
|
|
4399
|
-
role,
|
|
4400
|
-
existingId
|
|
4401
|
-
]);
|
|
4402
|
-
}
|
|
4403
|
-
return getAgent(existingId, d);
|
|
4404
|
-
}
|
|
4405
|
-
const id = shortUuid();
|
|
4406
|
-
d.run("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", [id, normalizedName, description || null, role || "agent", timestamp, timestamp]);
|
|
4407
|
-
return getAgent(id, d);
|
|
4408
|
-
}
|
|
4409
|
-
function getAgent(idOrName, db) {
|
|
4410
|
-
const d = db || getDatabase();
|
|
4411
|
-
let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
|
|
4412
|
-
if (row)
|
|
4413
|
-
return parseAgentRow(row);
|
|
4414
|
-
row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
|
|
4415
|
-
if (row)
|
|
4416
|
-
return parseAgentRow(row);
|
|
4417
|
-
const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
|
|
4418
|
-
if (rows.length === 1)
|
|
4419
|
-
return parseAgentRow(rows[0]);
|
|
4420
|
-
return null;
|
|
4421
|
-
}
|
|
4422
|
-
function listAgents(db) {
|
|
4423
|
-
const d = db || getDatabase();
|
|
4424
|
-
const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
|
|
4425
|
-
return rows.map(parseAgentRow);
|
|
4424
|
+
d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
|
|
4425
|
+
VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
|
|
4426
|
+
const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
|
|
4427
|
+
return parseEntityMemoryRow(row);
|
|
4426
4428
|
}
|
|
4427
|
-
function
|
|
4429
|
+
function unlinkEntityFromMemory(entityId, memoryId, db) {
|
|
4428
4430
|
const d = db || getDatabase();
|
|
4429
|
-
|
|
4430
|
-
if (!agent)
|
|
4431
|
-
return;
|
|
4432
|
-
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), agent.id]);
|
|
4431
|
+
d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
|
|
4433
4432
|
}
|
|
4434
|
-
function
|
|
4433
|
+
function getMemoriesForEntity(entityId, db) {
|
|
4435
4434
|
const d = db || getDatabase();
|
|
4436
|
-
const rows = d.query(
|
|
4437
|
-
|
|
4435
|
+
const rows = d.query(`SELECT m.* FROM memories m
|
|
4436
|
+
INNER JOIN entity_memories em ON em.memory_id = m.id
|
|
4437
|
+
WHERE em.entity_id = ?
|
|
4438
|
+
ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
|
|
4439
|
+
return rows.map(parseMemoryRow);
|
|
4438
4440
|
}
|
|
4439
|
-
function
|
|
4441
|
+
function getEntityMemoryLinks(entityId, memoryId, db) {
|
|
4440
4442
|
const d = db || getDatabase();
|
|
4441
|
-
const
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
const normalizedNewName = updates.name.trim().toLowerCase();
|
|
4447
|
-
if (normalizedNewName !== agent.name) {
|
|
4448
|
-
const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
|
|
4449
|
-
if (existing) {
|
|
4450
|
-
throw new Error(`Agent name already taken: ${normalizedNewName}`);
|
|
4451
|
-
}
|
|
4452
|
-
d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
|
|
4453
|
-
}
|
|
4454
|
-
}
|
|
4455
|
-
if (updates.description !== undefined) {
|
|
4456
|
-
d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
|
|
4457
|
-
}
|
|
4458
|
-
if (updates.role !== undefined) {
|
|
4459
|
-
d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
|
|
4443
|
+
const conditions = [];
|
|
4444
|
+
const params = [];
|
|
4445
|
+
if (entityId) {
|
|
4446
|
+
conditions.push("entity_id = ?");
|
|
4447
|
+
params.push(entityId);
|
|
4460
4448
|
}
|
|
4461
|
-
if (
|
|
4462
|
-
|
|
4449
|
+
if (memoryId) {
|
|
4450
|
+
conditions.push("memory_id = ?");
|
|
4451
|
+
params.push(memoryId);
|
|
4463
4452
|
}
|
|
4464
|
-
|
|
4465
|
-
|
|
4453
|
+
let sql = "SELECT * FROM entity_memories";
|
|
4454
|
+
if (conditions.length > 0) {
|
|
4455
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
4466
4456
|
}
|
|
4467
|
-
|
|
4468
|
-
|
|
4457
|
+
sql += " ORDER BY created_at DESC";
|
|
4458
|
+
const rows = d.query(sql).all(...params);
|
|
4459
|
+
return rows.map(parseEntityMemoryRow);
|
|
4469
4460
|
}
|
|
4470
4461
|
|
|
4471
|
-
// src/db/
|
|
4472
|
-
function
|
|
4462
|
+
// src/db/memories.ts
|
|
4463
|
+
function runEntityExtraction(_memory, _projectId, _d) {}
|
|
4464
|
+
function parseMemoryRow(row) {
|
|
4473
4465
|
return {
|
|
4474
4466
|
id: row["id"],
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4467
|
+
key: row["key"],
|
|
4468
|
+
value: row["value"],
|
|
4469
|
+
category: row["category"],
|
|
4470
|
+
scope: row["scope"],
|
|
4471
|
+
summary: row["summary"] || null,
|
|
4472
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
4473
|
+
importance: row["importance"],
|
|
4474
|
+
source: row["source"],
|
|
4475
|
+
status: row["status"],
|
|
4476
|
+
pinned: !!row["pinned"],
|
|
4477
|
+
agent_id: row["agent_id"] || null,
|
|
4478
|
+
project_id: row["project_id"] || null,
|
|
4479
|
+
session_id: row["session_id"] || null,
|
|
4480
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
4481
|
+
access_count: row["access_count"],
|
|
4482
|
+
version: row["version"],
|
|
4483
|
+
expires_at: row["expires_at"] || null,
|
|
4479
4484
|
created_at: row["created_at"],
|
|
4480
|
-
updated_at: row["updated_at"]
|
|
4485
|
+
updated_at: row["updated_at"],
|
|
4486
|
+
accessed_at: row["accessed_at"] || null
|
|
4481
4487
|
};
|
|
4482
4488
|
}
|
|
4483
|
-
function
|
|
4489
|
+
function createMemory(input, dedupeMode = "merge", db) {
|
|
4484
4490
|
const d = db || getDatabase();
|
|
4485
4491
|
const timestamp = now();
|
|
4486
|
-
|
|
4487
|
-
if (
|
|
4488
|
-
|
|
4489
|
-
d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
|
|
4490
|
-
timestamp,
|
|
4491
|
-
existingId
|
|
4492
|
-
]);
|
|
4493
|
-
return parseProjectRow(existing);
|
|
4492
|
+
let expiresAt = input.expires_at || null;
|
|
4493
|
+
if (input.ttl_ms && !expiresAt) {
|
|
4494
|
+
expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
|
|
4494
4495
|
}
|
|
4495
4496
|
const id = uuid();
|
|
4496
|
-
|
|
4497
|
-
|
|
4497
|
+
const tags = input.tags || [];
|
|
4498
|
+
const tagsJson = JSON.stringify(tags);
|
|
4499
|
+
const metadataJson = JSON.stringify(input.metadata || {});
|
|
4500
|
+
const safeValue = redactSecrets(input.value);
|
|
4501
|
+
const safeSummary = input.summary ? redactSecrets(input.summary) : null;
|
|
4502
|
+
if (dedupeMode === "merge") {
|
|
4503
|
+
const existing = d.query(`SELECT id, version FROM memories
|
|
4504
|
+
WHERE key = ? AND scope = ?
|
|
4505
|
+
AND COALESCE(agent_id, '') = ?
|
|
4506
|
+
AND COALESCE(project_id, '') = ?
|
|
4507
|
+
AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
|
|
4508
|
+
if (existing) {
|
|
4509
|
+
d.run(`UPDATE memories SET
|
|
4510
|
+
value = ?, category = ?, summary = ?, tags = ?,
|
|
4511
|
+
importance = ?, metadata = ?, expires_at = ?,
|
|
4512
|
+
pinned = COALESCE(pinned, 0),
|
|
4513
|
+
version = version + 1, updated_at = ?
|
|
4514
|
+
WHERE id = ?`, [
|
|
4515
|
+
safeValue,
|
|
4516
|
+
input.category || "knowledge",
|
|
4517
|
+
safeSummary,
|
|
4518
|
+
tagsJson,
|
|
4519
|
+
input.importance ?? 5,
|
|
4520
|
+
metadataJson,
|
|
4521
|
+
expiresAt,
|
|
4522
|
+
timestamp,
|
|
4523
|
+
existing.id
|
|
4524
|
+
]);
|
|
4525
|
+
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
|
|
4526
|
+
const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
4527
|
+
for (const tag of tags) {
|
|
4528
|
+
insertTag2.run(existing.id, tag);
|
|
4529
|
+
}
|
|
4530
|
+
const merged = getMemory(existing.id, d);
|
|
4531
|
+
try {
|
|
4532
|
+
const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
|
|
4533
|
+
for (const link of oldLinks) {
|
|
4534
|
+
unlinkEntityFromMemory(link.entity_id, merged.id, d);
|
|
4535
|
+
}
|
|
4536
|
+
runEntityExtraction(merged, input.project_id, d);
|
|
4537
|
+
} catch {}
|
|
4538
|
+
return merged;
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
d.run(`INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, metadata, access_count, version, expires_at, created_at, updated_at)
|
|
4542
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
|
|
4543
|
+
id,
|
|
4544
|
+
input.key,
|
|
4545
|
+
input.value,
|
|
4546
|
+
input.category || "knowledge",
|
|
4547
|
+
input.scope || "private",
|
|
4548
|
+
input.summary || null,
|
|
4549
|
+
tagsJson,
|
|
4550
|
+
input.importance ?? 5,
|
|
4551
|
+
input.source || "agent",
|
|
4552
|
+
input.agent_id || null,
|
|
4553
|
+
input.project_id || null,
|
|
4554
|
+
input.session_id || null,
|
|
4555
|
+
metadataJson,
|
|
4556
|
+
expiresAt,
|
|
4557
|
+
timestamp,
|
|
4558
|
+
timestamp
|
|
4559
|
+
]);
|
|
4560
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
4561
|
+
for (const tag of tags) {
|
|
4562
|
+
insertTag.run(id, tag);
|
|
4563
|
+
}
|
|
4564
|
+
const memory = getMemory(id, d);
|
|
4565
|
+
try {
|
|
4566
|
+
runEntityExtraction(memory, input.project_id, d);
|
|
4567
|
+
} catch {}
|
|
4568
|
+
return memory;
|
|
4498
4569
|
}
|
|
4499
|
-
function
|
|
4570
|
+
function getMemory(id, db) {
|
|
4500
4571
|
const d = db || getDatabase();
|
|
4501
|
-
|
|
4502
|
-
if (row)
|
|
4503
|
-
return
|
|
4504
|
-
|
|
4505
|
-
if (row)
|
|
4506
|
-
return parseProjectRow(row);
|
|
4507
|
-
row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
|
|
4508
|
-
if (row)
|
|
4509
|
-
return parseProjectRow(row);
|
|
4510
|
-
return null;
|
|
4572
|
+
const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
|
|
4573
|
+
if (!row)
|
|
4574
|
+
return null;
|
|
4575
|
+
return parseMemoryRow(row);
|
|
4511
4576
|
}
|
|
4512
|
-
function
|
|
4577
|
+
function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
|
|
4513
4578
|
const d = db || getDatabase();
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
var TECH_KEYWORDS = new Set([
|
|
4520
|
-
"typescript",
|
|
4521
|
-
"javascript",
|
|
4522
|
-
"python",
|
|
4523
|
-
"rust",
|
|
4524
|
-
"go",
|
|
4525
|
-
"java",
|
|
4526
|
-
"ruby",
|
|
4527
|
-
"swift",
|
|
4528
|
-
"kotlin",
|
|
4529
|
-
"react",
|
|
4530
|
-
"vue",
|
|
4531
|
-
"angular",
|
|
4532
|
-
"svelte",
|
|
4533
|
-
"nextjs",
|
|
4534
|
-
"bun",
|
|
4535
|
-
"node",
|
|
4536
|
-
"deno",
|
|
4537
|
-
"sqlite",
|
|
4538
|
-
"postgres",
|
|
4539
|
-
"mysql",
|
|
4540
|
-
"redis",
|
|
4541
|
-
"docker",
|
|
4542
|
-
"kubernetes",
|
|
4543
|
-
"git",
|
|
4544
|
-
"npm",
|
|
4545
|
-
"yarn",
|
|
4546
|
-
"pnpm",
|
|
4547
|
-
"webpack",
|
|
4548
|
-
"vite",
|
|
4549
|
-
"tailwind",
|
|
4550
|
-
"prisma",
|
|
4551
|
-
"drizzle",
|
|
4552
|
-
"zod",
|
|
4553
|
-
"commander",
|
|
4554
|
-
"express",
|
|
4555
|
-
"fastify",
|
|
4556
|
-
"hono"
|
|
4557
|
-
]);
|
|
4558
|
-
var FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)?(?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
|
|
4559
|
-
var URL_RE = /https?:\/\/[^\s)]+/g;
|
|
4560
|
-
var NPM_PACKAGE_RE = /@[\w-]+\/[\w.-]+/g;
|
|
4561
|
-
var PASCAL_CASE_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
|
|
4562
|
-
function getSearchText(memory) {
|
|
4563
|
-
const parts = [memory.key, memory.value];
|
|
4564
|
-
if (memory.summary)
|
|
4565
|
-
parts.push(memory.summary);
|
|
4566
|
-
return parts.join(" ");
|
|
4567
|
-
}
|
|
4568
|
-
function extractEntities(memory, db) {
|
|
4569
|
-
const text = getSearchText(memory);
|
|
4570
|
-
const entityMap = new Map;
|
|
4571
|
-
function add(name, type, confidence) {
|
|
4572
|
-
const normalized = name.toLowerCase();
|
|
4573
|
-
if (normalized.length < 3)
|
|
4574
|
-
return;
|
|
4575
|
-
const existing = entityMap.get(normalized);
|
|
4576
|
-
if (!existing || existing.confidence < confidence) {
|
|
4577
|
-
entityMap.set(normalized, { name: normalized, type, confidence });
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4580
|
-
for (const match of text.matchAll(FILE_PATH_RE)) {
|
|
4581
|
-
add(match[1].trim(), "file", 0.9);
|
|
4582
|
-
}
|
|
4583
|
-
for (const match of text.matchAll(URL_RE)) {
|
|
4584
|
-
add(match[0], "api", 0.8);
|
|
4585
|
-
}
|
|
4586
|
-
for (const match of text.matchAll(NPM_PACKAGE_RE)) {
|
|
4587
|
-
add(match[0], "tool", 0.85);
|
|
4588
|
-
}
|
|
4589
|
-
try {
|
|
4590
|
-
const d = db || getDatabase();
|
|
4591
|
-
const agents = listAgents(d);
|
|
4592
|
-
const textLower2 = text.toLowerCase();
|
|
4593
|
-
for (const agent of agents) {
|
|
4594
|
-
const nameLower = agent.name.toLowerCase();
|
|
4595
|
-
if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
|
|
4596
|
-
add(agent.name, "person", 0.95);
|
|
4597
|
-
}
|
|
4598
|
-
}
|
|
4599
|
-
} catch {}
|
|
4600
|
-
try {
|
|
4601
|
-
const d = db || getDatabase();
|
|
4602
|
-
const projects = listProjects(d);
|
|
4603
|
-
const textLower2 = text.toLowerCase();
|
|
4604
|
-
for (const project of projects) {
|
|
4605
|
-
const nameLower = project.name.toLowerCase();
|
|
4606
|
-
if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
|
|
4607
|
-
add(project.name, "project", 0.95);
|
|
4608
|
-
}
|
|
4609
|
-
}
|
|
4610
|
-
} catch {}
|
|
4611
|
-
const textLower = text.toLowerCase();
|
|
4612
|
-
for (const keyword of TECH_KEYWORDS) {
|
|
4613
|
-
const re = new RegExp(`\\b${keyword}\\b`, "i");
|
|
4614
|
-
if (re.test(textLower)) {
|
|
4615
|
-
add(keyword, "tool", 0.7);
|
|
4616
|
-
}
|
|
4617
|
-
}
|
|
4618
|
-
for (const match of text.matchAll(PASCAL_CASE_RE)) {
|
|
4619
|
-
add(match[1], "concept", 0.5);
|
|
4620
|
-
}
|
|
4621
|
-
return Array.from(entityMap.values()).sort((a, b) => b.confidence - a.confidence);
|
|
4622
|
-
}
|
|
4623
|
-
|
|
4624
|
-
// src/db/entities.ts
|
|
4625
|
-
function parseEntityRow(row) {
|
|
4626
|
-
return {
|
|
4627
|
-
id: row["id"],
|
|
4628
|
-
name: row["name"],
|
|
4629
|
-
type: row["type"],
|
|
4630
|
-
description: row["description"] || null,
|
|
4631
|
-
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
4632
|
-
project_id: row["project_id"] || null,
|
|
4633
|
-
created_at: row["created_at"],
|
|
4634
|
-
updated_at: row["updated_at"]
|
|
4635
|
-
};
|
|
4636
|
-
}
|
|
4637
|
-
function createEntity(input, db) {
|
|
4638
|
-
const d = db || getDatabase();
|
|
4639
|
-
const timestamp = now();
|
|
4640
|
-
const metadataJson = JSON.stringify(input.metadata || {});
|
|
4641
|
-
const existing = d.query(`SELECT * FROM entities
|
|
4642
|
-
WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
|
|
4643
|
-
if (existing) {
|
|
4644
|
-
const sets = ["updated_at = ?"];
|
|
4645
|
-
const params = [timestamp];
|
|
4646
|
-
if (input.description !== undefined) {
|
|
4647
|
-
sets.push("description = ?");
|
|
4648
|
-
params.push(input.description);
|
|
4649
|
-
}
|
|
4650
|
-
if (input.metadata !== undefined) {
|
|
4651
|
-
sets.push("metadata = ?");
|
|
4652
|
-
params.push(metadataJson);
|
|
4653
|
-
}
|
|
4654
|
-
const existingId = existing["id"];
|
|
4655
|
-
params.push(existingId);
|
|
4656
|
-
d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
4657
|
-
return getEntity(existingId, d);
|
|
4579
|
+
let sql = "SELECT * FROM memories WHERE key = ?";
|
|
4580
|
+
const params = [key];
|
|
4581
|
+
if (scope) {
|
|
4582
|
+
sql += " AND scope = ?";
|
|
4583
|
+
params.push(scope);
|
|
4658
4584
|
}
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
id,
|
|
4663
|
-
input.name,
|
|
4664
|
-
input.type,
|
|
4665
|
-
input.description || null,
|
|
4666
|
-
metadataJson,
|
|
4667
|
-
input.project_id || null,
|
|
4668
|
-
timestamp,
|
|
4669
|
-
timestamp
|
|
4670
|
-
]);
|
|
4671
|
-
return getEntity(id, d);
|
|
4672
|
-
}
|
|
4673
|
-
function getEntity(id, db) {
|
|
4674
|
-
const d = db || getDatabase();
|
|
4675
|
-
const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
|
|
4676
|
-
if (!row)
|
|
4677
|
-
throw new EntityNotFoundError(id);
|
|
4678
|
-
return parseEntityRow(row);
|
|
4679
|
-
}
|
|
4680
|
-
function getEntityByName(name, type, projectId, db) {
|
|
4681
|
-
const d = db || getDatabase();
|
|
4682
|
-
let sql = "SELECT * FROM entities WHERE name = ?";
|
|
4683
|
-
const params = [name];
|
|
4684
|
-
if (type) {
|
|
4685
|
-
sql += " AND type = ?";
|
|
4686
|
-
params.push(type);
|
|
4585
|
+
if (agentId) {
|
|
4586
|
+
sql += " AND agent_id = ?";
|
|
4587
|
+
params.push(agentId);
|
|
4687
4588
|
}
|
|
4688
|
-
if (projectId
|
|
4589
|
+
if (projectId) {
|
|
4689
4590
|
sql += " AND project_id = ?";
|
|
4690
4591
|
params.push(projectId);
|
|
4691
4592
|
}
|
|
4692
|
-
|
|
4593
|
+
if (sessionId) {
|
|
4594
|
+
sql += " AND session_id = ?";
|
|
4595
|
+
params.push(sessionId);
|
|
4596
|
+
}
|
|
4597
|
+
sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
|
|
4693
4598
|
const row = d.query(sql).get(...params);
|
|
4694
4599
|
if (!row)
|
|
4695
4600
|
return null;
|
|
4696
|
-
return
|
|
4601
|
+
return parseMemoryRow(row);
|
|
4697
4602
|
}
|
|
4698
|
-
function
|
|
4603
|
+
function listMemories(filter, db) {
|
|
4699
4604
|
const d = db || getDatabase();
|
|
4700
4605
|
const conditions = [];
|
|
4701
4606
|
const params = [];
|
|
4702
|
-
if (filter
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4607
|
+
if (filter) {
|
|
4608
|
+
if (filter.scope) {
|
|
4609
|
+
if (Array.isArray(filter.scope)) {
|
|
4610
|
+
conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
|
|
4611
|
+
params.push(...filter.scope);
|
|
4612
|
+
} else {
|
|
4613
|
+
conditions.push("scope = ?");
|
|
4614
|
+
params.push(filter.scope);
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
if (filter.category) {
|
|
4618
|
+
if (Array.isArray(filter.category)) {
|
|
4619
|
+
conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
|
|
4620
|
+
params.push(...filter.category);
|
|
4621
|
+
} else {
|
|
4622
|
+
conditions.push("category = ?");
|
|
4623
|
+
params.push(filter.category);
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
if (filter.source) {
|
|
4627
|
+
if (Array.isArray(filter.source)) {
|
|
4628
|
+
conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
|
|
4629
|
+
params.push(...filter.source);
|
|
4630
|
+
} else {
|
|
4631
|
+
conditions.push("source = ?");
|
|
4632
|
+
params.push(filter.source);
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
if (filter.status) {
|
|
4636
|
+
if (Array.isArray(filter.status)) {
|
|
4637
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
4638
|
+
params.push(...filter.status);
|
|
4639
|
+
} else {
|
|
4640
|
+
conditions.push("status = ?");
|
|
4641
|
+
params.push(filter.status);
|
|
4642
|
+
}
|
|
4643
|
+
} else {
|
|
4644
|
+
conditions.push("status = 'active'");
|
|
4645
|
+
}
|
|
4646
|
+
if (filter.project_id) {
|
|
4647
|
+
conditions.push("project_id = ?");
|
|
4648
|
+
params.push(filter.project_id);
|
|
4649
|
+
}
|
|
4650
|
+
if (filter.agent_id) {
|
|
4651
|
+
conditions.push("agent_id = ?");
|
|
4652
|
+
params.push(filter.agent_id);
|
|
4653
|
+
}
|
|
4654
|
+
if (filter.session_id) {
|
|
4655
|
+
conditions.push("session_id = ?");
|
|
4656
|
+
params.push(filter.session_id);
|
|
4657
|
+
}
|
|
4658
|
+
if (filter.min_importance) {
|
|
4659
|
+
conditions.push("importance >= ?");
|
|
4660
|
+
params.push(filter.min_importance);
|
|
4661
|
+
}
|
|
4662
|
+
if (filter.pinned !== undefined) {
|
|
4663
|
+
conditions.push("pinned = ?");
|
|
4664
|
+
params.push(filter.pinned ? 1 : 0);
|
|
4665
|
+
}
|
|
4666
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
4667
|
+
for (const tag of filter.tags) {
|
|
4668
|
+
conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
4669
|
+
params.push(tag);
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
if (filter.search) {
|
|
4673
|
+
conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
|
|
4674
|
+
const term = `%${filter.search}%`;
|
|
4675
|
+
params.push(term, term, term);
|
|
4676
|
+
}
|
|
4677
|
+
} else {
|
|
4678
|
+
conditions.push("status = 'active'");
|
|
4714
4679
|
}
|
|
4715
|
-
let sql = "SELECT * FROM
|
|
4680
|
+
let sql = "SELECT * FROM memories";
|
|
4716
4681
|
if (conditions.length > 0) {
|
|
4717
4682
|
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
4718
4683
|
}
|
|
4719
|
-
sql += " ORDER BY
|
|
4720
|
-
if (filter
|
|
4684
|
+
sql += " ORDER BY importance DESC, created_at DESC";
|
|
4685
|
+
if (filter?.limit) {
|
|
4721
4686
|
sql += " LIMIT ?";
|
|
4722
4687
|
params.push(filter.limit);
|
|
4723
4688
|
}
|
|
4724
|
-
if (filter
|
|
4689
|
+
if (filter?.offset) {
|
|
4725
4690
|
sql += " OFFSET ?";
|
|
4726
4691
|
params.push(filter.offset);
|
|
4727
4692
|
}
|
|
4728
4693
|
const rows = d.query(sql).all(...params);
|
|
4729
|
-
return rows.map(
|
|
4694
|
+
return rows.map(parseMemoryRow);
|
|
4730
4695
|
}
|
|
4731
|
-
function
|
|
4696
|
+
function updateMemory(id, input, db) {
|
|
4732
4697
|
const d = db || getDatabase();
|
|
4733
|
-
const existing =
|
|
4698
|
+
const existing = getMemory(id, d);
|
|
4734
4699
|
if (!existing)
|
|
4735
|
-
throw new
|
|
4736
|
-
|
|
4700
|
+
throw new MemoryNotFoundError(id);
|
|
4701
|
+
if (existing.version !== input.version) {
|
|
4702
|
+
throw new VersionConflictError(id, input.version, existing.version);
|
|
4703
|
+
}
|
|
4704
|
+
try {
|
|
4705
|
+
d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
|
|
4706
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
4707
|
+
uuid(),
|
|
4708
|
+
existing.id,
|
|
4709
|
+
existing.version,
|
|
4710
|
+
existing.value,
|
|
4711
|
+
existing.importance,
|
|
4712
|
+
existing.scope,
|
|
4713
|
+
existing.category,
|
|
4714
|
+
JSON.stringify(existing.tags),
|
|
4715
|
+
existing.summary,
|
|
4716
|
+
existing.pinned ? 1 : 0,
|
|
4717
|
+
existing.status,
|
|
4718
|
+
existing.updated_at
|
|
4719
|
+
]);
|
|
4720
|
+
} catch {}
|
|
4721
|
+
const sets = ["version = version + 1", "updated_at = ?"];
|
|
4737
4722
|
const params = [now()];
|
|
4738
|
-
if (input.
|
|
4739
|
-
sets.push("
|
|
4740
|
-
params.push(input.
|
|
4723
|
+
if (input.value !== undefined) {
|
|
4724
|
+
sets.push("value = ?");
|
|
4725
|
+
params.push(redactSecrets(input.value));
|
|
4741
4726
|
}
|
|
4742
|
-
if (input.
|
|
4743
|
-
sets.push("
|
|
4744
|
-
params.push(input.
|
|
4727
|
+
if (input.category !== undefined) {
|
|
4728
|
+
sets.push("category = ?");
|
|
4729
|
+
params.push(input.category);
|
|
4745
4730
|
}
|
|
4746
|
-
if (input.
|
|
4747
|
-
sets.push("
|
|
4748
|
-
params.push(input.
|
|
4731
|
+
if (input.scope !== undefined) {
|
|
4732
|
+
sets.push("scope = ?");
|
|
4733
|
+
params.push(input.scope);
|
|
4749
4734
|
}
|
|
4750
|
-
if (input.
|
|
4751
|
-
sets.push("
|
|
4752
|
-
params.push(
|
|
4735
|
+
if (input.summary !== undefined) {
|
|
4736
|
+
sets.push("summary = ?");
|
|
4737
|
+
params.push(input.summary);
|
|
4738
|
+
}
|
|
4739
|
+
if (input.importance !== undefined) {
|
|
4740
|
+
sets.push("importance = ?");
|
|
4741
|
+
params.push(input.importance);
|
|
4742
|
+
}
|
|
4743
|
+
if (input.pinned !== undefined) {
|
|
4744
|
+
sets.push("pinned = ?");
|
|
4745
|
+
params.push(input.pinned ? 1 : 0);
|
|
4746
|
+
}
|
|
4747
|
+
if (input.status !== undefined) {
|
|
4748
|
+
sets.push("status = ?");
|
|
4749
|
+
params.push(input.status);
|
|
4750
|
+
}
|
|
4751
|
+
if (input.metadata !== undefined) {
|
|
4752
|
+
sets.push("metadata = ?");
|
|
4753
|
+
params.push(JSON.stringify(input.metadata));
|
|
4754
|
+
}
|
|
4755
|
+
if (input.expires_at !== undefined) {
|
|
4756
|
+
sets.push("expires_at = ?");
|
|
4757
|
+
params.push(input.expires_at);
|
|
4758
|
+
}
|
|
4759
|
+
if (input.tags !== undefined) {
|
|
4760
|
+
sets.push("tags = ?");
|
|
4761
|
+
params.push(JSON.stringify(input.tags));
|
|
4762
|
+
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
|
|
4763
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
4764
|
+
for (const tag of input.tags) {
|
|
4765
|
+
insertTag.run(id, tag);
|
|
4766
|
+
}
|
|
4753
4767
|
}
|
|
4754
4768
|
params.push(id);
|
|
4755
|
-
d.run(`UPDATE
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4769
|
+
d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
4770
|
+
const updated = getMemory(id, d);
|
|
4771
|
+
try {
|
|
4772
|
+
if (input.value !== undefined) {
|
|
4773
|
+
const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
|
|
4774
|
+
for (const link of oldLinks) {
|
|
4775
|
+
unlinkEntityFromMemory(link.entity_id, updated.id, d);
|
|
4776
|
+
}
|
|
4777
|
+
runEntityExtraction(updated, existing.project_id || undefined, d);
|
|
4778
|
+
}
|
|
4779
|
+
} catch {}
|
|
4780
|
+
return updated;
|
|
4763
4781
|
}
|
|
4764
|
-
function
|
|
4782
|
+
function deleteMemory(id, db) {
|
|
4765
4783
|
const d = db || getDatabase();
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
|
|
4769
|
-
d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
|
|
4770
|
-
d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
|
|
4771
|
-
sourceId,
|
|
4772
|
-
sourceId
|
|
4773
|
-
]);
|
|
4774
|
-
d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
|
|
4775
|
-
d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
|
|
4776
|
-
d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
|
|
4777
|
-
d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
|
|
4778
|
-
return getEntity(targetId, d);
|
|
4779
|
-
}
|
|
4780
|
-
|
|
4781
|
-
// src/db/entity-memories.ts
|
|
4782
|
-
function parseEntityMemoryRow(row) {
|
|
4783
|
-
return {
|
|
4784
|
-
entity_id: row["entity_id"],
|
|
4785
|
-
memory_id: row["memory_id"],
|
|
4786
|
-
role: row["role"],
|
|
4787
|
-
created_at: row["created_at"]
|
|
4788
|
-
};
|
|
4784
|
+
const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
|
|
4785
|
+
return result.changes > 0;
|
|
4789
4786
|
}
|
|
4790
|
-
function
|
|
4787
|
+
function bulkDeleteMemories(ids, db) {
|
|
4791
4788
|
const d = db || getDatabase();
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
const
|
|
4796
|
-
|
|
4789
|
+
if (ids.length === 0)
|
|
4790
|
+
return 0;
|
|
4791
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
4792
|
+
const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
|
|
4793
|
+
const count = countRow.c;
|
|
4794
|
+
if (count > 0) {
|
|
4795
|
+
d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
|
|
4796
|
+
}
|
|
4797
|
+
return count;
|
|
4797
4798
|
}
|
|
4798
|
-
function
|
|
4799
|
+
function touchMemory(id, db) {
|
|
4799
4800
|
const d = db || getDatabase();
|
|
4800
|
-
d.run("
|
|
4801
|
+
d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
|
|
4801
4802
|
}
|
|
4802
|
-
function
|
|
4803
|
+
function cleanExpiredMemories(db) {
|
|
4803
4804
|
const d = db || getDatabase();
|
|
4804
|
-
const
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4805
|
+
const timestamp = now();
|
|
4806
|
+
const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
|
|
4807
|
+
const count = countRow.c;
|
|
4808
|
+
if (count > 0) {
|
|
4809
|
+
d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
|
|
4810
|
+
}
|
|
4811
|
+
return count;
|
|
4809
4812
|
}
|
|
4810
|
-
function
|
|
4813
|
+
function getMemoryVersions(memoryId, db) {
|
|
4811
4814
|
const d = db || getDatabase();
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4815
|
+
try {
|
|
4816
|
+
const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
|
|
4817
|
+
return rows.map((row) => ({
|
|
4818
|
+
id: row["id"],
|
|
4819
|
+
memory_id: row["memory_id"],
|
|
4820
|
+
version: row["version"],
|
|
4821
|
+
value: row["value"],
|
|
4822
|
+
importance: row["importance"],
|
|
4823
|
+
scope: row["scope"],
|
|
4824
|
+
category: row["category"],
|
|
4825
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
4826
|
+
summary: row["summary"] || null,
|
|
4827
|
+
pinned: !!row["pinned"],
|
|
4828
|
+
status: row["status"],
|
|
4829
|
+
created_at: row["created_at"]
|
|
4830
|
+
}));
|
|
4831
|
+
} catch {
|
|
4832
|
+
return [];
|
|
4825
4833
|
}
|
|
4826
|
-
sql += " ORDER BY created_at DESC";
|
|
4827
|
-
const rows = d.query(sql).all(...params);
|
|
4828
|
-
return rows.map(parseEntityMemoryRow);
|
|
4829
4834
|
}
|
|
4830
4835
|
|
|
4831
|
-
// src/db/
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
id: row["id"],
|
|
4835
|
-
source_entity_id: row["source_entity_id"],
|
|
4836
|
-
target_entity_id: row["target_entity_id"],
|
|
4837
|
-
relation_type: row["relation_type"],
|
|
4838
|
-
weight: row["weight"],
|
|
4839
|
-
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
4840
|
-
created_at: row["created_at"]
|
|
4841
|
-
};
|
|
4842
|
-
}
|
|
4843
|
-
function parseEntityRow2(row) {
|
|
4836
|
+
// src/db/agents.ts
|
|
4837
|
+
var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
|
|
4838
|
+
function parseAgentRow(row) {
|
|
4844
4839
|
return {
|
|
4845
4840
|
id: row["id"],
|
|
4846
4841
|
name: row["name"],
|
|
4847
|
-
|
|
4842
|
+
session_id: row["session_id"] || null,
|
|
4848
4843
|
description: row["description"] || null,
|
|
4844
|
+
role: row["role"] || null,
|
|
4849
4845
|
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
4850
|
-
|
|
4846
|
+
active_project_id: row["active_project_id"] || null,
|
|
4851
4847
|
created_at: row["created_at"],
|
|
4852
|
-
|
|
4848
|
+
last_seen_at: row["last_seen_at"]
|
|
4853
4849
|
};
|
|
4854
4850
|
}
|
|
4855
|
-
function
|
|
4851
|
+
function registerAgent(name, sessionId, description, role, projectId, db) {
|
|
4856
4852
|
const d = db || getDatabase();
|
|
4857
|
-
const id = shortUuid();
|
|
4858
4853
|
const timestamp = now();
|
|
4859
|
-
const
|
|
4860
|
-
const
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4854
|
+
const normalizedName = name.trim().toLowerCase();
|
|
4855
|
+
const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
|
|
4856
|
+
if (existing) {
|
|
4857
|
+
const existingId = existing["id"];
|
|
4858
|
+
const existingSessionId = existing["session_id"] || null;
|
|
4859
|
+
const existingLastSeen = existing["last_seen_at"];
|
|
4860
|
+
if (sessionId && existingSessionId && existingSessionId !== sessionId) {
|
|
4861
|
+
const lastSeenMs = new Date(existingLastSeen).getTime();
|
|
4862
|
+
const nowMs = Date.now();
|
|
4863
|
+
if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
|
|
4864
|
+
throw new AgentConflictError({
|
|
4865
|
+
existing_id: existingId,
|
|
4866
|
+
existing_name: normalizedName,
|
|
4867
|
+
last_seen_at: existingLastSeen,
|
|
4868
|
+
session_hint: existingSessionId.slice(0, 8),
|
|
4869
|
+
working_dir: null
|
|
4870
|
+
});
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
|
|
4874
|
+
timestamp,
|
|
4875
|
+
sessionId ?? existingSessionId,
|
|
4876
|
+
existingId
|
|
4877
|
+
]);
|
|
4878
|
+
if (description) {
|
|
4879
|
+
d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
|
|
4880
|
+
}
|
|
4881
|
+
if (role) {
|
|
4882
|
+
d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
|
|
4883
|
+
}
|
|
4884
|
+
if (projectId !== undefined) {
|
|
4885
|
+
d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
|
|
4886
|
+
}
|
|
4887
|
+
return getAgent(existingId, d);
|
|
4888
|
+
}
|
|
4889
|
+
const id = shortUuid();
|
|
4890
|
+
d.run("INSERT INTO agents (id, name, session_id, description, role, active_project_id, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, normalizedName, sessionId ?? null, description || null, role || "agent", projectId ?? null, timestamp, timestamp]);
|
|
4891
|
+
return getAgent(id, d);
|
|
4868
4892
|
}
|
|
4869
|
-
function
|
|
4893
|
+
function getAgent(idOrName, db) {
|
|
4870
4894
|
const d = db || getDatabase();
|
|
4871
|
-
|
|
4872
|
-
if (
|
|
4873
|
-
|
|
4874
|
-
|
|
4895
|
+
let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
|
|
4896
|
+
if (row)
|
|
4897
|
+
return parseAgentRow(row);
|
|
4898
|
+
row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
|
|
4899
|
+
if (row)
|
|
4900
|
+
return parseAgentRow(row);
|
|
4901
|
+
const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
|
|
4902
|
+
if (rows.length === 1)
|
|
4903
|
+
return parseAgentRow(rows[0]);
|
|
4904
|
+
return null;
|
|
4875
4905
|
}
|
|
4876
|
-
function
|
|
4906
|
+
function listAgents(db) {
|
|
4877
4907
|
const d = db || getDatabase();
|
|
4878
|
-
const
|
|
4879
|
-
|
|
4880
|
-
if (filter.entity_id) {
|
|
4881
|
-
const dir = filter.direction || "both";
|
|
4882
|
-
if (dir === "outgoing") {
|
|
4883
|
-
conditions.push("source_entity_id = ?");
|
|
4884
|
-
params.push(filter.entity_id);
|
|
4885
|
-
} else if (dir === "incoming") {
|
|
4886
|
-
conditions.push("target_entity_id = ?");
|
|
4887
|
-
params.push(filter.entity_id);
|
|
4888
|
-
} else {
|
|
4889
|
-
conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
|
|
4890
|
-
params.push(filter.entity_id, filter.entity_id);
|
|
4891
|
-
}
|
|
4892
|
-
}
|
|
4893
|
-
if (filter.relation_type) {
|
|
4894
|
-
conditions.push("relation_type = ?");
|
|
4895
|
-
params.push(filter.relation_type);
|
|
4896
|
-
}
|
|
4897
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4898
|
-
const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
|
|
4899
|
-
return rows.map(parseRelationRow);
|
|
4908
|
+
const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
|
|
4909
|
+
return rows.map(parseAgentRow);
|
|
4900
4910
|
}
|
|
4901
|
-
function
|
|
4911
|
+
function touchAgent(idOrName, db) {
|
|
4902
4912
|
const d = db || getDatabase();
|
|
4903
|
-
const
|
|
4904
|
-
if (
|
|
4905
|
-
|
|
4913
|
+
const agent = getAgent(idOrName, d);
|
|
4914
|
+
if (!agent)
|
|
4915
|
+
return;
|
|
4916
|
+
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), agent.id]);
|
|
4906
4917
|
}
|
|
4907
|
-
function
|
|
4918
|
+
function listAgentsByProject(projectId, db) {
|
|
4908
4919
|
const d = db || getDatabase();
|
|
4909
|
-
const
|
|
4910
|
-
|
|
4911
|
-
UNION
|
|
4912
|
-
SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
|
|
4913
|
-
FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
|
|
4914
|
-
WHERE g.depth < ?
|
|
4915
|
-
)
|
|
4916
|
-
SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
|
|
4917
|
-
const entities = entityRows.map(parseEntityRow2);
|
|
4918
|
-
const entityIds = new Set(entities.map((e) => e.id));
|
|
4919
|
-
if (entityIds.size === 0) {
|
|
4920
|
-
return { entities: [], relations: [] };
|
|
4921
|
-
}
|
|
4922
|
-
const placeholders = Array.from(entityIds).map(() => "?").join(",");
|
|
4923
|
-
const relationRows = d.query(`SELECT * FROM relations
|
|
4924
|
-
WHERE source_entity_id IN (${placeholders})
|
|
4925
|
-
AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
|
|
4926
|
-
const relations = relationRows.map(parseRelationRow);
|
|
4927
|
-
return { entities, relations };
|
|
4920
|
+
const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
|
|
4921
|
+
return rows.map(parseAgentRow);
|
|
4928
4922
|
}
|
|
4929
|
-
function
|
|
4923
|
+
function updateAgent(id, updates, db) {
|
|
4930
4924
|
const d = db || getDatabase();
|
|
4931
|
-
const
|
|
4932
|
-
|
|
4933
|
-
UNION
|
|
4934
|
-
SELECT
|
|
4935
|
-
CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
|
|
4936
|
-
p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
|
|
4937
|
-
p.depth + 1
|
|
4938
|
-
FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
|
|
4939
|
-
WHERE p.depth < ?
|
|
4940
|
-
AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
|
|
4941
|
-
)
|
|
4942
|
-
SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
|
|
4943
|
-
if (!rows)
|
|
4925
|
+
const agent = getAgent(id, d);
|
|
4926
|
+
if (!agent)
|
|
4944
4927
|
return null;
|
|
4945
|
-
const
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4928
|
+
const timestamp = now();
|
|
4929
|
+
if (updates.name) {
|
|
4930
|
+
const normalizedNewName = updates.name.trim().toLowerCase();
|
|
4931
|
+
if (normalizedNewName !== agent.name) {
|
|
4932
|
+
const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
|
|
4933
|
+
if (existing) {
|
|
4934
|
+
throw new Error(`Agent name already taken: ${normalizedNewName}`);
|
|
4935
|
+
}
|
|
4936
|
+
d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
|
|
4937
|
+
}
|
|
4951
4938
|
}
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
// src/lib/config.ts
|
|
4956
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
|
|
4957
|
-
import { homedir } from "os";
|
|
4958
|
-
import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
4959
|
-
var DEFAULT_CONFIG = {
|
|
4960
|
-
default_scope: "private",
|
|
4961
|
-
default_category: "knowledge",
|
|
4962
|
-
default_importance: 5,
|
|
4963
|
-
max_entries: 1000,
|
|
4964
|
-
max_entries_per_scope: {
|
|
4965
|
-
global: 500,
|
|
4966
|
-
shared: 300,
|
|
4967
|
-
private: 200
|
|
4968
|
-
},
|
|
4969
|
-
injection: {
|
|
4970
|
-
max_tokens: 500,
|
|
4971
|
-
min_importance: 5,
|
|
4972
|
-
categories: ["preference", "fact"],
|
|
4973
|
-
refresh_interval: 5
|
|
4974
|
-
},
|
|
4975
|
-
extraction: {
|
|
4976
|
-
enabled: true,
|
|
4977
|
-
min_confidence: 0.5
|
|
4978
|
-
},
|
|
4979
|
-
sync_agents: ["claude", "codex", "gemini"],
|
|
4980
|
-
auto_cleanup: {
|
|
4981
|
-
enabled: true,
|
|
4982
|
-
expired_check_interval: 3600,
|
|
4983
|
-
unused_archive_days: 7,
|
|
4984
|
-
stale_deprioritize_days: 14
|
|
4939
|
+
if (updates.description !== undefined) {
|
|
4940
|
+
d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
|
|
4985
4941
|
}
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
const result = { ...target };
|
|
4989
|
-
for (const key of Object.keys(source)) {
|
|
4990
|
-
const sourceVal = source[key];
|
|
4991
|
-
const targetVal = result[key];
|
|
4992
|
-
if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
|
|
4993
|
-
result[key] = deepMerge(targetVal, sourceVal);
|
|
4994
|
-
} else {
|
|
4995
|
-
result[key] = sourceVal;
|
|
4996
|
-
}
|
|
4942
|
+
if (updates.role !== undefined) {
|
|
4943
|
+
d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
|
|
4997
4944
|
}
|
|
4998
|
-
|
|
4945
|
+
if (updates.metadata !== undefined) {
|
|
4946
|
+
d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
|
|
4947
|
+
}
|
|
4948
|
+
if ("active_project_id" in updates) {
|
|
4949
|
+
d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
|
|
4950
|
+
}
|
|
4951
|
+
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
|
|
4952
|
+
return getAgent(agent.id, d);
|
|
4999
4953
|
}
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
];
|
|
5007
|
-
function isValidScope(value) {
|
|
5008
|
-
return VALID_SCOPES.includes(value);
|
|
4954
|
+
|
|
4955
|
+
// src/lib/focus.ts
|
|
4956
|
+
var sessionFocus = new Map;
|
|
4957
|
+
function setFocus(agentId, projectId) {
|
|
4958
|
+
sessionFocus.set(agentId, projectId);
|
|
4959
|
+
updateAgent(agentId, { active_project_id: projectId });
|
|
5009
4960
|
}
|
|
5010
|
-
function
|
|
5011
|
-
|
|
4961
|
+
function getFocus(agentId) {
|
|
4962
|
+
if (sessionFocus.has(agentId)) {
|
|
4963
|
+
return sessionFocus.get(agentId) ?? null;
|
|
4964
|
+
}
|
|
4965
|
+
const agent = getAgent(agentId);
|
|
4966
|
+
const projectId = agent?.active_project_id ?? null;
|
|
4967
|
+
sessionFocus.set(agentId, projectId);
|
|
4968
|
+
return projectId;
|
|
5012
4969
|
}
|
|
5013
|
-
function
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
fileConfig = JSON.parse(raw);
|
|
5020
|
-
} catch {}
|
|
4970
|
+
function unfocus(agentId) {
|
|
4971
|
+
setFocus(agentId, null);
|
|
4972
|
+
}
|
|
4973
|
+
function resolveProjectId(agentId, explicitProjectId) {
|
|
4974
|
+
if (explicitProjectId !== undefined && explicitProjectId !== null) {
|
|
4975
|
+
return explicitProjectId;
|
|
5021
4976
|
}
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
if (envScope && isValidScope(envScope)) {
|
|
5025
|
-
merged.default_scope = envScope;
|
|
4977
|
+
if (agentId) {
|
|
4978
|
+
return getFocus(agentId);
|
|
5026
4979
|
}
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
4980
|
+
return null;
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
// src/db/locks.ts
|
|
4984
|
+
function parseLockRow(row) {
|
|
4985
|
+
return {
|
|
4986
|
+
id: row["id"],
|
|
4987
|
+
resource_type: row["resource_type"],
|
|
4988
|
+
resource_id: row["resource_id"],
|
|
4989
|
+
agent_id: row["agent_id"],
|
|
4990
|
+
lock_type: row["lock_type"],
|
|
4991
|
+
locked_at: row["locked_at"],
|
|
4992
|
+
expires_at: row["expires_at"]
|
|
4993
|
+
};
|
|
4994
|
+
}
|
|
4995
|
+
function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
|
|
4996
|
+
const d = db || getDatabase();
|
|
4997
|
+
cleanExpiredLocks(d);
|
|
4998
|
+
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);
|
|
4999
|
+
if (ownLock) {
|
|
5000
|
+
const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
5001
|
+
d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
|
|
5002
|
+
newExpiry,
|
|
5003
|
+
ownLock["id"]
|
|
5004
|
+
]);
|
|
5005
|
+
return parseLockRow({ ...ownLock, expires_at: newExpiry });
|
|
5030
5006
|
}
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
merged.default_importance = parsed;
|
|
5007
|
+
if (lockType === "exclusive") {
|
|
5008
|
+
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);
|
|
5009
|
+
if (existing) {
|
|
5010
|
+
return null;
|
|
5036
5011
|
}
|
|
5037
5012
|
}
|
|
5038
|
-
|
|
5013
|
+
const id = shortUuid();
|
|
5014
|
+
const lockedAt = now();
|
|
5015
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
5016
|
+
d.run("INSERT INTO resource_locks (id, resource_type, resource_id, agent_id, lock_type, locked_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, resourceType, resourceId, agentId, lockType, lockedAt, expiresAt]);
|
|
5017
|
+
return {
|
|
5018
|
+
id,
|
|
5019
|
+
resource_type: resourceType,
|
|
5020
|
+
resource_id: resourceId,
|
|
5021
|
+
agent_id: agentId,
|
|
5022
|
+
lock_type: lockType,
|
|
5023
|
+
locked_at: lockedAt,
|
|
5024
|
+
expires_at: expiresAt
|
|
5025
|
+
};
|
|
5026
|
+
}
|
|
5027
|
+
function releaseLock(lockId, agentId, db) {
|
|
5028
|
+
const d = db || getDatabase();
|
|
5029
|
+
const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
|
|
5030
|
+
return result.changes > 0;
|
|
5031
|
+
}
|
|
5032
|
+
function checkLock(resourceType, resourceId, lockType, db) {
|
|
5033
|
+
const d = db || getDatabase();
|
|
5034
|
+
cleanExpiredLocks(d);
|
|
5035
|
+
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')";
|
|
5036
|
+
const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
|
|
5037
|
+
return rows.map(parseLockRow);
|
|
5038
|
+
}
|
|
5039
|
+
function listAgentLocks(agentId, db) {
|
|
5040
|
+
const d = db || getDatabase();
|
|
5041
|
+
cleanExpiredLocks(d);
|
|
5042
|
+
const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
|
|
5043
|
+
return rows.map(parseLockRow);
|
|
5044
|
+
}
|
|
5045
|
+
function cleanExpiredLocks(db) {
|
|
5046
|
+
const d = db || getDatabase();
|
|
5047
|
+
const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
|
|
5048
|
+
return result.changes;
|
|
5039
5049
|
}
|
|
5040
5050
|
|
|
5041
|
-
// src/
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
return;
|
|
5046
|
-
const extracted = extractEntities(memory, d);
|
|
5047
|
-
const minConfidence = config.extraction?.min_confidence ?? 0.5;
|
|
5048
|
-
const entityIds = [];
|
|
5049
|
-
for (const ext of extracted) {
|
|
5050
|
-
if (ext.confidence >= minConfidence) {
|
|
5051
|
-
const entity = createEntity({ name: ext.name, type: ext.type, project_id: projectId }, d);
|
|
5052
|
-
linkEntityToMemory(entity.id, memory.id, "context", d);
|
|
5053
|
-
entityIds.push(entity.id);
|
|
5054
|
-
}
|
|
5055
|
-
}
|
|
5056
|
-
for (let i = 0;i < entityIds.length; i++) {
|
|
5057
|
-
for (let j = i + 1;j < entityIds.length; j++) {
|
|
5058
|
-
try {
|
|
5059
|
-
createRelation({ source_entity_id: entityIds[i], target_entity_id: entityIds[j], relation_type: "related_to" }, d);
|
|
5060
|
-
} catch {}
|
|
5061
|
-
}
|
|
5062
|
-
}
|
|
5051
|
+
// src/lib/memory-lock.ts
|
|
5052
|
+
var MEMORY_WRITE_TTL = 30;
|
|
5053
|
+
function memoryLockId(key, scope, projectId) {
|
|
5054
|
+
return `${scope}:${key}:${projectId ?? ""}`;
|
|
5063
5055
|
}
|
|
5064
|
-
function
|
|
5056
|
+
function acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds = MEMORY_WRITE_TTL, db) {
|
|
5057
|
+
const d = db || getDatabase();
|
|
5058
|
+
return acquireLock(agentId, "memory", memoryLockId(key, scope, projectId), "exclusive", ttlSeconds, d);
|
|
5059
|
+
}
|
|
5060
|
+
function releaseMemoryWriteLock(lockId, agentId, db) {
|
|
5061
|
+
const d = db || getDatabase();
|
|
5062
|
+
return releaseLock(lockId, agentId, d);
|
|
5063
|
+
}
|
|
5064
|
+
function checkMemoryWriteLock(key, scope, projectId, db) {
|
|
5065
|
+
const d = db || getDatabase();
|
|
5066
|
+
const locks = checkLock("memory", memoryLockId(key, scope, projectId), "exclusive", d);
|
|
5067
|
+
return locks[0] ?? null;
|
|
5068
|
+
}
|
|
5069
|
+
|
|
5070
|
+
// src/db/projects.ts
|
|
5071
|
+
function parseProjectRow(row) {
|
|
5065
5072
|
return {
|
|
5066
5073
|
id: row["id"],
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
summary: row["summary"] || null,
|
|
5072
|
-
tags: JSON.parse(row["tags"] || "[]"),
|
|
5073
|
-
importance: row["importance"],
|
|
5074
|
-
source: row["source"],
|
|
5075
|
-
status: row["status"],
|
|
5076
|
-
pinned: !!row["pinned"],
|
|
5077
|
-
agent_id: row["agent_id"] || null,
|
|
5078
|
-
project_id: row["project_id"] || null,
|
|
5079
|
-
session_id: row["session_id"] || null,
|
|
5080
|
-
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
5081
|
-
access_count: row["access_count"],
|
|
5082
|
-
version: row["version"],
|
|
5083
|
-
expires_at: row["expires_at"] || null,
|
|
5074
|
+
name: row["name"],
|
|
5075
|
+
path: row["path"],
|
|
5076
|
+
description: row["description"] || null,
|
|
5077
|
+
memory_prefix: row["memory_prefix"] || null,
|
|
5084
5078
|
created_at: row["created_at"],
|
|
5085
|
-
updated_at: row["updated_at"]
|
|
5086
|
-
accessed_at: row["accessed_at"] || null
|
|
5079
|
+
updated_at: row["updated_at"]
|
|
5087
5080
|
};
|
|
5088
5081
|
}
|
|
5089
|
-
function
|
|
5082
|
+
function registerProject(name, path, description, memoryPrefix, db) {
|
|
5090
5083
|
const d = db || getDatabase();
|
|
5091
5084
|
const timestamp = now();
|
|
5092
|
-
|
|
5093
|
-
if (
|
|
5094
|
-
|
|
5085
|
+
const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
|
|
5086
|
+
if (existing) {
|
|
5087
|
+
const existingId = existing["id"];
|
|
5088
|
+
d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
|
|
5089
|
+
timestamp,
|
|
5090
|
+
existingId
|
|
5091
|
+
]);
|
|
5092
|
+
return parseProjectRow(existing);
|
|
5095
5093
|
}
|
|
5096
5094
|
const id = uuid();
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
const metadataJson = JSON.stringify(input.metadata || {});
|
|
5100
|
-
const safeValue = redactSecrets(input.value);
|
|
5101
|
-
const safeSummary = input.summary ? redactSecrets(input.summary) : null;
|
|
5102
|
-
if (dedupeMode === "merge") {
|
|
5103
|
-
const existing = d.query(`SELECT id, version FROM memories
|
|
5104
|
-
WHERE key = ? AND scope = ?
|
|
5105
|
-
AND COALESCE(agent_id, '') = ?
|
|
5106
|
-
AND COALESCE(project_id, '') = ?
|
|
5107
|
-
AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
|
|
5108
|
-
if (existing) {
|
|
5109
|
-
d.run(`UPDATE memories SET
|
|
5110
|
-
value = ?, category = ?, summary = ?, tags = ?,
|
|
5111
|
-
importance = ?, metadata = ?, expires_at = ?,
|
|
5112
|
-
pinned = COALESCE(pinned, 0),
|
|
5113
|
-
version = version + 1, updated_at = ?
|
|
5114
|
-
WHERE id = ?`, [
|
|
5115
|
-
safeValue,
|
|
5116
|
-
input.category || "knowledge",
|
|
5117
|
-
safeSummary,
|
|
5118
|
-
tagsJson,
|
|
5119
|
-
input.importance ?? 5,
|
|
5120
|
-
metadataJson,
|
|
5121
|
-
expiresAt,
|
|
5122
|
-
timestamp,
|
|
5123
|
-
existing.id
|
|
5124
|
-
]);
|
|
5125
|
-
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
|
|
5126
|
-
const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
5127
|
-
for (const tag of tags) {
|
|
5128
|
-
insertTag2.run(existing.id, tag);
|
|
5129
|
-
}
|
|
5130
|
-
const merged = getMemory(existing.id, d);
|
|
5131
|
-
try {
|
|
5132
|
-
const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
|
|
5133
|
-
for (const link of oldLinks) {
|
|
5134
|
-
unlinkEntityFromMemory(link.entity_id, merged.id, d);
|
|
5135
|
-
}
|
|
5136
|
-
runEntityExtraction(merged, input.project_id, d);
|
|
5137
|
-
} catch {}
|
|
5138
|
-
return merged;
|
|
5139
|
-
}
|
|
5140
|
-
}
|
|
5141
|
-
d.run(`INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, metadata, access_count, version, expires_at, created_at, updated_at)
|
|
5142
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
|
|
5143
|
-
id,
|
|
5144
|
-
input.key,
|
|
5145
|
-
input.value,
|
|
5146
|
-
input.category || "knowledge",
|
|
5147
|
-
input.scope || "private",
|
|
5148
|
-
input.summary || null,
|
|
5149
|
-
tagsJson,
|
|
5150
|
-
input.importance ?? 5,
|
|
5151
|
-
input.source || "agent",
|
|
5152
|
-
input.agent_id || null,
|
|
5153
|
-
input.project_id || null,
|
|
5154
|
-
input.session_id || null,
|
|
5155
|
-
metadataJson,
|
|
5156
|
-
expiresAt,
|
|
5157
|
-
timestamp,
|
|
5158
|
-
timestamp
|
|
5159
|
-
]);
|
|
5160
|
-
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
5161
|
-
for (const tag of tags) {
|
|
5162
|
-
insertTag.run(id, tag);
|
|
5163
|
-
}
|
|
5164
|
-
const memory = getMemory(id, d);
|
|
5165
|
-
try {
|
|
5166
|
-
runEntityExtraction(memory, input.project_id, d);
|
|
5167
|
-
} catch {}
|
|
5168
|
-
return memory;
|
|
5095
|
+
d.run("INSERT INTO projects (id, name, path, description, memory_prefix, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, name, path, description || null, memoryPrefix || null, timestamp, timestamp]);
|
|
5096
|
+
return getProject(id, d);
|
|
5169
5097
|
}
|
|
5170
|
-
function
|
|
5098
|
+
function getProject(idOrPath, db) {
|
|
5171
5099
|
const d = db || getDatabase();
|
|
5172
|
-
|
|
5173
|
-
if (
|
|
5174
|
-
return
|
|
5175
|
-
|
|
5100
|
+
let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
|
|
5101
|
+
if (row)
|
|
5102
|
+
return parseProjectRow(row);
|
|
5103
|
+
row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
|
|
5104
|
+
if (row)
|
|
5105
|
+
return parseProjectRow(row);
|
|
5106
|
+
row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
|
|
5107
|
+
if (row)
|
|
5108
|
+
return parseProjectRow(row);
|
|
5109
|
+
return null;
|
|
5176
5110
|
}
|
|
5177
|
-
function
|
|
5111
|
+
function listProjects(db) {
|
|
5178
5112
|
const d = db || getDatabase();
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
if (scope) {
|
|
5182
|
-
sql += " AND scope = ?";
|
|
5183
|
-
params.push(scope);
|
|
5184
|
-
}
|
|
5185
|
-
if (agentId) {
|
|
5186
|
-
sql += " AND agent_id = ?";
|
|
5187
|
-
params.push(agentId);
|
|
5188
|
-
}
|
|
5189
|
-
if (projectId) {
|
|
5190
|
-
sql += " AND project_id = ?";
|
|
5191
|
-
params.push(projectId);
|
|
5192
|
-
}
|
|
5193
|
-
if (sessionId) {
|
|
5194
|
-
sql += " AND session_id = ?";
|
|
5195
|
-
params.push(sessionId);
|
|
5196
|
-
}
|
|
5197
|
-
sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
|
|
5198
|
-
const row = d.query(sql).get(...params);
|
|
5199
|
-
if (!row)
|
|
5200
|
-
return null;
|
|
5201
|
-
return parseMemoryRow(row);
|
|
5113
|
+
const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
|
|
5114
|
+
return rows.map(parseProjectRow);
|
|
5202
5115
|
}
|
|
5203
|
-
|
|
5116
|
+
|
|
5117
|
+
// src/db/entities.ts
|
|
5118
|
+
function parseEntityRow(row) {
|
|
5119
|
+
return {
|
|
5120
|
+
id: row["id"],
|
|
5121
|
+
name: row["name"],
|
|
5122
|
+
type: row["type"],
|
|
5123
|
+
description: row["description"] || null,
|
|
5124
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
5125
|
+
project_id: row["project_id"] || null,
|
|
5126
|
+
created_at: row["created_at"],
|
|
5127
|
+
updated_at: row["updated_at"]
|
|
5128
|
+
};
|
|
5129
|
+
}
|
|
5130
|
+
function createEntity(input, db) {
|
|
5204
5131
|
const d = db || getDatabase();
|
|
5205
|
-
const
|
|
5206
|
-
const
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
}
|
|
5216
|
-
}
|
|
5217
|
-
if (filter.category) {
|
|
5218
|
-
if (Array.isArray(filter.category)) {
|
|
5219
|
-
conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
|
|
5220
|
-
params.push(...filter.category);
|
|
5221
|
-
} else {
|
|
5222
|
-
conditions.push("category = ?");
|
|
5223
|
-
params.push(filter.category);
|
|
5224
|
-
}
|
|
5225
|
-
}
|
|
5226
|
-
if (filter.source) {
|
|
5227
|
-
if (Array.isArray(filter.source)) {
|
|
5228
|
-
conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
|
|
5229
|
-
params.push(...filter.source);
|
|
5230
|
-
} else {
|
|
5231
|
-
conditions.push("source = ?");
|
|
5232
|
-
params.push(filter.source);
|
|
5233
|
-
}
|
|
5234
|
-
}
|
|
5235
|
-
if (filter.status) {
|
|
5236
|
-
if (Array.isArray(filter.status)) {
|
|
5237
|
-
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
5238
|
-
params.push(...filter.status);
|
|
5239
|
-
} else {
|
|
5240
|
-
conditions.push("status = ?");
|
|
5241
|
-
params.push(filter.status);
|
|
5242
|
-
}
|
|
5243
|
-
} else {
|
|
5244
|
-
conditions.push("status = 'active'");
|
|
5245
|
-
}
|
|
5246
|
-
if (filter.project_id) {
|
|
5247
|
-
conditions.push("project_id = ?");
|
|
5248
|
-
params.push(filter.project_id);
|
|
5249
|
-
}
|
|
5250
|
-
if (filter.agent_id) {
|
|
5251
|
-
conditions.push("agent_id = ?");
|
|
5252
|
-
params.push(filter.agent_id);
|
|
5253
|
-
}
|
|
5254
|
-
if (filter.session_id) {
|
|
5255
|
-
conditions.push("session_id = ?");
|
|
5256
|
-
params.push(filter.session_id);
|
|
5257
|
-
}
|
|
5258
|
-
if (filter.min_importance) {
|
|
5259
|
-
conditions.push("importance >= ?");
|
|
5260
|
-
params.push(filter.min_importance);
|
|
5261
|
-
}
|
|
5262
|
-
if (filter.pinned !== undefined) {
|
|
5263
|
-
conditions.push("pinned = ?");
|
|
5264
|
-
params.push(filter.pinned ? 1 : 0);
|
|
5265
|
-
}
|
|
5266
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
5267
|
-
for (const tag of filter.tags) {
|
|
5268
|
-
conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
5269
|
-
params.push(tag);
|
|
5270
|
-
}
|
|
5132
|
+
const timestamp = now();
|
|
5133
|
+
const metadataJson = JSON.stringify(input.metadata || {});
|
|
5134
|
+
const existing = d.query(`SELECT * FROM entities
|
|
5135
|
+
WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
|
|
5136
|
+
if (existing) {
|
|
5137
|
+
const sets = ["updated_at = ?"];
|
|
5138
|
+
const params = [timestamp];
|
|
5139
|
+
if (input.description !== undefined) {
|
|
5140
|
+
sets.push("description = ?");
|
|
5141
|
+
params.push(input.description);
|
|
5271
5142
|
}
|
|
5272
|
-
if (
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
params.push(term, term, term);
|
|
5143
|
+
if (input.metadata !== undefined) {
|
|
5144
|
+
sets.push("metadata = ?");
|
|
5145
|
+
params.push(metadataJson);
|
|
5276
5146
|
}
|
|
5277
|
-
|
|
5278
|
-
|
|
5147
|
+
const existingId = existing["id"];
|
|
5148
|
+
params.push(existingId);
|
|
5149
|
+
d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
5150
|
+
return getEntity(existingId, d);
|
|
5151
|
+
}
|
|
5152
|
+
const id = shortUuid();
|
|
5153
|
+
d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
|
|
5154
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5155
|
+
id,
|
|
5156
|
+
input.name,
|
|
5157
|
+
input.type,
|
|
5158
|
+
input.description || null,
|
|
5159
|
+
metadataJson,
|
|
5160
|
+
input.project_id || null,
|
|
5161
|
+
timestamp,
|
|
5162
|
+
timestamp
|
|
5163
|
+
]);
|
|
5164
|
+
return getEntity(id, d);
|
|
5165
|
+
}
|
|
5166
|
+
function getEntity(id, db) {
|
|
5167
|
+
const d = db || getDatabase();
|
|
5168
|
+
const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
|
|
5169
|
+
if (!row)
|
|
5170
|
+
throw new EntityNotFoundError(id);
|
|
5171
|
+
return parseEntityRow(row);
|
|
5172
|
+
}
|
|
5173
|
+
function getEntityByName(name, type, projectId, db) {
|
|
5174
|
+
const d = db || getDatabase();
|
|
5175
|
+
let sql = "SELECT * FROM entities WHERE name = ?";
|
|
5176
|
+
const params = [name];
|
|
5177
|
+
if (type) {
|
|
5178
|
+
sql += " AND type = ?";
|
|
5179
|
+
params.push(type);
|
|
5279
5180
|
}
|
|
5280
|
-
|
|
5181
|
+
if (projectId !== undefined) {
|
|
5182
|
+
sql += " AND project_id = ?";
|
|
5183
|
+
params.push(projectId);
|
|
5184
|
+
}
|
|
5185
|
+
sql += " LIMIT 1";
|
|
5186
|
+
const row = d.query(sql).get(...params);
|
|
5187
|
+
if (!row)
|
|
5188
|
+
return null;
|
|
5189
|
+
return parseEntityRow(row);
|
|
5190
|
+
}
|
|
5191
|
+
function listEntities(filter = {}, db) {
|
|
5192
|
+
const d = db || getDatabase();
|
|
5193
|
+
const conditions = [];
|
|
5194
|
+
const params = [];
|
|
5195
|
+
if (filter.type) {
|
|
5196
|
+
conditions.push("type = ?");
|
|
5197
|
+
params.push(filter.type);
|
|
5198
|
+
}
|
|
5199
|
+
if (filter.project_id) {
|
|
5200
|
+
conditions.push("project_id = ?");
|
|
5201
|
+
params.push(filter.project_id);
|
|
5202
|
+
}
|
|
5203
|
+
if (filter.search) {
|
|
5204
|
+
conditions.push("(name LIKE ? OR description LIKE ?)");
|
|
5205
|
+
const term = `%${filter.search}%`;
|
|
5206
|
+
params.push(term, term);
|
|
5207
|
+
}
|
|
5208
|
+
let sql = "SELECT * FROM entities";
|
|
5281
5209
|
if (conditions.length > 0) {
|
|
5282
5210
|
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
5283
5211
|
}
|
|
5284
|
-
sql += " ORDER BY
|
|
5285
|
-
if (filter
|
|
5212
|
+
sql += " ORDER BY updated_at DESC";
|
|
5213
|
+
if (filter.limit) {
|
|
5286
5214
|
sql += " LIMIT ?";
|
|
5287
5215
|
params.push(filter.limit);
|
|
5288
5216
|
}
|
|
5289
|
-
if (filter
|
|
5217
|
+
if (filter.offset) {
|
|
5290
5218
|
sql += " OFFSET ?";
|
|
5291
5219
|
params.push(filter.offset);
|
|
5292
5220
|
}
|
|
5293
5221
|
const rows = d.query(sql).all(...params);
|
|
5294
|
-
return rows.map(
|
|
5222
|
+
return rows.map(parseEntityRow);
|
|
5295
5223
|
}
|
|
5296
|
-
function
|
|
5224
|
+
function updateEntity(id, input, db) {
|
|
5297
5225
|
const d = db || getDatabase();
|
|
5298
|
-
const existing =
|
|
5226
|
+
const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
|
|
5299
5227
|
if (!existing)
|
|
5300
|
-
throw new
|
|
5301
|
-
|
|
5302
|
-
throw new VersionConflictError(id, input.version, existing.version);
|
|
5303
|
-
}
|
|
5304
|
-
try {
|
|
5305
|
-
d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
|
|
5306
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5307
|
-
uuid(),
|
|
5308
|
-
existing.id,
|
|
5309
|
-
existing.version,
|
|
5310
|
-
existing.value,
|
|
5311
|
-
existing.importance,
|
|
5312
|
-
existing.scope,
|
|
5313
|
-
existing.category,
|
|
5314
|
-
JSON.stringify(existing.tags),
|
|
5315
|
-
existing.summary,
|
|
5316
|
-
existing.pinned ? 1 : 0,
|
|
5317
|
-
existing.status,
|
|
5318
|
-
existing.updated_at
|
|
5319
|
-
]);
|
|
5320
|
-
} catch {}
|
|
5321
|
-
const sets = ["version = version + 1", "updated_at = ?"];
|
|
5228
|
+
throw new EntityNotFoundError(id);
|
|
5229
|
+
const sets = ["updated_at = ?"];
|
|
5322
5230
|
const params = [now()];
|
|
5323
|
-
if (input.
|
|
5324
|
-
sets.push("
|
|
5325
|
-
params.push(
|
|
5326
|
-
}
|
|
5327
|
-
if (input.category !== undefined) {
|
|
5328
|
-
sets.push("category = ?");
|
|
5329
|
-
params.push(input.category);
|
|
5330
|
-
}
|
|
5331
|
-
if (input.scope !== undefined) {
|
|
5332
|
-
sets.push("scope = ?");
|
|
5333
|
-
params.push(input.scope);
|
|
5334
|
-
}
|
|
5335
|
-
if (input.summary !== undefined) {
|
|
5336
|
-
sets.push("summary = ?");
|
|
5337
|
-
params.push(input.summary);
|
|
5338
|
-
}
|
|
5339
|
-
if (input.importance !== undefined) {
|
|
5340
|
-
sets.push("importance = ?");
|
|
5341
|
-
params.push(input.importance);
|
|
5231
|
+
if (input.name !== undefined) {
|
|
5232
|
+
sets.push("name = ?");
|
|
5233
|
+
params.push(input.name);
|
|
5342
5234
|
}
|
|
5343
|
-
if (input.
|
|
5344
|
-
sets.push("
|
|
5345
|
-
params.push(input.
|
|
5235
|
+
if (input.type !== undefined) {
|
|
5236
|
+
sets.push("type = ?");
|
|
5237
|
+
params.push(input.type);
|
|
5346
5238
|
}
|
|
5347
|
-
if (input.
|
|
5348
|
-
sets.push("
|
|
5349
|
-
params.push(input.
|
|
5239
|
+
if (input.description !== undefined) {
|
|
5240
|
+
sets.push("description = ?");
|
|
5241
|
+
params.push(input.description);
|
|
5350
5242
|
}
|
|
5351
5243
|
if (input.metadata !== undefined) {
|
|
5352
5244
|
sets.push("metadata = ?");
|
|
5353
5245
|
params.push(JSON.stringify(input.metadata));
|
|
5354
5246
|
}
|
|
5355
|
-
if (input.expires_at !== undefined) {
|
|
5356
|
-
sets.push("expires_at = ?");
|
|
5357
|
-
params.push(input.expires_at);
|
|
5358
|
-
}
|
|
5359
|
-
if (input.tags !== undefined) {
|
|
5360
|
-
sets.push("tags = ?");
|
|
5361
|
-
params.push(JSON.stringify(input.tags));
|
|
5362
|
-
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
|
|
5363
|
-
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
5364
|
-
for (const tag of input.tags) {
|
|
5365
|
-
insertTag.run(id, tag);
|
|
5366
|
-
}
|
|
5367
|
-
}
|
|
5368
5247
|
params.push(id);
|
|
5369
|
-
d.run(`UPDATE
|
|
5370
|
-
|
|
5371
|
-
try {
|
|
5372
|
-
if (input.value !== undefined) {
|
|
5373
|
-
const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
|
|
5374
|
-
for (const link of oldLinks) {
|
|
5375
|
-
unlinkEntityFromMemory(link.entity_id, updated.id, d);
|
|
5376
|
-
}
|
|
5377
|
-
runEntityExtraction(updated, existing.project_id || undefined, d);
|
|
5378
|
-
}
|
|
5379
|
-
} catch {}
|
|
5380
|
-
return updated;
|
|
5248
|
+
d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
5249
|
+
return getEntity(id, d);
|
|
5381
5250
|
}
|
|
5382
|
-
function
|
|
5251
|
+
function deleteEntity(id, db) {
|
|
5383
5252
|
const d = db || getDatabase();
|
|
5384
|
-
const result = d.run("DELETE FROM
|
|
5385
|
-
|
|
5253
|
+
const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
|
|
5254
|
+
if (result.changes === 0)
|
|
5255
|
+
throw new EntityNotFoundError(id);
|
|
5386
5256
|
}
|
|
5387
|
-
function
|
|
5257
|
+
function mergeEntities(sourceId, targetId, db) {
|
|
5388
5258
|
const d = db || getDatabase();
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5259
|
+
getEntity(sourceId, d);
|
|
5260
|
+
getEntity(targetId, d);
|
|
5261
|
+
d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
|
|
5262
|
+
d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
|
|
5263
|
+
d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
|
|
5264
|
+
sourceId,
|
|
5265
|
+
sourceId
|
|
5266
|
+
]);
|
|
5267
|
+
d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
|
|
5268
|
+
d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
|
|
5269
|
+
d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
|
|
5270
|
+
d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
|
|
5271
|
+
return getEntity(targetId, d);
|
|
5398
5272
|
}
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5273
|
+
|
|
5274
|
+
// src/db/relations.ts
|
|
5275
|
+
function parseRelationRow(row) {
|
|
5276
|
+
return {
|
|
5277
|
+
id: row["id"],
|
|
5278
|
+
source_entity_id: row["source_entity_id"],
|
|
5279
|
+
target_entity_id: row["target_entity_id"],
|
|
5280
|
+
relation_type: row["relation_type"],
|
|
5281
|
+
weight: row["weight"],
|
|
5282
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
5283
|
+
created_at: row["created_at"]
|
|
5284
|
+
};
|
|
5402
5285
|
}
|
|
5403
|
-
function
|
|
5286
|
+
function parseEntityRow2(row) {
|
|
5287
|
+
return {
|
|
5288
|
+
id: row["id"],
|
|
5289
|
+
name: row["name"],
|
|
5290
|
+
type: row["type"],
|
|
5291
|
+
description: row["description"] || null,
|
|
5292
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
5293
|
+
project_id: row["project_id"] || null,
|
|
5294
|
+
created_at: row["created_at"],
|
|
5295
|
+
updated_at: row["updated_at"]
|
|
5296
|
+
};
|
|
5297
|
+
}
|
|
5298
|
+
function createRelation(input, db) {
|
|
5404
5299
|
const d = db || getDatabase();
|
|
5300
|
+
const id = shortUuid();
|
|
5405
5301
|
const timestamp = now();
|
|
5406
|
-
const
|
|
5407
|
-
const
|
|
5408
|
-
|
|
5409
|
-
|
|
5302
|
+
const weight = input.weight ?? 1;
|
|
5303
|
+
const metadata = JSON.stringify(input.metadata ?? {});
|
|
5304
|
+
d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
|
|
5305
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
5306
|
+
ON CONFLICT(source_entity_id, target_entity_id, relation_type)
|
|
5307
|
+
DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
|
|
5308
|
+
const row = d.query(`SELECT * FROM relations
|
|
5309
|
+
WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
|
|
5310
|
+
return parseRelationRow(row);
|
|
5311
|
+
}
|
|
5312
|
+
function getRelation(id, db) {
|
|
5313
|
+
const d = db || getDatabase();
|
|
5314
|
+
const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
|
|
5315
|
+
if (!row)
|
|
5316
|
+
throw new Error(`Relation not found: ${id}`);
|
|
5317
|
+
return parseRelationRow(row);
|
|
5318
|
+
}
|
|
5319
|
+
function listRelations(filter, db) {
|
|
5320
|
+
const d = db || getDatabase();
|
|
5321
|
+
const conditions = [];
|
|
5322
|
+
const params = [];
|
|
5323
|
+
if (filter.entity_id) {
|
|
5324
|
+
const dir = filter.direction || "both";
|
|
5325
|
+
if (dir === "outgoing") {
|
|
5326
|
+
conditions.push("source_entity_id = ?");
|
|
5327
|
+
params.push(filter.entity_id);
|
|
5328
|
+
} else if (dir === "incoming") {
|
|
5329
|
+
conditions.push("target_entity_id = ?");
|
|
5330
|
+
params.push(filter.entity_id);
|
|
5331
|
+
} else {
|
|
5332
|
+
conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
|
|
5333
|
+
params.push(filter.entity_id, filter.entity_id);
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
if (filter.relation_type) {
|
|
5337
|
+
conditions.push("relation_type = ?");
|
|
5338
|
+
params.push(filter.relation_type);
|
|
5339
|
+
}
|
|
5340
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
5341
|
+
const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
|
|
5342
|
+
return rows.map(parseRelationRow);
|
|
5343
|
+
}
|
|
5344
|
+
function deleteRelation(id, db) {
|
|
5345
|
+
const d = db || getDatabase();
|
|
5346
|
+
const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
|
|
5347
|
+
if (result.changes === 0)
|
|
5348
|
+
throw new Error(`Relation not found: ${id}`);
|
|
5349
|
+
}
|
|
5350
|
+
function getEntityGraph(entityId, depth = 2, db) {
|
|
5351
|
+
const d = db || getDatabase();
|
|
5352
|
+
const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
|
|
5353
|
+
VALUES(?, 0)
|
|
5354
|
+
UNION
|
|
5355
|
+
SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
|
|
5356
|
+
FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
|
|
5357
|
+
WHERE g.depth < ?
|
|
5358
|
+
)
|
|
5359
|
+
SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
|
|
5360
|
+
const entities = entityRows.map(parseEntityRow2);
|
|
5361
|
+
const entityIds = new Set(entities.map((e) => e.id));
|
|
5362
|
+
if (entityIds.size === 0) {
|
|
5363
|
+
return { entities: [], relations: [] };
|
|
5410
5364
|
}
|
|
5411
|
-
|
|
5365
|
+
const placeholders = Array.from(entityIds).map(() => "?").join(",");
|
|
5366
|
+
const relationRows = d.query(`SELECT * FROM relations
|
|
5367
|
+
WHERE source_entity_id IN (${placeholders})
|
|
5368
|
+
AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
|
|
5369
|
+
const relations = relationRows.map(parseRelationRow);
|
|
5370
|
+
return { entities, relations };
|
|
5412
5371
|
}
|
|
5413
|
-
function
|
|
5372
|
+
function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
|
|
5414
5373
|
const d = db || getDatabase();
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5374
|
+
const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
|
|
5375
|
+
SELECT ?, ?, 0
|
|
5376
|
+
UNION
|
|
5377
|
+
SELECT
|
|
5378
|
+
CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
|
|
5379
|
+
p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
|
|
5380
|
+
p.depth + 1
|
|
5381
|
+
FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
|
|
5382
|
+
WHERE p.depth < ?
|
|
5383
|
+
AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
|
|
5384
|
+
)
|
|
5385
|
+
SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
|
|
5386
|
+
if (!rows)
|
|
5387
|
+
return null;
|
|
5388
|
+
const ids = rows.trail.split(",");
|
|
5389
|
+
const entities = [];
|
|
5390
|
+
for (const id of ids) {
|
|
5391
|
+
const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
|
|
5392
|
+
if (row)
|
|
5393
|
+
entities.push(parseEntityRow2(row));
|
|
5433
5394
|
}
|
|
5395
|
+
return entities.length > 0 ? entities : null;
|
|
5434
5396
|
}
|
|
5435
5397
|
|
|
5436
5398
|
// src/lib/search.ts
|
|
@@ -5954,103 +5916,795 @@ function searchMemories(query, filter, db) {
|
|
|
5954
5916
|
return b.memory.importance - a.memory.importance;
|
|
5955
5917
|
});
|
|
5956
5918
|
}
|
|
5957
|
-
const offset = filter?.offset ?? 0;
|
|
5958
|
-
const limit = filter?.limit ?? scored.length;
|
|
5959
|
-
const finalResults = scored.slice(offset, offset + limit);
|
|
5960
|
-
|
|
5961
|
-
|
|
5919
|
+
const offset = filter?.offset ?? 0;
|
|
5920
|
+
const limit = filter?.limit ?? scored.length;
|
|
5921
|
+
const finalResults = scored.slice(offset, offset + limit);
|
|
5922
|
+
if (finalResults.length > 0 && scored.length > 0) {
|
|
5923
|
+
const topScore = scored[0]?.score ?? 0;
|
|
5924
|
+
const secondScore = scored[1]?.score ?? 0;
|
|
5925
|
+
const confidence = topScore > 0 ? Math.max(0, Math.min(1, (topScore - secondScore) / topScore)) : 0;
|
|
5926
|
+
finalResults[0] = { ...finalResults[0], confidence };
|
|
5927
|
+
}
|
|
5928
|
+
logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
|
|
5929
|
+
return finalResults;
|
|
5930
|
+
}
|
|
5931
|
+
function logSearchQuery(query, resultCount, agentId, projectId, db) {
|
|
5932
|
+
try {
|
|
5933
|
+
const d = db || getDatabase();
|
|
5934
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
5935
|
+
d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
|
|
5936
|
+
} catch {}
|
|
5937
|
+
}
|
|
5938
|
+
|
|
5939
|
+
// src/lib/project-detect.ts
|
|
5940
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5941
|
+
import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
5942
|
+
function findGitRoot2(startDir) {
|
|
5943
|
+
let dir = resolve2(startDir);
|
|
5944
|
+
while (true) {
|
|
5945
|
+
if (existsSync2(join2(dir, ".git")))
|
|
5946
|
+
return dir;
|
|
5947
|
+
const parent = dirname2(dir);
|
|
5948
|
+
if (parent === dir)
|
|
5949
|
+
break;
|
|
5950
|
+
dir = parent;
|
|
5951
|
+
}
|
|
5952
|
+
return null;
|
|
5953
|
+
}
|
|
5954
|
+
var _cachedProject = undefined;
|
|
5955
|
+
function detectProject(db) {
|
|
5956
|
+
if (_cachedProject !== undefined)
|
|
5957
|
+
return _cachedProject;
|
|
5958
|
+
const d = db || getDatabase();
|
|
5959
|
+
const cwd = process.cwd();
|
|
5960
|
+
const gitRoot = findGitRoot2(cwd);
|
|
5961
|
+
if (!gitRoot) {
|
|
5962
|
+
_cachedProject = null;
|
|
5963
|
+
return null;
|
|
5964
|
+
}
|
|
5965
|
+
const repoName = basename(gitRoot);
|
|
5966
|
+
const absPath = resolve2(gitRoot);
|
|
5967
|
+
const existing = getProject(absPath, d);
|
|
5968
|
+
if (existing) {
|
|
5969
|
+
_cachedProject = existing;
|
|
5970
|
+
return existing;
|
|
5971
|
+
}
|
|
5972
|
+
const project = registerProject(repoName, absPath, undefined, undefined, d);
|
|
5973
|
+
_cachedProject = project;
|
|
5974
|
+
return project;
|
|
5975
|
+
}
|
|
5976
|
+
|
|
5977
|
+
// src/lib/duration.ts
|
|
5978
|
+
var UNIT_MS = {
|
|
5979
|
+
s: 1000,
|
|
5980
|
+
m: 60000,
|
|
5981
|
+
h: 3600000,
|
|
5982
|
+
d: 86400000,
|
|
5983
|
+
w: 604800000
|
|
5984
|
+
};
|
|
5985
|
+
var DURATION_RE = /^(\d+[smhdw])+$/;
|
|
5986
|
+
var SEGMENT_RE = /(\d+)([smhdw])/g;
|
|
5987
|
+
function parseDuration(input) {
|
|
5988
|
+
if (typeof input === "number")
|
|
5989
|
+
return input;
|
|
5990
|
+
const trimmed = input.trim();
|
|
5991
|
+
if (trimmed === "")
|
|
5992
|
+
throw new Error("Invalid duration: empty string");
|
|
5993
|
+
if (/^\d+$/.test(trimmed)) {
|
|
5994
|
+
return parseInt(trimmed, 10);
|
|
5995
|
+
}
|
|
5996
|
+
if (!DURATION_RE.test(trimmed)) {
|
|
5997
|
+
throw new Error(`Invalid duration format: "${trimmed}". Use combinations of Ns, Nm, Nh, Nd, Nw (e.g. "1d12h", "30m") or plain milliseconds.`);
|
|
5998
|
+
}
|
|
5999
|
+
let total = 0;
|
|
6000
|
+
let match;
|
|
6001
|
+
SEGMENT_RE.lastIndex = 0;
|
|
6002
|
+
while ((match = SEGMENT_RE.exec(trimmed)) !== null) {
|
|
6003
|
+
const value = parseInt(match[1], 10);
|
|
6004
|
+
const unit = match[2];
|
|
6005
|
+
total += value * UNIT_MS[unit];
|
|
6006
|
+
}
|
|
6007
|
+
if (total === 0) {
|
|
6008
|
+
throw new Error(`Invalid duration: "${trimmed}" resolves to 0ms`);
|
|
6009
|
+
}
|
|
6010
|
+
return total;
|
|
6011
|
+
}
|
|
6012
|
+
var FORMAT_UNITS = [
|
|
6013
|
+
["w", UNIT_MS["w"]],
|
|
6014
|
+
["d", UNIT_MS["d"]],
|
|
6015
|
+
["h", UNIT_MS["h"]],
|
|
6016
|
+
["m", UNIT_MS["m"]],
|
|
6017
|
+
["s", UNIT_MS["s"]]
|
|
6018
|
+
];
|
|
6019
|
+
|
|
6020
|
+
// src/mcp/index.ts
|
|
6021
|
+
import { createRequire } from "module";
|
|
6022
|
+
|
|
6023
|
+
// src/lib/providers/base.ts
|
|
6024
|
+
var DEFAULT_AUTO_MEMORY_CONFIG = {
|
|
6025
|
+
provider: "anthropic",
|
|
6026
|
+
model: "claude-haiku-4-5",
|
|
6027
|
+
enabled: true,
|
|
6028
|
+
minImportance: 4,
|
|
6029
|
+
autoEntityLink: true,
|
|
6030
|
+
fallback: ["cerebras", "openai"]
|
|
6031
|
+
};
|
|
6032
|
+
|
|
6033
|
+
class BaseProvider {
|
|
6034
|
+
config;
|
|
6035
|
+
constructor(config) {
|
|
6036
|
+
this.config = config;
|
|
6037
|
+
}
|
|
6038
|
+
parseJSON(raw) {
|
|
6039
|
+
try {
|
|
6040
|
+
const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim();
|
|
6041
|
+
return JSON.parse(cleaned);
|
|
6042
|
+
} catch {
|
|
6043
|
+
return null;
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
clampImportance(value) {
|
|
6047
|
+
const n = Number(value);
|
|
6048
|
+
if (isNaN(n))
|
|
6049
|
+
return 5;
|
|
6050
|
+
return Math.max(0, Math.min(10, Math.round(n)));
|
|
6051
|
+
}
|
|
6052
|
+
normaliseMemory(raw) {
|
|
6053
|
+
if (!raw || typeof raw !== "object")
|
|
6054
|
+
return null;
|
|
6055
|
+
const m = raw;
|
|
6056
|
+
if (typeof m.content !== "string" || !m.content.trim())
|
|
6057
|
+
return null;
|
|
6058
|
+
const validScopes = ["private", "shared", "global"];
|
|
6059
|
+
const validCategories = [
|
|
6060
|
+
"preference",
|
|
6061
|
+
"fact",
|
|
6062
|
+
"knowledge",
|
|
6063
|
+
"history"
|
|
6064
|
+
];
|
|
6065
|
+
return {
|
|
6066
|
+
content: m.content.trim(),
|
|
6067
|
+
category: validCategories.includes(m.category) ? m.category : "knowledge",
|
|
6068
|
+
importance: this.clampImportance(m.importance),
|
|
6069
|
+
tags: Array.isArray(m.tags) ? m.tags.filter((t) => typeof t === "string").map((t) => t.toLowerCase()) : [],
|
|
6070
|
+
suggestedScope: validScopes.includes(m.suggestedScope) ? m.suggestedScope : "shared",
|
|
6071
|
+
reasoning: typeof m.reasoning === "string" ? m.reasoning : undefined
|
|
6072
|
+
};
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
var MEMORY_EXTRACTION_SYSTEM_PROMPT = `You are a precise memory extraction engine for an AI agent.
|
|
6076
|
+
Given text, extract facts worth remembering as structured JSON.
|
|
6077
|
+
Focus on: decisions made, preferences revealed, corrections, architectural choices, established facts, user preferences.
|
|
6078
|
+
Ignore: greetings, filler, questions without answers, temporary states.
|
|
6079
|
+
Output ONLY a JSON array \u2014 no markdown, no explanation.`;
|
|
6080
|
+
var MEMORY_EXTRACTION_USER_TEMPLATE = (text, context) => `Extract memories from this text.
|
|
6081
|
+
${context.projectName ? `Project: ${context.projectName}` : ""}
|
|
6082
|
+
${context.existingMemoriesSummary ? `Existing memories (avoid duplicates):
|
|
6083
|
+
${context.existingMemoriesSummary}` : ""}
|
|
6084
|
+
|
|
6085
|
+
Text:
|
|
6086
|
+
${text}
|
|
6087
|
+
|
|
6088
|
+
Return a JSON array of objects with these exact fields:
|
|
6089
|
+
- content: string (the memory, concise and specific)
|
|
6090
|
+
- category: "preference" | "fact" | "knowledge" | "history"
|
|
6091
|
+
- importance: number 0-10 (10 = critical, 0 = trivial)
|
|
6092
|
+
- tags: string[] (lowercase keywords)
|
|
6093
|
+
- suggestedScope: "private" | "shared" | "global"
|
|
6094
|
+
- reasoning: string (one sentence why this is worth remembering)
|
|
6095
|
+
|
|
6096
|
+
Return [] if nothing is worth remembering.`;
|
|
6097
|
+
var ENTITY_EXTRACTION_SYSTEM_PROMPT = `You are a knowledge graph entity extractor.
|
|
6098
|
+
Given text, identify named entities and their relationships.
|
|
6099
|
+
Output ONLY valid JSON \u2014 no markdown, no explanation.`;
|
|
6100
|
+
var ENTITY_EXTRACTION_USER_TEMPLATE = (text) => `Extract entities and relations from this text.
|
|
6101
|
+
|
|
6102
|
+
Text: ${text}
|
|
6103
|
+
|
|
6104
|
+
Return JSON with this exact shape:
|
|
6105
|
+
{
|
|
6106
|
+
"entities": [
|
|
6107
|
+
{ "name": string, "type": "person"|"project"|"tool"|"concept"|"file"|"api"|"pattern"|"organization", "confidence": 0-1 }
|
|
6108
|
+
],
|
|
6109
|
+
"relations": [
|
|
6110
|
+
{ "from": string, "to": string, "type": "uses"|"knows"|"depends_on"|"created_by"|"related_to"|"contradicts"|"part_of"|"implements" }
|
|
6111
|
+
]
|
|
6112
|
+
}`;
|
|
6113
|
+
|
|
6114
|
+
// src/lib/providers/anthropic.ts
|
|
6115
|
+
var ANTHROPIC_MODELS = {
|
|
6116
|
+
default: "claude-haiku-4-5",
|
|
6117
|
+
premium: "claude-sonnet-4-5"
|
|
6118
|
+
};
|
|
6119
|
+
|
|
6120
|
+
class AnthropicProvider extends BaseProvider {
|
|
6121
|
+
name = "anthropic";
|
|
6122
|
+
baseUrl = "https://api.anthropic.com/v1";
|
|
6123
|
+
constructor(config) {
|
|
6124
|
+
const apiKey = config?.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
|
|
6125
|
+
super({
|
|
6126
|
+
apiKey,
|
|
6127
|
+
model: config?.model ?? ANTHROPIC_MODELS.default,
|
|
6128
|
+
maxTokens: config?.maxTokens ?? 1024,
|
|
6129
|
+
temperature: config?.temperature ?? 0,
|
|
6130
|
+
timeoutMs: config?.timeoutMs ?? 15000
|
|
6131
|
+
});
|
|
6132
|
+
}
|
|
6133
|
+
async extractMemories(text, context) {
|
|
6134
|
+
if (!this.config.apiKey)
|
|
6135
|
+
return [];
|
|
6136
|
+
try {
|
|
6137
|
+
const response = await this.callAPI(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
|
|
6138
|
+
const parsed = this.parseJSON(response);
|
|
6139
|
+
if (!Array.isArray(parsed))
|
|
6140
|
+
return [];
|
|
6141
|
+
return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
|
|
6142
|
+
} catch (err) {
|
|
6143
|
+
console.error("[anthropic] extractMemories failed:", err);
|
|
6144
|
+
return [];
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
async extractEntities(text) {
|
|
6148
|
+
const empty = { entities: [], relations: [] };
|
|
6149
|
+
if (!this.config.apiKey)
|
|
6150
|
+
return empty;
|
|
6151
|
+
try {
|
|
6152
|
+
const response = await this.callAPI(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
|
|
6153
|
+
const parsed = this.parseJSON(response);
|
|
6154
|
+
if (!parsed || typeof parsed !== "object")
|
|
6155
|
+
return empty;
|
|
6156
|
+
return {
|
|
6157
|
+
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
6158
|
+
relations: Array.isArray(parsed.relations) ? parsed.relations : []
|
|
6159
|
+
};
|
|
6160
|
+
} catch (err) {
|
|
6161
|
+
console.error("[anthropic] extractEntities failed:", err);
|
|
6162
|
+
return empty;
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
async scoreImportance(content, _context) {
|
|
6166
|
+
if (!this.config.apiKey)
|
|
6167
|
+
return 5;
|
|
6168
|
+
try {
|
|
6169
|
+
const response = await this.callAPI("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
|
|
6170
|
+
|
|
6171
|
+
"${content}"
|
|
6172
|
+
|
|
6173
|
+
Return only a number 0-10.`);
|
|
6174
|
+
return this.clampImportance(response.trim());
|
|
6175
|
+
} catch {
|
|
6176
|
+
return 5;
|
|
6177
|
+
}
|
|
6178
|
+
}
|
|
6179
|
+
async callAPI(systemPrompt, userMessage) {
|
|
6180
|
+
const controller = new AbortController;
|
|
6181
|
+
const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
|
|
6182
|
+
try {
|
|
6183
|
+
const res = await fetch(`${this.baseUrl}/messages`, {
|
|
6184
|
+
method: "POST",
|
|
6185
|
+
headers: {
|
|
6186
|
+
"Content-Type": "application/json",
|
|
6187
|
+
"x-api-key": this.config.apiKey,
|
|
6188
|
+
"anthropic-version": "2023-06-01"
|
|
6189
|
+
},
|
|
6190
|
+
body: JSON.stringify({
|
|
6191
|
+
model: this.config.model,
|
|
6192
|
+
max_tokens: this.config.maxTokens ?? 1024,
|
|
6193
|
+
temperature: this.config.temperature ?? 0,
|
|
6194
|
+
system: systemPrompt,
|
|
6195
|
+
messages: [{ role: "user", content: userMessage }]
|
|
6196
|
+
}),
|
|
6197
|
+
signal: controller.signal
|
|
6198
|
+
});
|
|
6199
|
+
if (!res.ok) {
|
|
6200
|
+
const body = await res.text().catch(() => "");
|
|
6201
|
+
throw new Error(`Anthropic API ${res.status}: ${body.slice(0, 200)}`);
|
|
6202
|
+
}
|
|
6203
|
+
const data = await res.json();
|
|
6204
|
+
return data.content?.[0]?.text ?? "";
|
|
6205
|
+
} finally {
|
|
6206
|
+
clearTimeout(timeout);
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
}
|
|
6210
|
+
|
|
6211
|
+
// src/lib/providers/openai-compat.ts
|
|
6212
|
+
class OpenAICompatProvider extends BaseProvider {
|
|
6213
|
+
constructor(config) {
|
|
6214
|
+
super(config);
|
|
6215
|
+
}
|
|
6216
|
+
async extractMemories(text, context) {
|
|
6217
|
+
if (!this.config.apiKey)
|
|
6218
|
+
return [];
|
|
6219
|
+
try {
|
|
6220
|
+
const response = await this.callWithRetry(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
|
|
6221
|
+
const parsed = this.parseJSON(response);
|
|
6222
|
+
if (!Array.isArray(parsed))
|
|
6223
|
+
return [];
|
|
6224
|
+
return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
|
|
6225
|
+
} catch (err) {
|
|
6226
|
+
console.error(`[${this.name}] extractMemories failed:`, err);
|
|
6227
|
+
return [];
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
async extractEntities(text) {
|
|
6231
|
+
const empty = { entities: [], relations: [] };
|
|
6232
|
+
if (!this.config.apiKey)
|
|
6233
|
+
return empty;
|
|
6234
|
+
try {
|
|
6235
|
+
const response = await this.callWithRetry(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
|
|
6236
|
+
const parsed = this.parseJSON(response);
|
|
6237
|
+
if (!parsed || typeof parsed !== "object")
|
|
6238
|
+
return empty;
|
|
6239
|
+
return {
|
|
6240
|
+
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
6241
|
+
relations: Array.isArray(parsed.relations) ? parsed.relations : []
|
|
6242
|
+
};
|
|
6243
|
+
} catch (err) {
|
|
6244
|
+
console.error(`[${this.name}] extractEntities failed:`, err);
|
|
6245
|
+
return empty;
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
6248
|
+
async scoreImportance(content, _context) {
|
|
6249
|
+
if (!this.config.apiKey)
|
|
6250
|
+
return 5;
|
|
6251
|
+
try {
|
|
6252
|
+
const response = await this.callWithRetry("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
|
|
6253
|
+
|
|
6254
|
+
"${content}"
|
|
6255
|
+
|
|
6256
|
+
Return only a number 0-10.`);
|
|
6257
|
+
return this.clampImportance(response.trim());
|
|
6258
|
+
} catch {
|
|
6259
|
+
return 5;
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6262
|
+
async callWithRetry(systemPrompt, userMessage, retries = 3) {
|
|
6263
|
+
let lastError = null;
|
|
6264
|
+
for (let attempt = 0;attempt < retries; attempt++) {
|
|
6265
|
+
try {
|
|
6266
|
+
return await this.callAPI(systemPrompt, userMessage);
|
|
6267
|
+
} catch (err) {
|
|
6268
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
6269
|
+
const isRateLimit = lastError.message.includes("429") || lastError.message.toLowerCase().includes("rate limit");
|
|
6270
|
+
if (!isRateLimit || attempt === retries - 1)
|
|
6271
|
+
throw lastError;
|
|
6272
|
+
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
throw lastError ?? new Error("Unknown error");
|
|
6276
|
+
}
|
|
6277
|
+
async callAPI(systemPrompt, userMessage) {
|
|
6278
|
+
const controller = new AbortController;
|
|
6279
|
+
const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
|
|
6280
|
+
try {
|
|
6281
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
6282
|
+
method: "POST",
|
|
6283
|
+
headers: {
|
|
6284
|
+
"Content-Type": "application/json",
|
|
6285
|
+
[this.authHeader]: `Bearer ${this.config.apiKey}`
|
|
6286
|
+
},
|
|
6287
|
+
body: JSON.stringify({
|
|
6288
|
+
model: this.config.model,
|
|
6289
|
+
max_tokens: this.config.maxTokens ?? 1024,
|
|
6290
|
+
temperature: this.config.temperature ?? 0,
|
|
6291
|
+
messages: [
|
|
6292
|
+
{ role: "system", content: systemPrompt },
|
|
6293
|
+
{ role: "user", content: userMessage }
|
|
6294
|
+
]
|
|
6295
|
+
}),
|
|
6296
|
+
signal: controller.signal
|
|
6297
|
+
});
|
|
6298
|
+
if (!res.ok) {
|
|
6299
|
+
const body = await res.text().catch(() => "");
|
|
6300
|
+
throw new Error(`${this.name} API ${res.status}: ${body.slice(0, 200)}`);
|
|
6301
|
+
}
|
|
6302
|
+
const data = await res.json();
|
|
6303
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
6304
|
+
} finally {
|
|
6305
|
+
clearTimeout(timeout);
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
}
|
|
6309
|
+
|
|
6310
|
+
// src/lib/providers/openai.ts
|
|
6311
|
+
var OPENAI_MODELS = {
|
|
6312
|
+
default: "gpt-4.1-nano",
|
|
6313
|
+
mini: "gpt-4.1-mini",
|
|
6314
|
+
full: "gpt-4.1"
|
|
6315
|
+
};
|
|
6316
|
+
|
|
6317
|
+
class OpenAIProvider extends OpenAICompatProvider {
|
|
6318
|
+
name = "openai";
|
|
6319
|
+
baseUrl = "https://api.openai.com/v1";
|
|
6320
|
+
authHeader = "Authorization";
|
|
6321
|
+
constructor(config) {
|
|
6322
|
+
super({
|
|
6323
|
+
apiKey: config?.apiKey ?? process.env.OPENAI_API_KEY ?? "",
|
|
6324
|
+
model: config?.model ?? OPENAI_MODELS.default,
|
|
6325
|
+
maxTokens: config?.maxTokens ?? 1024,
|
|
6326
|
+
temperature: config?.temperature ?? 0,
|
|
6327
|
+
timeoutMs: config?.timeoutMs ?? 15000
|
|
6328
|
+
});
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
|
|
6332
|
+
// src/lib/providers/cerebras.ts
|
|
6333
|
+
var CEREBRAS_MODELS = {
|
|
6334
|
+
default: "llama-3.3-70b",
|
|
6335
|
+
fast: "llama3.1-8b"
|
|
6336
|
+
};
|
|
6337
|
+
|
|
6338
|
+
class CerebrasProvider extends OpenAICompatProvider {
|
|
6339
|
+
name = "cerebras";
|
|
6340
|
+
baseUrl = "https://api.cerebras.ai/v1";
|
|
6341
|
+
authHeader = "Authorization";
|
|
6342
|
+
constructor(config) {
|
|
6343
|
+
super({
|
|
6344
|
+
apiKey: config?.apiKey ?? process.env.CEREBRAS_API_KEY ?? "",
|
|
6345
|
+
model: config?.model ?? CEREBRAS_MODELS.default,
|
|
6346
|
+
maxTokens: config?.maxTokens ?? 1024,
|
|
6347
|
+
temperature: config?.temperature ?? 0,
|
|
6348
|
+
timeoutMs: config?.timeoutMs ?? 1e4
|
|
6349
|
+
});
|
|
6350
|
+
}
|
|
6351
|
+
}
|
|
6352
|
+
|
|
6353
|
+
// src/lib/providers/grok.ts
|
|
6354
|
+
var GROK_MODELS = {
|
|
6355
|
+
default: "grok-3-mini",
|
|
6356
|
+
premium: "grok-3"
|
|
6357
|
+
};
|
|
6358
|
+
|
|
6359
|
+
class GrokProvider extends OpenAICompatProvider {
|
|
6360
|
+
name = "grok";
|
|
6361
|
+
baseUrl = "https://api.x.ai/v1";
|
|
6362
|
+
authHeader = "Authorization";
|
|
6363
|
+
constructor(config) {
|
|
6364
|
+
super({
|
|
6365
|
+
apiKey: config?.apiKey ?? process.env.XAI_API_KEY ?? "",
|
|
6366
|
+
model: config?.model ?? GROK_MODELS.default,
|
|
6367
|
+
maxTokens: config?.maxTokens ?? 1024,
|
|
6368
|
+
temperature: config?.temperature ?? 0,
|
|
6369
|
+
timeoutMs: config?.timeoutMs ?? 15000
|
|
6370
|
+
});
|
|
6371
|
+
}
|
|
6372
|
+
}
|
|
6373
|
+
|
|
6374
|
+
// src/lib/providers/registry.ts
|
|
6375
|
+
class ProviderRegistry {
|
|
6376
|
+
config = { ...DEFAULT_AUTO_MEMORY_CONFIG };
|
|
6377
|
+
_instances = new Map;
|
|
6378
|
+
configure(partial) {
|
|
6379
|
+
this.config = { ...this.config, ...partial };
|
|
6380
|
+
this._instances.clear();
|
|
6381
|
+
}
|
|
6382
|
+
getConfig() {
|
|
6383
|
+
return this.config;
|
|
6384
|
+
}
|
|
6385
|
+
getPrimary() {
|
|
6386
|
+
return this.getProvider(this.config.provider);
|
|
6387
|
+
}
|
|
6388
|
+
getFallbacks() {
|
|
6389
|
+
const fallbackNames = this.config.fallback ?? [];
|
|
6390
|
+
return fallbackNames.filter((n) => n !== this.config.provider).map((n) => this.getProvider(n)).filter((p) => p !== null);
|
|
6391
|
+
}
|
|
6392
|
+
getAvailable() {
|
|
6393
|
+
const primary = this.getPrimary();
|
|
6394
|
+
if (primary)
|
|
6395
|
+
return primary;
|
|
6396
|
+
const fallbacks = this.getFallbacks();
|
|
6397
|
+
return fallbacks[0] ?? null;
|
|
6398
|
+
}
|
|
6399
|
+
getProvider(name) {
|
|
6400
|
+
const cached = this._instances.get(name);
|
|
6401
|
+
if (cached)
|
|
6402
|
+
return cached;
|
|
6403
|
+
const provider = this.createProvider(name);
|
|
6404
|
+
if (!provider)
|
|
6405
|
+
return null;
|
|
6406
|
+
if (!provider.config.apiKey)
|
|
6407
|
+
return null;
|
|
6408
|
+
this._instances.set(name, provider);
|
|
6409
|
+
return provider;
|
|
6410
|
+
}
|
|
6411
|
+
health() {
|
|
6412
|
+
const providers = ["anthropic", "openai", "cerebras", "grok"];
|
|
6413
|
+
const result = {};
|
|
6414
|
+
for (const name of providers) {
|
|
6415
|
+
const p = this.createProvider(name);
|
|
6416
|
+
result[name] = {
|
|
6417
|
+
available: Boolean(p?.config.apiKey),
|
|
6418
|
+
model: p?.config.model ?? "unknown"
|
|
6419
|
+
};
|
|
6420
|
+
}
|
|
6421
|
+
return result;
|
|
6422
|
+
}
|
|
6423
|
+
createProvider(name) {
|
|
6424
|
+
const modelOverride = name === this.config.provider ? this.config.model : undefined;
|
|
6425
|
+
switch (name) {
|
|
6426
|
+
case "anthropic":
|
|
6427
|
+
return new AnthropicProvider(modelOverride ? { model: modelOverride } : undefined);
|
|
6428
|
+
case "openai":
|
|
6429
|
+
return new OpenAIProvider(modelOverride ? { model: modelOverride } : undefined);
|
|
6430
|
+
case "cerebras":
|
|
6431
|
+
return new CerebrasProvider(modelOverride ? { model: modelOverride } : undefined);
|
|
6432
|
+
case "grok":
|
|
6433
|
+
return new GrokProvider(modelOverride ? { model: modelOverride } : undefined);
|
|
6434
|
+
default:
|
|
6435
|
+
return null;
|
|
6436
|
+
}
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
var providerRegistry = new ProviderRegistry;
|
|
6440
|
+
function autoConfigureFromEnv() {
|
|
6441
|
+
const hasAnthropicKey = Boolean(process.env.ANTHROPIC_API_KEY);
|
|
6442
|
+
const hasCerebrasKey = Boolean(process.env.CEREBRAS_API_KEY);
|
|
6443
|
+
const hasOpenAIKey = Boolean(process.env.OPENAI_API_KEY);
|
|
6444
|
+
const hasGrokKey = Boolean(process.env.XAI_API_KEY);
|
|
6445
|
+
if (!hasAnthropicKey) {
|
|
6446
|
+
if (hasCerebrasKey) {
|
|
6447
|
+
providerRegistry.configure({ provider: "cerebras" });
|
|
6448
|
+
} else if (hasOpenAIKey) {
|
|
6449
|
+
providerRegistry.configure({ provider: "openai" });
|
|
6450
|
+
} else if (hasGrokKey) {
|
|
6451
|
+
providerRegistry.configure({ provider: "grok" });
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6454
|
+
const allProviders = ["anthropic", "cerebras", "openai", "grok"];
|
|
6455
|
+
const available = allProviders.filter((p) => {
|
|
6456
|
+
switch (p) {
|
|
6457
|
+
case "anthropic":
|
|
6458
|
+
return hasAnthropicKey;
|
|
6459
|
+
case "cerebras":
|
|
6460
|
+
return hasCerebrasKey;
|
|
6461
|
+
case "openai":
|
|
6462
|
+
return hasOpenAIKey;
|
|
6463
|
+
case "grok":
|
|
6464
|
+
return hasGrokKey;
|
|
6465
|
+
}
|
|
6466
|
+
});
|
|
6467
|
+
const primary = providerRegistry.getConfig().provider;
|
|
6468
|
+
const fallback = available.filter((p) => p !== primary);
|
|
6469
|
+
providerRegistry.configure({ fallback });
|
|
6470
|
+
}
|
|
6471
|
+
autoConfigureFromEnv();
|
|
6472
|
+
|
|
6473
|
+
// src/lib/auto-memory-queue.ts
|
|
6474
|
+
var MAX_QUEUE_SIZE = 100;
|
|
6475
|
+
var CONCURRENCY = 3;
|
|
6476
|
+
|
|
6477
|
+
class AutoMemoryQueue {
|
|
6478
|
+
queue = [];
|
|
6479
|
+
handler = null;
|
|
6480
|
+
running = false;
|
|
6481
|
+
activeCount = 0;
|
|
6482
|
+
stats = {
|
|
6483
|
+
pending: 0,
|
|
6484
|
+
processing: 0,
|
|
6485
|
+
processed: 0,
|
|
6486
|
+
failed: 0,
|
|
6487
|
+
dropped: 0
|
|
6488
|
+
};
|
|
6489
|
+
setHandler(handler) {
|
|
6490
|
+
this.handler = handler;
|
|
6491
|
+
if (!this.running)
|
|
6492
|
+
this.startLoop();
|
|
6493
|
+
}
|
|
6494
|
+
enqueue(job) {
|
|
6495
|
+
if (this.queue.length >= MAX_QUEUE_SIZE) {
|
|
6496
|
+
this.queue.shift();
|
|
6497
|
+
this.stats.dropped++;
|
|
6498
|
+
this.stats.pending = Math.max(0, this.stats.pending - 1);
|
|
6499
|
+
}
|
|
6500
|
+
this.queue.push(job);
|
|
6501
|
+
this.stats.pending++;
|
|
6502
|
+
if (!this.running && this.handler)
|
|
6503
|
+
this.startLoop();
|
|
6504
|
+
}
|
|
6505
|
+
getStats() {
|
|
6506
|
+
return { ...this.stats, pending: this.queue.length };
|
|
6507
|
+
}
|
|
6508
|
+
startLoop() {
|
|
6509
|
+
this.running = true;
|
|
6510
|
+
this.loop();
|
|
6511
|
+
}
|
|
6512
|
+
async loop() {
|
|
6513
|
+
while (this.queue.length > 0 || this.activeCount > 0) {
|
|
6514
|
+
while (this.queue.length > 0 && this.activeCount < CONCURRENCY) {
|
|
6515
|
+
const job = this.queue.shift();
|
|
6516
|
+
if (!job)
|
|
6517
|
+
break;
|
|
6518
|
+
this.stats.pending = Math.max(0, this.stats.pending - 1);
|
|
6519
|
+
this.activeCount++;
|
|
6520
|
+
this.stats.processing = this.activeCount;
|
|
6521
|
+
this.processJob(job);
|
|
6522
|
+
}
|
|
6523
|
+
await new Promise((r) => setImmediate(r));
|
|
6524
|
+
}
|
|
6525
|
+
this.running = false;
|
|
6526
|
+
}
|
|
6527
|
+
async processJob(job) {
|
|
6528
|
+
if (!this.handler) {
|
|
6529
|
+
this.activeCount--;
|
|
6530
|
+
this.stats.processing = this.activeCount;
|
|
6531
|
+
return;
|
|
6532
|
+
}
|
|
6533
|
+
try {
|
|
6534
|
+
await this.handler(job);
|
|
6535
|
+
this.stats.processed++;
|
|
6536
|
+
} catch (err) {
|
|
6537
|
+
this.stats.failed++;
|
|
6538
|
+
console.error("[auto-memory-queue] job failed:", err);
|
|
6539
|
+
} finally {
|
|
6540
|
+
this.activeCount--;
|
|
6541
|
+
this.stats.processing = this.activeCount;
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
5962
6544
|
}
|
|
5963
|
-
|
|
6545
|
+
var autoMemoryQueue = new AutoMemoryQueue;
|
|
6546
|
+
|
|
6547
|
+
// src/lib/auto-memory.ts
|
|
6548
|
+
var DEDUP_SIMILARITY_THRESHOLD = 0.85;
|
|
6549
|
+
function isDuplicate(content, agentId, projectId) {
|
|
5964
6550
|
try {
|
|
5965
|
-
const
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
6551
|
+
const query = content.split(/\s+/).filter((w) => w.length > 3).slice(0, 10).join(" ");
|
|
6552
|
+
if (!query)
|
|
6553
|
+
return false;
|
|
6554
|
+
const results = searchMemories(query, {
|
|
6555
|
+
agent_id: agentId,
|
|
6556
|
+
project_id: projectId,
|
|
6557
|
+
limit: 3
|
|
6558
|
+
});
|
|
6559
|
+
if (results.length === 0)
|
|
6560
|
+
return false;
|
|
6561
|
+
const contentWords = new Set(content.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
|
|
6562
|
+
for (const result of results) {
|
|
6563
|
+
const existingWords = new Set(result.memory.value.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
|
|
6564
|
+
if (contentWords.size === 0 || existingWords.size === 0)
|
|
6565
|
+
continue;
|
|
6566
|
+
const intersection = [...contentWords].filter((w) => existingWords.has(w)).length;
|
|
6567
|
+
const union = new Set([...contentWords, ...existingWords]).size;
|
|
6568
|
+
const similarity = intersection / union;
|
|
6569
|
+
if (similarity >= DEDUP_SIMILARITY_THRESHOLD)
|
|
6570
|
+
return true;
|
|
6571
|
+
}
|
|
6572
|
+
return false;
|
|
6573
|
+
} catch {
|
|
6574
|
+
return false;
|
|
6575
|
+
}
|
|
5969
6576
|
}
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
6577
|
+
async function linkEntitiesToMemory(memoryId, content, _agentId, projectId) {
|
|
6578
|
+
const provider = providerRegistry.getAvailable();
|
|
6579
|
+
if (!provider)
|
|
6580
|
+
return;
|
|
6581
|
+
try {
|
|
6582
|
+
const { entities, relations } = await provider.extractEntities(content);
|
|
6583
|
+
const entityIdMap = new Map;
|
|
6584
|
+
for (const extracted of entities) {
|
|
6585
|
+
if (extracted.confidence < 0.6)
|
|
6586
|
+
continue;
|
|
6587
|
+
try {
|
|
6588
|
+
const existing = getEntityByName(extracted.name);
|
|
6589
|
+
const entityId = existing ? existing.id : createEntity({
|
|
6590
|
+
name: extracted.name,
|
|
6591
|
+
type: extracted.type,
|
|
6592
|
+
project_id: projectId
|
|
6593
|
+
}).id;
|
|
6594
|
+
entityIdMap.set(extracted.name, entityId);
|
|
6595
|
+
linkEntityToMemory(entityId, memoryId, "subject");
|
|
6596
|
+
} catch {}
|
|
6597
|
+
}
|
|
6598
|
+
for (const rel of relations) {
|
|
6599
|
+
const fromId = entityIdMap.get(rel.from);
|
|
6600
|
+
const toId = entityIdMap.get(rel.to);
|
|
6601
|
+
if (!fromId || !toId)
|
|
6602
|
+
continue;
|
|
6603
|
+
try {
|
|
6604
|
+
createRelation({
|
|
6605
|
+
source_entity_id: fromId,
|
|
6606
|
+
target_entity_id: toId,
|
|
6607
|
+
relation_type: rel.type
|
|
6608
|
+
});
|
|
6609
|
+
} catch {}
|
|
6610
|
+
}
|
|
6611
|
+
} catch (err) {
|
|
6612
|
+
console.error("[auto-memory] entity linking failed:", err);
|
|
5983
6613
|
}
|
|
5984
|
-
return null;
|
|
5985
6614
|
}
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
if (
|
|
5989
|
-
return
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
if (!gitRoot) {
|
|
5994
|
-
_cachedProject = null;
|
|
6615
|
+
async function saveExtractedMemory(extracted, context) {
|
|
6616
|
+
const minImportance = providerRegistry.getConfig().minImportance;
|
|
6617
|
+
if (extracted.importance < minImportance)
|
|
6618
|
+
return null;
|
|
6619
|
+
if (!extracted.content.trim())
|
|
6620
|
+
return null;
|
|
6621
|
+
if (isDuplicate(extracted.content, context.agentId, context.projectId)) {
|
|
5995
6622
|
return null;
|
|
5996
6623
|
}
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6624
|
+
try {
|
|
6625
|
+
const input = {
|
|
6626
|
+
key: extracted.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
|
|
6627
|
+
value: extracted.content,
|
|
6628
|
+
category: extracted.category,
|
|
6629
|
+
scope: extracted.suggestedScope,
|
|
6630
|
+
importance: extracted.importance,
|
|
6631
|
+
tags: [
|
|
6632
|
+
...extracted.tags,
|
|
6633
|
+
"auto-extracted",
|
|
6634
|
+
...context.sessionId ? [`session:${context.sessionId}`] : []
|
|
6635
|
+
],
|
|
6636
|
+
agent_id: context.agentId,
|
|
6637
|
+
project_id: context.projectId,
|
|
6638
|
+
session_id: context.sessionId,
|
|
6639
|
+
metadata: {
|
|
6640
|
+
reasoning: extracted.reasoning,
|
|
6641
|
+
auto_extracted: true,
|
|
6642
|
+
extracted_at: new Date().toISOString()
|
|
6643
|
+
}
|
|
6644
|
+
};
|
|
6645
|
+
const memory = createMemory(input, "merge");
|
|
6646
|
+
return memory.id;
|
|
6647
|
+
} catch (err) {
|
|
6648
|
+
console.error("[auto-memory] saveExtractedMemory failed:", err);
|
|
6649
|
+
return null;
|
|
6003
6650
|
}
|
|
6004
|
-
const project = registerProject(repoName, absPath, undefined, undefined, d);
|
|
6005
|
-
_cachedProject = project;
|
|
6006
|
-
return project;
|
|
6007
6651
|
}
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
SEGMENT_RE.lastIndex = 0;
|
|
6034
|
-
while ((match = SEGMENT_RE.exec(trimmed)) !== null) {
|
|
6035
|
-
const value = parseInt(match[1], 10);
|
|
6036
|
-
const unit = match[2];
|
|
6037
|
-
total += value * UNIT_MS[unit];
|
|
6652
|
+
async function processJob(job) {
|
|
6653
|
+
if (!providerRegistry.getConfig().enabled)
|
|
6654
|
+
return;
|
|
6655
|
+
const provider = providerRegistry.getAvailable();
|
|
6656
|
+
if (!provider)
|
|
6657
|
+
return;
|
|
6658
|
+
const context = {
|
|
6659
|
+
agentId: job.agentId,
|
|
6660
|
+
projectId: job.projectId,
|
|
6661
|
+
sessionId: job.sessionId
|
|
6662
|
+
};
|
|
6663
|
+
let extracted = [];
|
|
6664
|
+
try {
|
|
6665
|
+
extracted = await provider.extractMemories(job.turn, context);
|
|
6666
|
+
} catch {
|
|
6667
|
+
const fallbacks = providerRegistry.getFallbacks();
|
|
6668
|
+
for (const fallback of fallbacks) {
|
|
6669
|
+
try {
|
|
6670
|
+
extracted = await fallback.extractMemories(job.turn, context);
|
|
6671
|
+
if (extracted.length > 0)
|
|
6672
|
+
break;
|
|
6673
|
+
} catch {
|
|
6674
|
+
continue;
|
|
6675
|
+
}
|
|
6676
|
+
}
|
|
6038
6677
|
}
|
|
6039
|
-
if (
|
|
6040
|
-
|
|
6678
|
+
if (extracted.length === 0)
|
|
6679
|
+
return;
|
|
6680
|
+
for (const memory of extracted) {
|
|
6681
|
+
const memoryId = await saveExtractedMemory(memory, context);
|
|
6682
|
+
if (!memoryId)
|
|
6683
|
+
continue;
|
|
6684
|
+
if (providerRegistry.getConfig().autoEntityLink) {
|
|
6685
|
+
linkEntitiesToMemory(memoryId, memory.content, job.agentId, job.projectId);
|
|
6686
|
+
}
|
|
6041
6687
|
}
|
|
6042
|
-
return total;
|
|
6043
6688
|
}
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6689
|
+
autoMemoryQueue.setHandler(processJob);
|
|
6690
|
+
function processConversationTurn(turn, context, source = "turn") {
|
|
6691
|
+
if (!turn?.trim())
|
|
6692
|
+
return;
|
|
6693
|
+
autoMemoryQueue.enqueue({
|
|
6694
|
+
...context,
|
|
6695
|
+
turn,
|
|
6696
|
+
timestamp: Date.now(),
|
|
6697
|
+
source
|
|
6698
|
+
});
|
|
6699
|
+
}
|
|
6700
|
+
function getAutoMemoryStats() {
|
|
6701
|
+
return autoMemoryQueue.getStats();
|
|
6702
|
+
}
|
|
6703
|
+
function configureAutoMemory(config) {
|
|
6704
|
+
providerRegistry.configure(config);
|
|
6705
|
+
}
|
|
6051
6706
|
|
|
6052
6707
|
// src/mcp/index.ts
|
|
6053
|
-
import { createRequire } from "module";
|
|
6054
6708
|
var _require = createRequire(import.meta.url);
|
|
6055
6709
|
var _pkg = _require("../../package.json");
|
|
6056
6710
|
var server = new McpServer({
|
|
@@ -6141,6 +6795,11 @@ server.tool("memory_save", "Save/upsert a memory. scope: global=all agents, shar
|
|
|
6141
6795
|
if (args.ttl_ms !== undefined) {
|
|
6142
6796
|
input.ttl_ms = parseDuration(args.ttl_ms);
|
|
6143
6797
|
}
|
|
6798
|
+
if (!input.project_id && input.agent_id) {
|
|
6799
|
+
const focusedProject = resolveProjectId(input.agent_id, null);
|
|
6800
|
+
if (focusedProject)
|
|
6801
|
+
input.project_id = focusedProject;
|
|
6802
|
+
}
|
|
6144
6803
|
const memory = createMemory(input);
|
|
6145
6804
|
if (args.agent_id)
|
|
6146
6805
|
touchAgent(args.agent_id);
|
|
@@ -6158,7 +6817,11 @@ server.tool("memory_recall", "Recall a memory by key. Returns the best matching
|
|
|
6158
6817
|
}, async (args) => {
|
|
6159
6818
|
try {
|
|
6160
6819
|
ensureAutoProject();
|
|
6161
|
-
|
|
6820
|
+
let effectiveProjectId = args.project_id;
|
|
6821
|
+
if (!args.scope && !args.project_id && args.agent_id) {
|
|
6822
|
+
effectiveProjectId = resolveProjectId(args.agent_id, null) ?? undefined;
|
|
6823
|
+
}
|
|
6824
|
+
const memory = getMemoryByKey(args.key, args.scope, args.agent_id, effectiveProjectId, args.session_id);
|
|
6162
6825
|
if (memory) {
|
|
6163
6826
|
touchMemory(memory.id);
|
|
6164
6827
|
if (args.agent_id)
|
|
@@ -6168,7 +6831,7 @@ server.tool("memory_recall", "Recall a memory by key. Returns the best matching
|
|
|
6168
6831
|
const results = searchMemories(args.key, {
|
|
6169
6832
|
scope: args.scope,
|
|
6170
6833
|
agent_id: args.agent_id,
|
|
6171
|
-
project_id:
|
|
6834
|
+
project_id: effectiveProjectId,
|
|
6172
6835
|
session_id: args.session_id,
|
|
6173
6836
|
limit: 1
|
|
6174
6837
|
});
|
|
@@ -6249,9 +6912,15 @@ server.tool("memory_list", "List memories. Default: compact lines. full=true for
|
|
|
6249
6912
|
}, async (args) => {
|
|
6250
6913
|
try {
|
|
6251
6914
|
const { full, fields, ...filterArgs } = args;
|
|
6915
|
+
let resolvedFilter = { ...filterArgs };
|
|
6916
|
+
if (!resolvedFilter.scope && !resolvedFilter.project_id && resolvedFilter.agent_id) {
|
|
6917
|
+
const focusedProject = resolveProjectId(resolvedFilter.agent_id, null);
|
|
6918
|
+
if (focusedProject)
|
|
6919
|
+
resolvedFilter.project_id = focusedProject;
|
|
6920
|
+
}
|
|
6252
6921
|
const filter = {
|
|
6253
|
-
...
|
|
6254
|
-
limit:
|
|
6922
|
+
...resolvedFilter,
|
|
6923
|
+
limit: resolvedFilter.limit || 10
|
|
6255
6924
|
};
|
|
6256
6925
|
const memories = listMemories(filter);
|
|
6257
6926
|
if (memories.length === 0) {
|
|
@@ -6415,12 +7084,16 @@ server.tool("memory_search", "Search memories by keyword across key, value, summ
|
|
|
6415
7084
|
limit: exports_external.coerce.number().optional()
|
|
6416
7085
|
}, async (args) => {
|
|
6417
7086
|
try {
|
|
7087
|
+
let effectiveProjectId = args.project_id;
|
|
7088
|
+
if (!args.scope && !args.project_id && args.agent_id) {
|
|
7089
|
+
effectiveProjectId = resolveProjectId(args.agent_id, null) ?? undefined;
|
|
7090
|
+
}
|
|
6418
7091
|
const filter = {
|
|
6419
7092
|
scope: args.scope,
|
|
6420
7093
|
category: args.category,
|
|
6421
7094
|
tags: args.tags,
|
|
6422
7095
|
agent_id: args.agent_id,
|
|
6423
|
-
project_id:
|
|
7096
|
+
project_id: effectiveProjectId,
|
|
6424
7097
|
session_id: args.session_id,
|
|
6425
7098
|
search: args.query,
|
|
6426
7099
|
limit: args.limit || 20
|
|
@@ -6709,11 +7382,13 @@ ${lines.join(`
|
|
|
6709
7382
|
});
|
|
6710
7383
|
server.tool("register_agent", "Register an agent. Idempotent \u2014 same name returns existing agent.", {
|
|
6711
7384
|
name: exports_external.string(),
|
|
7385
|
+
session_id: exports_external.string().optional(),
|
|
6712
7386
|
description: exports_external.string().optional(),
|
|
6713
|
-
role: exports_external.string().optional()
|
|
7387
|
+
role: exports_external.string().optional(),
|
|
7388
|
+
project_id: exports_external.string().optional()
|
|
6714
7389
|
}, async (args) => {
|
|
6715
7390
|
try {
|
|
6716
|
-
const agent = registerAgent(args.name, args.description, args.role);
|
|
7391
|
+
const agent = registerAgent(args.name, args.session_id, args.description, args.role, args.project_id);
|
|
6717
7392
|
return {
|
|
6718
7393
|
content: [{
|
|
6719
7394
|
type: "text",
|
|
@@ -7817,6 +8492,252 @@ server.resource("projects", "mementos://projects", { description: "All registere
|
|
|
7817
8492
|
const projects = listProjects();
|
|
7818
8493
|
return { contents: [{ uri: "mementos://projects", text: JSON.stringify(projects, null, 2), mimeType: "application/json" }] };
|
|
7819
8494
|
});
|
|
8495
|
+
server.tool("memory_lock", "Acquire an exclusive write lock on a memory key to prevent concurrent writes.", {
|
|
8496
|
+
agent_id: exports_external.string(),
|
|
8497
|
+
key: exports_external.string(),
|
|
8498
|
+
scope: exports_external.string().optional().default("shared"),
|
|
8499
|
+
project_id: exports_external.string().optional(),
|
|
8500
|
+
ttl_seconds: exports_external.number().optional().default(30)
|
|
8501
|
+
}, async (args) => {
|
|
8502
|
+
const lock = acquireMemoryWriteLock(args.agent_id, args.key, args.scope, args.project_id, args.ttl_seconds);
|
|
8503
|
+
if (!lock) {
|
|
8504
|
+
const existing = checkMemoryWriteLock(args.key, args.scope, args.project_id);
|
|
8505
|
+
return {
|
|
8506
|
+
content: [{
|
|
8507
|
+
type: "text",
|
|
8508
|
+
text: `Lock conflict: memory key "${args.key}" is write-locked by agent ${existing?.agent_id ?? "unknown"} (expires ${existing?.expires_at ?? "unknown"}). Retry after a few seconds.`
|
|
8509
|
+
}],
|
|
8510
|
+
isError: true
|
|
8511
|
+
};
|
|
8512
|
+
}
|
|
8513
|
+
return {
|
|
8514
|
+
content: [{
|
|
8515
|
+
type: "text",
|
|
8516
|
+
text: `Lock acquired: ${lock.id} on key "${args.key}" (expires ${lock.expires_at})`
|
|
8517
|
+
}]
|
|
8518
|
+
};
|
|
8519
|
+
});
|
|
8520
|
+
server.tool("memory_unlock", "Release a memory write lock.", {
|
|
8521
|
+
lock_id: exports_external.string(),
|
|
8522
|
+
agent_id: exports_external.string()
|
|
8523
|
+
}, async (args) => {
|
|
8524
|
+
const released = releaseMemoryWriteLock(args.lock_id, args.agent_id);
|
|
8525
|
+
return {
|
|
8526
|
+
content: [{
|
|
8527
|
+
type: "text",
|
|
8528
|
+
text: released ? `Lock ${args.lock_id} released.` : `Lock ${args.lock_id} not found or not owned by ${args.agent_id}.`
|
|
8529
|
+
}]
|
|
8530
|
+
};
|
|
8531
|
+
});
|
|
8532
|
+
server.tool("memory_check_lock", "Check if a memory key is currently write-locked.", {
|
|
8533
|
+
key: exports_external.string(),
|
|
8534
|
+
scope: exports_external.string().optional().default("shared"),
|
|
8535
|
+
project_id: exports_external.string().optional()
|
|
8536
|
+
}, async (args) => {
|
|
8537
|
+
const lock = checkMemoryWriteLock(args.key, args.scope, args.project_id);
|
|
8538
|
+
return {
|
|
8539
|
+
content: [{
|
|
8540
|
+
type: "text",
|
|
8541
|
+
text: lock ? `Locked: key "${args.key}" held by agent ${lock.agent_id} (expires ${lock.expires_at})` : `Unlocked: key "${args.key}" is free to write.`
|
|
8542
|
+
}]
|
|
8543
|
+
};
|
|
8544
|
+
});
|
|
8545
|
+
server.tool("resource_lock", "Acquire a lock on any resource (project, memory, entity, agent, connector).", {
|
|
8546
|
+
agent_id: exports_external.string(),
|
|
8547
|
+
resource_type: exports_external.enum(["project", "memory", "entity", "agent", "connector"]),
|
|
8548
|
+
resource_id: exports_external.string(),
|
|
8549
|
+
lock_type: exports_external.enum(["advisory", "exclusive"]).optional().default("exclusive"),
|
|
8550
|
+
ttl_seconds: exports_external.number().optional().default(300)
|
|
8551
|
+
}, async (args) => {
|
|
8552
|
+
const lock = acquireLock(args.agent_id, args.resource_type, args.resource_id, args.lock_type, args.ttl_seconds);
|
|
8553
|
+
if (!lock) {
|
|
8554
|
+
return {
|
|
8555
|
+
content: [{ type: "text", text: `Lock conflict on ${args.resource_type}:${args.resource_id}. Another agent holds an exclusive lock.` }],
|
|
8556
|
+
isError: true
|
|
8557
|
+
};
|
|
8558
|
+
}
|
|
8559
|
+
return {
|
|
8560
|
+
content: [{ type: "text", text: `Lock acquired: ${lock.id} (expires ${lock.expires_at})` }]
|
|
8561
|
+
};
|
|
8562
|
+
});
|
|
8563
|
+
server.tool("resource_unlock", "Release a resource lock.", {
|
|
8564
|
+
lock_id: exports_external.string(),
|
|
8565
|
+
agent_id: exports_external.string()
|
|
8566
|
+
}, async (args) => {
|
|
8567
|
+
const released = releaseLock(args.lock_id, args.agent_id);
|
|
8568
|
+
return {
|
|
8569
|
+
content: [{ type: "text", text: released ? `Released.` : `Not found or not owned.` }]
|
|
8570
|
+
};
|
|
8571
|
+
});
|
|
8572
|
+
server.tool("resource_check_lock", "Check active locks on a resource.", {
|
|
8573
|
+
resource_type: exports_external.enum(["project", "memory", "entity", "agent", "connector"]),
|
|
8574
|
+
resource_id: exports_external.string(),
|
|
8575
|
+
lock_type: exports_external.enum(["advisory", "exclusive"]).optional()
|
|
8576
|
+
}, async (args) => {
|
|
8577
|
+
const locks = checkLock(args.resource_type, args.resource_id, args.lock_type);
|
|
8578
|
+
return {
|
|
8579
|
+
content: [{ type: "text", text: locks.length === 0 ? "No active locks." : JSON.stringify(locks, null, 2) }]
|
|
8580
|
+
};
|
|
8581
|
+
});
|
|
8582
|
+
server.tool("list_agent_locks", "List all active resource locks held by an agent.", { agent_id: exports_external.string() }, async (args) => {
|
|
8583
|
+
const locks = listAgentLocks(args.agent_id);
|
|
8584
|
+
return {
|
|
8585
|
+
content: [{ type: "text", text: locks.length === 0 ? "No active locks." : JSON.stringify(locks, null, 2) }]
|
|
8586
|
+
};
|
|
8587
|
+
});
|
|
8588
|
+
server.tool("clean_expired_locks", "Delete all expired resource locks.", {}, async () => {
|
|
8589
|
+
const count = cleanExpiredLocks();
|
|
8590
|
+
return { content: [{ type: "text", text: `Cleaned ${count} expired lock(s).` }] };
|
|
8591
|
+
});
|
|
8592
|
+
server.tool("set_focus", "Set focus for an agent on a project. Memory ops will auto-scope to that project's shared + agent private + global memories.", {
|
|
8593
|
+
agent_id: exports_external.string(),
|
|
8594
|
+
project_id: exports_external.string().nullable().optional()
|
|
8595
|
+
}, async (args) => {
|
|
8596
|
+
try {
|
|
8597
|
+
const projectId = args.project_id ?? null;
|
|
8598
|
+
setFocus(args.agent_id, projectId);
|
|
8599
|
+
return {
|
|
8600
|
+
content: [{
|
|
8601
|
+
type: "text",
|
|
8602
|
+
text: projectId ? `Focus set: agent ${args.agent_id} is now focused on project ${projectId}. Memory ops will auto-scope.` : `Focus cleared for agent ${args.agent_id}.`
|
|
8603
|
+
}]
|
|
8604
|
+
};
|
|
8605
|
+
} catch (e) {
|
|
8606
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
8607
|
+
}
|
|
8608
|
+
});
|
|
8609
|
+
server.tool("get_focus", "Get the current focus project for an agent.", { agent_id: exports_external.string() }, async (args) => {
|
|
8610
|
+
try {
|
|
8611
|
+
const projectId = getFocus(args.agent_id);
|
|
8612
|
+
return {
|
|
8613
|
+
content: [{
|
|
8614
|
+
type: "text",
|
|
8615
|
+
text: projectId ? `Agent ${args.agent_id} is focused on project: ${projectId}` : `Agent ${args.agent_id} has no active focus.`
|
|
8616
|
+
}]
|
|
8617
|
+
};
|
|
8618
|
+
} catch (e) {
|
|
8619
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
8620
|
+
}
|
|
8621
|
+
});
|
|
8622
|
+
server.tool("unfocus", "Remove focus for an agent (clears project scoping).", { agent_id: exports_external.string() }, async (args) => {
|
|
8623
|
+
try {
|
|
8624
|
+
unfocus(args.agent_id);
|
|
8625
|
+
return {
|
|
8626
|
+
content: [{ type: "text", text: `Focus cleared for agent ${args.agent_id}. Memory ops will no longer auto-scope.` }]
|
|
8627
|
+
};
|
|
8628
|
+
} catch (e) {
|
|
8629
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
8630
|
+
}
|
|
8631
|
+
});
|
|
8632
|
+
server.tool("memory_auto_process", "Enqueue a conversation turn or text for async LLM memory extraction. Returns immediately (non-blocking). Set async=false to run synchronously and return extracted memories.", {
|
|
8633
|
+
turn: exports_external.string().describe("The text / conversation turn to extract memories from"),
|
|
8634
|
+
agent_id: exports_external.string().optional(),
|
|
8635
|
+
project_id: exports_external.string().optional(),
|
|
8636
|
+
session_id: exports_external.string().optional(),
|
|
8637
|
+
async: exports_external.boolean().optional().default(true)
|
|
8638
|
+
}, async (args) => {
|
|
8639
|
+
try {
|
|
8640
|
+
if (args.async !== false) {
|
|
8641
|
+
processConversationTurn(args.turn, {
|
|
8642
|
+
agentId: args.agent_id,
|
|
8643
|
+
projectId: args.project_id,
|
|
8644
|
+
sessionId: args.session_id
|
|
8645
|
+
});
|
|
8646
|
+
const stats = getAutoMemoryStats();
|
|
8647
|
+
return {
|
|
8648
|
+
content: [{
|
|
8649
|
+
type: "text",
|
|
8650
|
+
text: JSON.stringify({ queued: true, queue: stats })
|
|
8651
|
+
}]
|
|
8652
|
+
};
|
|
8653
|
+
}
|
|
8654
|
+
const provider = providerRegistry.getAvailable();
|
|
8655
|
+
if (!provider) {
|
|
8656
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "No LLM provider configured. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, CEREBRAS_API_KEY, or XAI_API_KEY." }) }], isError: true };
|
|
8657
|
+
}
|
|
8658
|
+
const memories = await provider.extractMemories(args.turn, {
|
|
8659
|
+
agentId: args.agent_id,
|
|
8660
|
+
projectId: args.project_id,
|
|
8661
|
+
sessionId: args.session_id
|
|
8662
|
+
});
|
|
8663
|
+
return { content: [{ type: "text", text: JSON.stringify({ extracted: memories, count: memories.length }) }] };
|
|
8664
|
+
} catch (e) {
|
|
8665
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
8666
|
+
}
|
|
8667
|
+
});
|
|
8668
|
+
server.tool("memory_auto_status", "Get the current auto-memory extraction queue stats (pending, processing, processed, failed, dropped).", {}, async () => {
|
|
8669
|
+
try {
|
|
8670
|
+
const stats = getAutoMemoryStats();
|
|
8671
|
+
const config = providerRegistry.getConfig();
|
|
8672
|
+
const health = providerRegistry.health();
|
|
8673
|
+
return {
|
|
8674
|
+
content: [{
|
|
8675
|
+
type: "text",
|
|
8676
|
+
text: JSON.stringify({ queue: stats, config, providers: health })
|
|
8677
|
+
}]
|
|
8678
|
+
};
|
|
8679
|
+
} catch (e) {
|
|
8680
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
8681
|
+
}
|
|
8682
|
+
});
|
|
8683
|
+
server.tool("memory_auto_config", "Update auto-memory configuration at runtime (no restart needed). Set provider, model, enabled, minImportance, autoEntityLink.", {
|
|
8684
|
+
provider: exports_external.enum(["anthropic", "openai", "cerebras", "grok"]).optional(),
|
|
8685
|
+
model: exports_external.string().optional(),
|
|
8686
|
+
enabled: exports_external.boolean().optional(),
|
|
8687
|
+
min_importance: exports_external.number().min(0).max(10).optional(),
|
|
8688
|
+
auto_entity_link: exports_external.boolean().optional()
|
|
8689
|
+
}, async (args) => {
|
|
8690
|
+
try {
|
|
8691
|
+
configureAutoMemory({
|
|
8692
|
+
...args.provider && { provider: args.provider },
|
|
8693
|
+
...args.model && { model: args.model },
|
|
8694
|
+
...args.enabled !== undefined && { enabled: args.enabled },
|
|
8695
|
+
...args.min_importance !== undefined && { minImportance: args.min_importance },
|
|
8696
|
+
...args.auto_entity_link !== undefined && { autoEntityLink: args.auto_entity_link }
|
|
8697
|
+
});
|
|
8698
|
+
return {
|
|
8699
|
+
content: [{
|
|
8700
|
+
type: "text",
|
|
8701
|
+
text: JSON.stringify({ updated: true, config: providerRegistry.getConfig() })
|
|
8702
|
+
}]
|
|
8703
|
+
};
|
|
8704
|
+
} catch (e) {
|
|
8705
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
8706
|
+
}
|
|
8707
|
+
});
|
|
8708
|
+
server.tool("memory_auto_test", "Test memory extraction on text WITHOUT saving anything. Returns what would be extracted. Useful for tuning prompts and checking provider output.", {
|
|
8709
|
+
turn: exports_external.string().describe("Text to test extraction on"),
|
|
8710
|
+
provider: exports_external.enum(["anthropic", "openai", "cerebras", "grok"]).optional(),
|
|
8711
|
+
agent_id: exports_external.string().optional(),
|
|
8712
|
+
project_id: exports_external.string().optional()
|
|
8713
|
+
}, async (args) => {
|
|
8714
|
+
try {
|
|
8715
|
+
let provider;
|
|
8716
|
+
if (args.provider) {
|
|
8717
|
+
provider = providerRegistry.getProvider(args.provider);
|
|
8718
|
+
if (!provider) {
|
|
8719
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `Provider '${args.provider}' not available \u2014 no API key configured.` }) }], isError: true };
|
|
8720
|
+
}
|
|
8721
|
+
} else {
|
|
8722
|
+
provider = providerRegistry.getAvailable();
|
|
8723
|
+
if (!provider) {
|
|
8724
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "No LLM provider configured." }) }], isError: true };
|
|
8725
|
+
}
|
|
8726
|
+
}
|
|
8727
|
+
const memories = await provider.extractMemories(args.turn, {
|
|
8728
|
+
agentId: args.agent_id,
|
|
8729
|
+
projectId: args.project_id
|
|
8730
|
+
});
|
|
8731
|
+
return {
|
|
8732
|
+
content: [{
|
|
8733
|
+
type: "text",
|
|
8734
|
+
text: JSON.stringify({ provider: provider.name, model: provider.config.model, extracted: memories, count: memories.length, note: "DRY RUN \u2014 nothing was saved" })
|
|
8735
|
+
}]
|
|
8736
|
+
};
|
|
8737
|
+
} catch (e) {
|
|
8738
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
8739
|
+
}
|
|
8740
|
+
});
|
|
7820
8741
|
async function main() {
|
|
7821
8742
|
const transport = new StdioServerTransport;
|
|
7822
8743
|
await server.connect(transport);
|