@hasna/conversations 0.1.28 → 0.1.30

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
@@ -118,12 +118,27 @@ function getDb() {
118
118
  agent TEXT PRIMARY KEY,
119
119
  session_id TEXT,
120
120
  role TEXT NOT NULL DEFAULT 'agent',
121
+ project_id TEXT,
121
122
  status TEXT NOT NULL DEFAULT 'online',
122
123
  last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
123
124
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
124
125
  metadata TEXT
125
126
  )
126
127
  `);
128
+ db.exec(`
129
+ CREATE TABLE IF NOT EXISTS resource_locks (
130
+ resource_type TEXT NOT NULL,
131
+ resource_id TEXT NOT NULL,
132
+ agent_id TEXT NOT NULL,
133
+ lock_type TEXT NOT NULL DEFAULT 'advisory',
134
+ locked_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
135
+ expires_at TEXT NOT NULL,
136
+ UNIQUE(resource_type, resource_id, lock_type)
137
+ )
138
+ `);
139
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_resource ON resource_locks(resource_type, resource_id)");
140
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_agent ON resource_locks(agent_id)");
141
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_expires ON resource_locks(expires_at)");
127
142
  db.exec(`
128
143
  CREATE TABLE IF NOT EXISTS reactions (
129
144
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -219,6 +234,9 @@ function getDb() {
219
234
  db.exec("ALTER TABLE agent_presence ADD COLUMN created_at TEXT NOT NULL DEFAULT ''");
220
235
  db.exec("UPDATE agent_presence SET created_at = last_seen_at WHERE created_at = ''");
221
236
  }
237
+ if (!presenceColNames.includes("project_id")) {
238
+ db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
239
+ }
222
240
  const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
223
241
  if (!ftsExists) {
224
242
  db.exec(`
package/bin/index.js CHANGED
@@ -1972,12 +1972,27 @@ function getDb() {
1972
1972
  agent TEXT PRIMARY KEY,
1973
1973
  session_id TEXT,
1974
1974
  role TEXT NOT NULL DEFAULT 'agent',
1975
+ project_id TEXT,
1975
1976
  status TEXT NOT NULL DEFAULT 'online',
1976
1977
  last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1977
1978
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1978
1979
  metadata TEXT
1979
1980
  )
1980
1981
  `);
1982
+ db.exec(`
1983
+ CREATE TABLE IF NOT EXISTS resource_locks (
1984
+ resource_type TEXT NOT NULL,
1985
+ resource_id TEXT NOT NULL,
1986
+ agent_id TEXT NOT NULL,
1987
+ lock_type TEXT NOT NULL DEFAULT 'advisory',
1988
+ locked_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1989
+ expires_at TEXT NOT NULL,
1990
+ UNIQUE(resource_type, resource_id, lock_type)
1991
+ )
1992
+ `);
1993
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_resource ON resource_locks(resource_type, resource_id)");
1994
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_agent ON resource_locks(agent_id)");
1995
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_expires ON resource_locks(expires_at)");
1981
1996
  db.exec(`
1982
1997
  CREATE TABLE IF NOT EXISTS reactions (
1983
1998
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -2073,6 +2088,9 @@ function getDb() {
2073
2088
  db.exec("ALTER TABLE agent_presence ADD COLUMN created_at TEXT NOT NULL DEFAULT ''");
2074
2089
  db.exec("UPDATE agent_presence SET created_at = last_seen_at WHERE created_at = ''");
2075
2090
  }
2091
+ if (!presenceColNames.includes("project_id")) {
2092
+ db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
2093
+ }
2076
2094
  const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
2077
2095
  if (!ftsExists) {
2078
2096
  db.exec(`
@@ -3339,6 +3357,7 @@ function parsePresence(row) {
3339
3357
  agent: row.agent,
3340
3358
  session_id: row.session_id ?? null,
3341
3359
  role: row.role || "agent",
3360
+ project_id: row.project_id ?? null,
3342
3361
  status: row.status,
3343
3362
  last_seen_at: lastSeenAt,
3344
3363
  created_at: row.created_at || lastSeenAt,
@@ -3351,43 +3370,46 @@ function isActiveSession(lastSeenAt) {
3351
3370
  const nowMs = Date.now();
3352
3371
  return nowMs - lastSeenMs < CONFLICT_THRESHOLD_SECONDS * 1000;
3353
3372
  }
3354
- function registerAgent(name, sessionId, role) {
3373
+ function registerAgent(name, sessionId, role, projectId) {
3355
3374
  const db2 = getDb();
3356
3375
  const normalizedName = name.trim().toLowerCase();
3357
- const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3358
- if (existing) {
3359
- const lastSeenAt = existing.last_seen_at;
3360
- const existingSessionId = existing.session_id;
3361
- if (isActiveSession(lastSeenAt) && existingSessionId && existingSessionId !== sessionId) {
3362
- return {
3363
- conflict: true,
3364
- error: "agent_conflict",
3365
- message: `Agent "${normalizedName}" is already active (last seen: ${lastSeenAt}). Wait 30 minutes or use force takeover.`,
3366
- existing_id: existing.id,
3367
- existing_name: normalizedName,
3368
- existing_session_id: existingSessionId,
3369
- last_seen_at: lastSeenAt,
3370
- session_hint: existingSessionId ? existingSessionId.slice(0, 8) : null,
3371
- working_dir: null
3372
- };
3373
- }
3374
- const tookOver = existingSessionId !== sessionId;
3376
+ const result = db2.transaction(() => {
3377
+ const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3378
+ if (existing) {
3379
+ const lastSeenAt = existing.last_seen_at;
3380
+ const existingSessionId = existing.session_id;
3381
+ if (isActiveSession(lastSeenAt) && existingSessionId && existingSessionId !== sessionId) {
3382
+ return {
3383
+ conflict: true,
3384
+ error: "agent_conflict",
3385
+ message: `Agent "${normalizedName}" is already active (last seen: ${lastSeenAt}). Wait 30 minutes or use force takeover.`,
3386
+ existing_id: existing.id,
3387
+ existing_name: normalizedName,
3388
+ existing_session_id: existingSessionId,
3389
+ last_seen_at: lastSeenAt,
3390
+ session_hint: existingSessionId ? existingSessionId.slice(0, 8) : null,
3391
+ working_dir: null
3392
+ };
3393
+ }
3394
+ const tookOver = existingSessionId !== sessionId;
3395
+ db2.prepare(`
3396
+ UPDATE agent_presence
3397
+ SET session_id = ?, role = ?, project_id = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
3398
+ WHERE agent = ?
3399
+ `).run(sessionId, role || existing.role || "agent", projectId ?? existing.project_id ?? null, normalizedName);
3400
+ const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3401
+ return { agent: parsePresence(updated), created: false, took_over: tookOver };
3402
+ }
3403
+ const id = crypto.randomUUID().slice(0, 8);
3404
+ const resolvedRole = role || "agent";
3375
3405
  db2.prepare(`
3376
- UPDATE agent_presence
3377
- SET session_id = ?, role = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
3378
- WHERE agent = ?
3379
- `).run(sessionId, role || existing.role || "agent", normalizedName);
3380
- const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3381
- return { agent: parsePresence(updated), created: false, took_over: tookOver };
3382
- }
3383
- const id = crypto.randomUUID().slice(0, 8);
3384
- const resolvedRole = role || "agent";
3385
- db2.prepare(`
3386
- INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at)
3387
- VALUES (?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
3388
- `).run(id, normalizedName, sessionId, resolvedRole);
3389
- const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3390
- return { agent: parsePresence(created), created: true, took_over: false };
3406
+ INSERT INTO agent_presence (id, agent, session_id, role, project_id, status, last_seen_at, created_at)
3407
+ VALUES (?, ?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
3408
+ `).run(id, normalizedName, sessionId, resolvedRole, projectId ?? null);
3409
+ const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3410
+ return { agent: parsePresence(created), created: true, took_over: false };
3411
+ }).immediate();
3412
+ return result;
3391
3413
  }
3392
3414
  function heartbeat(agent, status, metadata, sessionId) {
3393
3415
  const db2 = getDb();
@@ -3591,7 +3613,7 @@ var init_poll = __esm(() => {
3591
3613
  var require_package = __commonJS((exports, module) => {
3592
3614
  module.exports = {
3593
3615
  name: "@hasna/conversations",
3594
- version: "0.1.28",
3616
+ version: "0.1.30",
3595
3617
  description: "Real-time CLI messaging for AI agents",
3596
3618
  type: "module",
3597
3619
  bin: {
@@ -33189,15 +33211,16 @@ var init_mcp2 = __esm(() => {
33189
33211
  };
33190
33212
  });
33191
33213
  server.registerTool("register_agent", {
33192
- description: "Register an agent with conflict detection. Returns AgentConflictError if another active session exists (active = heartbeat within last 30 min).",
33214
+ description: "Register an agent with conflict detection. Returns AgentConflictError if another active session exists (active = heartbeat within last 30 min). Optional project_id locks agent to a project for the session.",
33193
33215
  inputSchema: {
33194
33216
  name: exports_external.string(),
33195
33217
  session_id: exports_external.string(),
33196
- role: exports_external.string().optional()
33218
+ role: exports_external.string().optional(),
33219
+ project_id: exports_external.string().optional()
33197
33220
  }
33198
33221
  }, async (args) => {
33199
- const { name, session_id, role } = args;
33200
- const result = registerAgent(name, session_id, role);
33222
+ const { name, session_id, role, project_id } = args;
33223
+ const result = registerAgent(name, session_id, role, project_id);
33201
33224
  return {
33202
33225
  content: [{ type: "text", text: JSON.stringify(result) }]
33203
33226
  };
package/bin/mcp.js CHANGED
@@ -6604,12 +6604,27 @@ function getDb() {
6604
6604
  agent TEXT PRIMARY KEY,
6605
6605
  session_id TEXT,
6606
6606
  role TEXT NOT NULL DEFAULT 'agent',
6607
+ project_id TEXT,
6607
6608
  status TEXT NOT NULL DEFAULT 'online',
6608
6609
  last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
6609
6610
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
6610
6611
  metadata TEXT
6611
6612
  )
6612
6613
  `);
6614
+ db.exec(`
6615
+ CREATE TABLE IF NOT EXISTS resource_locks (
6616
+ resource_type TEXT NOT NULL,
6617
+ resource_id TEXT NOT NULL,
6618
+ agent_id TEXT NOT NULL,
6619
+ lock_type TEXT NOT NULL DEFAULT 'advisory',
6620
+ locked_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
6621
+ expires_at TEXT NOT NULL,
6622
+ UNIQUE(resource_type, resource_id, lock_type)
6623
+ )
6624
+ `);
6625
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_resource ON resource_locks(resource_type, resource_id)");
6626
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_agent ON resource_locks(agent_id)");
6627
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_expires ON resource_locks(expires_at)");
6613
6628
  db.exec(`
6614
6629
  CREATE TABLE IF NOT EXISTS reactions (
6615
6630
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -6705,6 +6720,9 @@ function getDb() {
6705
6720
  db.exec("ALTER TABLE agent_presence ADD COLUMN created_at TEXT NOT NULL DEFAULT ''");
6706
6721
  db.exec("UPDATE agent_presence SET created_at = last_seen_at WHERE created_at = ''");
6707
6722
  }
6723
+ if (!presenceColNames.includes("project_id")) {
6724
+ db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
6725
+ }
6708
6726
  const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
6709
6727
  if (!ftsExists) {
6710
6728
  db.exec(`
@@ -29765,6 +29783,7 @@ function parsePresence(row) {
29765
29783
  agent: row.agent,
29766
29784
  session_id: row.session_id ?? null,
29767
29785
  role: row.role || "agent",
29786
+ project_id: row.project_id ?? null,
29768
29787
  status: row.status,
29769
29788
  last_seen_at: lastSeenAt,
29770
29789
  created_at: row.created_at || lastSeenAt,
@@ -29777,43 +29796,46 @@ function isActiveSession(lastSeenAt) {
29777
29796
  const nowMs = Date.now();
29778
29797
  return nowMs - lastSeenMs < CONFLICT_THRESHOLD_SECONDS * 1000;
29779
29798
  }
29780
- function registerAgent(name, sessionId, role) {
29799
+ function registerAgent(name, sessionId, role, projectId) {
29781
29800
  const db2 = getDb();
29782
29801
  const normalizedName = name.trim().toLowerCase();
29783
- const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
29784
- if (existing) {
29785
- const lastSeenAt = existing.last_seen_at;
29786
- const existingSessionId = existing.session_id;
29787
- if (isActiveSession(lastSeenAt) && existingSessionId && existingSessionId !== sessionId) {
29788
- return {
29789
- conflict: true,
29790
- error: "agent_conflict",
29791
- message: `Agent "${normalizedName}" is already active (last seen: ${lastSeenAt}). Wait 30 minutes or use force takeover.`,
29792
- existing_id: existing.id,
29793
- existing_name: normalizedName,
29794
- existing_session_id: existingSessionId,
29795
- last_seen_at: lastSeenAt,
29796
- session_hint: existingSessionId ? existingSessionId.slice(0, 8) : null,
29797
- working_dir: null
29798
- };
29799
- }
29800
- const tookOver = existingSessionId !== sessionId;
29802
+ const result = db2.transaction(() => {
29803
+ const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
29804
+ if (existing) {
29805
+ const lastSeenAt = existing.last_seen_at;
29806
+ const existingSessionId = existing.session_id;
29807
+ if (isActiveSession(lastSeenAt) && existingSessionId && existingSessionId !== sessionId) {
29808
+ return {
29809
+ conflict: true,
29810
+ error: "agent_conflict",
29811
+ message: `Agent "${normalizedName}" is already active (last seen: ${lastSeenAt}). Wait 30 minutes or use force takeover.`,
29812
+ existing_id: existing.id,
29813
+ existing_name: normalizedName,
29814
+ existing_session_id: existingSessionId,
29815
+ last_seen_at: lastSeenAt,
29816
+ session_hint: existingSessionId ? existingSessionId.slice(0, 8) : null,
29817
+ working_dir: null
29818
+ };
29819
+ }
29820
+ const tookOver = existingSessionId !== sessionId;
29821
+ db2.prepare(`
29822
+ UPDATE agent_presence
29823
+ SET session_id = ?, role = ?, project_id = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
29824
+ WHERE agent = ?
29825
+ `).run(sessionId, role || existing.role || "agent", projectId ?? existing.project_id ?? null, normalizedName);
29826
+ const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
29827
+ return { agent: parsePresence(updated), created: false, took_over: tookOver };
29828
+ }
29829
+ const id = crypto.randomUUID().slice(0, 8);
29830
+ const resolvedRole = role || "agent";
29801
29831
  db2.prepare(`
29802
- UPDATE agent_presence
29803
- SET session_id = ?, role = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
29804
- WHERE agent = ?
29805
- `).run(sessionId, role || existing.role || "agent", normalizedName);
29806
- const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
29807
- return { agent: parsePresence(updated), created: false, took_over: tookOver };
29808
- }
29809
- const id = crypto.randomUUID().slice(0, 8);
29810
- const resolvedRole = role || "agent";
29811
- db2.prepare(`
29812
- INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at)
29813
- VALUES (?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
29814
- `).run(id, normalizedName, sessionId, resolvedRole);
29815
- const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
29816
- return { agent: parsePresence(created), created: true, took_over: false };
29832
+ INSERT INTO agent_presence (id, agent, session_id, role, project_id, status, last_seen_at, created_at)
29833
+ VALUES (?, ?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
29834
+ `).run(id, normalizedName, sessionId, resolvedRole, projectId ?? null);
29835
+ const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
29836
+ return { agent: parsePresence(created), created: true, took_over: false };
29837
+ }).immediate();
29838
+ return result;
29817
29839
  }
29818
29840
  function heartbeat(agent, status, metadata, sessionId) {
29819
29841
  const db2 = getDb();
@@ -29864,7 +29886,7 @@ function renameAgent(oldName, newName) {
29864
29886
  // package.json
29865
29887
  var package_default = {
29866
29888
  name: "@hasna/conversations",
29867
- version: "0.1.28",
29889
+ version: "0.1.30",
29868
29890
  description: "Real-time CLI messaging for AI agents",
29869
29891
  type: "module",
29870
29892
  bin: {
@@ -30574,15 +30596,16 @@ server.registerTool("get_pinned_messages", {
30574
30596
  };
30575
30597
  });
30576
30598
  server.registerTool("register_agent", {
30577
- description: "Register an agent with conflict detection. Returns AgentConflictError if another active session exists (active = heartbeat within last 30 min).",
30599
+ description: "Register an agent with conflict detection. Returns AgentConflictError if another active session exists (active = heartbeat within last 30 min). Optional project_id locks agent to a project for the session.",
30578
30600
  inputSchema: {
30579
30601
  name: exports_external.string(),
30580
30602
  session_id: exports_external.string(),
30581
- role: exports_external.string().optional()
30603
+ role: exports_external.string().optional(),
30604
+ project_id: exports_external.string().optional()
30582
30605
  }
30583
30606
  }, async (args) => {
30584
- const { name, session_id, role } = args;
30585
- const result = registerAgent(name, session_id, role);
30607
+ const { name, session_id, role, project_id } = args;
30608
+ const result = registerAgent(name, session_id, role, project_id);
30586
30609
  return {
30587
30610
  content: [{ type: "text", text: JSON.stringify(result) }]
30588
30611
  };
package/dist/index.d.ts CHANGED
@@ -19,4 +19,6 @@ export { resolveIdentity, requireIdentity, } from "./lib/identity.js";
19
19
  export { addReaction, removeReaction, getReactions, getReactionSummary, } from "./lib/reactions.js";
20
20
  export { fireWebhooks, } from "./lib/webhooks.js";
21
21
  export { heartbeat, registerAgent, isAgentConflict, getPresence, listAgents, removePresence, renameAgent, } from "./lib/presence.js";
22
+ export { acquireLock, releaseLock, checkLock, cleanExpiredLocks, listLocks, } from "./lib/locks.js";
23
+ export type { ResourceLock } from "./lib/locks.js";
22
24
  export type { Message, Session, Space, SpaceInfo, SpaceMember, Project, ProjectInfo, Priority, SendMessageOptions, ReadMessagesOptions, SearchMessagesOptions, AgentPresence, AgentConflictError, RegisterAgentResult, Reaction, Attachment, } from "./types.js";
package/dist/index.js CHANGED
@@ -131,12 +131,27 @@ function getDb() {
131
131
  agent TEXT PRIMARY KEY,
132
132
  session_id TEXT,
133
133
  role TEXT NOT NULL DEFAULT 'agent',
134
+ project_id TEXT,
134
135
  status TEXT NOT NULL DEFAULT 'online',
135
136
  last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
136
137
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
137
138
  metadata TEXT
138
139
  )
139
140
  `);
141
+ db.exec(`
142
+ CREATE TABLE IF NOT EXISTS resource_locks (
143
+ resource_type TEXT NOT NULL,
144
+ resource_id TEXT NOT NULL,
145
+ agent_id TEXT NOT NULL,
146
+ lock_type TEXT NOT NULL DEFAULT 'advisory',
147
+ locked_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
148
+ expires_at TEXT NOT NULL,
149
+ UNIQUE(resource_type, resource_id, lock_type)
150
+ )
151
+ `);
152
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_resource ON resource_locks(resource_type, resource_id)");
153
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_agent ON resource_locks(agent_id)");
154
+ db.exec("CREATE INDEX IF NOT EXISTS idx_locks_expires ON resource_locks(expires_at)");
140
155
  db.exec(`
141
156
  CREATE TABLE IF NOT EXISTS reactions (
142
157
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -232,6 +247,9 @@ function getDb() {
232
247
  db.exec("ALTER TABLE agent_presence ADD COLUMN created_at TEXT NOT NULL DEFAULT ''");
233
248
  db.exec("UPDATE agent_presence SET created_at = last_seen_at WHERE created_at = ''");
234
249
  }
250
+ if (!presenceColNames.includes("project_id")) {
251
+ db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
252
+ }
235
253
  const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
236
254
  if (!ftsExists) {
237
255
  db.exec(`
@@ -3444,6 +3462,7 @@ function parsePresence(row) {
3444
3462
  agent: row.agent,
3445
3463
  session_id: row.session_id ?? null,
3446
3464
  role: row.role || "agent",
3465
+ project_id: row.project_id ?? null,
3447
3466
  status: row.status,
3448
3467
  last_seen_at: lastSeenAt,
3449
3468
  created_at: row.created_at || lastSeenAt,
@@ -3459,43 +3478,46 @@ function isActiveSession(lastSeenAt) {
3459
3478
  function isAgentConflict(result) {
3460
3479
  return result.conflict === true;
3461
3480
  }
3462
- function registerAgent(name, sessionId, role) {
3481
+ function registerAgent(name, sessionId, role, projectId) {
3463
3482
  const db2 = getDb();
3464
3483
  const normalizedName = name.trim().toLowerCase();
3465
- const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3466
- if (existing) {
3467
- const lastSeenAt = existing.last_seen_at;
3468
- const existingSessionId = existing.session_id;
3469
- if (isActiveSession(lastSeenAt) && existingSessionId && existingSessionId !== sessionId) {
3470
- return {
3471
- conflict: true,
3472
- error: "agent_conflict",
3473
- message: `Agent "${normalizedName}" is already active (last seen: ${lastSeenAt}). Wait 30 minutes or use force takeover.`,
3474
- existing_id: existing.id,
3475
- existing_name: normalizedName,
3476
- existing_session_id: existingSessionId,
3477
- last_seen_at: lastSeenAt,
3478
- session_hint: existingSessionId ? existingSessionId.slice(0, 8) : null,
3479
- working_dir: null
3480
- };
3484
+ const result = db2.transaction(() => {
3485
+ const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3486
+ if (existing) {
3487
+ const lastSeenAt = existing.last_seen_at;
3488
+ const existingSessionId = existing.session_id;
3489
+ if (isActiveSession(lastSeenAt) && existingSessionId && existingSessionId !== sessionId) {
3490
+ return {
3491
+ conflict: true,
3492
+ error: "agent_conflict",
3493
+ message: `Agent "${normalizedName}" is already active (last seen: ${lastSeenAt}). Wait 30 minutes or use force takeover.`,
3494
+ existing_id: existing.id,
3495
+ existing_name: normalizedName,
3496
+ existing_session_id: existingSessionId,
3497
+ last_seen_at: lastSeenAt,
3498
+ session_hint: existingSessionId ? existingSessionId.slice(0, 8) : null,
3499
+ working_dir: null
3500
+ };
3501
+ }
3502
+ const tookOver = existingSessionId !== sessionId;
3503
+ db2.prepare(`
3504
+ UPDATE agent_presence
3505
+ SET session_id = ?, role = ?, project_id = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
3506
+ WHERE agent = ?
3507
+ `).run(sessionId, role || existing.role || "agent", projectId ?? existing.project_id ?? null, normalizedName);
3508
+ const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3509
+ return { agent: parsePresence(updated), created: false, took_over: tookOver };
3481
3510
  }
3482
- const tookOver = existingSessionId !== sessionId;
3511
+ const id = crypto.randomUUID().slice(0, 8);
3512
+ const resolvedRole = role || "agent";
3483
3513
  db2.prepare(`
3484
- UPDATE agent_presence
3485
- SET session_id = ?, role = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
3486
- WHERE agent = ?
3487
- `).run(sessionId, role || existing.role || "agent", normalizedName);
3488
- const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3489
- return { agent: parsePresence(updated), created: false, took_over: tookOver };
3490
- }
3491
- const id = crypto.randomUUID().slice(0, 8);
3492
- const resolvedRole = role || "agent";
3493
- db2.prepare(`
3494
- INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at)
3495
- VALUES (?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
3496
- `).run(id, normalizedName, sessionId, resolvedRole);
3497
- const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3498
- return { agent: parsePresence(created), created: true, took_over: false };
3514
+ INSERT INTO agent_presence (id, agent, session_id, role, project_id, status, last_seen_at, created_at)
3515
+ VALUES (?, ?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
3516
+ `).run(id, normalizedName, sessionId, resolvedRole, projectId ?? null);
3517
+ const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
3518
+ return { agent: parsePresence(created), created: true, took_over: false };
3519
+ }).immediate();
3520
+ return result;
3499
3521
  }
3500
3522
  function heartbeat(agent, status, metadata, sessionId) {
3501
3523
  const db2 = getDb();
@@ -3549,6 +3571,80 @@ function renameAgent(oldName, newName) {
3549
3571
  db2.prepare("UPDATE agent_presence SET agent = ? WHERE LOWER(agent) = ?").run(normalizedNew, normalizedOld);
3550
3572
  return true;
3551
3573
  }
3574
+ // src/lib/locks.ts
3575
+ init_db();
3576
+ var DEFAULT_LOCK_EXPIRY_MS = 5 * 60 * 1000;
3577
+ function acquireLock(resourceType, resourceId, agentId, lockType = "advisory", expiryMs = DEFAULT_LOCK_EXPIRY_MS) {
3578
+ const db2 = getDb();
3579
+ return db2.transaction(() => {
3580
+ cleanExpiredLocks();
3581
+ const existing = db2.prepare(`
3582
+ SELECT * FROM resource_locks
3583
+ WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
3584
+ `).get(resourceType, resourceId, lockType);
3585
+ if (existing) {
3586
+ if (existing.agent_id !== agentId) {
3587
+ return { acquired: false, lock: null, held_by: existing.agent_id };
3588
+ }
3589
+ const expiresAt = new Date(Date.now() + expiryMs).toISOString().replace("T", "T").replace("Z", "");
3590
+ db2.prepare(`
3591
+ UPDATE resource_locks SET expires_at = ?, locked_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
3592
+ WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
3593
+ `).run(expiresAt, resourceType, resourceId, lockType);
3594
+ } else {
3595
+ const expiresAt = new Date(Date.now() + expiryMs).toISOString().slice(0, -1);
3596
+ db2.prepare(`
3597
+ INSERT INTO resource_locks (resource_type, resource_id, agent_id, lock_type, locked_at, expires_at)
3598
+ VALUES (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
3599
+ `).run(resourceType, resourceId, agentId, lockType, expiresAt);
3600
+ }
3601
+ const lock = db2.prepare(`
3602
+ SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
3603
+ `).get(resourceType, resourceId, lockType);
3604
+ return { acquired: true, lock };
3605
+ }).immediate();
3606
+ }
3607
+ function releaseLock(resourceType, resourceId, agentId) {
3608
+ const db2 = getDb();
3609
+ const result = db2.prepare(`
3610
+ DELETE FROM resource_locks
3611
+ WHERE resource_type = ? AND resource_id = ? AND agent_id = ?
3612
+ `).run(resourceType, resourceId, agentId);
3613
+ return result.changes > 0;
3614
+ }
3615
+ function checkLock(resourceType, resourceId) {
3616
+ const db2 = getDb();
3617
+ cleanExpiredLocks();
3618
+ return db2.prepare(`
3619
+ SELECT * FROM resource_locks
3620
+ WHERE resource_type = ? AND resource_id = ?
3621
+ ORDER BY locked_at ASC
3622
+ LIMIT 1
3623
+ `).get(resourceType, resourceId);
3624
+ }
3625
+ function cleanExpiredLocks() {
3626
+ const db2 = getDb();
3627
+ const result = db2.prepare(`
3628
+ DELETE FROM resource_locks WHERE expires_at < strftime('%Y-%m-%dT%H:%M:%f', 'now')
3629
+ `).run();
3630
+ return result.changes;
3631
+ }
3632
+ function listLocks(opts) {
3633
+ const db2 = getDb();
3634
+ cleanExpiredLocks();
3635
+ let query = "SELECT * FROM resource_locks WHERE 1=1";
3636
+ const params = [];
3637
+ if (opts?.resource_type) {
3638
+ query += " AND resource_type = ?";
3639
+ params.push(opts.resource_type);
3640
+ }
3641
+ if (opts?.agent_id) {
3642
+ query += " AND agent_id = ?";
3643
+ params.push(opts.agent_id);
3644
+ }
3645
+ query += " ORDER BY locked_at ASC";
3646
+ return db2.prepare(query).all(...params);
3647
+ }
3552
3648
  export {
3553
3649
  useSpaceMessages,
3554
3650
  updateSpace,
@@ -3563,6 +3659,7 @@ export {
3563
3659
  renameAgent,
3564
3660
  removeReaction,
3565
3661
  removePresence,
3662
+ releaseLock,
3566
3663
  registerAgent,
3567
3664
  readMessages,
3568
3665
  pinMessage,
@@ -3573,6 +3670,7 @@ export {
3573
3670
  listSpaces,
3574
3671
  listSessions,
3575
3672
  listProjects,
3673
+ listLocks,
3576
3674
  listAgents,
3577
3675
  leaveSpace,
3578
3676
  joinSpace,
@@ -3602,6 +3700,9 @@ export {
3602
3700
  createSpace,
3603
3701
  createProject,
3604
3702
  closeDb,
3703
+ cleanExpiredLocks,
3704
+ checkLock,
3605
3705
  archiveSpace,
3606
- addReaction
3706
+ addReaction,
3707
+ acquireLock
3607
3708
  };
@@ -0,0 +1,20 @@
1
+ export interface ResourceLock {
2
+ resource_type: string;
3
+ resource_id: string;
4
+ agent_id: string;
5
+ lock_type: "advisory" | "exclusive";
6
+ locked_at: string;
7
+ expires_at: string;
8
+ }
9
+ export declare function acquireLock(resourceType: string, resourceId: string, agentId: string, lockType?: "advisory" | "exclusive", expiryMs?: number): {
10
+ acquired: boolean;
11
+ lock: ResourceLock | null;
12
+ held_by?: string;
13
+ };
14
+ export declare function releaseLock(resourceType: string, resourceId: string, agentId: string): boolean;
15
+ export declare function checkLock(resourceType: string, resourceId: string): ResourceLock | null;
16
+ export declare function cleanExpiredLocks(): number;
17
+ export declare function listLocks(opts?: {
18
+ resource_type?: string;
19
+ agent_id?: string;
20
+ }): ResourceLock[];
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,6 @@
1
1
  import type { AgentPresence, AgentConflictError, RegisterAgentResult } from "../types.js";
2
2
  export declare function isAgentConflict(result: RegisterAgentResult | AgentConflictError): result is AgentConflictError;
3
- export declare function registerAgent(name: string, sessionId: string, role?: string): RegisterAgentResult | AgentConflictError;
3
+ export declare function registerAgent(name: string, sessionId: string, role?: string, projectId?: string): RegisterAgentResult | AgentConflictError;
4
4
  export declare function heartbeat(agent: string, status?: string, metadata?: Record<string, unknown>, sessionId?: string): void;
5
5
  export declare function getPresence(agent: string): AgentPresence | null;
6
6
  export declare function listAgents(opts?: {
package/dist/types.d.ts CHANGED
@@ -116,6 +116,7 @@ export interface AgentPresence {
116
116
  agent: string;
117
117
  session_id: string | null;
118
118
  role: string;
119
+ project_id: string | null;
119
120
  status: string;
120
121
  last_seen_at: string;
121
122
  created_at: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/conversations",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Real-time CLI messaging for AI agents",
5
5
  "type": "module",
6
6
  "bin": {