@hasna/conversations 0.1.24 → 0.1.26

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
@@ -114,9 +114,13 @@ function getDb() {
114
114
  `);
115
115
  db.exec(`
116
116
  CREATE TABLE IF NOT EXISTS agent_presence (
117
+ id TEXT NOT NULL,
117
118
  agent TEXT PRIMARY KEY,
119
+ session_id TEXT,
120
+ role TEXT NOT NULL DEFAULT 'agent',
118
121
  status TEXT NOT NULL DEFAULT 'online',
119
122
  last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
123
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
120
124
  metadata TEXT
121
125
  )
122
126
  `);
@@ -195,6 +199,25 @@ function getDb() {
195
199
  db.exec("ALTER TABLE messages ADD COLUMN reply_to INTEGER REFERENCES messages(id)");
196
200
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_reply_to ON messages(reply_to)");
197
201
  }
202
+ const presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
203
+ const presenceColNames = presenceCols.map((c) => c.name);
204
+ if (!presenceColNames.includes("id")) {
205
+ db.exec("ALTER TABLE agent_presence ADD COLUMN id TEXT NOT NULL DEFAULT ''");
206
+ const rows = db.prepare("SELECT agent FROM agent_presence").all();
207
+ for (const row of rows) {
208
+ const id = crypto.randomUUID().slice(0, 8);
209
+ db.prepare("UPDATE agent_presence SET id = ? WHERE agent = ?").run(id, row.agent);
210
+ }
211
+ }
212
+ if (!presenceColNames.includes("session_id")) {
213
+ db.exec("ALTER TABLE agent_presence ADD COLUMN session_id TEXT");
214
+ }
215
+ if (!presenceColNames.includes("role")) {
216
+ db.exec("ALTER TABLE agent_presence ADD COLUMN role TEXT NOT NULL DEFAULT 'agent'");
217
+ }
218
+ if (!presenceColNames.includes("created_at")) {
219
+ db.exec("ALTER TABLE agent_presence ADD COLUMN created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))");
220
+ }
198
221
  const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
199
222
  if (!ftsExists) {
200
223
  db.exec(`
package/bin/index.js CHANGED
@@ -1968,9 +1968,13 @@ function getDb() {
1968
1968
  `);
1969
1969
  db.exec(`
1970
1970
  CREATE TABLE IF NOT EXISTS agent_presence (
1971
+ id TEXT NOT NULL,
1971
1972
  agent TEXT PRIMARY KEY,
1973
+ session_id TEXT,
1974
+ role TEXT NOT NULL DEFAULT 'agent',
1972
1975
  status TEXT NOT NULL DEFAULT 'online',
1973
1976
  last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1977
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
1974
1978
  metadata TEXT
1975
1979
  )
1976
1980
  `);
@@ -2049,6 +2053,25 @@ function getDb() {
2049
2053
  db.exec("ALTER TABLE messages ADD COLUMN reply_to INTEGER REFERENCES messages(id)");
2050
2054
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_reply_to ON messages(reply_to)");
2051
2055
  }
2056
+ const presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
2057
+ const presenceColNames = presenceCols.map((c) => c.name);
2058
+ if (!presenceColNames.includes("id")) {
2059
+ db.exec("ALTER TABLE agent_presence ADD COLUMN id TEXT NOT NULL DEFAULT ''");
2060
+ const rows = db.prepare("SELECT agent FROM agent_presence").all();
2061
+ for (const row of rows) {
2062
+ const id = crypto.randomUUID().slice(0, 8);
2063
+ db.prepare("UPDATE agent_presence SET id = ? WHERE agent = ?").run(id, row.agent);
2064
+ }
2065
+ }
2066
+ if (!presenceColNames.includes("session_id")) {
2067
+ db.exec("ALTER TABLE agent_presence ADD COLUMN session_id TEXT");
2068
+ }
2069
+ if (!presenceColNames.includes("role")) {
2070
+ db.exec("ALTER TABLE agent_presence ADD COLUMN role TEXT NOT NULL DEFAULT 'agent'");
2071
+ }
2072
+ if (!presenceColNames.includes("created_at")) {
2073
+ db.exec("ALTER TABLE agent_presence ADD COLUMN created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))");
2074
+ }
2052
2075
  const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
2053
2076
  if (!ftsExists) {
2054
2077
  db.exec(`
@@ -3282,14 +3305,6 @@ function resolveIdentity(explicit) {
3282
3305
  return envValue;
3283
3306
  return getAutoName();
3284
3307
  }
3285
- function updateCachedAutoName(newName) {
3286
- cachedAutoName = newName;
3287
- try {
3288
- mkdirSync3(dirname2(AGENT_ID_FILE), { recursive: true });
3289
- writeFileSync(AGENT_ID_FILE, newName + `
3290
- `, "utf-8");
3291
- } catch {}
3292
- }
3293
3308
  var AGENT_ID_FILE, cachedAutoName = null;
3294
3309
  var init_identity = __esm(() => {
3295
3310
  init_names();
@@ -3311,66 +3326,105 @@ function parsePresence(row) {
3311
3326
  const nowMs = Date.now();
3312
3327
  const online = nowMs - lastSeenMs < ONLINE_THRESHOLD_SECONDS * 1000;
3313
3328
  return {
3329
+ id: row.id || "",
3314
3330
  agent: row.agent,
3331
+ session_id: row.session_id ?? null,
3332
+ role: row.role || "agent",
3315
3333
  status: row.status,
3316
3334
  last_seen_at: lastSeenAt,
3335
+ created_at: row.created_at || lastSeenAt,
3317
3336
  online,
3318
3337
  metadata
3319
3338
  };
3320
3339
  }
3321
- function heartbeat(agent, status, metadata) {
3340
+ function isActiveSession(lastSeenAt) {
3341
+ const lastSeenMs = new Date(lastSeenAt + "Z").getTime();
3342
+ const nowMs = Date.now();
3343
+ return nowMs - lastSeenMs < CONFLICT_THRESHOLD_SECONDS * 1000;
3344
+ }
3345
+ function registerAgent(name, sessionId, role) {
3346
+ const db2 = getDb();
3347
+ const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(name);
3348
+ if (existing) {
3349
+ const lastSeenAt = existing.last_seen_at;
3350
+ const existingSessionId = existing.session_id;
3351
+ if (isActiveSession(lastSeenAt) && existingSessionId && existingSessionId !== sessionId) {
3352
+ return {
3353
+ error: "agent_conflict",
3354
+ message: `Agent "${name}" is already active (last seen: ${lastSeenAt}). Wait 30 minutes or use force takeover.`,
3355
+ existing_session_id: existingSessionId,
3356
+ last_seen_at: lastSeenAt
3357
+ };
3358
+ }
3359
+ const tookOver = existingSessionId !== sessionId;
3360
+ db2.prepare(`
3361
+ UPDATE agent_presence
3362
+ SET session_id = ?, role = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
3363
+ WHERE agent = ?
3364
+ `).run(sessionId, role || existing.role || "agent", name);
3365
+ const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(name);
3366
+ return { agent: parsePresence(updated), created: false, took_over: tookOver };
3367
+ }
3368
+ const id = crypto.randomUUID().slice(0, 8);
3369
+ const resolvedRole = role || "agent";
3370
+ db2.prepare(`
3371
+ INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at)
3372
+ VALUES (?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
3373
+ `).run(id, name, sessionId, resolvedRole);
3374
+ const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(name);
3375
+ return { agent: parsePresence(created), created: true, took_over: false };
3376
+ }
3377
+ function heartbeat(agent, status, metadata, sessionId) {
3322
3378
  const db2 = getDb();
3323
3379
  const metadataJson = metadata ? JSON.stringify(metadata) : null;
3324
3380
  const resolvedStatus = status || "online";
3325
- const normalizedAgent = agent.trim().toLowerCase();
3381
+ const existing = db2.prepare("SELECT id FROM agent_presence WHERE agent = ?").get(agent);
3382
+ const id = existing?.id || crypto.randomUUID().slice(0, 8);
3326
3383
  db2.prepare(`
3327
- INSERT INTO agent_presence (agent, status, last_seen_at, metadata)
3328
- VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
3384
+ INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at, metadata)
3385
+ VALUES (?, ?, ?, 'agent', ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
3329
3386
  ON CONFLICT(agent) DO UPDATE SET
3330
3387
  status = excluded.status,
3331
3388
  last_seen_at = excluded.last_seen_at,
3389
+ session_id = COALESCE(excluded.session_id, agent_presence.session_id),
3332
3390
  metadata = excluded.metadata
3333
- `).run(normalizedAgent, resolvedStatus, metadataJson);
3391
+ `).run(id, agent, sessionId ?? null, resolvedStatus, metadataJson);
3334
3392
  }
3335
3393
  function getPresence(agent) {
3336
3394
  const db2 = getDb();
3337
- const normalizedAgent = agent.trim().toLowerCase();
3338
- const row = db2.prepare("SELECT * FROM agent_presence WHERE LOWER(agent) = ?").get(normalizedAgent);
3395
+ const row = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(agent);
3339
3396
  return row ? parsePresence(row) : null;
3340
3397
  }
3341
3398
  function listAgents(opts) {
3342
3399
  const db2 = getDb();
3343
3400
  let query = "SELECT * FROM agent_presence";
3344
- const params = [];
3345
3401
  if (opts?.online_only) {
3346
3402
  query += " WHERE last_seen_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-60 seconds')";
3347
3403
  }
3348
3404
  query += " ORDER BY last_seen_at DESC";
3349
- const rows = db2.prepare(query).all(...params);
3405
+ const rows = db2.prepare(query).all();
3350
3406
  return rows.map(parsePresence);
3351
3407
  }
3352
3408
  function removePresence(agent) {
3353
3409
  const db2 = getDb();
3354
- const normalizedAgent = agent.trim().toLowerCase();
3355
- const result = db2.prepare("DELETE FROM agent_presence WHERE LOWER(agent) = ?").run(normalizedAgent);
3410
+ const result = db2.prepare("DELETE FROM agent_presence WHERE agent = ?").run(agent);
3356
3411
  return result.changes > 0;
3357
3412
  }
3358
3413
  function renameAgent(oldName, newName) {
3359
3414
  const db2 = getDb();
3360
- const normalizedOld = oldName.trim().toLowerCase();
3361
- const normalizedNew = newName.trim().toLowerCase();
3362
- const existing = db2.prepare("SELECT agent FROM agent_presence WHERE LOWER(agent) = ?").get(normalizedOld);
3415
+ const existing = db2.prepare("SELECT agent FROM agent_presence WHERE agent = ?").get(oldName);
3363
3416
  if (!existing)
3364
3417
  return false;
3365
- const conflict = db2.prepare("SELECT agent FROM agent_presence WHERE LOWER(agent) = ?").get(normalizedNew);
3418
+ const conflict = db2.prepare("SELECT agent FROM agent_presence WHERE agent = ?").get(newName);
3366
3419
  if (conflict)
3367
- throw new Error(`Agent "${normalizedNew}" already exists`);
3368
- db2.prepare("UPDATE agent_presence SET agent = ? WHERE LOWER(agent) = ?").run(normalizedNew, normalizedOld);
3420
+ throw new Error(`Agent "${newName}" already exists`);
3421
+ db2.prepare("UPDATE agent_presence SET agent = ? WHERE agent = ?").run(newName, oldName);
3369
3422
  return true;
3370
3423
  }
3371
- var ONLINE_THRESHOLD_SECONDS = 60;
3424
+ var ONLINE_THRESHOLD_SECONDS = 60, CONFLICT_THRESHOLD_SECONDS;
3372
3425
  var init_presence = __esm(() => {
3373
3426
  init_db();
3427
+ CONFLICT_THRESHOLD_SECONDS = 30 * 60;
3374
3428
  });
3375
3429
 
3376
3430
  // src/lib/terminal-markdown.ts
@@ -3517,7 +3571,7 @@ var init_poll = __esm(() => {
3517
3571
  var require_package = __commonJS((exports, module) => {
3518
3572
  module.exports = {
3519
3573
  name: "@hasna/conversations",
3520
- version: "0.1.24",
3574
+ version: "0.1.26",
3521
3575
  description: "Real-time CLI messaging for AI agents",
3522
3576
  type: "module",
3523
3577
  bin: {
@@ -30885,7 +30939,7 @@ var require_formats = __commonJS((exports) => {
30885
30939
  }
30886
30940
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
30887
30941
  function getTime(strictTimeZone) {
30888
- return function time3(str) {
30942
+ return function time(str) {
30889
30943
  const matches = TIME.exec(str);
30890
30944
  if (!matches)
30891
30945
  return false;
@@ -31149,62 +31203,6 @@ class ExperimentalServerTasks {
31149
31203
  requestStream(request, resultSchema, options) {
31150
31204
  return this._server.requestStream(request, resultSchema, options);
31151
31205
  }
31152
- createMessageStream(params, options) {
31153
- const clientCapabilities = this._server.getClientCapabilities();
31154
- if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
31155
- throw new Error("Client does not support sampling tools capability.");
31156
- }
31157
- if (params.messages.length > 0) {
31158
- const lastMessage = params.messages[params.messages.length - 1];
31159
- const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
31160
- const hasToolResults = lastContent.some((c) => c.type === "tool_result");
31161
- const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
31162
- const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
31163
- const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
31164
- if (hasToolResults) {
31165
- if (lastContent.some((c) => c.type !== "tool_result")) {
31166
- throw new Error("The last message must contain only tool_result content if any is present");
31167
- }
31168
- if (!hasPreviousToolUse) {
31169
- throw new Error("tool_result blocks are not matching any tool_use from the previous message");
31170
- }
31171
- }
31172
- if (hasPreviousToolUse) {
31173
- const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
31174
- const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
31175
- if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) {
31176
- throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match");
31177
- }
31178
- }
31179
- }
31180
- return this.requestStream({
31181
- method: "sampling/createMessage",
31182
- params
31183
- }, CreateMessageResultSchema, options);
31184
- }
31185
- elicitInputStream(params, options) {
31186
- const clientCapabilities = this._server.getClientCapabilities();
31187
- const mode = params.mode ?? "form";
31188
- switch (mode) {
31189
- case "url": {
31190
- if (!clientCapabilities?.elicitation?.url) {
31191
- throw new Error("Client does not support url elicitation.");
31192
- }
31193
- break;
31194
- }
31195
- case "form": {
31196
- if (!clientCapabilities?.elicitation?.form) {
31197
- throw new Error("Client does not support form elicitation.");
31198
- }
31199
- break;
31200
- }
31201
- }
31202
- const normalizedParams = mode === "form" && params.mode === undefined ? { ...params, mode: "form" } : params;
31203
- return this.requestStream({
31204
- method: "elicitation/create",
31205
- params: normalizedParams
31206
- }, ElicitResultSchema, options);
31207
- }
31208
31206
  async getTask(taskId, options) {
31209
31207
  return this._server.getTask({ taskId }, options);
31210
31208
  }
@@ -31218,9 +31216,6 @@ class ExperimentalServerTasks {
31218
31216
  return this._server.cancelTask({ taskId }, options);
31219
31217
  }
31220
31218
  }
31221
- var init_server = __esm(() => {
31222
- init_types2();
31223
- });
31224
31219
 
31225
31220
  // node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
31226
31221
  function assertToolsCallTaskCapability(requests, method, entityName) {
@@ -31259,12 +31254,11 @@ function assertClientRequestTaskCapability(requests, method, entityName) {
31259
31254
 
31260
31255
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js
31261
31256
  var Server;
31262
- var init_server2 = __esm(() => {
31257
+ var init_server = __esm(() => {
31263
31258
  init_protocol();
31264
31259
  init_types2();
31265
31260
  init_ajv_provider();
31266
31261
  init_zod_compat();
31267
- init_server();
31268
31262
  Server = class Server extends Protocol {
31269
31263
  constructor(_serverInfo, options) {
31270
31264
  super(options);
@@ -32406,7 +32400,7 @@ function createCompletionResult(suggestions) {
32406
32400
  }
32407
32401
  var EMPTY_OBJECT_JSON_SCHEMA, EMPTY_COMPLETION_RESULT;
32408
32402
  var init_mcp = __esm(() => {
32409
- init_server2();
32403
+ init_server();
32410
32404
  init_zod_compat();
32411
32405
  init_zod_json_schema_compat();
32412
32406
  init_types2();
@@ -33174,6 +33168,20 @@ var init_mcp2 = __esm(() => {
33174
33168
  content: [{ type: "text", text: JSON.stringify(messages) }]
33175
33169
  };
33176
33170
  });
33171
+ server.registerTool("register_agent", {
33172
+ description: "Register an agent with conflict detection. Returns AgentConflictError if another active session exists (active = heartbeat within last 30 min).",
33173
+ inputSchema: {
33174
+ name: exports_external.string(),
33175
+ session_id: exports_external.string(),
33176
+ role: exports_external.string().optional()
33177
+ }
33178
+ }, async (args) => {
33179
+ const { name, session_id, role } = args;
33180
+ const result = registerAgent(name, session_id, role);
33181
+ return {
33182
+ content: [{ type: "text", text: JSON.stringify(result) }]
33183
+ };
33184
+ });
33177
33185
  server.registerTool("heartbeat", {
33178
33186
  description: "Send presence heartbeat.",
33179
33187
  inputSchema: {
@@ -33258,9 +33266,6 @@ var init_mcp2 = __esm(() => {
33258
33266
  isError: true
33259
33267
  };
33260
33268
  }
33261
- if (!fromParam) {
33262
- updateCachedAutoName(newName);
33263
- }
33264
33269
  return {
33265
33270
  content: [{ type: "text", text: JSON.stringify({ old_name: oldName, new_name: newName, renamed: true }) }]
33266
33271
  };
@@ -33305,6 +33310,7 @@ var init_mcp2 = __esm(() => {
33305
33310
  "pin_message",
33306
33311
  "unpin_message",
33307
33312
  "get_pinned_messages",
33313
+ "register_agent",
33308
33314
  "heartbeat",
33309
33315
  "list_agents",
33310
33316
  "get_blockers",
@@ -33350,6 +33356,7 @@ var init_mcp2 = __esm(() => {
33350
33356
  pin_message: "Pin a message. Required: id",
33351
33357
  unpin_message: "Unpin a message. Required: id",
33352
33358
  get_pinned_messages: "Get pinned messages. Optional: space?, session_id?, limit?",
33359
+ register_agent: "Register agent with conflict detection (30min active window). Required: name, session_id. Optional: role?. Returns AgentConflictError if another session is active.",
33353
33360
  heartbeat: "Register/refresh agent presence. Optional: from?, status?(online|busy|idle, default: online)",
33354
33361
  list_agents: "List agents with presence timestamps. Optional: online_only?(only agents seen in last 60s)",
33355
33362
  get_blockers: "Get unread blocking messages for agent. Optional: from?",
package/bin/mcp.js CHANGED
@@ -6302,7 +6302,7 @@ var require_formats = __commonJS((exports) => {
6302
6302
  }
6303
6303
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
6304
6304
  function getTime(strictTimeZone) {
6305
- return function time3(str) {
6305
+ return function time(str) {
6306
6306
  const matches = TIME.exec(str);
6307
6307
  if (!matches)
6308
6308
  return false;
@@ -6600,9 +6600,13 @@ function getDb() {
6600
6600
  `);
6601
6601
  db.exec(`
6602
6602
  CREATE TABLE IF NOT EXISTS agent_presence (
6603
+ id TEXT NOT NULL,
6603
6604
  agent TEXT PRIMARY KEY,
6605
+ session_id TEXT,
6606
+ role TEXT NOT NULL DEFAULT 'agent',
6604
6607
  status TEXT NOT NULL DEFAULT 'online',
6605
6608
  last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
6609
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
6606
6610
  metadata TEXT
6607
6611
  )
6608
6612
  `);
@@ -6681,6 +6685,25 @@ function getDb() {
6681
6685
  db.exec("ALTER TABLE messages ADD COLUMN reply_to INTEGER REFERENCES messages(id)");
6682
6686
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_reply_to ON messages(reply_to)");
6683
6687
  }
6688
+ const presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
6689
+ const presenceColNames = presenceCols.map((c) => c.name);
6690
+ if (!presenceColNames.includes("id")) {
6691
+ db.exec("ALTER TABLE agent_presence ADD COLUMN id TEXT NOT NULL DEFAULT ''");
6692
+ const rows = db.prepare("SELECT agent FROM agent_presence").all();
6693
+ for (const row of rows) {
6694
+ const id = crypto.randomUUID().slice(0, 8);
6695
+ db.prepare("UPDATE agent_presence SET id = ? WHERE agent = ?").run(id, row.agent);
6696
+ }
6697
+ }
6698
+ if (!presenceColNames.includes("session_id")) {
6699
+ db.exec("ALTER TABLE agent_presence ADD COLUMN session_id TEXT");
6700
+ }
6701
+ if (!presenceColNames.includes("role")) {
6702
+ db.exec("ALTER TABLE agent_presence ADD COLUMN role TEXT NOT NULL DEFAULT 'agent'");
6703
+ }
6704
+ if (!presenceColNames.includes("created_at")) {
6705
+ db.exec("ALTER TABLE agent_presence ADD COLUMN created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))");
6706
+ }
6684
6707
  const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
6685
6708
  if (!ftsExists) {
6686
6709
  db.exec(`
@@ -27263,62 +27286,6 @@ class ExperimentalServerTasks {
27263
27286
  requestStream(request, resultSchema, options) {
27264
27287
  return this._server.requestStream(request, resultSchema, options);
27265
27288
  }
27266
- createMessageStream(params, options) {
27267
- const clientCapabilities = this._server.getClientCapabilities();
27268
- if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
27269
- throw new Error("Client does not support sampling tools capability.");
27270
- }
27271
- if (params.messages.length > 0) {
27272
- const lastMessage = params.messages[params.messages.length - 1];
27273
- const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
27274
- const hasToolResults = lastContent.some((c) => c.type === "tool_result");
27275
- const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
27276
- const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
27277
- const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
27278
- if (hasToolResults) {
27279
- if (lastContent.some((c) => c.type !== "tool_result")) {
27280
- throw new Error("The last message must contain only tool_result content if any is present");
27281
- }
27282
- if (!hasPreviousToolUse) {
27283
- throw new Error("tool_result blocks are not matching any tool_use from the previous message");
27284
- }
27285
- }
27286
- if (hasPreviousToolUse) {
27287
- const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
27288
- const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
27289
- if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) {
27290
- throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match");
27291
- }
27292
- }
27293
- }
27294
- return this.requestStream({
27295
- method: "sampling/createMessage",
27296
- params
27297
- }, CreateMessageResultSchema, options);
27298
- }
27299
- elicitInputStream(params, options) {
27300
- const clientCapabilities = this._server.getClientCapabilities();
27301
- const mode = params.mode ?? "form";
27302
- switch (mode) {
27303
- case "url": {
27304
- if (!clientCapabilities?.elicitation?.url) {
27305
- throw new Error("Client does not support url elicitation.");
27306
- }
27307
- break;
27308
- }
27309
- case "form": {
27310
- if (!clientCapabilities?.elicitation?.form) {
27311
- throw new Error("Client does not support form elicitation.");
27312
- }
27313
- break;
27314
- }
27315
- }
27316
- const normalizedParams = mode === "form" && params.mode === undefined ? { ...params, mode: "form" } : params;
27317
- return this.requestStream({
27318
- method: "elicitation/create",
27319
- params: normalizedParams
27320
- }, ElicitResultSchema, options);
27321
- }
27322
27289
  async getTask(taskId, options) {
27323
27290
  return this._server.getTask({ taskId }, options);
27324
27291
  }
@@ -29766,18 +29733,11 @@ function resolveIdentity(explicit) {
29766
29733
  return envValue;
29767
29734
  return getAutoName();
29768
29735
  }
29769
- function updateCachedAutoName(newName) {
29770
- cachedAutoName = newName;
29771
- try {
29772
- mkdirSync3(dirname2(AGENT_ID_FILE), { recursive: true });
29773
- writeFileSync(AGENT_ID_FILE, newName + `
29774
- `, "utf-8");
29775
- } catch {}
29776
- }
29777
29736
 
29778
29737
  // src/lib/presence.ts
29779
29738
  init_db();
29780
29739
  var ONLINE_THRESHOLD_SECONDS = 60;
29740
+ var CONFLICT_THRESHOLD_SECONDS = 30 * 60;
29781
29741
  function parsePresence(row) {
29782
29742
  let metadata = null;
29783
29743
  if (row.metadata) {
@@ -29792,61 +29752,100 @@ function parsePresence(row) {
29792
29752
  const nowMs = Date.now();
29793
29753
  const online = nowMs - lastSeenMs < ONLINE_THRESHOLD_SECONDS * 1000;
29794
29754
  return {
29755
+ id: row.id || "",
29795
29756
  agent: row.agent,
29757
+ session_id: row.session_id ?? null,
29758
+ role: row.role || "agent",
29796
29759
  status: row.status,
29797
29760
  last_seen_at: lastSeenAt,
29761
+ created_at: row.created_at || lastSeenAt,
29798
29762
  online,
29799
29763
  metadata
29800
29764
  };
29801
29765
  }
29802
- function heartbeat(agent, status, metadata) {
29766
+ function isActiveSession(lastSeenAt) {
29767
+ const lastSeenMs = new Date(lastSeenAt + "Z").getTime();
29768
+ const nowMs = Date.now();
29769
+ return nowMs - lastSeenMs < CONFLICT_THRESHOLD_SECONDS * 1000;
29770
+ }
29771
+ function registerAgent(name, sessionId, role) {
29772
+ const db2 = getDb();
29773
+ const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(name);
29774
+ if (existing) {
29775
+ const lastSeenAt = existing.last_seen_at;
29776
+ const existingSessionId = existing.session_id;
29777
+ if (isActiveSession(lastSeenAt) && existingSessionId && existingSessionId !== sessionId) {
29778
+ return {
29779
+ error: "agent_conflict",
29780
+ message: `Agent "${name}" is already active (last seen: ${lastSeenAt}). Wait 30 minutes or use force takeover.`,
29781
+ existing_session_id: existingSessionId,
29782
+ last_seen_at: lastSeenAt
29783
+ };
29784
+ }
29785
+ const tookOver = existingSessionId !== sessionId;
29786
+ db2.prepare(`
29787
+ UPDATE agent_presence
29788
+ SET session_id = ?, role = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
29789
+ WHERE agent = ?
29790
+ `).run(sessionId, role || existing.role || "agent", name);
29791
+ const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(name);
29792
+ return { agent: parsePresence(updated), created: false, took_over: tookOver };
29793
+ }
29794
+ const id = crypto.randomUUID().slice(0, 8);
29795
+ const resolvedRole = role || "agent";
29796
+ db2.prepare(`
29797
+ INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at)
29798
+ VALUES (?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
29799
+ `).run(id, name, sessionId, resolvedRole);
29800
+ const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(name);
29801
+ return { agent: parsePresence(created), created: true, took_over: false };
29802
+ }
29803
+ function heartbeat(agent, status, metadata, sessionId) {
29803
29804
  const db2 = getDb();
29804
29805
  const metadataJson = metadata ? JSON.stringify(metadata) : null;
29805
29806
  const resolvedStatus = status || "online";
29806
- const normalizedAgent = agent.trim().toLowerCase();
29807
+ const existing = db2.prepare("SELECT id FROM agent_presence WHERE agent = ?").get(agent);
29808
+ const id = existing?.id || crypto.randomUUID().slice(0, 8);
29807
29809
  db2.prepare(`
29808
- INSERT INTO agent_presence (agent, status, last_seen_at, metadata)
29809
- VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
29810
+ INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at, metadata)
29811
+ VALUES (?, ?, ?, 'agent', ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
29810
29812
  ON CONFLICT(agent) DO UPDATE SET
29811
29813
  status = excluded.status,
29812
29814
  last_seen_at = excluded.last_seen_at,
29815
+ session_id = COALESCE(excluded.session_id, agent_presence.session_id),
29813
29816
  metadata = excluded.metadata
29814
- `).run(normalizedAgent, resolvedStatus, metadataJson);
29817
+ `).run(id, agent, sessionId ?? null, resolvedStatus, metadataJson);
29815
29818
  }
29816
29819
  function listAgents(opts) {
29817
29820
  const db2 = getDb();
29818
29821
  let query = "SELECT * FROM agent_presence";
29819
- const params = [];
29820
29822
  if (opts?.online_only) {
29821
29823
  query += " WHERE last_seen_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-60 seconds')";
29822
29824
  }
29823
29825
  query += " ORDER BY last_seen_at DESC";
29824
- const rows = db2.prepare(query).all(...params);
29826
+ const rows = db2.prepare(query).all();
29825
29827
  return rows.map(parsePresence);
29826
29828
  }
29827
29829
  function removePresence(agent) {
29828
29830
  const db2 = getDb();
29829
- const normalizedAgent = agent.trim().toLowerCase();
29830
- const result = db2.prepare("DELETE FROM agent_presence WHERE LOWER(agent) = ?").run(normalizedAgent);
29831
+ const result = db2.prepare("DELETE FROM agent_presence WHERE agent = ?").run(agent);
29831
29832
  return result.changes > 0;
29832
29833
  }
29833
29834
  function renameAgent(oldName, newName) {
29834
29835
  const db2 = getDb();
29835
- const normalizedOld = oldName.trim().toLowerCase();
29836
- const normalizedNew = newName.trim().toLowerCase();
29837
- const existing = db2.prepare("SELECT agent FROM agent_presence WHERE LOWER(agent) = ?").get(normalizedOld);
29836
+ const existing = db2.prepare("SELECT agent FROM agent_presence WHERE agent = ?").get(oldName);
29838
29837
  if (!existing)
29839
29838
  return false;
29840
- const conflict = db2.prepare("SELECT agent FROM agent_presence WHERE LOWER(agent) = ?").get(normalizedNew);
29839
+ const conflict = db2.prepare("SELECT agent FROM agent_presence WHERE agent = ?").get(newName);
29841
29840
  if (conflict)
29842
- throw new Error(`Agent "${normalizedNew}" already exists`);
29843
- db2.prepare("UPDATE agent_presence SET agent = ? WHERE LOWER(agent) = ?").run(normalizedNew, normalizedOld);
29841
+ throw new Error(`Agent "${newName}" already exists`);
29842
+ db2.prepare("UPDATE agent_presence SET agent = ? WHERE agent = ?").run(newName, oldName);
29844
29843
  return true;
29845
29844
  }
29846
29845
  // package.json
29847
29846
  var package_default = {
29848
29847
  name: "@hasna/conversations",
29849
- version: "0.1.24",
29848
+ version: "0.1.26",
29850
29849
  description: "Real-time CLI messaging for AI agents",
29851
29850
  type: "module",
29852
29851
  bin: {
@@ -30555,6 +30554,20 @@ server.registerTool("get_pinned_messages", {
30555
30554
  content: [{ type: "text", text: JSON.stringify(messages) }]
30556
30555
  };
30557
30556
  });
30557
+ server.registerTool("register_agent", {
30558
+ description: "Register an agent with conflict detection. Returns AgentConflictError if another active session exists (active = heartbeat within last 30 min).",
30559
+ inputSchema: {
30560
+ name: exports_external.string(),
30561
+ session_id: exports_external.string(),
30562
+ role: exports_external.string().optional()
30563
+ }
30564
+ }, async (args) => {
30565
+ const { name, session_id, role } = args;
30566
+ const result = registerAgent(name, session_id, role);
30567
+ return {
30568
+ content: [{ type: "text", text: JSON.stringify(result) }]
30569
+ };
30570
+ });
30558
30571
  server.registerTool("heartbeat", {
30559
30572
  description: "Send presence heartbeat.",
30560
30573
  inputSchema: {
@@ -30639,9 +30652,6 @@ server.registerTool("rename_agent", {
30639
30652
  isError: true
30640
30653
  };
30641
30654
  }
30642
- if (!fromParam) {
30643
- updateCachedAutoName(newName);
30644
- }
30645
30655
  return {
30646
30656
  content: [{ type: "text", text: JSON.stringify({ old_name: oldName, new_name: newName, renamed: true }) }]
30647
30657
  };
@@ -30686,6 +30696,7 @@ server.registerTool("search_tools", {
30686
30696
  "pin_message",
30687
30697
  "unpin_message",
30688
30698
  "get_pinned_messages",
30699
+ "register_agent",
30689
30700
  "heartbeat",
30690
30701
  "list_agents",
30691
30702
  "get_blockers",
@@ -30731,6 +30742,7 @@ server.registerTool("describe_tools", {
30731
30742
  pin_message: "Pin a message. Required: id",
30732
30743
  unpin_message: "Unpin a message. Required: id",
30733
30744
  get_pinned_messages: "Get pinned messages. Optional: space?, session_id?, limit?",
30745
+ register_agent: "Register agent with conflict detection (30min active window). Required: name, session_id. Optional: role?. Returns AgentConflictError if another session is active.",
30734
30746
  heartbeat: "Register/refresh agent presence. Optional: from?, status?(online|busy|idle, default: online)",
30735
30747
  list_agents: "List agents with presence timestamps. Optional: online_only?(only agents seen in last 60s)",
30736
30748
  get_blockers: "Get unread blocking messages for agent. Optional: from?",