@agentvault/claude-bridge 0.3.0 → 0.3.2

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/bridge.d.ts CHANGED
@@ -9,13 +9,30 @@ export interface RoomMessage {
9
9
  export interface MessageMeta {
10
10
  roomId?: string;
11
11
  }
12
+ /** #392 room hush (advisory) — emitted by SecureChannel from the backend's
13
+ * `room_hushed` event. ``hushedUntil`` is an ISO string, or null when cleared. */
14
+ export interface RoomHushed {
15
+ roomId: string;
16
+ hushedUntil: string | null;
17
+ }
12
18
  export interface RoomChannel {
13
19
  on(ev: "room_message", cb: (e: RoomMessage) => void): unknown;
14
20
  on(ev: "message", cb: (text: string, metadata: MessageMeta) => void): unknown;
21
+ on(ev: "room_hushed", cb: (e: RoomHushed) => void): unknown;
15
22
  on(ev: "error", cb: (err: unknown) => void): unknown;
16
23
  sendToRoom(roomId: string, text: string): Promise<void>;
17
24
  send(text: string): Promise<void>;
18
25
  }
26
+ /**
27
+ * #392 cooperative quiet: tracks per-room hush windows so the native agent holds
28
+ * (does not room_say) while the owner has hushed the room. Advisory — the agent
29
+ * cooperates; the hard backstops are owner mute / remove (enforced server-side).
30
+ */
31
+ export declare class RoomHushState {
32
+ private until;
33
+ set(roomId: string, hushedUntilIso: string | null): void;
34
+ isHushed(roomId: string, now?: number): boolean;
35
+ }
19
36
  export interface RoomSession {
20
37
  /** `reply` is the immutable reply sink captured for THIS message (see
21
38
  * ActiveTarget.snapshotReply) — the session invokes it when Claude answers. */
package/dist/index.js CHANGED
@@ -66743,6 +66743,19 @@ var init_channel = __esm({
66743
66743
  }
66744
66744
  });
66745
66745
  }
66746
+ if (data.event === "room_hushed") {
66747
+ this.emit("room_hushed", {
66748
+ roomId: data.data?.room_id,
66749
+ hushedUntil: data.data?.hushed_until ?? null
66750
+ });
66751
+ }
66752
+ if (data.event === "room_advisory") {
66753
+ this.emit("room_advisory", {
66754
+ roomId: data.data?.room_id,
66755
+ kind: data.data?.kind,
66756
+ message: data.data?.message
66757
+ });
66758
+ }
66746
66759
  if (data.event === "policy_blocked") {
66747
66760
  this.emit("policy_blocked", data.data);
66748
66761
  }
@@ -68497,6 +68510,21 @@ ${messageText}`;
68497
68510
  return;
68498
68511
  }
68499
68512
  }
68513
+ if (welcomeRoomId && this._persisted) {
68514
+ await this._registerRoomFromWelcome(
68515
+ welcomeRoomId,
68516
+ groupId,
68517
+ mgr,
68518
+ data.room_name
68519
+ );
68520
+ this._pendingMlsKpBundle = void 0;
68521
+ await releaseA2ASyncLock(this.config.dataDir, `room-kp-${groupId}`).catch(() => {
68522
+ });
68523
+ console.log(
68524
+ `[SecureChannel] Registered room ${welcomeRoomId.slice(0, 8)} from unmatched Welcome (epoch=${mgr.epoch})`
68525
+ );
68526
+ return;
68527
+ }
68500
68528
  const a2aChannelId = data.a2a_channel_id;
68501
68529
  for (const [channelId, entry] of Object.entries(this._persisted?.a2aChannels ?? {})) {
68502
68530
  if (entry.mlsGroupId === groupId || channelId === a2aChannelId) {
@@ -68553,6 +68581,33 @@ ${messageText}`;
68553
68581
  throw err;
68554
68582
  }
68555
68583
  }
68584
+ /**
68585
+ * Self-bootstrap a room entry + MLS group mapping from a Welcome whose room is
68586
+ * not yet persisted. Happens when we were added to an EXISTING room and the
68587
+ * `room_joined` that registers the room raced behind (or was lost relative to)
68588
+ * the Welcome. Without a room entry the Welcome room-match loop can't match, and
68589
+ * every subsequent room message is dropped with "No room found for MLS group".
68590
+ * Mirrors the A2A self-bootstrap fallback. The backend now also sends the new
68591
+ * device its own `room_joined` (PR #412); this is defense-in-depth for the race
68592
+ * where that delivery is missed. A later `room_joined` enriches name/members.
68593
+ */
68594
+ async _registerRoomFromWelcome(roomId, groupId, mgr, roomName) {
68595
+ if (!this._persisted) return;
68596
+ if (!this._persisted.rooms) this._persisted.rooms = {};
68597
+ const existing = this._persisted.rooms[roomId];
68598
+ this._persisted.rooms[roomId] = {
68599
+ roomId,
68600
+ name: existing?.name ?? (roomName ?? ""),
68601
+ conversationIds: existing?.conversationIds ?? [],
68602
+ members: existing?.members ?? [],
68603
+ mlsGroupId: groupId,
68604
+ lastMlsMessageTs: existing?.lastMlsMessageTs
68605
+ };
68606
+ this._mlsGroups.set(roomId, mgr);
68607
+ await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mgr.exportState()));
68608
+ await this._persistState();
68609
+ await this._applyBufferedMlsCommits(groupId, mgr);
68610
+ }
68556
68611
  /**
68557
68612
  * Pull pending MLS messages from the delivery queue and process them.
68558
68613
  * Called on WS connect, on mls_delivery ping, and every 30s heartbeat.
@@ -131587,6 +131642,23 @@ var PersistentClaudeSession = class {
131587
131642
  };
131588
131643
 
131589
131644
  // src/bridge.ts
131645
+ var RoomHushState = class {
131646
+ until = /* @__PURE__ */ new Map();
131647
+ set(roomId, hushedUntilIso) {
131648
+ if (!roomId) return;
131649
+ if (!hushedUntilIso) {
131650
+ this.until.delete(roomId);
131651
+ return;
131652
+ }
131653
+ const t7 = new Date(hushedUntilIso).getTime();
131654
+ if (Number.isNaN(t7)) return;
131655
+ this.until.set(roomId, t7);
131656
+ }
131657
+ isHushed(roomId, now = Date.now()) {
131658
+ const t7 = this.until.get(roomId);
131659
+ return t7 !== void 0 && now < t7;
131660
+ }
131661
+ };
131590
131662
  var ActiveTarget = class {
131591
131663
  target = null;
131592
131664
  setRoom(roomId) {
@@ -131654,8 +131726,19 @@ function wireBridge(channel, session, target, opts = {}) {
131654
131726
  const msg = err instanceof Error ? err.message : String(err);
131655
131727
  log(`channel error (auto-reconnecting): ${msg}`);
131656
131728
  });
131729
+ const hush = new RoomHushState();
131730
+ channel.on("room_hushed", (e7) => {
131731
+ hush.set(e7.roomId, e7.hushedUntil);
131732
+ log(
131733
+ e7.hushedUntil ? `room ${e7.roomId.slice(0, 8)} hushed until ${e7.hushedUntil} \u2014 holding` : `room ${e7.roomId.slice(0, 8)} hush cleared`
131734
+ );
131735
+ });
131657
131736
  channel.on("room_message", (e7) => {
131658
131737
  if (opts.roomFilter && e7.roomId !== opts.roomFilter) return;
131738
+ if (hush.isHushed(e7.roomId)) {
131739
+ log(`inbound from ${e7.senderName} in ${e7.roomId.slice(0, 8)} \u2014 room hushed, holding (not replying)`);
131740
+ return;
131741
+ }
131659
131742
  log(`inbound from ${e7.senderName} in ${e7.roomId.slice(0, 8)}`);
131660
131743
  target.setRoom(e7.roomId);
131661
131744
  session.push(`[${e7.senderName}]: ${e7.plaintext}`, target.snapshotReply(channel, log));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "AgentVault Claude Bridge — daemon for bridging a Claude agent into secure E2E-encrypted AgentVault 1:1 direct messages and rooms.",
6
6
  "main": "dist/index.js",