@hasna/mementos 0.4.38 → 0.4.41

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 CHANGED
@@ -2405,13 +2405,53 @@ var init_database = __esm(() => {
2405
2405
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
2406
2406
  CREATE INDEX IF NOT EXISTS idx_agents_active_project ON agents(active_project_id);
2407
2407
  INSERT OR IGNORE INTO _migrations (id) VALUES (6);
2408
+ `,
2409
+ `
2410
+ ALTER TABLE agents ADD COLUMN session_id TEXT;
2411
+ CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id);
2412
+ INSERT OR IGNORE INTO _migrations (id) VALUES (7);
2413
+ `,
2414
+ `
2415
+ CREATE TABLE IF NOT EXISTS resource_locks (
2416
+ id TEXT PRIMARY KEY,
2417
+ resource_type TEXT NOT NULL CHECK(resource_type IN ('project', 'memory', 'entity', 'agent', 'connector')),
2418
+ resource_id TEXT NOT NULL,
2419
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
2420
+ lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
2421
+ locked_at TEXT NOT NULL DEFAULT (datetime('now')),
2422
+ expires_at TEXT NOT NULL
2423
+ );
2424
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
2425
+ ON resource_locks(resource_type, resource_id)
2426
+ WHERE lock_type = 'exclusive';
2427
+ CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
2428
+ CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
2429
+ INSERT OR IGNORE INTO _migrations (id) VALUES (8);
2408
2430
  `
2409
2431
  ];
2410
2432
  });
2411
2433
 
2412
2434
  // src/types/index.ts
2413
- var EntityNotFoundError, MemoryNotFoundError, VersionConflictError;
2435
+ var AgentConflictError, EntityNotFoundError, MemoryNotFoundError, VersionConflictError;
2414
2436
  var init_types = __esm(() => {
2437
+ AgentConflictError = class AgentConflictError extends Error {
2438
+ conflict = true;
2439
+ existing_id;
2440
+ existing_name;
2441
+ last_seen_at;
2442
+ session_hint;
2443
+ working_dir;
2444
+ constructor(opts) {
2445
+ 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.`;
2446
+ super(msg);
2447
+ this.name = "AgentConflictError";
2448
+ this.existing_id = opts.existing_id;
2449
+ this.existing_name = opts.existing_name;
2450
+ this.last_seen_at = opts.last_seen_at;
2451
+ this.session_hint = opts.session_hint;
2452
+ this.working_dir = opts.working_dir ?? null;
2453
+ }
2454
+ };
2415
2455
  EntityNotFoundError = class EntityNotFoundError extends Error {
2416
2456
  constructor(id) {
2417
2457
  super(`Entity not found: ${id}`);
@@ -2471,6 +2511,7 @@ function parseAgentRow(row) {
2471
2511
  return {
2472
2512
  id: row["id"],
2473
2513
  name: row["name"],
2514
+ session_id: row["session_id"] || null,
2474
2515
  description: row["description"] || null,
2475
2516
  role: row["role"] || null,
2476
2517
  metadata: JSON.parse(row["metadata"] || "{}"),
@@ -2479,33 +2520,46 @@ function parseAgentRow(row) {
2479
2520
  last_seen_at: row["last_seen_at"]
2480
2521
  };
2481
2522
  }
2482
- function registerAgent(name, description, role, db) {
2523
+ function registerAgent(name, sessionId, description, role, projectId, db) {
2483
2524
  const d = db || getDatabase();
2484
2525
  const timestamp = now();
2485
2526
  const normalizedName = name.trim().toLowerCase();
2486
2527
  const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
2487
2528
  if (existing) {
2488
2529
  const existingId = existing["id"];
2489
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [
2530
+ const existingSessionId = existing["session_id"] || null;
2531
+ const existingLastSeen = existing["last_seen_at"];
2532
+ if (sessionId && existingSessionId && existingSessionId !== sessionId) {
2533
+ const lastSeenMs = new Date(existingLastSeen).getTime();
2534
+ const nowMs = Date.now();
2535
+ if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
2536
+ throw new AgentConflictError({
2537
+ existing_id: existingId,
2538
+ existing_name: normalizedName,
2539
+ last_seen_at: existingLastSeen,
2540
+ session_hint: existingSessionId.slice(0, 8),
2541
+ working_dir: null
2542
+ });
2543
+ }
2544
+ }
2545
+ d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
2490
2546
  timestamp,
2547
+ sessionId ?? existingSessionId,
2491
2548
  existingId
2492
2549
  ]);
2493
2550
  if (description) {
2494
- d.run("UPDATE agents SET description = ? WHERE id = ?", [
2495
- description,
2496
- existingId
2497
- ]);
2551
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
2498
2552
  }
2499
2553
  if (role) {
2500
- d.run("UPDATE agents SET role = ? WHERE id = ?", [
2501
- role,
2502
- existingId
2503
- ]);
2554
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
2555
+ }
2556
+ if (projectId !== undefined) {
2557
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
2504
2558
  }
2505
2559
  return getAgent(existingId, d);
2506
2560
  }
2507
2561
  const id = shortUuid();
2508
- d.run("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", [id, normalizedName, description || null, role || "agent", timestamp, timestamp]);
2562
+ 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]);
2509
2563
  return getAgent(id, d);
2510
2564
  }
2511
2565
  function getAgent(idOrName, db) {
@@ -2557,8 +2611,11 @@ function updateAgent(id, updates, db) {
2557
2611
  d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
2558
2612
  return getAgent(agent.id, d);
2559
2613
  }
2614
+ var CONFLICT_WINDOW_MS;
2560
2615
  var init_agents = __esm(() => {
2616
+ init_types();
2561
2617
  init_database();
2618
+ CONFLICT_WINDOW_MS = 30 * 60 * 1000;
2562
2619
  });
2563
2620
 
2564
2621
  // src/db/projects.ts
@@ -5347,10 +5404,10 @@ program2.command("clean").description("Remove expired memories and enforce quota
5347
5404
  handleError(e);
5348
5405
  }
5349
5406
  });
5350
- program2.command("init <name>").description("Register an agent (returns ID)").option("-d, --description <text>", "Agent description").option("-r, --role <role>", "Agent role").action((name, opts) => {
5407
+ program2.command("init <name>").description("Register an agent (returns ID)").option("-d, --description <text>", "Agent description").option("-r, --role <role>", "Agent role").option("-p, --project <id>", "Lock agent to a project (sets active_project_id)").action((name, opts) => {
5351
5408
  try {
5352
5409
  const globalOpts = program2.opts();
5353
- const agent = registerAgent(name, opts.description, opts.role);
5410
+ const agent = registerAgent(name, undefined, opts.description, opts.role, opts.project);
5354
5411
  if (globalOpts.json) {
5355
5412
  outputJson(agent);
5356
5413
  } else {
@@ -5463,7 +5520,7 @@ program2.command("projects").description("Manage projects").option("--add", "Add
5463
5520
  handleError(e);
5464
5521
  }
5465
5522
  });
5466
- program2.command("inject").description("Output injection context for agent system prompts").option("--agent <name>", "Agent ID for scope filtering").option("--project <path>", "Project path for scope filtering").option("--session <id>", "Session ID for scope filtering").option("--max-tokens <n>", "Max approximate token budget", parseInt).option("--categories <cats>", "Comma-separated categories to include").action((opts) => {
5523
+ program2.command("inject").description("Output injection context for agent system prompts").option("--agent <name>", "Agent ID for scope filtering").option("--project <path>", "Project path for scope filtering").option("--session <id>", "Session ID for scope filtering").option("--max-tokens <n>", "Max approximate token budget", parseInt).option("--categories <cats>", "Comma-separated categories to include").option("--format <fmt>", "Output format: xml (default), compact, markdown, json").action((opts) => {
5467
5524
  try {
5468
5525
  const globalOpts = program2.opts();
5469
5526
  const maxTokens = opts.maxTokens || 500;
@@ -5527,8 +5584,16 @@ program2.command("inject").description("Output injection context for agent syste
5527
5584
  const charBudget = maxTokens * 4;
5528
5585
  const lines = [];
5529
5586
  let totalChars = 0;
5587
+ const fmt = opts.format || "xml";
5530
5588
  for (const m of unique) {
5531
- const line = `- [${m.scope}/${m.category}] ${m.key}: ${m.value}`;
5589
+ let line;
5590
+ if (fmt === "compact") {
5591
+ line = `${m.key}: ${m.value}`;
5592
+ } else if (fmt === "json") {
5593
+ line = JSON.stringify({ key: m.key, value: m.value, scope: m.scope, category: m.category, importance: m.importance });
5594
+ } else {
5595
+ line = `- [${m.scope}/${m.category}] ${m.key}: ${m.value}`;
5596
+ }
5532
5597
  if (totalChars + line.length > charBudget)
5533
5598
  break;
5534
5599
  lines.push(line);
@@ -5543,10 +5608,23 @@ program2.command("inject").description("Output injection context for agent syste
5543
5608
  }
5544
5609
  return;
5545
5610
  }
5546
- const context = `<agent-memories>
5611
+ let context;
5612
+ if (fmt === "compact") {
5613
+ context = lines.join(`
5614
+ `);
5615
+ } else if (fmt === "json") {
5616
+ context = `[${lines.join(",")}]`;
5617
+ } else if (fmt === "markdown") {
5618
+ context = `## Agent Memories
5619
+
5620
+ ${lines.join(`
5621
+ `)}`;
5622
+ } else {
5623
+ context = `<agent-memories>
5547
5624
  ${lines.join(`
5548
5625
  `)}
5549
5626
  </agent-memories>`;
5627
+ }
5550
5628
  if (globalOpts.json) {
5551
5629
  outputJson({ context, count: lines.length });
5552
5630
  } else {
@@ -1,6 +1,6 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import type { Agent } from "../types/index.js";
3
- export declare function registerAgent(name: string, description?: string, role?: string, db?: Database): Agent;
3
+ export declare function registerAgent(name: string, sessionId?: string, description?: string, role?: string, projectId?: string, db?: Database): Agent;
4
4
  export declare function getAgent(idOrName: string, db?: Database): Agent | null;
5
5
  export declare function listAgents(db?: Database): Agent[];
6
6
  export declare function touchAgent(idOrName: string, db?: Database): void;
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/db/agents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAgB/C,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,MAAM,EACb,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,CAsCP;AAED,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,GAAG,IAAI,CAsBd;AAED,wBAAgB,UAAU,CAAC,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAMjD;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAKhE;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAM7E;AAED,wBAAgB,WAAW,CACzB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACtI,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,GAAG,IAAI,CAyCd"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/db/agents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAoB/C,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,EAClB,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,CAuDP;AAED,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,GAAG,IAAI,CAsBd;AAED,wBAAgB,UAAU,CAAC,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAMjD;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAKhE;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAM7E;AAED,wBAAgB,WAAW,CACzB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACtI,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,GAAG,IAAI,CAyCd"}
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAmCtC,wBAAgB,SAAS,IAAI,MAAM,CAkBlC;AAyOD,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAerD;AA+BD,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,IAAI,IAAI,MAAM,CAE7B;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAef"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAmCtC,wBAAgB,SAAS,IAAI,MAAM,CAkBlC;AAmQD,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAerD;AA+BD,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,IAAI,IAAI,MAAM,CAE7B;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAef"}
@@ -0,0 +1,52 @@
1
+ import { Database } from "bun:sqlite";
2
+ export type ResourceType = "project" | "memory" | "entity" | "agent" | "connector";
3
+ export type LockType = "advisory" | "exclusive";
4
+ export interface ResourceLock {
5
+ id: string;
6
+ resource_type: ResourceType;
7
+ resource_id: string;
8
+ agent_id: string;
9
+ lock_type: LockType;
10
+ locked_at: string;
11
+ expires_at: string;
12
+ }
13
+ /**
14
+ * Acquire a lock on a resource.
15
+ * - advisory: multiple agents can hold advisory locks simultaneously
16
+ * - exclusive: only one agent can hold an exclusive lock at a time
17
+ *
18
+ * Returns the lock if acquired, null if blocked by an existing exclusive lock.
19
+ * TTL is in seconds (default: 5 minutes).
20
+ */
21
+ export declare function acquireLock(agentId: string, resourceType: ResourceType, resourceId: string, lockType?: LockType, ttlSeconds?: number, db?: Database): ResourceLock | null;
22
+ /**
23
+ * Release a specific lock by ID. Only the owning agent can release it.
24
+ * Returns true if released, false if not found or not owned.
25
+ */
26
+ export declare function releaseLock(lockId: string, agentId: string, db?: Database): boolean;
27
+ /**
28
+ * Release all locks held by an agent on a specific resource.
29
+ */
30
+ export declare function releaseResourceLocks(agentId: string, resourceType: ResourceType, resourceId: string, db?: Database): number;
31
+ /**
32
+ * Release all locks held by an agent (e.g., on session end).
33
+ */
34
+ export declare function releaseAllAgentLocks(agentId: string, db?: Database): number;
35
+ /**
36
+ * Check if a resource is currently locked.
37
+ * Returns the active lock(s) or empty array.
38
+ */
39
+ export declare function checkLock(resourceType: ResourceType, resourceId: string, lockType?: LockType, db?: Database): ResourceLock[];
40
+ /**
41
+ * Check if a specific agent holds a lock on a resource.
42
+ */
43
+ export declare function agentHoldsLock(agentId: string, resourceType: ResourceType, resourceId: string, lockType?: LockType, db?: Database): ResourceLock | null;
44
+ /**
45
+ * List all active locks for an agent.
46
+ */
47
+ export declare function listAgentLocks(agentId: string, db?: Database): ResourceLock[];
48
+ /**
49
+ * Delete all expired locks. Called automatically by other lock functions.
50
+ */
51
+ export declare function cleanExpiredLocks(db?: Database): number;
52
+ //# sourceMappingURL=locks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locks.d.ts","sourceRoot":"","sources":["../../src/db/locks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,WAAW,CAAC;AACnF,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,WAAW,CAAC;AAEhD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,YAAY,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,QAAQ,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAcD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,MAAM,EAClB,QAAQ,GAAE,QAAsB,EAChC,UAAU,SAAM,EAChB,EAAE,CAAC,EAAE,QAAQ,GACZ,YAAY,GAAG,IAAI,CAuDrB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAOnF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,MAAM,EAClB,EAAE,CAAC,EAAE,QAAQ,GACZ,MAAM,CAOR;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAI3E;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,QAAQ,EACnB,EAAE,CAAC,EAAE,QAAQ,GACZ,YAAY,EAAE,CAgBhB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,QAAQ,EACnB,EAAE,CAAC,EAAE,QAAQ,GACZ,YAAY,GAAG,IAAI,CAcrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,EAAE,CAS7E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAIvD"}
package/dist/index.d.ts CHANGED
@@ -3,6 +3,9 @@ export { MemoryNotFoundError, DuplicateMemoryError, MemoryExpiredError, InvalidS
3
3
  export { getDatabase, closeDatabase, resetDatabase, getDbPath, resolvePartialId, now, uuid, shortUuid, } from "./db/database.js";
4
4
  export { createMemory, getMemory, getMemoryByKey, getMemoriesByKey, listMemories, updateMemory, deleteMemory, bulkDeleteMemories, touchMemory, cleanExpiredMemories, getMemoryVersions, } from "./db/memories.js";
5
5
  export { registerAgent, getAgent, listAgents, listAgentsByProject, updateAgent, touchAgent, } from "./db/agents.js";
6
+ export { acquireLock, releaseLock, releaseResourceLocks, releaseAllAgentLocks, checkLock, agentHoldsLock, listAgentLocks, cleanExpiredLocks, } from "./db/locks.js";
7
+ export type { ResourceLock, ResourceType, LockType } from "./db/locks.js";
8
+ export { acquireMemoryWriteLock, releaseMemoryWriteLock, checkMemoryWriteLock, withMemoryLock, MemoryLockConflictError, memoryLockId, } from "./lib/memory-lock.js";
6
9
  export { registerProject, getProject, listProjects, } from "./db/projects.js";
7
10
  export { searchMemories } from "./lib/search.js";
8
11
  export { loadConfig, DEFAULT_CONFIG, getActiveProfile, setActiveProfile, listProfiles, deleteProfile, } from "./lib/config.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,MAAM,EACN,mBAAmB,EACnB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,UAAU,EACV,KAAK,EACL,OAAO,EACP,cAAc,EACd,aAAa,EACb,WAAW,EACX,UAAU,EACV,kBAAkB,EAElB,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,UAAU,EACV,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,WAAW,EACX,aAAa,EACb,aAAa,EACb,SAAS,EACT,gBAAgB,EAChB,GAAG,EACH,IAAI,EACJ,SAAS,GACV,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,YAAY,EACZ,SAAS,EACT,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,aAAa,EACb,QAAQ,EACR,UAAU,EACV,mBAAmB,EACnB,WAAW,EACX,UAAU,GACX,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,eAAe,EACf,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGjD,OAAO,EACL,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAG1D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAG/G,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGhE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGjE,OAAO,EACL,YAAY,EACZ,SAAS,EACT,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,cAAc,GACf,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,WAAW,EACX,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,cAAc,EACd,QAAQ,EACR,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,eAAe,EACf,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,MAAM,EACN,mBAAmB,EACnB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,UAAU,EACV,KAAK,EACL,OAAO,EACP,cAAc,EACd,aAAa,EACb,WAAW,EACX,UAAU,EACV,kBAAkB,EAElB,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,UAAU,EACV,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,WAAW,EACX,aAAa,EACb,aAAa,EACb,SAAS,EACT,gBAAgB,EAChB,GAAG,EACH,IAAI,EACJ,SAAS,GACV,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,YAAY,EACZ,SAAS,EACT,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,aAAa,EACb,QAAQ,EACR,UAAU,EACV,mBAAmB,EACnB,WAAW,EACX,UAAU,GACX,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,WAAW,EACX,WAAW,EACX,oBAAoB,EACpB,oBAAoB,EACpB,SAAS,EACT,cAAc,EACd,cAAc,EACd,iBAAiB,GAClB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG1E,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,cAAc,EACd,uBAAuB,EACvB,YAAY,GACb,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,eAAe,EACf,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGjD,OAAO,EACL,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAG1D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAG/G,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGhE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGjE,OAAO,EACL,YAAY,EACZ,SAAS,EACT,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,cAAc,GACf,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,WAAW,EACX,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,cAAc,EACd,QAAQ,EACR,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,eAAe,EACf,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,23 @@
1
1
  // @bun
2
2
  // src/types/index.ts
3
+ class AgentConflictError extends Error {
4
+ conflict = true;
5
+ existing_id;
6
+ existing_name;
7
+ last_seen_at;
8
+ session_hint;
9
+ working_dir;
10
+ constructor(opts) {
11
+ 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.`;
12
+ super(msg);
13
+ this.name = "AgentConflictError";
14
+ this.existing_id = opts.existing_id;
15
+ this.existing_name = opts.existing_name;
16
+ this.last_seen_at = opts.last_seen_at;
17
+ this.session_hint = opts.session_hint;
18
+ this.working_dir = opts.working_dir ?? null;
19
+ }
20
+ }
3
21
  class EntityNotFoundError extends Error {
4
22
  constructor(id) {
5
23
  super(`Entity not found: ${id}`);
@@ -301,6 +319,28 @@ var MIGRATIONS = [
301
319
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
302
320
  CREATE INDEX IF NOT EXISTS idx_agents_active_project ON agents(active_project_id);
303
321
  INSERT OR IGNORE INTO _migrations (id) VALUES (6);
322
+ `,
323
+ `
324
+ ALTER TABLE agents ADD COLUMN session_id TEXT;
325
+ CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id);
326
+ INSERT OR IGNORE INTO _migrations (id) VALUES (7);
327
+ `,
328
+ `
329
+ CREATE TABLE IF NOT EXISTS resource_locks (
330
+ id TEXT PRIMARY KEY,
331
+ resource_type TEXT NOT NULL CHECK(resource_type IN ('project', 'memory', 'entity', 'agent', 'connector')),
332
+ resource_id TEXT NOT NULL,
333
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
334
+ lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
335
+ locked_at TEXT NOT NULL DEFAULT (datetime('now')),
336
+ expires_at TEXT NOT NULL
337
+ );
338
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
339
+ ON resource_locks(resource_type, resource_id)
340
+ WHERE lock_type = 'exclusive';
341
+ CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
342
+ CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
343
+ INSERT OR IGNORE INTO _migrations (id) VALUES (8);
304
344
  `
305
345
  ];
306
346
  var _db = null;
@@ -399,10 +439,12 @@ function containsSecrets(text) {
399
439
  }
400
440
 
401
441
  // src/db/agents.ts
442
+ var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
402
443
  function parseAgentRow(row) {
403
444
  return {
404
445
  id: row["id"],
405
446
  name: row["name"],
447
+ session_id: row["session_id"] || null,
406
448
  description: row["description"] || null,
407
449
  role: row["role"] || null,
408
450
  metadata: JSON.parse(row["metadata"] || "{}"),
@@ -411,33 +453,46 @@ function parseAgentRow(row) {
411
453
  last_seen_at: row["last_seen_at"]
412
454
  };
413
455
  }
414
- function registerAgent(name, description, role, db) {
456
+ function registerAgent(name, sessionId, description, role, projectId, db) {
415
457
  const d = db || getDatabase();
416
458
  const timestamp = now();
417
459
  const normalizedName = name.trim().toLowerCase();
418
460
  const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
419
461
  if (existing) {
420
462
  const existingId = existing["id"];
421
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [
463
+ const existingSessionId = existing["session_id"] || null;
464
+ const existingLastSeen = existing["last_seen_at"];
465
+ if (sessionId && existingSessionId && existingSessionId !== sessionId) {
466
+ const lastSeenMs = new Date(existingLastSeen).getTime();
467
+ const nowMs = Date.now();
468
+ if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
469
+ throw new AgentConflictError({
470
+ existing_id: existingId,
471
+ existing_name: normalizedName,
472
+ last_seen_at: existingLastSeen,
473
+ session_hint: existingSessionId.slice(0, 8),
474
+ working_dir: null
475
+ });
476
+ }
477
+ }
478
+ d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
422
479
  timestamp,
480
+ sessionId ?? existingSessionId,
423
481
  existingId
424
482
  ]);
425
483
  if (description) {
426
- d.run("UPDATE agents SET description = ? WHERE id = ?", [
427
- description,
428
- existingId
429
- ]);
484
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
430
485
  }
431
486
  if (role) {
432
- d.run("UPDATE agents SET role = ? WHERE id = ?", [
433
- role,
434
- existingId
435
- ]);
487
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
488
+ }
489
+ if (projectId !== undefined) {
490
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
436
491
  }
437
492
  return getAgent(existingId, d);
438
493
  }
439
494
  const id = shortUuid();
440
- d.run("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", [id, normalizedName, description || null, role || "agent", timestamp, timestamp]);
495
+ 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]);
441
496
  return getAgent(id, d);
442
497
  }
443
498
  function getAgent(idOrName, db) {
@@ -1602,6 +1657,134 @@ function getMemoryVersions(memoryId, db) {
1602
1657
  return [];
1603
1658
  }
1604
1659
  }
1660
+ // src/db/locks.ts
1661
+ function parseLockRow(row) {
1662
+ return {
1663
+ id: row["id"],
1664
+ resource_type: row["resource_type"],
1665
+ resource_id: row["resource_id"],
1666
+ agent_id: row["agent_id"],
1667
+ lock_type: row["lock_type"],
1668
+ locked_at: row["locked_at"],
1669
+ expires_at: row["expires_at"]
1670
+ };
1671
+ }
1672
+ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
1673
+ const d = db || getDatabase();
1674
+ cleanExpiredLocks(d);
1675
+ 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);
1676
+ if (ownLock) {
1677
+ const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1678
+ d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
1679
+ newExpiry,
1680
+ ownLock["id"]
1681
+ ]);
1682
+ return parseLockRow({ ...ownLock, expires_at: newExpiry });
1683
+ }
1684
+ if (lockType === "exclusive") {
1685
+ 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);
1686
+ if (existing) {
1687
+ return null;
1688
+ }
1689
+ }
1690
+ const id = shortUuid();
1691
+ const lockedAt = now();
1692
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1693
+ 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]);
1694
+ return {
1695
+ id,
1696
+ resource_type: resourceType,
1697
+ resource_id: resourceId,
1698
+ agent_id: agentId,
1699
+ lock_type: lockType,
1700
+ locked_at: lockedAt,
1701
+ expires_at: expiresAt
1702
+ };
1703
+ }
1704
+ function releaseLock(lockId, agentId, db) {
1705
+ const d = db || getDatabase();
1706
+ const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
1707
+ return result.changes > 0;
1708
+ }
1709
+ function releaseResourceLocks(agentId, resourceType, resourceId, db) {
1710
+ const d = db || getDatabase();
1711
+ const result = d.run("DELETE FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ?", [agentId, resourceType, resourceId]);
1712
+ return result.changes;
1713
+ }
1714
+ function releaseAllAgentLocks(agentId, db) {
1715
+ const d = db || getDatabase();
1716
+ const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
1717
+ return result.changes;
1718
+ }
1719
+ function checkLock(resourceType, resourceId, lockType, db) {
1720
+ const d = db || getDatabase();
1721
+ cleanExpiredLocks(d);
1722
+ 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')";
1723
+ const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
1724
+ return rows.map(parseLockRow);
1725
+ }
1726
+ function agentHoldsLock(agentId, resourceType, resourceId, lockType, db) {
1727
+ const d = db || getDatabase();
1728
+ const query = lockType ? "SELECT * FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
1729
+ const row = lockType ? d.query(query).get(agentId, resourceType, resourceId, lockType) : d.query(query).get(agentId, resourceType, resourceId);
1730
+ return row ? parseLockRow(row) : null;
1731
+ }
1732
+ function listAgentLocks(agentId, db) {
1733
+ const d = db || getDatabase();
1734
+ cleanExpiredLocks(d);
1735
+ const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
1736
+ return rows.map(parseLockRow);
1737
+ }
1738
+ function cleanExpiredLocks(db) {
1739
+ const d = db || getDatabase();
1740
+ const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
1741
+ return result.changes;
1742
+ }
1743
+ // src/lib/memory-lock.ts
1744
+ var MEMORY_WRITE_TTL = 30;
1745
+ function memoryLockId(key, scope, projectId) {
1746
+ return `${scope}:${key}:${projectId ?? ""}`;
1747
+ }
1748
+ function acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds = MEMORY_WRITE_TTL, db) {
1749
+ const d = db || getDatabase();
1750
+ return acquireLock(agentId, "memory", memoryLockId(key, scope, projectId), "exclusive", ttlSeconds, d);
1751
+ }
1752
+ function releaseMemoryWriteLock(lockId, agentId, db) {
1753
+ const d = db || getDatabase();
1754
+ return releaseLock(lockId, agentId, d);
1755
+ }
1756
+ function checkMemoryWriteLock(key, scope, projectId, db) {
1757
+ const d = db || getDatabase();
1758
+ const locks = checkLock("memory", memoryLockId(key, scope, projectId), "exclusive", d);
1759
+ return locks[0] ?? null;
1760
+ }
1761
+ function withMemoryLock(agentId, key, scope, projectId, fn, ttlSeconds = MEMORY_WRITE_TTL, db) {
1762
+ const d = db || getDatabase();
1763
+ const lock = acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds, d);
1764
+ if (!lock) {
1765
+ const existing = checkMemoryWriteLock(key, scope, projectId, d);
1766
+ throw new MemoryLockConflictError(key, scope, existing?.agent_id ?? "unknown");
1767
+ }
1768
+ try {
1769
+ return fn();
1770
+ } finally {
1771
+ releaseLock(lock.id, agentId, d);
1772
+ }
1773
+ }
1774
+
1775
+ class MemoryLockConflictError extends Error {
1776
+ conflict = true;
1777
+ key;
1778
+ scope;
1779
+ blocking_agent_id;
1780
+ constructor(key, scope, blockingAgentId) {
1781
+ super(`Memory key "${key}" (scope: ${scope}) is currently write-locked by agent ${blockingAgentId}. ` + "Retry after a few seconds or use optimistic locking (version field).");
1782
+ this.name = "MemoryLockConflictError";
1783
+ this.key = key;
1784
+ this.scope = scope;
1785
+ this.blocking_agent_id = blockingAgentId;
1786
+ }
1787
+ }
1605
1788
  // src/lib/search.ts
1606
1789
  function parseMemoryRow2(row) {
1607
1790
  return {
@@ -2481,6 +2664,7 @@ function syncMemories(agentName, direction = "both", options = {}) {
2481
2664
  }
2482
2665
  var defaultSyncAgents = ["claude", "codex", "gemini"];
2483
2666
  export {
2667
+ withMemoryLock,
2484
2668
  uuid,
2485
2669
  updateMemory,
2486
2670
  updateEntity,
@@ -2495,6 +2679,10 @@ export {
2495
2679
  runCleanup,
2496
2680
  resolvePartialId,
2497
2681
  resetDatabase,
2682
+ releaseResourceLocks,
2683
+ releaseMemoryWriteLock,
2684
+ releaseLock,
2685
+ releaseAllAgentLocks,
2498
2686
  registerProject,
2499
2687
  registerAgent,
2500
2688
  redactSecrets,
@@ -2502,6 +2690,7 @@ export {
2502
2690
  parseEntityRow,
2503
2691
  now,
2504
2692
  mergeEntities,
2693
+ memoryLockId,
2505
2694
  loadConfig,
2506
2695
  listRelations,
2507
2696
  listProjects,
@@ -2510,6 +2699,7 @@ export {
2510
2699
  listEntities,
2511
2700
  listAgentsByProject,
2512
2701
  listAgents,
2702
+ listAgentLocks,
2513
2703
  linkEntityToMemory,
2514
2704
  getRelation,
2515
2705
  getRelatedEntities,
@@ -2543,12 +2733,19 @@ export {
2543
2733
  containsSecrets,
2544
2734
  closeDatabase,
2545
2735
  cleanExpiredMemories,
2736
+ cleanExpiredLocks,
2737
+ checkMemoryWriteLock,
2738
+ checkLock,
2546
2739
  bulkLinkEntities,
2547
2740
  bulkDeleteMemories,
2548
2741
  archiveUnused,
2549
2742
  archiveStale,
2743
+ agentHoldsLock,
2744
+ acquireMemoryWriteLock,
2745
+ acquireLock,
2550
2746
  VersionConflictError,
2551
2747
  MemoryNotFoundError,
2748
+ MemoryLockConflictError,
2552
2749
  MemoryInjector,
2553
2750
  MemoryExpiredError,
2554
2751
  InvalidScopeError,
@@ -0,0 +1,58 @@
1
+ /**
2
+ * OPE4-00111: Concurrent memory access coordination
3
+ *
4
+ * Uses the resource_locks table (Migration 8) to provide:
5
+ * - Advisory locks for read-heavy coordination
6
+ * - Exclusive locks to prevent simultaneous writes to the same key
7
+ *
8
+ * Design:
9
+ * - Reads: always parallel, no locking needed
10
+ * - Writes: agents SHOULD acquire an exclusive lock on the memory key before writing
11
+ * The lock resource_id uses format: "{scope}:{key}:{agent_id}:{project_id}"
12
+ * This prevents two agents overwriting the same shared memory simultaneously
13
+ * - Optimistic fallback: if lock can't be acquired, VersionConflictError from
14
+ * updateMemory() provides a second layer of protection
15
+ */
16
+ import { Database } from "bun:sqlite";
17
+ import { type ResourceLock } from "../db/locks.js";
18
+ /**
19
+ * Compute the lock resource_id for a memory key.
20
+ * Scoped by scope + key + project so different projects don't block each other.
21
+ */
22
+ export declare function memoryLockId(key: string, scope: string, projectId?: string | null): string;
23
+ /**
24
+ * Acquire an exclusive write lock on a memory key.
25
+ * Returns the lock if acquired, null if another agent is writing.
26
+ */
27
+ export declare function acquireMemoryWriteLock(agentId: string, key: string, scope: string, projectId?: string | null, ttlSeconds?: number, db?: Database): ResourceLock | null;
28
+ /**
29
+ * Release a memory write lock.
30
+ */
31
+ export declare function releaseMemoryWriteLock(lockId: string, agentId: string, db?: Database): boolean;
32
+ /**
33
+ * Check if a memory key is currently write-locked.
34
+ * Returns the active lock or null.
35
+ */
36
+ export declare function checkMemoryWriteLock(key: string, scope: string, projectId?: string | null, db?: Database): ResourceLock | null;
37
+ /**
38
+ * Execute a callback with an exclusive memory write lock.
39
+ * Throws if the lock cannot be acquired (another agent is writing).
40
+ * Automatically releases the lock after the callback completes.
41
+ *
42
+ * Usage:
43
+ * withMemoryLock(agentId, "my-key", "shared", projectId, () => {
44
+ * createMemory({ key: "my-key", ... });
45
+ * });
46
+ */
47
+ export declare function withMemoryLock<T>(agentId: string, key: string, scope: string, projectId: string | null | undefined, fn: () => T, ttlSeconds?: number, db?: Database): T;
48
+ /**
49
+ * Error thrown when a memory write lock cannot be acquired.
50
+ */
51
+ export declare class MemoryLockConflictError extends Error {
52
+ readonly conflict: true;
53
+ readonly key: string;
54
+ readonly scope: string;
55
+ readonly blocking_agent_id: string;
56
+ constructor(key: string, scope: string, blockingAgentId: string);
57
+ }
58
+ //# sourceMappingURL=memory-lock.d.ts.map