@hasna/conversations 0.2.1 → 0.2.2

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/bin/hook.js CHANGED
@@ -4,27 +4,37 @@ var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __moduleCache = /* @__PURE__ */ new WeakMap;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
8
10
  var __toCommonJS = (from) => {
9
- var entry = __moduleCache.get(from), desc;
11
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
12
  if (entry)
11
13
  return entry;
12
14
  entry = __defProp({}, "__esModule", { value: true });
13
- if (from && typeof from === "object" || typeof from === "function")
14
- __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
15
- get: () => from[key],
16
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
- }));
15
+ if (from && typeof from === "object" || typeof from === "function") {
16
+ for (var key of __getOwnPropNames(from))
17
+ if (!__hasOwnProp.call(entry, key))
18
+ __defProp(entry, key, {
19
+ get: __accessProp.bind(from, key),
20
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
21
+ });
22
+ }
18
23
  __moduleCache.set(from, entry);
19
24
  return entry;
20
25
  };
26
+ var __moduleCache;
27
+ var __returnValue = (v) => v;
28
+ function __exportSetter(name, newValue) {
29
+ this[name] = __returnValue.bind(null, newValue);
30
+ }
21
31
  var __export = (target, all) => {
22
32
  for (var name in all)
23
33
  __defProp(target, name, {
24
34
  get: all[name],
25
35
  enumerable: true,
26
36
  configurable: true,
27
- set: (newValue) => all[name] = () => newValue
37
+ set: __exportSetter.bind(all, name)
28
38
  });
29
39
  };
30
40
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
package/bin/index.js CHANGED
@@ -6,39 +6,60 @@ var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ function __accessProp(key) {
10
+ return this[key];
11
+ }
12
+ var __toESMCache_node;
13
+ var __toESMCache_esm;
9
14
  var __toESM = (mod, isNodeMode, target) => {
15
+ var canCache = mod != null && typeof mod === "object";
16
+ if (canCache) {
17
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
18
+ var cached = cache.get(mod);
19
+ if (cached)
20
+ return cached;
21
+ }
10
22
  target = mod != null ? __create(__getProtoOf(mod)) : {};
11
23
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
12
24
  for (let key of __getOwnPropNames(mod))
13
25
  if (!__hasOwnProp.call(to, key))
14
26
  __defProp(to, key, {
15
- get: () => mod[key],
27
+ get: __accessProp.bind(mod, key),
16
28
  enumerable: true
17
29
  });
30
+ if (canCache)
31
+ cache.set(mod, to);
18
32
  return to;
19
33
  };
20
- var __moduleCache = /* @__PURE__ */ new WeakMap;
21
34
  var __toCommonJS = (from) => {
22
- var entry = __moduleCache.get(from), desc;
35
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
23
36
  if (entry)
24
37
  return entry;
25
38
  entry = __defProp({}, "__esModule", { value: true });
26
- if (from && typeof from === "object" || typeof from === "function")
27
- __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
28
- get: () => from[key],
29
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
30
- }));
39
+ if (from && typeof from === "object" || typeof from === "function") {
40
+ for (var key of __getOwnPropNames(from))
41
+ if (!__hasOwnProp.call(entry, key))
42
+ __defProp(entry, key, {
43
+ get: __accessProp.bind(from, key),
44
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
45
+ });
46
+ }
31
47
  __moduleCache.set(from, entry);
32
48
  return entry;
33
49
  };
50
+ var __moduleCache;
34
51
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
52
+ var __returnValue = (v) => v;
53
+ function __exportSetter(name, newValue) {
54
+ this[name] = __returnValue.bind(null, newValue);
55
+ }
35
56
  var __export = (target, all) => {
36
57
  for (var name in all)
37
58
  __defProp(target, name, {
38
59
  get: all[name],
39
60
  enumerable: true,
40
61
  configurable: true,
41
- set: (newValue) => all[name] = () => newValue
62
+ set: __exportSetter.bind(all, name)
42
63
  });
43
64
  };
44
65
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -2382,12 +2403,64 @@ function getMessageById(id) {
2382
2403
  const row = db2.prepare("SELECT * FROM messages WHERE id = ?").get(id);
2383
2404
  return row ? parseMessage(row) : null;
2384
2405
  }
2406
+ function markReadByIds(ids) {
2407
+ const db2 = getDb();
2408
+ if (ids.length === 0)
2409
+ return 0;
2410
+ const placeholders = ids.map(() => "?").join(", ");
2411
+ const stmt = db2.prepare(`UPDATE messages SET read_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE id IN (${placeholders}) AND read_at IS NULL`);
2412
+ const result = stmt.run(...ids);
2413
+ return result.changes;
2414
+ }
2385
2415
  function markAllRead(agent) {
2386
2416
  const db2 = getDb();
2387
2417
  const stmt = db2.prepare(`UPDATE messages SET read_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE to_agent = ? AND read_at IS NULL`);
2388
2418
  const result = stmt.run(agent);
2389
2419
  return result.changes;
2390
2420
  }
2421
+ function readDigest(opts = {}) {
2422
+ const db2 = getDb();
2423
+ const countConditions = ["read_at IS NULL"];
2424
+ const countParams = [];
2425
+ if (opts.space) {
2426
+ countConditions.push("space = ?");
2427
+ countParams.push(opts.space);
2428
+ }
2429
+ if (opts.session_id) {
2430
+ countConditions.push("session_id = ?");
2431
+ countParams.push(opts.session_id);
2432
+ }
2433
+ if (opts.to) {
2434
+ countConditions.push("to_agent = ?");
2435
+ countParams.push(opts.to);
2436
+ }
2437
+ if (opts.since) {
2438
+ countConditions.push("created_at > ?");
2439
+ countParams.push(opts.since);
2440
+ }
2441
+ if (opts.project_id) {
2442
+ countConditions.push("project_id = ?");
2443
+ countParams.push(opts.project_id);
2444
+ }
2445
+ const countWhere = `WHERE ${countConditions.join(" AND ")}`;
2446
+ const totalUnread = db2.prepare(`SELECT COUNT(*) as n FROM messages ${countWhere}`).get(...countParams).n;
2447
+ const messages = readMessages({ ...opts, unread_only: opts.unread_only ?? true });
2448
+ if (messages.length > 0) {
2449
+ markReadByIds(messages.map((m) => m.id));
2450
+ }
2451
+ const digest = messages.map((m) => ({
2452
+ id: m.id,
2453
+ from: m.from_agent,
2454
+ created_at: m.created_at,
2455
+ preview: m.content.slice(0, 100) + (m.content.length > 100 ? "\u2026" : ""),
2456
+ priority: m.priority,
2457
+ has_attachments: Array.isArray(m.attachments) && m.attachments.length > 0,
2458
+ space: m.space,
2459
+ to: m.to_agent,
2460
+ unread: !m.read_at
2461
+ }));
2462
+ return { messages: digest, total_unread: totalUnread, shown: digest.length };
2463
+ }
2391
2464
  function escapeCsvField(value) {
2392
2465
  if (value === null || value === undefined)
2393
2466
  return "";
@@ -3431,6 +3504,9 @@ function isActiveSession(lastSeenAt) {
3431
3504
  const nowMs = Date.now();
3432
3505
  return nowMs - lastSeenMs < CONFLICT_THRESHOLD_SECONDS * 1000;
3433
3506
  }
3507
+ function isAgentConflict(result) {
3508
+ return result.conflict === true;
3509
+ }
3434
3510
  function registerAgent(name, sessionId, role, projectId) {
3435
3511
  const db2 = getDb();
3436
3512
  const normalizedName = name.trim().toLowerCase();
@@ -4207,7 +4283,7 @@ var init_poll = __esm(() => {
4207
4283
  var require_package = __commonJS((exports, module) => {
4208
4284
  module.exports = {
4209
4285
  name: "@hasna/conversations",
4210
- version: "0.2.1",
4286
+ version: "0.2.2",
4211
4287
  description: "Real-time CLI messaging for AI agents",
4212
4288
  type: "module",
4213
4289
  bin: {
@@ -33216,6 +33292,7 @@ function acquireLock(resourceType, resourceId, agentId, lockType = "advisory", e
33216
33292
  const db2 = getDb();
33217
33293
  return db2.transaction(() => {
33218
33294
  cleanExpiredLocks();
33295
+ releaseStaleAgentLocks();
33219
33296
  const existing = db2.prepare(`
33220
33297
  SELECT * FROM resource_locks
33221
33298
  WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
@@ -33242,6 +33319,55 @@ function acquireLock(resourceType, resourceId, agentId, lockType = "advisory", e
33242
33319
  return { acquired: true, lock };
33243
33320
  }).immediate();
33244
33321
  }
33322
+ function bulkAcquireLock(resources, agentId) {
33323
+ const db2 = getDb();
33324
+ return db2.transaction(() => {
33325
+ cleanExpiredLocks();
33326
+ releaseStaleAgentLocks();
33327
+ const acquired = [];
33328
+ for (const { resource_type, resource_id, lock_type = "advisory", expiry_ms = DEFAULT_LOCK_EXPIRY_MS } of resources) {
33329
+ const existing = db2.prepare(`
33330
+ SELECT * FROM resource_locks
33331
+ WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
33332
+ `).get(resource_type, resource_id, lock_type);
33333
+ if (existing && existing.agent_id !== agentId) {
33334
+ throw { _bulkConflict: true, resource_type, resource_id, held_by: existing.agent_id };
33335
+ }
33336
+ const expiresAt = new Date(Date.now() + expiry_ms).toISOString().slice(0, -1);
33337
+ if (existing) {
33338
+ db2.prepare(`
33339
+ UPDATE resource_locks SET expires_at = ?, locked_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
33340
+ WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
33341
+ `).run(expiresAt, resource_type, resource_id, lock_type);
33342
+ } else {
33343
+ db2.prepare(`
33344
+ INSERT INTO resource_locks (resource_type, resource_id, agent_id, lock_type, locked_at, expires_at)
33345
+ VALUES (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
33346
+ `).run(resource_type, resource_id, agentId, lock_type, expiresAt);
33347
+ }
33348
+ const lock = db2.prepare(`
33349
+ SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
33350
+ `).get(resource_type, resource_id, lock_type);
33351
+ acquired.push(lock);
33352
+ }
33353
+ return { acquired: true, locks: acquired };
33354
+ }).immediate();
33355
+ }
33356
+ function tryBulkAcquireLock(resources, agentId) {
33357
+ try {
33358
+ return bulkAcquireLock(resources, agentId);
33359
+ } catch (err) {
33360
+ const e = err;
33361
+ if (e?._bulkConflict) {
33362
+ return {
33363
+ acquired: false,
33364
+ locks: [],
33365
+ blocked_by: { resource_type: e.resource_type, resource_id: e.resource_id, held_by: e.held_by }
33366
+ };
33367
+ }
33368
+ throw err;
33369
+ }
33370
+ }
33245
33371
  function releaseLock(resourceType, resourceId, agentId) {
33246
33372
  const db2 = getDb();
33247
33373
  const result = db2.prepare(`
@@ -33253,6 +33379,7 @@ function releaseLock(resourceType, resourceId, agentId) {
33253
33379
  function checkLock(resourceType, resourceId) {
33254
33380
  const db2 = getDb();
33255
33381
  cleanExpiredLocks();
33382
+ releaseStaleAgentLocks();
33256
33383
  return db2.prepare(`
33257
33384
  SELECT * FROM resource_locks
33258
33385
  WHERE resource_type = ? AND resource_id = ?
@@ -33260,6 +33387,17 @@ function checkLock(resourceType, resourceId) {
33260
33387
  LIMIT 1
33261
33388
  `).get(resourceType, resourceId);
33262
33389
  }
33390
+ function releaseStaleAgentLocks() {
33391
+ const db2 = getDb();
33392
+ const result = db2.prepare(`
33393
+ DELETE FROM resource_locks
33394
+ WHERE LOWER(agent_id) IN (
33395
+ SELECT LOWER(agent) FROM agent_presence
33396
+ WHERE last_seen_at < strftime('%Y-%m-%dT%H:%M:%f', 'now', '-${STALE_HEARTBEAT_SECONDS} seconds')
33397
+ )
33398
+ `).run();
33399
+ return result.changes;
33400
+ }
33263
33401
  function cleanExpiredLocks() {
33264
33402
  const db2 = getDb();
33265
33403
  const result = db2.prepare(`
@@ -33270,6 +33408,7 @@ function cleanExpiredLocks() {
33270
33408
  function listLocks(opts) {
33271
33409
  const db2 = getDb();
33272
33410
  cleanExpiredLocks();
33411
+ releaseStaleAgentLocks();
33273
33412
  let query = "SELECT * FROM resource_locks WHERE 1=1";
33274
33413
  const params = [];
33275
33414
  if (opts?.resource_type) {
@@ -33283,10 +33422,36 @@ function listLocks(opts) {
33283
33422
  query += " ORDER BY locked_at ASC";
33284
33423
  return db2.prepare(query).all(...params);
33285
33424
  }
33286
- var DEFAULT_LOCK_EXPIRY_MS;
33425
+ function listLocksEnriched(opts) {
33426
+ const locks = listLocks(opts);
33427
+ const db2 = getDb();
33428
+ const nowMs = Date.now();
33429
+ return locks.map((lock) => {
33430
+ const lockedMs = new Date(lock.locked_at + "Z").getTime();
33431
+ const expiresMs = new Date(lock.expires_at + "Z").getTime();
33432
+ const presenceRow = db2.prepare(`
33433
+ SELECT role, status, last_seen_at, project_id FROM agent_presence WHERE LOWER(agent) = LOWER(?)
33434
+ `).get(lock.agent_id);
33435
+ const agent = presenceRow ? {
33436
+ role: presenceRow.role ?? null,
33437
+ status: presenceRow.status ?? null,
33438
+ online: presenceRow.last_seen_at ? nowMs - new Date(presenceRow.last_seen_at + "Z").getTime() < 60000 : false,
33439
+ last_seen_at: presenceRow.last_seen_at ?? null,
33440
+ project_id: presenceRow.project_id ?? null
33441
+ } : null;
33442
+ return {
33443
+ ...lock,
33444
+ locked_seconds_ago: Math.round((nowMs - lockedMs) / 1000),
33445
+ expires_in_seconds: Math.round((expiresMs - nowMs) / 1000),
33446
+ agent
33447
+ };
33448
+ });
33449
+ }
33450
+ var DEFAULT_LOCK_EXPIRY_MS, STALE_HEARTBEAT_SECONDS;
33287
33451
  var init_locks = __esm(() => {
33288
33452
  init_db();
33289
33453
  DEFAULT_LOCK_EXPIRY_MS = 5 * 60 * 1000;
33454
+ STALE_HEARTBEAT_SECONDS = 30 * 60;
33290
33455
  });
33291
33456
 
33292
33457
  // src/mcp/index.ts
@@ -33369,7 +33534,8 @@ var init_mcp2 = __esm(() => {
33369
33534
  project_id: exports_external.string().optional(),
33370
33535
  since: exports_external.string().optional(),
33371
33536
  limit: exports_external.coerce.number().optional(),
33372
- unread_only: exports_external.coerce.boolean().optional()
33537
+ unread_only: exports_external.coerce.boolean().optional(),
33538
+ mark_read: exports_external.coerce.boolean().optional()
33373
33539
  }
33374
33540
  }, async (args) => {
33375
33541
  const agent = resolveIdentity(args.from);
@@ -33377,6 +33543,9 @@ var init_mcp2 = __esm(() => {
33377
33543
  ...args,
33378
33544
  project_id: args.project_id ?? resolveProjectId(undefined, agent)
33379
33545
  });
33546
+ if (args.mark_read !== false && messages.length > 0) {
33547
+ markReadByIds(messages.map((m) => m.id));
33548
+ }
33380
33549
  return {
33381
33550
  content: [{ type: "text", text: JSON.stringify(messages) }]
33382
33551
  };
@@ -33572,15 +33741,36 @@ var init_mcp2 = __esm(() => {
33572
33741
  inputSchema: {
33573
33742
  space: exports_external.string(),
33574
33743
  since: exports_external.string().optional(),
33575
- limit: exports_external.coerce.number().optional()
33744
+ limit: exports_external.coerce.number().optional(),
33745
+ mark_read: exports_external.coerce.boolean().optional()
33576
33746
  }
33577
33747
  }, async (args) => {
33578
- const { space, since, limit } = args;
33748
+ const { space, since, limit, mark_read } = args;
33579
33749
  const messages = readMessages({ space, since, limit });
33750
+ if (mark_read !== false && messages.length > 0) {
33751
+ markReadByIds(messages.map((m) => m.id));
33752
+ }
33580
33753
  return {
33581
33754
  content: [{ type: "text", text: JSON.stringify(messages) }]
33582
33755
  };
33583
33756
  });
33757
+ server.registerTool("read_digest", {
33758
+ description: "Lightweight unread message digest \u2014 returns preview-only summaries, auto-marks as read. Use instead of read_messages on busy spaces to avoid token overflow.",
33759
+ inputSchema: {
33760
+ space: exports_external.string().optional(),
33761
+ session_id: exports_external.string().optional(),
33762
+ to: exports_external.string().optional(),
33763
+ since: exports_external.string().optional(),
33764
+ limit: exports_external.coerce.number().optional(),
33765
+ project_id: exports_external.string().optional()
33766
+ }
33767
+ }, async (args) => {
33768
+ const { space, session_id, to, since, limit, project_id } = args;
33769
+ const result = readDigest({ space, session_id, to, since, limit, project_id });
33770
+ return {
33771
+ content: [{ type: "text", text: JSON.stringify(result) }]
33772
+ };
33773
+ });
33584
33774
  server.registerTool("join_space", {
33585
33775
  description: "Join a space as a member.",
33586
33776
  inputSchema: {
@@ -34113,18 +34303,29 @@ var init_mcp2 = __esm(() => {
34113
34303
  return { content: [{ type: "text", text: JSON.stringify(summary) }] };
34114
34304
  });
34115
34305
  server.registerTool("acquire_lock", {
34116
- description: "Acquire an advisory or exclusive lock on a resource. Returns conflict info if another agent holds the lock.",
34306
+ description: "Acquire an advisory or exclusive lock on a resource. Returns conflict info if another agent holds the lock. On conflict, auto-DMs the holding agent.",
34117
34307
  inputSchema: {
34118
34308
  resource_type: exports_external.string(),
34119
34309
  resource_id: exports_external.string(),
34120
34310
  lock_type: exports_external.enum(["advisory", "exclusive"]).optional(),
34121
34311
  expiry_ms: exports_external.coerce.number().optional(),
34122
- from: exports_external.string().optional()
34312
+ from: exports_external.string().optional(),
34313
+ auto_dm: exports_external.coerce.boolean().optional()
34123
34314
  }
34124
34315
  }, async (args) => {
34125
- const { resource_type, resource_id, lock_type, expiry_ms, from: fromParam } = args;
34316
+ const { resource_type, resource_id, lock_type, expiry_ms, from: fromParam, auto_dm } = args;
34126
34317
  const agent = resolveIdentity(fromParam);
34127
34318
  const result = acquireLock(resource_type, resource_id, agent, lock_type ?? "advisory", expiry_ms);
34319
+ if (!result.acquired && result.held_by && auto_dm !== false) {
34320
+ try {
34321
+ sendMessage({
34322
+ from: agent,
34323
+ to: result.held_by,
34324
+ content: `Lock conflict: I (@${agent}) tried to acquire ${lock_type ?? "advisory"} lock on \`${resource_type}/${resource_id}\` but you hold it. If you no longer need it, release it with \`release_lock\`.`,
34325
+ priority: "high"
34326
+ });
34327
+ } catch {}
34328
+ }
34128
34329
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
34129
34330
  });
34130
34331
  server.registerTool("release_lock", {
@@ -34151,15 +34352,50 @@ var init_mcp2 = __esm(() => {
34151
34352
  return { content: [{ type: "text", text: JSON.stringify(lock ?? { locked: false }) }] };
34152
34353
  });
34153
34354
  server.registerTool("list_locks", {
34154
- description: "List all active (non-expired) locks. Filter by resource_type or agent.",
34355
+ description: "List all active (non-expired) locks enriched with agent presence details (status, online, last_seen_at) and time context (locked_seconds_ago, expires_in_seconds). Filter by resource_type or agent.",
34155
34356
  inputSchema: {
34156
34357
  resource_type: exports_external.string().optional(),
34157
34358
  agent_id: exports_external.string().optional()
34158
34359
  }
34159
34360
  }, async (args) => {
34160
- const locks = listLocks({ resource_type: args.resource_type, agent_id: args.agent_id });
34361
+ const locks = listLocksEnriched({ resource_type: args.resource_type, agent_id: args.agent_id });
34161
34362
  return { content: [{ type: "text", text: JSON.stringify(locks) }] };
34162
34363
  });
34364
+ server.registerTool("bulk_acquire_lock", {
34365
+ description: "Atomically acquire multiple locks at once. All-or-nothing: if any lock is held by another agent, none are acquired. Returns blocked_by info on conflict.",
34366
+ inputSchema: {
34367
+ resources: exports_external.array(exports_external.object({
34368
+ resource_type: exports_external.string(),
34369
+ resource_id: exports_external.string(),
34370
+ lock_type: exports_external.enum(["advisory", "exclusive"]).optional(),
34371
+ expiry_ms: exports_external.coerce.number().optional()
34372
+ })),
34373
+ from: exports_external.string().optional(),
34374
+ auto_dm: exports_external.coerce.boolean().optional()
34375
+ }
34376
+ }, async (args) => {
34377
+ const agent = resolveIdentity(args.from);
34378
+ const result = tryBulkAcquireLock(args.resources, agent);
34379
+ if (!result.acquired && result.blocked_by && args.auto_dm !== false) {
34380
+ try {
34381
+ sendMessage({
34382
+ from: agent,
34383
+ to: result.blocked_by.held_by,
34384
+ content: `Bulk lock conflict: I (@${agent}) tried to atomically acquire ${args.resources.length} locks but you hold \`${result.blocked_by.resource_type}/${result.blocked_by.resource_id}\`. Release it when done.`,
34385
+ priority: "high"
34386
+ });
34387
+ } catch {}
34388
+ }
34389
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
34390
+ });
34391
+ server.registerTool("clean_expired_locks", {
34392
+ description: "Clean up expired locks and auto-release locks held by agents whose heartbeat has been stale for >30 minutes. Returns counts of removed locks.",
34393
+ inputSchema: {}
34394
+ }, async () => {
34395
+ const stale = releaseStaleAgentLocks();
34396
+ const expired = cleanExpiredLocks();
34397
+ return { content: [{ type: "text", text: JSON.stringify({ released_stale_agent: stale, released_expired: expired, total: stale + expired }) }] };
34398
+ });
34163
34399
  server.registerTool("get_thread_replies", {
34164
34400
  description: "Get all replies in a thread for a given parent message ID.",
34165
34401
  inputSchema: {
@@ -34343,6 +34579,7 @@ var init_mcp2 = __esm(() => {
34343
34579
  const all = [
34344
34580
  "send_message",
34345
34581
  "read_messages",
34582
+ "read_digest",
34346
34583
  "list_sessions",
34347
34584
  "reply",
34348
34585
  "mark_read",
@@ -34381,9 +34618,11 @@ var init_mcp2 = __esm(() => {
34381
34618
  "get_reactions",
34382
34619
  "get_reaction_summary",
34383
34620
  "acquire_lock",
34621
+ "bulk_acquire_lock",
34384
34622
  "release_lock",
34385
34623
  "check_lock",
34386
34624
  "list_locks",
34625
+ "clean_expired_locks",
34387
34626
  "get_thread_replies",
34388
34627
  "set_focus",
34389
34628
  "get_focus",
@@ -34409,7 +34648,8 @@ var init_mcp2 = __esm(() => {
34409
34648
  }, async ({ names }) => {
34410
34649
  const descriptions = {
34411
34650
  send_message: "Send DM to agent. Required: to, content. Optional: from?, priority?(low|normal|high|urgent), blocking?",
34412
- read_messages: "Read messages with filters. Optional: session_id?, from?, to?, space?, since?(ISO), limit?, unread_only?",
34651
+ read_messages: "Read messages with filters. Optional: session_id?, from?, to?, space?, since?(ISO), limit?, unread_only?, mark_read?(default true \u2014 auto-marks returned messages as read, pass false to peek without consuming)",
34652
+ read_digest: "Lightweight unread digest \u2014 preview only (no full bodies), auto-marks read, never overflows tokens. Returns { messages, total_unread, shown }. Optional: space?, session_id?, to?, since?(ISO), limit?, project_id?",
34413
34653
  list_sessions: "List all DM sessions. Optional: agent?(filter by participant)",
34414
34654
  reply: "Reply to a message in same session. Required: message_id, content. Optional: from?",
34415
34655
  mark_read: "Mark messages as read. Optional: from?, ids?(array), all?(bool \u2014 mark all unread)",
@@ -34418,7 +34658,7 @@ var init_mcp2 = __esm(() => {
34418
34658
  create_space: "Create space and auto-join. Required: name. Optional: from?, description?, parent_id?(max 3 levels), project_id?",
34419
34659
  list_spaces: "List spaces with member/message counts. Optional: project_id?, parent_id?(use 'null' for top-level), include_archived?",
34420
34660
  send_to_space: "Post message to space. Required: space, content. Optional: from?, priority?(low|normal|high|urgent), blocking?",
34421
- read_space: "Read messages in a space. Required: space. Optional: since?(ISO), limit?",
34661
+ read_space: "Read messages in a space. Required: space. Optional: since?(ISO), limit?, mark_read?(default true \u2014 auto-marks returned messages as read)",
34422
34662
  join_space: "Join a space. Required: space. Optional: from?",
34423
34663
  leave_space: "Leave a space. Required: space. Optional: from?",
34424
34664
  update_space: "Update space fields. Required: name. Optional: description?, parent_id?(use 'null' to remove), project_id?(use 'null' to remove)",
@@ -34447,10 +34687,12 @@ var init_mcp2 = __esm(() => {
34447
34687
  remove_reaction: "Remove emoji reaction from a message. Required: message_id, emoji. Optional: from?",
34448
34688
  get_reactions: "Get all reactions for a message. Required: message_id",
34449
34689
  get_reaction_summary: "Get emoji counts + agent lists for a message. Required: message_id",
34450
- acquire_lock: "Acquire advisory/exclusive lock on a resource. Required: resource_type, resource_id. Optional: lock_type?(advisory|exclusive), expiry_ms?, from?",
34690
+ acquire_lock: "Acquire advisory/exclusive lock on a resource. On conflict, auto-DMs the holding agent. Required: resource_type, resource_id. Optional: lock_type?(advisory|exclusive), expiry_ms?, from?, auto_dm?(default true)",
34691
+ bulk_acquire_lock: "Atomically acquire multiple locks (all-or-nothing). Required: resources[]{resource_type,resource_id,lock_type?,expiry_ms?}. Optional: from?, auto_dm?(default true). Returns blocked_by on conflict.",
34451
34692
  release_lock: "Release lock held by agent. Required: resource_type, resource_id. Optional: from?",
34452
34693
  check_lock: "Check if resource is locked and who holds it. Required: resource_type, resource_id",
34453
- list_locks: "List active locks. Optional: resource_type?, agent_id?",
34694
+ list_locks: "List active locks enriched with agent presence + time context. Optional: resource_type?, agent_id?",
34695
+ clean_expired_locks: "Release expired locks + locks held by agents with stale heartbeat (>30 min). Returns {released_stale_agent, released_expired, total}",
34454
34696
  get_thread_replies: "Get all replies in a thread. Required: message_id. Optional: limit?",
34455
34697
  set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
34456
34698
  get_focus: "Get current focus: session focus, DB project_id, effective project_id. Optional: from?",
@@ -36099,6 +36341,33 @@ program2.command("read").description("Read messages").option("--session <id>", "
36099
36341
  }
36100
36342
  closeDb();
36101
36343
  });
36344
+ program2.command("digest").description("Show unread message digest (preview only, auto-marks read)").argument("[space]", "Space name to digest (omit for DMs)").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to show", parseInt).option("--to <agent>", "Filter by recipient (for DMs)").option("--json", "Output as JSON").action((spaceArg, opts) => {
36345
+ const result = readDigest({
36346
+ space: spaceArg || undefined,
36347
+ since: opts.since,
36348
+ limit: opts.limit,
36349
+ to: opts.to
36350
+ });
36351
+ if (opts.json) {
36352
+ console.log(JSON.stringify(result, null, 2));
36353
+ } else {
36354
+ console.log(chalk3.bold(`Unread: ${result.total_unread} total, showing ${result.shown}`));
36355
+ if (result.messages.length === 0) {
36356
+ console.log(chalk3.dim(" No unread messages."));
36357
+ } else {
36358
+ for (const msg of result.messages) {
36359
+ const time3 = chalk3.dim(msg.created_at.slice(11, 19));
36360
+ const from = chalk3.cyan(msg.from);
36361
+ const dest = msg.space ? chalk3.magenta(`#${msg.space}`) : chalk3.yellow(msg.to ?? "?");
36362
+ const priority = msg.priority !== "normal" ? chalk3.red(` [${msg.priority}]`) : "";
36363
+ const att = msg.has_attachments ? chalk3.dim(" \uD83D\uDCCE") : "";
36364
+ console.log(`${time3} ${from} \u2192 ${dest}${priority}${att}`);
36365
+ console.log(` ${chalk3.dim(msg.preview)}`);
36366
+ }
36367
+ }
36368
+ }
36369
+ closeDb();
36370
+ });
36102
36371
  program2.command("search").description("Search messages by content").argument("<query>", "Search query string").option("--space <name>", "Filter by space").option("--from <agent>", "Filter by sender").option("--to <agent>", "Filter by recipient").option("--limit <n>", "Max results to return", parseInt).option("--json", "Output as JSON").action((query, opts) => {
36103
36372
  const q = typeof query === "string" ? query.trim() : "";
36104
36373
  if (!q) {
@@ -37184,6 +37453,85 @@ agents.command("rename").description("Rename an agent in the presence list").arg
37184
37453
  }
37185
37454
  closeDb();
37186
37455
  });
37456
+ agents.command("register").description("Register an agent with conflict detection (30 min active window)").argument("<name>", "Agent name to register").option("--session <id>", "Session ID (default: random UUID)").option("--role <role>", "Agent role (default: agent)").option("--project <id>", "Project ID to lock agent to").option("--force", "Force takeover even if another session is active").option("--json", "Output as JSON").action((name, opts) => {
37457
+ const agentName = (typeof name === "string" ? name : "").trim();
37458
+ if (!agentName) {
37459
+ console.error(chalk3.red("Agent name is required."));
37460
+ process.exit(1);
37461
+ }
37462
+ const sessionId = opts.session || crypto.randomUUID();
37463
+ const result = registerAgent(agentName, sessionId, opts.role, opts.project);
37464
+ if (isAgentConflict(result)) {
37465
+ if (opts.json) {
37466
+ console.log(JSON.stringify(result));
37467
+ } else {
37468
+ console.error(chalk3.red(`Conflict: agent "${agentName}" is already active (last seen: ${result.last_seen_at}).`));
37469
+ console.error(chalk3.dim("Use --force or wait 30 minutes for the session to expire."));
37470
+ }
37471
+ process.exit(1);
37472
+ }
37473
+ if (opts.json) {
37474
+ console.log(JSON.stringify(result));
37475
+ } else {
37476
+ const action = result.took_over ? chalk3.yellow("took over") : result.created ? chalk3.green("registered") : chalk3.cyan("updated");
37477
+ console.log(` ${action} ${chalk3.bold(result.agent.agent)} session: ${chalk3.dim(sessionId)}`);
37478
+ }
37479
+ closeDb();
37480
+ });
37481
+ agents.command("heartbeat").description("Send a presence heartbeat to mark yourself as active").option("--from <agent>", "Agent identity (default: CONVERSATIONS_AGENT_ID or auto)").option("--status <status>", "Status: online, busy, idle (default: online)").option("--json", "Output as JSON").action((opts) => {
37482
+ const agent = resolveIdentity(opts.from);
37483
+ const status = opts.status || "online";
37484
+ heartbeat(agent, status);
37485
+ if (opts.json) {
37486
+ console.log(JSON.stringify({ agent, status, heartbeat: true }));
37487
+ } else {
37488
+ console.log(` ${chalk3.green("\u2665")} ${chalk3.cyan(agent)} ${chalk3.dim(status)}`);
37489
+ }
37490
+ closeDb();
37491
+ });
37492
+ var focus = program2.command("focus").description("Manage agent project focus");
37493
+ focus.command("set").description("Set your project focus \u2014 scopes read operations to this project").argument("<project>", "Project ID or name").option("--from <agent>", "Agent identity").option("--json", "Output as JSON").action((projectArg, opts) => {
37494
+ const agent = resolveIdentity(opts.from);
37495
+ const project2 = getProject(projectArg) || getProjectByName(projectArg);
37496
+ if (!project2) {
37497
+ console.error(chalk3.red(`Project "${projectArg}" not found.`));
37498
+ process.exit(1);
37499
+ }
37500
+ getDb().prepare("UPDATE agent_presence SET project_id = ? WHERE agent = ?").run(project2.id, agent);
37501
+ if (opts.json) {
37502
+ console.log(JSON.stringify({ agent, project_id: project2.id, project_name: project2.name, focused: true }));
37503
+ } else {
37504
+ console.log(` ${chalk3.green("focused")} ${chalk3.cyan(agent)} \u2192 ${chalk3.bold(project2.name)} ${chalk3.dim(`(${project2.id})`)}`);
37505
+ }
37506
+ closeDb();
37507
+ });
37508
+ focus.command("clear").description("Clear your project focus").option("--from <agent>", "Agent identity").option("--json", "Output as JSON").action((opts) => {
37509
+ const agent = resolveIdentity(opts.from);
37510
+ getDb().prepare("UPDATE agent_presence SET project_id = NULL WHERE agent = ?").run(agent);
37511
+ if (opts.json) {
37512
+ console.log(JSON.stringify({ agent, project_id: null, focused: false }));
37513
+ } else {
37514
+ console.log(` ${chalk3.yellow("unfocused")} ${chalk3.cyan(agent)}`);
37515
+ }
37516
+ closeDb();
37517
+ });
37518
+ focus.command("get").description("Show current project focus").option("--from <agent>", "Agent identity").option("--json", "Output as JSON").action((opts) => {
37519
+ const agent = resolveIdentity(opts.from);
37520
+ const presence = getPresence(agent);
37521
+ const projectId = presence?.project_id ?? null;
37522
+ const project2 = projectId ? getProject(projectId) || null : null;
37523
+ if (opts.json) {
37524
+ console.log(JSON.stringify({ agent, project_id: projectId, project_name: project2?.name ?? null }));
37525
+ } else {
37526
+ if (projectId) {
37527
+ const name = project2?.name ?? chalk3.dim("(unknown)");
37528
+ console.log(` ${chalk3.cyan(agent)} focused on ${chalk3.bold(name)} ${chalk3.dim(`(${projectId})`)}`);
37529
+ } else {
37530
+ console.log(` ${chalk3.cyan(agent)} ${chalk3.dim("no focus set")}`);
37531
+ }
37532
+ }
37533
+ closeDb();
37534
+ });
37187
37535
  program2.command("whoami").description("Show current agent identity and online status").option("--from <agent>", "Explicit agent identity").action((opts) => {
37188
37536
  const envValue = process.env.CONVERSATIONS_AGENT_ID?.trim();
37189
37537
  const agent = resolveIdentity(opts.from);