lattice-talk 0.1.2 → 0.1.3
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/README.md +82 -275
- package/dist/index.js +184 -5
- package/dist/index.js.map +1 -1
- package/dist/tui/main.js +185 -57
- package/dist/tui/main.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -53317,6 +53317,7 @@ var PRESENCE_TTL_MIN_SECONDS = 5;
|
|
|
53317
53317
|
var PRESENCE_TTL_MAX_SECONDS = 86400;
|
|
53318
53318
|
var STREAM_MAXLEN_MIN = 10;
|
|
53319
53319
|
var STREAM_MAXLEN_MAX = 1e6;
|
|
53320
|
+
var PULL_WAIT_MAX_MS = 3e4;
|
|
53320
53321
|
|
|
53321
53322
|
// src/core/config.ts
|
|
53322
53323
|
function parseOtelHeaders(raw) {
|
|
@@ -53450,6 +53451,7 @@ init_esm_shims();
|
|
|
53450
53451
|
|
|
53451
53452
|
// src/core/memory-store.ts
|
|
53452
53453
|
init_esm_shims();
|
|
53454
|
+
import { EventEmitter } from "events";
|
|
53453
53455
|
|
|
53454
53456
|
// src/core/keys.ts
|
|
53455
53457
|
init_esm_shims();
|
|
@@ -53504,9 +53506,55 @@ var keys = {
|
|
|
53504
53506
|
},
|
|
53505
53507
|
memoryMeta(ns, sid, key) {
|
|
53506
53508
|
return `lattice:${ns}:memory:${sessionTag(sid)}:meta:${key}`;
|
|
53509
|
+
},
|
|
53510
|
+
/**
|
|
53511
|
+
* Pub/sub wake-up channels. Payloads are small hints only — streams remain
|
|
53512
|
+
* the source of truth, so a missed notify only delays a waiter until its
|
|
53513
|
+
* timeout or next poll.
|
|
53514
|
+
*/
|
|
53515
|
+
notifyRoom(ns, sid, rid) {
|
|
53516
|
+
return `lattice:${ns}:notify:${sessionTag(sid)}:room:${rid}`;
|
|
53517
|
+
},
|
|
53518
|
+
/** DM delivery channel — one per recipient agent. */
|
|
53519
|
+
notifyDm(ns, sid, agentId) {
|
|
53520
|
+
return `lattice:${ns}:notify:${sessionTag(sid)}:dm:${agentId}`;
|
|
53521
|
+
},
|
|
53522
|
+
/** Session roster/room-list changes (join, leave, room create/delete). */
|
|
53523
|
+
notifyMeta(ns, sid) {
|
|
53524
|
+
return `lattice:${ns}:notify:${sessionTag(sid)}:meta`;
|
|
53507
53525
|
}
|
|
53508
53526
|
};
|
|
53509
53527
|
|
|
53528
|
+
// src/core/notify.ts
|
|
53529
|
+
init_esm_shims();
|
|
53530
|
+
async function publishNotify(store, channel, payload) {
|
|
53531
|
+
try {
|
|
53532
|
+
await store.publish(channel, JSON.stringify(payload));
|
|
53533
|
+
} catch {
|
|
53534
|
+
}
|
|
53535
|
+
}
|
|
53536
|
+
async function waitForNotify(store, channels, timeoutMs, recheck) {
|
|
53537
|
+
if (channels.length === 0 || timeoutMs <= 0) return false;
|
|
53538
|
+
let wake;
|
|
53539
|
+
const woke = new Promise((resolve) => {
|
|
53540
|
+
wake = resolve;
|
|
53541
|
+
});
|
|
53542
|
+
let unsub;
|
|
53543
|
+
try {
|
|
53544
|
+
unsub = await store.subscribe(channels, () => wake());
|
|
53545
|
+
} catch {
|
|
53546
|
+
return false;
|
|
53547
|
+
}
|
|
53548
|
+
try {
|
|
53549
|
+
if (recheck && await recheck()) return true;
|
|
53550
|
+
const timer = new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs));
|
|
53551
|
+
const hit = await Promise.race([woke.then(() => true), timer]);
|
|
53552
|
+
return hit;
|
|
53553
|
+
} finally {
|
|
53554
|
+
await unsub();
|
|
53555
|
+
}
|
|
53556
|
+
}
|
|
53557
|
+
|
|
53510
53558
|
// src/core/stream.ts
|
|
53511
53559
|
init_esm_shims();
|
|
53512
53560
|
function compareStreamIds(a, b) {
|
|
@@ -53531,6 +53579,7 @@ var MemoryStore = class {
|
|
|
53531
53579
|
constructor(ns, clock = Date.now) {
|
|
53532
53580
|
this.ns = ns;
|
|
53533
53581
|
this.clock = clock;
|
|
53582
|
+
this.events.setMaxListeners(0);
|
|
53534
53583
|
}
|
|
53535
53584
|
ns;
|
|
53536
53585
|
clock;
|
|
@@ -53540,6 +53589,7 @@ var MemoryStore = class {
|
|
|
53540
53589
|
strings = /* @__PURE__ */ new Map();
|
|
53541
53590
|
streams = /* @__PURE__ */ new Map();
|
|
53542
53591
|
streamSeq = /* @__PURE__ */ new Map();
|
|
53592
|
+
events = new EventEmitter();
|
|
53543
53593
|
now() {
|
|
53544
53594
|
return this.clock();
|
|
53545
53595
|
}
|
|
@@ -53701,6 +53751,10 @@ var MemoryStore = class {
|
|
|
53701
53751
|
const rid = key.slice(cursorPrefix.length).split(":").slice(1).join(":");
|
|
53702
53752
|
if (rid === roomId) this.strings.delete(key);
|
|
53703
53753
|
}
|
|
53754
|
+
await publishNotify(this, keys.notifyMeta(this.ns, sessionId), {
|
|
53755
|
+
type: "rooms",
|
|
53756
|
+
room_id: roomId
|
|
53757
|
+
});
|
|
53704
53758
|
}
|
|
53705
53759
|
async getRoomMeta(sessionId, roomId) {
|
|
53706
53760
|
const h = this.hashes.get(keys.roomMeta(this.ns, sessionId, roomId));
|
|
@@ -53805,7 +53859,23 @@ var MemoryStore = class {
|
|
|
53805
53859
|
async ping() {
|
|
53806
53860
|
return true;
|
|
53807
53861
|
}
|
|
53862
|
+
async publish(channel, payload) {
|
|
53863
|
+
this.events.emit(channel, payload);
|
|
53864
|
+
}
|
|
53865
|
+
async subscribe(channels, onMessage) {
|
|
53866
|
+
const listeners = channels.map((channel) => {
|
|
53867
|
+
const handler = (payload) => onMessage(channel, payload);
|
|
53868
|
+
this.events.on(channel, handler);
|
|
53869
|
+
return { channel, handler };
|
|
53870
|
+
});
|
|
53871
|
+
return async () => {
|
|
53872
|
+
for (const { channel, handler } of listeners) {
|
|
53873
|
+
this.events.off(channel, handler);
|
|
53874
|
+
}
|
|
53875
|
+
};
|
|
53876
|
+
}
|
|
53808
53877
|
async close() {
|
|
53878
|
+
this.events.removeAllListeners();
|
|
53809
53879
|
this.hashes.clear();
|
|
53810
53880
|
this.sets.clear();
|
|
53811
53881
|
this.strings.clear();
|
|
@@ -53939,6 +54009,9 @@ var RedisStore = class _RedisStore {
|
|
|
53939
54009
|
redis;
|
|
53940
54010
|
ns;
|
|
53941
54011
|
kind = "redis";
|
|
54012
|
+
// A connection in subscriber mode can run no other commands, so it is a
|
|
54013
|
+
// dedicated duplicate created on first subscribe.
|
|
54014
|
+
subClient;
|
|
53942
54015
|
static connect(config2) {
|
|
53943
54016
|
return new _RedisStore(createRedisClient(config2), config2.namespace);
|
|
53944
54017
|
}
|
|
@@ -54072,6 +54145,10 @@ var RedisStore = class _RedisStore {
|
|
|
54072
54145
|
}
|
|
54073
54146
|
}
|
|
54074
54147
|
await pipeline.exec();
|
|
54148
|
+
await publishNotify(this, keys.notifyMeta(this.ns, sessionId), {
|
|
54149
|
+
type: "rooms",
|
|
54150
|
+
room_id: roomId
|
|
54151
|
+
});
|
|
54075
54152
|
}
|
|
54076
54153
|
async getRoomMeta(sessionId, roomId) {
|
|
54077
54154
|
const data = await this.redis.hgetall(keys.roomMeta(this.ns, sessionId, roomId));
|
|
@@ -54184,7 +54261,39 @@ var RedisStore = class _RedisStore {
|
|
|
54184
54261
|
return false;
|
|
54185
54262
|
}
|
|
54186
54263
|
}
|
|
54264
|
+
async publish(channel, payload) {
|
|
54265
|
+
await this.redis.publish(channel, payload);
|
|
54266
|
+
}
|
|
54267
|
+
async subscribe(channels, onMessage) {
|
|
54268
|
+
if (channels.length === 0) return async () => {
|
|
54269
|
+
};
|
|
54270
|
+
if (!this.subClient) {
|
|
54271
|
+
this.subClient = this.redis.duplicate();
|
|
54272
|
+
this.subClient.on("error", (err2) => {
|
|
54273
|
+
log("redis sub error", err2.message);
|
|
54274
|
+
});
|
|
54275
|
+
}
|
|
54276
|
+
const sub = this.subClient;
|
|
54277
|
+
sub.on("message", onMessage);
|
|
54278
|
+
await sub.subscribe(...channels);
|
|
54279
|
+
return async () => {
|
|
54280
|
+
sub.off("message", onMessage);
|
|
54281
|
+
try {
|
|
54282
|
+
await sub.unsubscribe(...channels);
|
|
54283
|
+
} catch {
|
|
54284
|
+
}
|
|
54285
|
+
};
|
|
54286
|
+
}
|
|
54187
54287
|
async close() {
|
|
54288
|
+
const sub = this.subClient;
|
|
54289
|
+
this.subClient = void 0;
|
|
54290
|
+
if (sub) {
|
|
54291
|
+
try {
|
|
54292
|
+
await sub.quit();
|
|
54293
|
+
} catch {
|
|
54294
|
+
sub.disconnect();
|
|
54295
|
+
}
|
|
54296
|
+
}
|
|
54188
54297
|
try {
|
|
54189
54298
|
await this.redis.quit();
|
|
54190
54299
|
} catch {
|
|
@@ -55527,7 +55636,7 @@ init_esm_shims();
|
|
|
55527
55636
|
// package.json
|
|
55528
55637
|
var package_default = {
|
|
55529
55638
|
name: "lattice-talk",
|
|
55530
|
-
version: "0.1.
|
|
55639
|
+
version: "0.1.3",
|
|
55531
55640
|
description: "Lattice \u2014 Cross-Harness MCP Session Bus. Shared sessions, DMs, rooms, memory, and OpenTelemetry for agents across Claude Code, Cursor, Codex, and custom harnesses.",
|
|
55532
55641
|
type: "module",
|
|
55533
55642
|
main: "./dist/index.js",
|
|
@@ -56042,6 +56151,12 @@ async function ensureRoom(deps, sessionId, roomId, createdBy, displayName) {
|
|
|
56042
56151
|
};
|
|
56043
56152
|
const created = await deps.store.addRoom(sessionId, rid, meta3);
|
|
56044
56153
|
const finalMeta = await deps.store.getRoomMeta(sessionId, rid) ?? meta3;
|
|
56154
|
+
if (created) {
|
|
56155
|
+
await publishNotify(deps.store, keys.notifyMeta(deps.config.namespace, sessionId), {
|
|
56156
|
+
type: "rooms",
|
|
56157
|
+
room_id: rid
|
|
56158
|
+
});
|
|
56159
|
+
}
|
|
56045
56160
|
return { created, meta: finalMeta };
|
|
56046
56161
|
}
|
|
56047
56162
|
async function ensureMainRoom(deps, sessionId, agentId) {
|
|
@@ -56069,6 +56184,10 @@ async function joinRoom(deps, input2) {
|
|
|
56069
56184
|
}
|
|
56070
56185
|
await deps.store.addRoomMember(sessionId, roomId, agentId);
|
|
56071
56186
|
const members2 = await deps.store.listRoomMembers(sessionId, roomId);
|
|
56187
|
+
await publishNotify(deps.store, keys.notifyMeta(deps.config.namespace, sessionId), {
|
|
56188
|
+
type: "rooms",
|
|
56189
|
+
room_id: roomId
|
|
56190
|
+
});
|
|
56072
56191
|
return { room_id: roomId, members: members2.length, session_id: sessionId };
|
|
56073
56192
|
}
|
|
56074
56193
|
|
|
@@ -56082,6 +56201,11 @@ function clampPullLimit(limit) {
|
|
|
56082
56201
|
if (!Number.isFinite(n)) return PULL_DEFAULT_LIMIT;
|
|
56083
56202
|
return Math.min(PULL_MAX_LIMIT, Math.max(1, Math.floor(n)));
|
|
56084
56203
|
}
|
|
56204
|
+
function clampWaitMs(waitMs) {
|
|
56205
|
+
const n = waitMs ?? 0;
|
|
56206
|
+
if (!Number.isFinite(n) || n <= 0) return 0;
|
|
56207
|
+
return Math.min(PULL_WAIT_MAX_MS, Math.floor(n));
|
|
56208
|
+
}
|
|
56085
56209
|
function truncateBody(body, max = BODY_TRUNCATE_CHARS) {
|
|
56086
56210
|
if (body.length <= max) return { body, truncated: false };
|
|
56087
56211
|
return { body: body.slice(0, max), truncated: true };
|
|
@@ -56154,6 +56278,12 @@ async function tellRoom(deps, input2) {
|
|
|
56154
56278
|
fields,
|
|
56155
56279
|
deps.config.streamMaxLen
|
|
56156
56280
|
);
|
|
56281
|
+
await publishNotify(deps.store, keys.notifyRoom(deps.config.namespace, sessionId, roomId), {
|
|
56282
|
+
type: "message",
|
|
56283
|
+
room_id: roomId,
|
|
56284
|
+
id: messageId,
|
|
56285
|
+
from: agentId
|
|
56286
|
+
});
|
|
56157
56287
|
return { message_id: messageId, room_id: roomId, session_id: sessionId };
|
|
56158
56288
|
}
|
|
56159
56289
|
async function tellAgent(deps, input2) {
|
|
@@ -56182,6 +56312,12 @@ async function tellAgent(deps, input2) {
|
|
|
56182
56312
|
fields,
|
|
56183
56313
|
deps.config.streamMaxLen
|
|
56184
56314
|
);
|
|
56315
|
+
await publishNotify(deps.store, keys.notifyDm(deps.config.namespace, sessionId, to), {
|
|
56316
|
+
type: "dm",
|
|
56317
|
+
pair,
|
|
56318
|
+
id: messageId,
|
|
56319
|
+
from
|
|
56320
|
+
});
|
|
56185
56321
|
return { message_id: messageId, pair, session_id: sessionId };
|
|
56186
56322
|
}
|
|
56187
56323
|
async function pullOneStream(deps, sessionId, agentId, rid, streamKey, limit) {
|
|
@@ -56200,11 +56336,43 @@ async function pullOneStream(deps, sessionId, agentId, rid, streamKey, limit) {
|
|
|
56200
56336
|
async function pullMessages(deps, input2) {
|
|
56201
56337
|
const { sessionId, agentId } = await requireJoinedSession(deps, input2.session_id);
|
|
56202
56338
|
const limit = clampPullLimit(input2.limit);
|
|
56339
|
+
const waitMs = clampWaitMs(input2.wait_ms);
|
|
56203
56340
|
const inbox = input2.inbox === true || input2.room_id === "inbox" || Boolean(input2.other_agent_id);
|
|
56204
56341
|
if (inbox) {
|
|
56205
|
-
|
|
56342
|
+
const channels2 = [keys.notifyDm(deps.config.namespace, sessionId, agentId)];
|
|
56343
|
+
return pullWithWait(
|
|
56344
|
+
deps,
|
|
56345
|
+
waitMs,
|
|
56346
|
+
channels2,
|
|
56347
|
+
() => pullInbox(deps, sessionId, agentId, input2.other_agent_id, limit)
|
|
56348
|
+
);
|
|
56206
56349
|
}
|
|
56207
56350
|
const roomId = assertId(input2.room_id?.trim() || DEFAULT_ROOM, "room_id");
|
|
56351
|
+
const channels = [keys.notifyRoom(deps.config.namespace, sessionId, roomId)];
|
|
56352
|
+
return pullWithWait(
|
|
56353
|
+
deps,
|
|
56354
|
+
waitMs,
|
|
56355
|
+
channels,
|
|
56356
|
+
() => pullRoomStream(deps, sessionId, agentId, roomId, limit)
|
|
56357
|
+
);
|
|
56358
|
+
}
|
|
56359
|
+
async function pullWithWait(deps, waitMs, channels, pullNow) {
|
|
56360
|
+
const first = await pullNow();
|
|
56361
|
+
if (waitMs <= 0 || first.messages.length > 0) return first;
|
|
56362
|
+
let buffered;
|
|
56363
|
+
const notified = await waitForNotify(deps.store, channels, waitMs, async () => {
|
|
56364
|
+
const recheck = await pullNow();
|
|
56365
|
+
if (recheck.messages.length > 0) {
|
|
56366
|
+
buffered = recheck;
|
|
56367
|
+
return true;
|
|
56368
|
+
}
|
|
56369
|
+
return false;
|
|
56370
|
+
});
|
|
56371
|
+
if (buffered) return buffered;
|
|
56372
|
+
if (!notified) return first;
|
|
56373
|
+
return pullNow();
|
|
56374
|
+
}
|
|
56375
|
+
async function pullRoomStream(deps, sessionId, agentId, roomId, limit) {
|
|
56208
56376
|
const meta3 = await deps.store.getRoomMeta(sessionId, roomId);
|
|
56209
56377
|
if (!meta3) {
|
|
56210
56378
|
throw new UserError(`Room ${roomId} does not exist. Create it with create_room first.`);
|
|
@@ -56451,6 +56619,10 @@ async function joinSession(deps, input2) {
|
|
|
56451
56619
|
await ensureMainRoom(deps, sessionId, agent.agent_id);
|
|
56452
56620
|
await refreshPresence(deps, sessionId, agent.agent_id);
|
|
56453
56621
|
deps.ctx.rememberJoin(sessionId, agent);
|
|
56622
|
+
await publishNotify(deps.store, keys.notifyMeta(deps.config.namespace, sessionId), {
|
|
56623
|
+
type: "agents",
|
|
56624
|
+
agent_id: agent.agent_id
|
|
56625
|
+
});
|
|
56454
56626
|
const peers = await listPeers(deps, { session_id: sessionId });
|
|
56455
56627
|
return {
|
|
56456
56628
|
session_id: sessionId,
|
|
@@ -56468,6 +56640,10 @@ async function leaveSession(deps, input2 = {}) {
|
|
|
56468
56640
|
await deps.store.clearAgentState(sessionId, agentId);
|
|
56469
56641
|
await deps.store.removeAgent(sessionId, agentId);
|
|
56470
56642
|
deps.ctx.clearIf(sessionId, agentId);
|
|
56643
|
+
await publishNotify(deps.store, keys.notifyMeta(deps.config.namespace, sessionId), {
|
|
56644
|
+
type: "agents",
|
|
56645
|
+
agent_id: agentId
|
|
56646
|
+
});
|
|
56471
56647
|
return { left: true, session_id: sessionId, agent_id: agentId };
|
|
56472
56648
|
}
|
|
56473
56649
|
async function listPeers(deps, input2 = {}) {
|
|
@@ -56797,7 +56973,10 @@ var pullMessagesSchema = external_exports.object({
|
|
|
56797
56973
|
room_id: external_exports.string().max(128).regex(OPTIONAL_ID_PATTERN).optional().describe("Room to read (default main). Caller must be a member. Use inbox to pull DMs instead."),
|
|
56798
56974
|
inbox: external_exports.boolean().optional().describe("If true, pull this process's DM inbox (all pair streams, or one if other_agent_id is set)."),
|
|
56799
56975
|
other_agent_id: external_exports.string().max(128).regex(ID_PATTERN).optional().describe("When pulling inbox, only this DM pair. Does not change who is reading."),
|
|
56800
|
-
limit: external_exports.coerce.number().int().min(1).max(200).optional().describe("Max messages to return. Default 50, max 200.")
|
|
56976
|
+
limit: external_exports.coerce.number().int().min(1).max(200).optional().describe("Max messages to return. Default 50, max 200."),
|
|
56977
|
+
wait_ms: external_exports.coerce.number().int().min(0).max(3e4).optional().describe(
|
|
56978
|
+
"Block up to this many ms waiting for a new message (push wake-up, not polling). 0 or omitted returns immediately. Max 30000."
|
|
56979
|
+
)
|
|
56801
56980
|
});
|
|
56802
56981
|
var createRoomSchema = external_exports.object({
|
|
56803
56982
|
session_id: joinedSessionField,
|
|
@@ -57057,7 +57236,7 @@ function registerMessagingTools(server, deps) {
|
|
|
57057
57236
|
server.registerTool(
|
|
57058
57237
|
"tell_room",
|
|
57059
57238
|
{
|
|
57060
|
-
description: "Broadcast a message from this joined process to a session room (default main). Caller must be a room member.
|
|
57239
|
+
description: "Broadcast a message from this joined process to a session room (default main). Caller must be a room member. Room members waiting on pull_messages with wait_ms are woken immediately.",
|
|
57061
57240
|
inputSchema: tellRoomSchema,
|
|
57062
57241
|
outputSchema: tellRoomOutputSchema,
|
|
57063
57242
|
annotations: write("Tell room")
|
|
@@ -57076,7 +57255,7 @@ function registerMessagingTools(server, deps) {
|
|
|
57076
57255
|
server.registerTool(
|
|
57077
57256
|
"pull_messages",
|
|
57078
57257
|
{
|
|
57079
|
-
description: "Read new messages since this process's cursor, then advance the cursor and refresh presence. Pass room_id for a room (must be a member), or inbox=true (optionally other_agent_id) for this process's DMs. Bodies longer than ~2k chars are truncated.",
|
|
57258
|
+
description: "Read new messages since this process's cursor, then advance the cursor and refresh presence. Pass room_id for a room (must be a member), or inbox=true (optionally other_agent_id) for this process's DMs. Set wait_ms (max 30000) to block until a new message arrives via push wake-up instead of polling. Bodies longer than ~2k chars are truncated.",
|
|
57080
57259
|
inputSchema: pullMessagesSchema,
|
|
57081
57260
|
outputSchema: pullMessagesOutputSchema,
|
|
57082
57261
|
annotations: write("Pull messages")
|