@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 +18 -0
- package/bin/index.js +62 -39
- package/bin/mcp.js +62 -39
- package/dist/index.d.ts +2 -0
- package/dist/index.js +135 -34
- package/dist/lib/locks.d.ts +20 -0
- package/dist/lib/locks.test.d.ts +1 -0
- package/dist/lib/presence.d.ts +1 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
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
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
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
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
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.
|
|
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
|
|
29784
|
-
|
|
29785
|
-
|
|
29786
|
-
|
|
29787
|
-
|
|
29788
|
-
|
|
29789
|
-
|
|
29790
|
-
|
|
29791
|
-
|
|
29792
|
-
|
|
29793
|
-
|
|
29794
|
-
|
|
29795
|
-
|
|
29796
|
-
|
|
29797
|
-
|
|
29798
|
-
|
|
29799
|
-
|
|
29800
|
-
|
|
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
|
-
|
|
29803
|
-
|
|
29804
|
-
|
|
29805
|
-
|
|
29806
|
-
|
|
29807
|
-
|
|
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.
|
|
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
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
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
|
|
3511
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
3512
|
+
const resolvedRole = role || "agent";
|
|
3483
3513
|
db2.prepare(`
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
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 {};
|
package/dist/lib/presence.d.ts
CHANGED
|
@@ -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