cozo-memory 1.2.6 → 1.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ const emotional_salience_1 = require("./emotional-salience");
24
24
  const proactive_suggestions_1 = require("./proactive-suggestions");
25
25
  const spreading_activation_1 = require("./spreading-activation");
26
26
  const temporal_conflict_resolution_1 = require("./temporal-conflict-resolution");
27
+ const explainable_retrieval_1 = require("./explainable-retrieval");
27
28
  exports.DB_PATH = path_1.default.resolve(__dirname, "..", "memory_db.cozo");
28
29
  const DB_ENGINE = process.env.DB_ENGINE || "sqlite"; // "sqlite" or "rocksdb"
29
30
  exports.USER_ENTITY_ID = "global_user_profile";
@@ -48,6 +49,7 @@ class MemoryServer {
48
49
  logicalEdgesService = null;
49
50
  hierarchicalMemoryService = null;
50
51
  queryAwareTraversal = null;
52
+ explainableService = null;
51
53
  // Metrics tracking
52
54
  metrics = {
53
55
  operations: {
@@ -245,6 +247,12 @@ class MemoryServer {
245
247
  }
246
248
  return this.queryAwareTraversal;
247
249
  }
250
+ getExplainableService() {
251
+ if (!this.explainableService) {
252
+ this.explainableService = new explainable_retrieval_1.ExplainableRetrievalService(this.db, this.embeddingService);
253
+ }
254
+ return this.explainableService;
255
+ }
248
256
  async janitorCleanup(args) {
249
257
  await this.initPromise;
250
258
  console.error(`[Janitor] Starting cleanup (auto-compressing sessions first)...`);
@@ -3158,13 +3166,13 @@ Format MUST start with "ExecutiveSummary: " followed by the consolidated content
3158
3166
  async exportMemory(args) {
3159
3167
  await this.initPromise;
3160
3168
  const dbService = { run: (query, params) => this.db.run(query, params) };
3161
- const exportService = new export_import_service_1.ExportImportService(dbService);
3169
+ const exportService = new export_import_service_1.ExportImportService(dbService, this.embeddingService.getDimensions());
3162
3170
  return exportService.exportMemory(args);
3163
3171
  }
3164
3172
  async importMemory(args) {
3165
3173
  await this.initPromise;
3166
3174
  const dbService = { run: (query, params) => this.db.run(query, params) };
3167
- const exportService = new export_import_service_1.ExportImportService(dbService);
3175
+ const exportService = new export_import_service_1.ExportImportService(dbService, this.embeddingService.getDimensions());
3168
3176
  return exportService.importMemory(args.data, {
3169
3177
  sourceFormat: args.sourceFormat,
3170
3178
  mergeStrategy: args.mergeStrategy,
@@ -3229,6 +3237,609 @@ Format MUST start with "ExecutiveSummary: " followed by the consolidated content
3229
3237
  };
3230
3238
  }
3231
3239
  }
3240
+ // ===========================================================================
3241
+ // Agent Features (query/mutation convenience actions)
3242
+ // ===========================================================================
3243
+ /** Feature 1: List entities with filtering, sorting and pagination. */
3244
+ async listEntities(args) {
3245
+ await this.initPromise;
3246
+ try {
3247
+ const limit = Math.min(Math.max(args.limit ?? 20, 1), 1000);
3248
+ const offset = Math.max(args.offset ?? 0, 0);
3249
+ const sortBy = args.sort_by ?? "created_at";
3250
+ const sortOrder = args.sort_order ?? "desc";
3251
+ const res = await this.db.run(`?[id, name, type, metadata, ts] := *entity{id, name, type, metadata, created_at, @ "NOW"}, ts = to_int(created_at)`);
3252
+ let rows = res.rows.map((r) => ({
3253
+ id: r[0],
3254
+ name: r[1],
3255
+ type: r[2],
3256
+ metadata: (r[3] || {}),
3257
+ created_at_us: r[4],
3258
+ }));
3259
+ // Filters (applied in JS for robustness across value types)
3260
+ if (args.type)
3261
+ rows = rows.filter((e) => e.type === args.type);
3262
+ if (args.types && args.types.length > 0) {
3263
+ const set = new Set(args.types);
3264
+ rows = rows.filter((e) => set.has(e.type));
3265
+ }
3266
+ if (args.name_contains) {
3267
+ const needle = args.name_contains.toLowerCase();
3268
+ rows = rows.filter((e) => String(e.name).toLowerCase().includes(needle));
3269
+ }
3270
+ if (args.tags && args.tags.length > 0) {
3271
+ const want = args.tags;
3272
+ rows = rows.filter((e) => {
3273
+ const t = Array.isArray(e.metadata?.tags) ? e.metadata.tags : [];
3274
+ return want.every((tag) => t.includes(tag));
3275
+ });
3276
+ }
3277
+ const total = rows.length;
3278
+ rows.sort((a, b) => {
3279
+ let cmp = 0;
3280
+ if (sortBy === "name")
3281
+ cmp = String(a.name).localeCompare(String(b.name));
3282
+ else
3283
+ cmp = a.created_at_us - b.created_at_us; // created_at / updated_at both track the validity timestamp
3284
+ return sortOrder === "asc" ? cmp : -cmp;
3285
+ });
3286
+ const page = rows.slice(offset, offset + limit);
3287
+ // Count maps (one query each, regardless of page size)
3288
+ const obsCount = new Map();
3289
+ const obsRes = await this.db.run('?[eid, count(oid)] := *observation{id: oid, entity_id: eid, @ "NOW"}');
3290
+ obsRes.rows.forEach((r) => obsCount.set(r[0], Number(r[1])));
3291
+ const relCount = new Map();
3292
+ const relOutRes = await this.db.run('?[eid, count(t)] := *relationship{from_id: eid, to_id: t, @ "NOW"}');
3293
+ relOutRes.rows.forEach((r) => relCount.set(r[0], Number(r[1])));
3294
+ const relInRes = await this.db.run('?[eid, count(f)] := *relationship{from_id: f, to_id: eid, @ "NOW"}');
3295
+ relInRes.rows.forEach((r) => relCount.set(r[0], (relCount.get(r[0]) || 0) + Number(r[1])));
3296
+ const entities = page.map((e) => ({
3297
+ id: e.id,
3298
+ name: e.name,
3299
+ type: e.type,
3300
+ metadata: e.metadata,
3301
+ tags: Array.isArray(e.metadata?.tags) ? e.metadata.tags : [],
3302
+ created_at: new Date(Math.floor(e.created_at_us / 1000)).toISOString(),
3303
+ observation_count: obsCount.get(e.id) || 0,
3304
+ relation_count: relCount.get(e.id) || 0,
3305
+ }));
3306
+ return { entities, total, offset, limit };
3307
+ }
3308
+ catch (error) {
3309
+ return { error: "Failed to list entities", message: error.message };
3310
+ }
3311
+ }
3312
+ /** Feature 2: Aggregate statistics overview. */
3313
+ async getStats() {
3314
+ await this.initPromise;
3315
+ try {
3316
+ const [eRes, oRes, rRes] = await Promise.all([
3317
+ this.db.run('?[id, type, ts] := *entity{id, type, created_at, @ "NOW"}, ts = to_int(created_at)'),
3318
+ this.db.run('?[count(id)] := *observation{id, @ "NOW"}'),
3319
+ this.db.run('?[count(f)] := *relationship{from_id: f, to_id, @ "NOW"}'),
3320
+ ]);
3321
+ const entities = eRes.rows.map((r) => ({ type: r[1], ts: r[2] }));
3322
+ const total_entities = entities.length;
3323
+ const total_observations = Number(oRes.rows[0]?.[0] || 0);
3324
+ const total_relations = Number(rRes.rows[0]?.[0] || 0);
3325
+ const typeMap = new Map();
3326
+ entities.forEach((e) => typeMap.set(e.type, (typeMap.get(e.type) || 0) + 1));
3327
+ const by_type = [...typeMap.entries()]
3328
+ .map(([type, count]) => ({ type, count }))
3329
+ .sort((a, b) => b.count - a.count);
3330
+ const tss = entities.map((e) => e.ts).filter((n) => typeof n === "number");
3331
+ const nowUs = Date.now() * 1000;
3332
+ const dayUs = 24 * 3600 * 1000 * 1000;
3333
+ const oldest = tss.length ? Math.min(...tss) : null;
3334
+ const newest = tss.length ? Math.max(...tss) : null;
3335
+ return {
3336
+ overview: { total_entities, total_observations, total_relations },
3337
+ by_type,
3338
+ timeline: {
3339
+ oldest_entity: oldest !== null ? new Date(Math.floor(oldest / 1000)).toISOString() : null,
3340
+ newest_entity: newest !== null ? new Date(Math.floor(newest / 1000)).toISOString() : null,
3341
+ entities_last_24h: tss.filter((t) => t >= nowUs - dayUs).length,
3342
+ entities_last_7d: tss.filter((t) => t >= nowUs - 7 * dayUs).length,
3343
+ },
3344
+ activity: {
3345
+ total_operations: this.metrics.performance.total_operations,
3346
+ top_types: by_type.slice(0, 5),
3347
+ },
3348
+ };
3349
+ }
3350
+ catch (error) {
3351
+ return { error: "Failed to compute stats", message: error.message };
3352
+ }
3353
+ }
3354
+ /** Feature 6: Update an observation's text and/or metadata (validity-preserving). */
3355
+ async updateObservation(args) {
3356
+ await this.initPromise;
3357
+ const startTime = Date.now();
3358
+ try {
3359
+ const existing = await this.db.run(`?[oid, eid, sid, tid, txt, emb, meta] := *observation{id: oid, entity_id: eid, session_id: sid, task_id: tid, text: txt, embedding: emb, metadata: meta, @ "NOW"}, oid = $id`, { id: args.observation_id });
3360
+ if (existing.rows.length === 0)
3361
+ return { error: "Observation not found" };
3362
+ const row = existing.rows[0];
3363
+ const prevText = row[4];
3364
+ const newText = args.text !== undefined && args.text !== null ? args.text : prevText;
3365
+ let newMeta;
3366
+ if (args.metadata !== undefined) {
3367
+ newMeta = args.merge_metadata ? { ...(row[6] || {}), ...args.metadata } : args.metadata;
3368
+ }
3369
+ else {
3370
+ newMeta = row[6] || {};
3371
+ }
3372
+ let embedding = row[5];
3373
+ if (args.text !== undefined && args.text !== prevText) {
3374
+ embedding = await this.embeddingService.embed(newText);
3375
+ }
3376
+ const now = Date.now() * 1000;
3377
+ await this.db.run(`
3378
+ ?[id, created_at, entity_id, session_id, task_id, text, embedding, metadata] <- [[$id, $v, $entity_id, $session_id, $task_id, $text, $embedding, $metadata]]
3379
+ :put observation {id, created_at => entity_id, session_id, task_id, text, embedding, metadata}
3380
+ `, {
3381
+ id: row[0],
3382
+ v: [now, true],
3383
+ entity_id: row[1],
3384
+ session_id: row[2],
3385
+ task_id: row[3],
3386
+ text: newText,
3387
+ embedding,
3388
+ metadata: newMeta,
3389
+ });
3390
+ this.trackOperation('update_observation', startTime);
3391
+ return {
3392
+ status: "updated",
3393
+ observation: {
3394
+ id: row[0],
3395
+ entity_id: row[1],
3396
+ text: newText,
3397
+ metadata: newMeta,
3398
+ updated_at: new Date(Math.floor(now / 1000)).toISOString(),
3399
+ previous_text: prevText,
3400
+ },
3401
+ };
3402
+ }
3403
+ catch (error) {
3404
+ this.trackError('update_observation');
3405
+ return { error: "Update failed", message: error.message };
3406
+ }
3407
+ }
3408
+ /** Feature 4: Aggregate full entity detail (observations, relations, optional community/timeline). */
3409
+ async getEntityDetail(args) {
3410
+ await this.initPromise;
3411
+ try {
3412
+ const incObs = args.include_observations !== false;
3413
+ const incRel = args.include_relations !== false;
3414
+ const incCom = args.include_community === true;
3415
+ const incTl = args.include_timeline === true;
3416
+ const entRes = await this.db.run(`?[name, type, metadata, ts] := *entity{id: $id, name, type, metadata, created_at, @ "NOW"}, ts = to_int(created_at)`, { id: args.entity_id });
3417
+ if (entRes.rows.length === 0)
3418
+ return { error: "Entity not found" };
3419
+ const e = entRes.rows[0];
3420
+ const metadata = (e[2] || {});
3421
+ const created_at_us = e[3];
3422
+ const result = {
3423
+ entity: {
3424
+ id: args.entity_id,
3425
+ name: e[0],
3426
+ type: e[1],
3427
+ metadata,
3428
+ tags: Array.isArray(metadata.tags) ? metadata.tags : [],
3429
+ created_at: new Date(Math.floor(created_at_us / 1000)).toISOString(),
3430
+ },
3431
+ };
3432
+ if (incObs) {
3433
+ const obsRes = await this.db.run(`?[oid, text, metadata, ts] := *observation{id: oid, entity_id: $id, text, metadata, created_at, @ "NOW"}, ts = to_int(created_at) :order ts`, { id: args.entity_id });
3434
+ result.observations = obsRes.rows.map((r) => ({
3435
+ id: r[0],
3436
+ text: r[1],
3437
+ metadata: r[2],
3438
+ created_at: new Date(Math.floor(r[3] / 1000)).toISOString(),
3439
+ }));
3440
+ }
3441
+ if (incRel) {
3442
+ const outRes = await this.db.run(`?[tid, tname, ttype, rtype, strength, metadata] := *relationship{from_id: $id, to_id: tid, relation_type: rtype, strength, metadata, @ "NOW"}, *entity{id: tid, name: tname, type: ttype, @ "NOW"}`, { id: args.entity_id });
3443
+ const inRes = await this.db.run(`?[sid, sname, stype, rtype, strength, metadata] := *relationship{from_id: sid, to_id: $id, relation_type: rtype, strength, metadata, @ "NOW"}, *entity{id: sid, name: sname, type: stype, @ "NOW"}`, { id: args.entity_id });
3444
+ result.relations = {
3445
+ outgoing: outRes.rows.map((r) => ({
3446
+ target_id: r[0],
3447
+ target_name: r[1],
3448
+ target_type: r[2],
3449
+ relation_type: r[3],
3450
+ strength: r[4],
3451
+ metadata: r[5],
3452
+ })),
3453
+ incoming: inRes.rows.map((r) => ({
3454
+ source_id: r[0],
3455
+ source_name: r[1],
3456
+ source_type: r[2],
3457
+ relation_type: r[3],
3458
+ strength: r[4],
3459
+ metadata: r[5],
3460
+ })),
3461
+ };
3462
+ }
3463
+ if (incCom) {
3464
+ try {
3465
+ const comRes = await this.db.run(`?[cid] := *entity_community{entity_id: $id, community_id: cid}`, { id: args.entity_id });
3466
+ if (comRes.rows.length > 0) {
3467
+ const cid = comRes.rows[0][0];
3468
+ const memRes = await this.db.run(`?[mid, name, type] := *entity_community{entity_id: mid, community_id: $cid}, *entity{id: mid, name, type, @ "NOW"}`, { cid });
3469
+ result.community = {
3470
+ id: cid,
3471
+ members: memRes.rows.map((r) => ({ id: r[0], name: r[1], type: r[2] })),
3472
+ };
3473
+ }
3474
+ else {
3475
+ result.community = null;
3476
+ }
3477
+ }
3478
+ catch {
3479
+ result.community = null;
3480
+ }
3481
+ }
3482
+ if (incTl) {
3483
+ const timeline = [
3484
+ {
3485
+ timestamp: new Date(Math.floor(created_at_us / 1000)).toISOString(),
3486
+ action: "created",
3487
+ detail: `Entity '${e[0]}' created`,
3488
+ },
3489
+ ];
3490
+ const obsT = await this.db.run(`?[text, ts, asserted] := *observation{entity_id: $id, text, created_at}, ts = to_int(created_at), asserted = to_bool(created_at)`, { id: args.entity_id });
3491
+ obsT.rows.forEach((r) => {
3492
+ if (r[2]) {
3493
+ timeline.push({
3494
+ timestamp: new Date(Math.floor(r[1] / 1000)).toISOString(),
3495
+ action: "observation_added",
3496
+ detail: String(r[0]).slice(0, 80),
3497
+ });
3498
+ }
3499
+ });
3500
+ const relT = await this.db.run(`?[rtype, tid, ts, asserted] := *relationship{from_id: $id, to_id: tid, relation_type: rtype, created_at}, ts = to_int(created_at), asserted = to_bool(created_at)`, { id: args.entity_id });
3501
+ relT.rows.forEach((r) => {
3502
+ if (r[3]) {
3503
+ timeline.push({
3504
+ timestamp: new Date(Math.floor(r[2] / 1000)).toISOString(),
3505
+ action: "relation_added",
3506
+ detail: `${r[0]} -> ${r[1]}`,
3507
+ });
3508
+ }
3509
+ });
3510
+ timeline.sort((a, b) => (a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0));
3511
+ result.timeline = timeline;
3512
+ }
3513
+ return result;
3514
+ }
3515
+ catch (error) {
3516
+ return { error: "Failed to get entity detail", message: error.message };
3517
+ }
3518
+ }
3519
+ /** Feature 3: Delete multiple entities by explicit IDs or by filter, with dry-run support. */
3520
+ async batchDeleteEntities(args) {
3521
+ await this.initPromise;
3522
+ try {
3523
+ const dryRun = args.dry_run === true;
3524
+ let targetIds = [];
3525
+ if (args.entity_ids && args.entity_ids.length > 0) {
3526
+ targetIds = [...args.entity_ids];
3527
+ }
3528
+ else if (args.filter) {
3529
+ const f = args.filter;
3530
+ const all = await this.db.run(`?[id, name, type, metadata, ts] := *entity{id, name, type, metadata, created_at, @ "NOW"}, ts = to_int(created_at)`);
3531
+ const beforeUs = f.created_before ? Date.parse(f.created_before) * 1000 : null;
3532
+ const afterUs = f.created_after ? Date.parse(f.created_after) * 1000 : null;
3533
+ targetIds = all.rows
3534
+ .filter((r) => {
3535
+ const [, name, type, meta, ts] = r;
3536
+ if (f.type && type !== f.type)
3537
+ return false;
3538
+ if (f.name_contains && !String(name).toLowerCase().includes(f.name_contains.toLowerCase()))
3539
+ return false;
3540
+ if (beforeUs !== null && !(ts < beforeUs))
3541
+ return false;
3542
+ if (afterUs !== null && !(ts > afterUs))
3543
+ return false;
3544
+ if (f.metadata) {
3545
+ for (const [k, v] of Object.entries(f.metadata)) {
3546
+ if (JSON.stringify((meta || {})[k]) !== JSON.stringify(v))
3547
+ return false;
3548
+ }
3549
+ }
3550
+ if (f.tags && f.tags.length > 0) {
3551
+ const t = Array.isArray(meta?.tags) ? meta.tags : [];
3552
+ if (!f.tags.every((tag) => t.includes(tag)))
3553
+ return false;
3554
+ }
3555
+ return true;
3556
+ })
3557
+ .map((r) => r[0]);
3558
+ }
3559
+ else {
3560
+ return { error: "Either entity_ids or filter is required" };
3561
+ }
3562
+ targetIds = [...new Set(targetIds)];
3563
+ let deleted_observations = 0;
3564
+ let deleted_relations = 0;
3565
+ const deleted_entities = [];
3566
+ const errors = [];
3567
+ for (const id of targetIds) {
3568
+ const exists = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id });
3569
+ if (exists.rows.length === 0) {
3570
+ errors.push({ id, error: "Entity not found" });
3571
+ continue;
3572
+ }
3573
+ if (dryRun) {
3574
+ const obsC = await this.db.run('?[count(oid)] := *observation{id: oid, entity_id: $id, @ "NOW"}', { id });
3575
+ const relOutC = await this.db.run('?[count(t)] := *relationship{from_id: $id, to_id: t, @ "NOW"}', { id });
3576
+ const relInC = await this.db.run('?[count(f)] := *relationship{from_id: f, to_id: $id, @ "NOW"}', { id });
3577
+ deleted_observations += Number(obsC.rows[0]?.[0] || 0);
3578
+ deleted_relations += Number(relOutC.rows[0]?.[0] || 0) + Number(relInC.rows[0]?.[0] || 0);
3579
+ deleted_entities.push(id);
3580
+ }
3581
+ else {
3582
+ const res = await this.deleteEntity({ entity_id: id });
3583
+ if (res.error) {
3584
+ errors.push({ id, error: res.message || res.error });
3585
+ continue;
3586
+ }
3587
+ deleted_observations += Number(res.deleted?.observations || 0);
3588
+ deleted_relations += Number(res.deleted?.outgoing_relations || 0) + Number(res.deleted?.incoming_relations || 0);
3589
+ deleted_entities.push(id);
3590
+ }
3591
+ }
3592
+ return {
3593
+ status: dryRun ? "dry_run" : "deleted",
3594
+ deleted_count: deleted_entities.length,
3595
+ deleted_entities,
3596
+ deleted_observations,
3597
+ deleted_relations,
3598
+ ...(errors.length ? { errors } : {}),
3599
+ };
3600
+ }
3601
+ catch (error) {
3602
+ return { error: "Batch delete failed", message: error.message };
3603
+ }
3604
+ }
3605
+ /** Feature 5: Lightweight tag management stored in metadata.tags. */
3606
+ async manageTags(args) {
3607
+ await this.initPromise;
3608
+ try {
3609
+ const op = args.operation;
3610
+ if (op === "search") {
3611
+ if (!args.search_tag)
3612
+ return { error: "search_tag is required for search" };
3613
+ const all = await this.db.run(`?[id, name, type, metadata] := *entity{id, name, type, metadata, @ "NOW"}`);
3614
+ const entities = all.rows
3615
+ .filter((r) => Array.isArray(r[3]?.tags) && r[3].tags.includes(args.search_tag))
3616
+ .map((r) => ({ id: r[0], name: r[1], type: r[2] }));
3617
+ return { tag: args.search_tag, entities, count: entities.length };
3618
+ }
3619
+ if (op === "list") {
3620
+ if (args.entity_id) {
3621
+ const res = await this.db.run(`?[metadata] := *entity{id: $id, metadata, @ "NOW"}`, { id: args.entity_id });
3622
+ if (res.rows.length === 0)
3623
+ return { error: "Entity not found" };
3624
+ const t = Array.isArray(res.rows[0][0]?.tags) ? res.rows[0][0].tags : [];
3625
+ return { entity_id: args.entity_id, tags: t };
3626
+ }
3627
+ const all = await this.db.run(`?[metadata] := *entity{metadata, @ "NOW"}`);
3628
+ const counts = new Map();
3629
+ all.rows.forEach((r) => {
3630
+ const t = Array.isArray(r[0]?.tags) ? r[0].tags : [];
3631
+ t.forEach((tag) => counts.set(tag, (counts.get(tag) || 0) + 1));
3632
+ });
3633
+ return {
3634
+ tags: [...counts.entries()].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count),
3635
+ };
3636
+ }
3637
+ // add / remove / set
3638
+ if (!args.entity_id)
3639
+ return { error: "entity_id is required" };
3640
+ const tags = args.tags || [];
3641
+ const res = await this.db.run(`?[metadata] := *entity{id: $id, metadata, @ "NOW"}`, { id: args.entity_id });
3642
+ if (res.rows.length === 0)
3643
+ return { error: "Entity not found" };
3644
+ const meta = res.rows[0][0] || {};
3645
+ const current = Array.isArray(meta.tags) ? meta.tags : [];
3646
+ let next;
3647
+ if (op === "add")
3648
+ next = [...new Set([...current, ...tags])];
3649
+ else if (op === "remove")
3650
+ next = current.filter((t) => !tags.includes(t));
3651
+ else if (op === "set")
3652
+ next = [...new Set(tags)];
3653
+ else
3654
+ return { error: `Invalid operation: ${op}` };
3655
+ // Replace metadata.tags fully. updateEntity uses `++` which concatenates
3656
+ // arrays (producing duplicates), so write metadata directly instead.
3657
+ const newMeta = { ...meta, tags: next };
3658
+ await this.db.run(`?[id, created_at, metadata] := *entity{id, created_at, @ "NOW"}, id = $id, metadata = $meta :update entity {id, created_at, metadata}`, { id: args.entity_id, meta: newMeta });
3659
+ return { status: "tags_updated", entity_id: args.entity_id, operation: op, tags: next };
3660
+ }
3661
+ catch (error) {
3662
+ return { error: "Tag operation failed", message: error.message };
3663
+ }
3664
+ }
3665
+ /** Feature 7: Execute a sequence of mutation operations (transactional or best-effort). */
3666
+ async executeBatch(args) {
3667
+ await this.initPromise;
3668
+ try {
3669
+ const ops = args.operations || [];
3670
+ if (ops.length === 0)
3671
+ return { error: "No operations provided" };
3672
+ const transactional = args.transactional !== false; // default: true (all-or-nothing)
3673
+ const continueOnError = args.continue_on_error === true;
3674
+ const opParams = (op) => {
3675
+ if (op.params !== undefined)
3676
+ return op.params;
3677
+ const { action, ...rest } = op;
3678
+ return rest;
3679
+ };
3680
+ if (transactional) {
3681
+ const mapped = ops.map((op) => ({ action: op.action, params: opParams(op) }));
3682
+ const txn = await this.runTransaction({ operations: mapped });
3683
+ if (txn.error) {
3684
+ return {
3685
+ status: "failed",
3686
+ results: ops.map((op, i) => ({ index: i, action: op.action, status: "error", error: txn.error })),
3687
+ summary: { total: ops.length, succeeded: 0, failed: ops.length },
3688
+ error: txn.error,
3689
+ message: txn.message,
3690
+ };
3691
+ }
3692
+ return {
3693
+ status: "completed",
3694
+ results: ops.map((op, i) => ({ index: i, action: op.action, status: "success" })),
3695
+ summary: { total: ops.length, succeeded: ops.length, failed: 0 },
3696
+ transaction: txn,
3697
+ };
3698
+ }
3699
+ const results = [];
3700
+ let succeeded = 0;
3701
+ let failed = 0;
3702
+ for (let i = 0; i < ops.length; i++) {
3703
+ const op = ops[i];
3704
+ const params = opParams(op);
3705
+ try {
3706
+ let r;
3707
+ if (op.action === "create_entity")
3708
+ r = await this.createEntity(params);
3709
+ else if (op.action === "add_observation")
3710
+ r = await this.addObservation(params);
3711
+ else if (op.action === "create_relation")
3712
+ r = await this.createRelation(params);
3713
+ else if (op.action === "delete_entity")
3714
+ r = await this.deleteEntity({ entity_id: params.entity_id });
3715
+ else if (op.action === "update_observation")
3716
+ r = await this.updateObservation(params);
3717
+ else
3718
+ r = { error: `Unsupported batch action: ${op.action}` };
3719
+ if (r && r.error) {
3720
+ failed++;
3721
+ results.push({ index: i, action: op.action, status: "error", error: r.message || r.error });
3722
+ if (!continueOnError)
3723
+ break;
3724
+ }
3725
+ else {
3726
+ succeeded++;
3727
+ results.push({ index: i, action: op.action, status: "success", result: r });
3728
+ }
3729
+ }
3730
+ catch (e) {
3731
+ failed++;
3732
+ results.push({ index: i, action: op.action, status: "error", error: e.message || String(e) });
3733
+ if (!continueOnError)
3734
+ break;
3735
+ }
3736
+ }
3737
+ const status = failed === 0 ? "completed" : succeeded === 0 ? "failed" : "partial";
3738
+ return { status, results, summary: { total: ops.length, succeeded, failed } };
3739
+ }
3740
+ catch (error) {
3741
+ return { error: "Batch execution failed", message: error.message };
3742
+ }
3743
+ }
3744
+ /** Feature 8: Retrieve the context (observations/entities/timeline) of a session. */
3745
+ async getSessionContext(args) {
3746
+ await this.initPromise;
3747
+ try {
3748
+ const incObs = args.include_observations !== false;
3749
+ const incEnt = args.include_entities === true;
3750
+ const incTl = args.include_timeline === true;
3751
+ const limit = Math.min(Math.max(args.limit ?? 50, 1), 1000);
3752
+ let sessionId = args.session_id;
3753
+ if (!sessionId) {
3754
+ const latest = await this.db.run(`?[sid, la] := *session_state{session_id: sid, last_active: la} :order -la :limit 1`);
3755
+ if (latest.rows.length === 0)
3756
+ return { error: "No sessions found" };
3757
+ sessionId = latest.rows[0][0];
3758
+ }
3759
+ const ss = await this.db.run(`?[la, status, meta] := *session_state{session_id: $sid, last_active: la, status, metadata: meta}`, { sid: sessionId });
3760
+ const obsRes = await this.db.run(`?[oid, eid, text, metadata, ts] := *observation{id: oid, entity_id: eid, session_id: $sid, text, metadata, created_at, @ "NOW"}, ts = to_int(created_at) :order -ts`, { sid: sessionId });
3761
+ const obsRows = obsRes.rows;
3762
+ const entityIds = [...new Set(obsRows.map((r) => r[1]))];
3763
+ const nameMap = new Map();
3764
+ if (entityIds.length) {
3765
+ const entRes = await this.db.run(`?[id, name, type, ts] := *entity{id, name, type, created_at, @ "NOW"}, ts = to_int(created_at)`);
3766
+ entRes.rows.forEach((r) => nameMap.set(r[0], { name: r[1], type: r[2], ts: r[3] }));
3767
+ }
3768
+ const tss = obsRows.map((r) => r[4]);
3769
+ const created_at = tss.length ? new Date(Math.floor(Math.min(...tss) / 1000)).toISOString() : null;
3770
+ const session_metadata = {
3771
+ observation_count: obsRows.length,
3772
+ entity_count: entityIds.length,
3773
+ created_at,
3774
+ };
3775
+ if (ss.rows.length > 0) {
3776
+ session_metadata.last_active = ss.rows[0][0];
3777
+ session_metadata.status = ss.rows[0][1];
3778
+ session_metadata.metadata = ss.rows[0][2];
3779
+ }
3780
+ const result = { session_id: sessionId, session_metadata };
3781
+ if (incObs) {
3782
+ result.observations = obsRows.slice(0, limit).map((r) => ({
3783
+ id: r[0],
3784
+ text: r[2],
3785
+ entity_id: r[1],
3786
+ entity_name: nameMap.get(r[1])?.name ?? null,
3787
+ created_at: new Date(Math.floor(r[4] / 1000)).toISOString(),
3788
+ metadata: r[3],
3789
+ }));
3790
+ }
3791
+ if (incEnt) {
3792
+ result.entities = entityIds.map((id) => {
3793
+ const info = nameMap.get(id);
3794
+ return {
3795
+ id,
3796
+ name: info?.name ?? null,
3797
+ type: info?.type ?? null,
3798
+ created_at: info?.ts ? new Date(Math.floor(info.ts / 1000)).toISOString() : null,
3799
+ };
3800
+ });
3801
+ }
3802
+ if (incTl) {
3803
+ result.timeline = obsRows
3804
+ .slice()
3805
+ .sort((a, b) => a[4] - b[4])
3806
+ .map((r) => ({
3807
+ timestamp: new Date(Math.floor(r[4] / 1000)).toISOString(),
3808
+ action: "observation_added",
3809
+ detail: String(r[2]).slice(0, 80),
3810
+ }));
3811
+ }
3812
+ return result;
3813
+ }
3814
+ catch (error) {
3815
+ return { error: "Failed to get session context", message: error.message };
3816
+ }
3817
+ }
3818
+ /** Feature 8: List known sessions with activity metadata. */
3819
+ async listSessions(args) {
3820
+ await this.initPromise;
3821
+ try {
3822
+ const limit = Math.min(Math.max(args.limit ?? 50, 1), 1000);
3823
+ const res = await this.db.run(`?[sid, la, status, meta] := *session_state{session_id: sid, last_active: la, status, metadata: meta} :order -la`);
3824
+ let rows = res.rows;
3825
+ if (args.active_only)
3826
+ rows = rows.filter((r) => r[2] === "active");
3827
+ const obsMap = new Map();
3828
+ const obsRes = await this.db.run(`?[sid, count(oid)] := *observation{id: oid, session_id: sid, @ "NOW"}, sid != ""`);
3829
+ obsRes.rows.forEach((r) => obsMap.set(r[0], Number(r[1])));
3830
+ const sessions = rows.slice(0, limit).map((r) => ({
3831
+ session_id: r[0],
3832
+ last_active: r[1],
3833
+ status: r[2],
3834
+ metadata: r[3],
3835
+ observation_count: obsMap.get(r[0]) || 0,
3836
+ }));
3837
+ return { sessions, total: rows.length };
3838
+ }
3839
+ catch (error) {
3840
+ return { error: "Failed to list sessions", message: error.message };
3841
+ }
3842
+ }
3232
3843
  registerTools() {
3233
3844
  const MetadataSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.any());
3234
3845
  const MutateMemorySchema = zod_1.z.discriminatedUnion("action", [
@@ -3401,10 +4012,49 @@ Format MUST start with "ExecutiveSummary: " followed by the consolidated content
3401
4012
  entity_id: zod_1.z.string().describe("Entity ID to resolve conflicts for"),
3402
4013
  auto_resolve: zod_1.z.boolean().optional().default(false).describe("Automatically resolve all conflicts"),
3403
4014
  }),
4015
+ zod_1.z.object({
4016
+ action: zod_1.z.literal("update_observation"),
4017
+ observation_id: zod_1.z.string().describe("ID of the observation to update"),
4018
+ text: zod_1.z.string().optional().describe("New observation text (re-embedded if changed)"),
4019
+ metadata: MetadataSchema.optional().describe("New metadata"),
4020
+ merge_metadata: zod_1.z.boolean().optional().default(false).describe("Merge with existing metadata instead of replacing"),
4021
+ }).passthrough(),
4022
+ zod_1.z.object({
4023
+ action: zod_1.z.literal("batch_delete"),
4024
+ entity_ids: zod_1.z.array(zod_1.z.string()).optional().describe("Explicit list of entity IDs to delete"),
4025
+ filter: zod_1.z.object({
4026
+ type: zod_1.z.string().optional().describe("Delete all entities of this type"),
4027
+ name_contains: zod_1.z.string().optional().describe("Name contains (case-insensitive)"),
4028
+ created_before: zod_1.z.string().optional().describe("ISO date, only entities created before"),
4029
+ created_after: zod_1.z.string().optional().describe("ISO date, only entities created after"),
4030
+ metadata: MetadataSchema.optional().describe("Metadata exact-match filter"),
4031
+ tags: zod_1.z.array(zod_1.z.string()).optional().describe("Only entities having all of these tags (metadata.tags)"),
4032
+ }).optional().describe("Filter criteria (alternative to entity_ids)"),
4033
+ dry_run: zod_1.z.boolean().optional().default(false).describe("If true, only count without deleting"),
4034
+ }).passthrough().refine((v) => Boolean(v.entity_ids) || Boolean(v.filter), {
4035
+ message: "entity_ids or filter is required",
4036
+ path: ["entity_ids"],
4037
+ }),
4038
+ zod_1.z.object({
4039
+ action: zod_1.z.literal("manage_tags"),
4040
+ operation: zod_1.z.enum(["add", "remove", "set", "list", "search"]).describe("Tag operation"),
4041
+ entity_id: zod_1.z.string().optional().describe("Required for add/remove/set; optional for list"),
4042
+ tags: zod_1.z.array(zod_1.z.string()).optional().describe("Tag list for add/remove/set"),
4043
+ search_tag: zod_1.z.string().optional().describe("Tag to search for (operation=search)"),
4044
+ }).passthrough(),
4045
+ zod_1.z.object({
4046
+ action: zod_1.z.literal("batch"),
4047
+ operations: zod_1.z.array(zod_1.z.object({
4048
+ action: zod_1.z.enum(["create_entity", "add_observation", "create_relation", "delete_entity", "update_observation"]),
4049
+ params: zod_1.z.any().optional().describe("Parameters for the operation (object); flat fields also accepted"),
4050
+ }).passthrough()).describe("List of operations to execute"),
4051
+ continue_on_error: zod_1.z.boolean().optional().default(false).describe("Keep going after a failed operation (non-transactional)"),
4052
+ transactional: zod_1.z.boolean().optional().default(true).describe("All-or-nothing execution (default true)"),
4053
+ }).passthrough(),
3404
4054
  ]);
3405
4055
  const MutateMemoryParameters = zod_1.z.object({
3406
4056
  action: zod_1.z
3407
- .enum(["create_entity", "update_entity", "delete_entity", "add_observation", "create_relation", "run_transaction", "add_inference_rule", "ingest_file", "start_session", "stop_session", "start_task", "stop_task", "invalidate_observation", "invalidate_relation", "enrich_observation", "record_memory_access", "prune_weak_memories", "detect_conflicts", "resolve_conflicts"])
4057
+ .enum(["create_entity", "update_entity", "delete_entity", "add_observation", "create_relation", "run_transaction", "add_inference_rule", "ingest_file", "start_session", "stop_session", "start_task", "stop_task", "invalidate_observation", "invalidate_relation", "enrich_observation", "record_memory_access", "prune_weak_memories", "detect_conflicts", "resolve_conflicts", "update_observation", "batch_delete", "manage_tags", "batch"])
3408
4058
  .describe("Action (determines which fields are required)"),
3409
4059
  name: zod_1.z.string().optional().describe("For create_entity (required) or add_inference_rule (required)"),
3410
4060
  type: zod_1.z.string().optional().describe("For create_entity (required)"),
@@ -3426,12 +4076,22 @@ Format MUST start with "ExecutiveSummary: " followed by the consolidated content
3426
4076
  relation_type: zod_1.z.string().optional().describe("For create_relation (required)"),
3427
4077
  strength: zod_1.z.number().min(0).max(1).optional().describe("Optional for create_relation"),
3428
4078
  metadata: MetadataSchema.optional().describe("Optional for create_entity/update_entity/add_observation/create_relation/ingest_file"),
3429
- observation_id: zod_1.z.string().optional().describe("For invalidate_observation (required) or enrich_observation (required) or record_memory_access (required)"),
3430
- dry_run: zod_1.z.boolean().optional().describe("For prune_weak_memories: if true, only shows candidates"),
4079
+ observation_id: zod_1.z.string().optional().describe("For invalidate_observation (required) or enrich_observation (required) or record_memory_access (required) or update_observation (required)"),
4080
+ session_id: zod_1.z.string().optional().describe("For add_observation/start_task: associate the observation/task with a session"),
4081
+ task_id: zod_1.z.string().optional().describe("For add_observation: associate the observation with a task"),
4082
+ dry_run: zod_1.z.boolean().optional().describe("For prune_weak_memories/batch_delete: if true, only shows candidates"),
3431
4083
  operations: zod_1.z.array(zod_1.z.object({
3432
- action: zod_1.z.enum(["create_entity", "add_observation", "create_relation", "delete_entity"]),
4084
+ action: zod_1.z.enum(["create_entity", "add_observation", "create_relation", "delete_entity", "update_observation"]),
3433
4085
  params: zod_1.z.any().describe("Parameters for the operation as an object")
3434
- })).optional().describe("For run_transaction: List of operations to be executed atomically"),
4086
+ })).optional().describe("For run_transaction/batch: List of operations to be executed"),
4087
+ merge_metadata: zod_1.z.boolean().optional().describe("For update_observation: merge instead of replace metadata"),
4088
+ entity_ids: zod_1.z.array(zod_1.z.string()).optional().describe("For batch_delete: explicit entity IDs"),
4089
+ filter: zod_1.z.any().optional().describe("For batch_delete: filter criteria (type, name_contains, created_before/after, metadata, tags)"),
4090
+ operation: zod_1.z.enum(["add", "remove", "set", "list", "search"]).optional().describe("For manage_tags"),
4091
+ tags: zod_1.z.array(zod_1.z.string()).optional().describe("For manage_tags (add/remove/set)"),
4092
+ search_tag: zod_1.z.string().optional().describe("For manage_tags (operation=search)"),
4093
+ continue_on_error: zod_1.z.boolean().optional().describe("For batch: continue after failures (non-transactional)"),
4094
+ transactional: zod_1.z.boolean().optional().describe("For batch: all-or-nothing (default true)"),
3435
4095
  });
3436
4096
  this.mcp.addTool({
3437
4097
  name: "mutate_memory",
@@ -3599,6 +4259,14 @@ Note: Inference rules must return exactly 5 columns: [from_id, to_id, relation_t
3599
4259
  });
3600
4260
  }
3601
4261
  }
4262
+ if (action === "update_observation")
4263
+ return JSON.stringify(await this.updateObservation(rest));
4264
+ if (action === "batch_delete")
4265
+ return JSON.stringify(await this.batchDeleteEntities(rest));
4266
+ if (action === "manage_tags")
4267
+ return JSON.stringify(await this.manageTags(rest));
4268
+ if (action === "batch")
4269
+ return JSON.stringify(await this.executeBatch(rest));
3602
4270
  return JSON.stringify({ error: "Unknown action" });
3603
4271
  },
3604
4272
  });
@@ -3762,10 +4430,42 @@ Note: Inference rules must return exactly 5 columns: [from_id, to_id, relation_t
3762
4430
  levels: zod_1.z.array(zod_1.z.number().min(0).max(3)).optional().describe("Memory levels to query (0-3)"),
3763
4431
  limit: zod_1.z.number().optional().default(10).describe("Maximum number of results"),
3764
4432
  }),
4433
+ zod_1.z.object({
4434
+ action: zod_1.z.literal("list_entities"),
4435
+ type: zod_1.z.string().optional().describe("Filter by a single entity type"),
4436
+ types: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by multiple entity types"),
4437
+ limit: zod_1.z.number().min(1).max(1000).optional().default(20).describe("Page size (default 20, max 1000)"),
4438
+ offset: zod_1.z.number().min(0).optional().default(0).describe("Pagination offset"),
4439
+ sort_by: zod_1.z.enum(["name", "created_at", "updated_at"]).optional().default("created_at").describe("Sort field"),
4440
+ sort_order: zod_1.z.enum(["asc", "desc"]).optional().default("desc").describe("Sort order"),
4441
+ name_contains: zod_1.z.string().optional().describe("Case-insensitive substring filter on name"),
4442
+ tags: zod_1.z.array(zod_1.z.string()).optional().describe("Only entities having all of these tags (metadata.tags)"),
4443
+ }),
4444
+ zod_1.z.object({
4445
+ action: zod_1.z.literal("get_entity_detail"),
4446
+ entity_id: zod_1.z.string().describe("ID of the entity"),
4447
+ include_observations: zod_1.z.boolean().optional().default(true).describe("Include observations"),
4448
+ include_relations: zod_1.z.boolean().optional().default(true).describe("Include outgoing/incoming relations"),
4449
+ include_community: zod_1.z.boolean().optional().default(false).describe("Include community membership"),
4450
+ include_timeline: zod_1.z.boolean().optional().default(false).describe("Include a chronological timeline"),
4451
+ }),
4452
+ zod_1.z.object({
4453
+ action: zod_1.z.literal("get_session_context"),
4454
+ session_id: zod_1.z.string().optional().describe("Session ID (defaults to the most recently active session)"),
4455
+ include_observations: zod_1.z.boolean().optional().default(true).describe("Include observations"),
4456
+ include_entities: zod_1.z.boolean().optional().default(false).describe("Include referenced entities"),
4457
+ include_timeline: zod_1.z.boolean().optional().default(false).describe("Include a chronological timeline"),
4458
+ limit: zod_1.z.number().min(1).max(1000).optional().default(50).describe("Max observations"),
4459
+ }),
4460
+ zod_1.z.object({
4461
+ action: zod_1.z.literal("list_sessions"),
4462
+ active_only: zod_1.z.boolean().optional().default(false).describe("Only sessions with status 'active'"),
4463
+ limit: zod_1.z.number().min(1).max(1000).optional().default(50).describe("Max sessions"),
4464
+ }),
3765
4465
  ]);
3766
4466
  const QueryMemoryParameters = zod_1.z.object({
3767
4467
  action: zod_1.z
3768
- .enum(["search", "advancedSearch", "context", "entity_details", "history", "graph_rag", "graph_walking", "agentic_search", "dynamic_fusion", "adaptive_retrieval", "get_zettelkasten_stats", "get_activation_stats", "get_salience_stats", "suggest_connections", "spreading_activation", "qafd_search", "hierarchical_memory_query"])
4468
+ .enum(["search", "advancedSearch", "context", "entity_details", "history", "graph_rag", "graph_walking", "agentic_search", "dynamic_fusion", "adaptive_retrieval", "get_zettelkasten_stats", "get_activation_stats", "get_salience_stats", "suggest_connections", "spreading_activation", "qafd_search", "hierarchical_memory_query", "explain_results", "list_entities", "get_entity_detail", "get_session_context", "list_sessions"])
3769
4469
  .describe("Retrieval strategy - use 'search' for simple queries, 'adaptive_retrieval' for auto-optimization, 'context' for exploration"),
3770
4470
  query: zod_1.z.string().optional().describe("Search query text (required for most actions)"),
3771
4471
  limit: zod_1.z.number().optional().describe("Maximum number of results to return (default: 10)"),
@@ -3785,6 +4485,17 @@ Note: Inference rules must return exactly 5 columns: [from_id, to_id, relation_t
3785
4485
  start_entity_id: zod_1.z.string().optional().describe("For graph_walking: Starting entity for traversal"),
3786
4486
  rerank: zod_1.z.boolean().optional().describe("For search/advancedSearch: Enable Cross-Encoder reranking for higher precision"),
3787
4487
  config: zod_1.z.any().optional().describe("For dynamic_fusion: Fine-tune vector/sparse/FTS/graph weights and fusion strategy"),
4488
+ type: zod_1.z.string().optional().describe("For list_entities: filter by a single entity type"),
4489
+ types: zod_1.z.array(zod_1.z.string()).optional().describe("For list_entities: filter by multiple entity types"),
4490
+ offset: zod_1.z.number().optional().describe("For list_entities: pagination offset"),
4491
+ sort_by: zod_1.z.enum(["name", "created_at", "updated_at"]).optional().describe("For list_entities: sort field"),
4492
+ sort_order: zod_1.z.enum(["asc", "desc"]).optional().describe("For list_entities: sort order"),
4493
+ name_contains: zod_1.z.string().optional().describe("For list_entities: case-insensitive substring filter on name"),
4494
+ tags: zod_1.z.array(zod_1.z.string()).optional().describe("For list_entities: only entities having all of these tags"),
4495
+ include_relations: zod_1.z.boolean().optional().describe("For get_entity_detail: include relations (default true)"),
4496
+ include_community: zod_1.z.boolean().optional().describe("For get_entity_detail: include community (default false)"),
4497
+ include_timeline: zod_1.z.boolean().optional().describe("For get_entity_detail/get_session_context: include timeline"),
4498
+ active_only: zod_1.z.boolean().optional().describe("For list_sessions: only active sessions"),
3788
4499
  });
3789
4500
  this.mcp.addTool({
3790
4501
  name: "query_memory",
@@ -4370,6 +5081,62 @@ Note: User profile observations (entity_id='global_user_profile') are automatica
4370
5081
  },
4371
5082
  });
4372
5083
  }
5084
+ if (input.action === "explain_results") {
5085
+ try {
5086
+ console.log('[query_memory] Explaining', input.results.length, 'results for query:', input.query);
5087
+ const explained = await this.getExplainableService().explainResults(input.results, input.query, input.search_type || 'hybrid', {
5088
+ includePathVisualization: input.include_path_viz,
5089
+ includeReasoningSteps: input.include_reasoning,
5090
+ includeScoreBreakdown: input.include_score_breakdown
5091
+ });
5092
+ console.log('[query_memory] Explanation completed for', explained.length, 'results');
5093
+ return JSON.stringify({
5094
+ query: input.query,
5095
+ search_type: input.search_type,
5096
+ explained_results: explained
5097
+ });
5098
+ }
5099
+ catch (error) {
5100
+ console.error('[query_memory] Error explaining results:', error);
5101
+ return JSON.stringify({ error: "Failed to explain results", details: error.message });
5102
+ }
5103
+ }
5104
+ if (input.action === "list_entities") {
5105
+ return JSON.stringify(await this.listEntities({
5106
+ type: input.type,
5107
+ types: input.types,
5108
+ limit: input.limit,
5109
+ offset: input.offset,
5110
+ sort_by: input.sort_by,
5111
+ sort_order: input.sort_order,
5112
+ name_contains: input.name_contains,
5113
+ tags: input.tags,
5114
+ }));
5115
+ }
5116
+ if (input.action === "get_entity_detail") {
5117
+ return JSON.stringify(await this.getEntityDetail({
5118
+ entity_id: input.entity_id,
5119
+ include_observations: input.include_observations,
5120
+ include_relations: input.include_relations,
5121
+ include_community: input.include_community,
5122
+ include_timeline: input.include_timeline,
5123
+ }));
5124
+ }
5125
+ if (input.action === "get_session_context") {
5126
+ return JSON.stringify(await this.getSessionContext({
5127
+ session_id: input.session_id,
5128
+ include_observations: input.include_observations,
5129
+ include_entities: input.include_entities,
5130
+ include_timeline: input.include_timeline,
5131
+ limit: input.limit,
5132
+ }));
5133
+ }
5134
+ if (input.action === "list_sessions") {
5135
+ return JSON.stringify(await this.listSessions({
5136
+ active_only: input.active_only,
5137
+ limit: input.limit,
5138
+ }));
5139
+ }
4373
5140
  return JSON.stringify({ error: "Unknown action" });
4374
5141
  },
4375
5142
  });
@@ -4811,6 +5578,7 @@ For detailed action descriptions and parameters, see docs/USAGE-GUIDE.md.`,
4811
5578
  const ManageSystemSchema = zod_1.z.discriminatedUnion("action", [
4812
5579
  zod_1.z.object({ action: zod_1.z.literal("health") }),
4813
5580
  zod_1.z.object({ action: zod_1.z.literal("metrics") }),
5581
+ zod_1.z.object({ action: zod_1.z.literal("stats") }),
4814
5582
  zod_1.z.object({
4815
5583
  action: zod_1.z.literal("export_memory"),
4816
5584
  format: zod_1.z.enum(["json", "markdown", "obsidian"]).describe("Export format"),
@@ -4881,10 +5649,15 @@ For detailed action descriptions and parameters, see docs/USAGE-GUIDE.md.`,
4881
5649
  action: zod_1.z.literal("analyze_memory_distribution"),
4882
5650
  entity_id: zod_1.z.string().describe("Entity ID to analyze memory distribution for"),
4883
5651
  }),
5652
+ zod_1.z.object({ action: zod_1.z.literal("list_inference_rules") }),
5653
+ zod_1.z.object({
5654
+ action: zod_1.z.literal("delete_inference_rule"),
5655
+ rule_id: zod_1.z.string().describe("ID of the inference rule to delete"),
5656
+ }),
4884
5657
  ]);
4885
5658
  const ManageSystemParameters = zod_1.z.object({
4886
5659
  action: zod_1.z
4887
- .enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "defrag", "reflect", "clear_memory", "summarize_communities", "compact", "compress_memory_levels", "analyze_memory_distribution"])
5660
+ .enum(["health", "metrics", "stats", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "defrag", "reflect", "clear_memory", "summarize_communities", "compact", "compress_memory_levels", "analyze_memory_distribution", "list_inference_rules", "delete_inference_rule"])
4888
5661
  .describe("Action (determines which fields are required)"),
4889
5662
  format: zod_1.z.enum(["json", "markdown", "obsidian"]).optional().describe("Export format (for export_memory)"),
4890
5663
  includeMetadata: zod_1.z.boolean().optional().describe("Include metadata (for export_memory)"),
@@ -4910,6 +5683,7 @@ For detailed action descriptions and parameters, see docs/USAGE-GUIDE.md.`,
4910
5683
  min_community_size: zod_1.z.number().optional().describe("Optional for summarize_communities"),
4911
5684
  mode: zod_1.z.enum(["summary", "discovery"]).optional().describe("Optional for reflect"),
4912
5685
  level: zod_1.z.number().optional().describe("Required for compress_memory_levels (0-3: L0_RAW, L1_SESSION, L2_WEEKLY, L3_MONTHLY)"),
5686
+ rule_id: zod_1.z.string().optional().describe("Required for delete_inference_rule"),
4913
5687
  });
4914
5688
  this.mcp.addTool({
4915
5689
  name: "manage_system",
@@ -4919,7 +5693,7 @@ Monitoring: health (status check), metrics (detailed stats)
4919
5693
  Data portability: export_memory (JSON/Markdown/Obsidian), import_memory (Mem0/MemGPT/Cozo)
4920
5694
  Backups: snapshot_create, snapshot_list, snapshot_diff
4921
5695
  Optimization: cleanup (LLM consolidation), defrag (merge duplicates), reflect (find contradictions)
4922
- Advanced: summarize_communities, compress_memory_levels, analyze_memory_distribution, compact
5696
+ Advanced: summarize_communities, compress_memory_levels, analyze_memory_distribution, compact, list_inference_rules, delete_inference_rule
4923
5697
 
4924
5698
  Important: Use confirm=false for dry-run before cleanup/defrag. clear_memory requires confirm=true.
4925
5699
 
@@ -4978,6 +5752,9 @@ For detailed action descriptions and parameters, see docs/USAGE-GUIDE.md.`,
4978
5752
  return JSON.stringify({ error: "Failed to retrieve metrics", message: error.message });
4979
5753
  }
4980
5754
  }
5755
+ if (input.action === "stats") {
5756
+ return JSON.stringify(await this.getStats());
5757
+ }
4981
5758
  if (input.action === "export_memory") {
4982
5759
  try {
4983
5760
  const result = await this.exportMemory({
@@ -5251,6 +6028,44 @@ For detailed action descriptions and parameters, see docs/USAGE-GUIDE.md.`,
5251
6028
  return JSON.stringify({ error: error.message || "Error analyzing memory distribution" });
5252
6029
  }
5253
6030
  }
6031
+ if (input.action === "list_inference_rules") {
6032
+ try {
6033
+ const res = await this.db.run('?[id, name, datalog, created_at] := *inference_rule{id, name, datalog, created_at}');
6034
+ const rules = res.rows.map((r) => ({
6035
+ id: r[0],
6036
+ name: r[1],
6037
+ datalog: r[2],
6038
+ created_at: r[3] ? new Date(Number(r[3])).toISOString() : null,
6039
+ }));
6040
+ return JSON.stringify({ count: rules.length, rules });
6041
+ }
6042
+ catch (error) {
6043
+ return JSON.stringify({ error: error.message || "Error listing inference rules" });
6044
+ }
6045
+ }
6046
+ if (input.action === "delete_inference_rule") {
6047
+ try {
6048
+ if (!input.rule_id) {
6049
+ return JSON.stringify({ error: "rule_id is required for delete_inference_rule" });
6050
+ }
6051
+ // Check if rule exists
6052
+ const existing = await this.db.run('?[id, name] := *inference_rule{id, name}, id = $id', { id: input.rule_id });
6053
+ if (existing.rows.length === 0) {
6054
+ return JSON.stringify({ error: `Inference rule with ID '${input.rule_id}' not found` });
6055
+ }
6056
+ const ruleName = existing.rows[0][1];
6057
+ // Delete the rule
6058
+ await this.db.run('{ ?[id, name, datalog, created_at] := *inference_rule{id, name, datalog, created_at}, id = $id :rm inference_rule {id, name, datalog, created_at} }', { id: input.rule_id });
6059
+ return JSON.stringify({
6060
+ status: "deleted",
6061
+ rule_id: input.rule_id,
6062
+ name: ruleName,
6063
+ });
6064
+ }
6065
+ catch (error) {
6066
+ return JSON.stringify({ error: error.message || "Error deleting inference rule" });
6067
+ }
6068
+ }
5254
6069
  return JSON.stringify({ error: "Unknown action" });
5255
6070
  },
5256
6071
  });