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/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.2",
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
- return pullInbox(deps, sessionId, agentId, input2.other_agent_id, limit);
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. Other agents see it on pull_messages after idle.",
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")