@agentvault/agentvault 0.23.7 → 0.23.9

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
@@ -64792,7 +64792,7 @@ var init_channel = __esm({
64792
64792
  */
64793
64793
  sendActivitySpan(spanData) {
64794
64794
  if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
64795
- const pluginVersion = true ? "0.23.7" : "0.0.0-dev";
64795
+ const pluginVersion = true ? "0.23.9" : "0.0.0-dev";
64796
64796
  const agentName = this.config.agentName ?? "Agent";
64797
64797
  const resource = {
64798
64798
  "service.name": "agentvault-agent",
@@ -65033,10 +65033,76 @@ var init_channel = __esm({
65033
65033
  this._sessions.delete(convId);
65034
65034
  delete this._persisted.sessions[convId];
65035
65035
  }
65036
+ if (room.mlsGroupId) {
65037
+ try {
65038
+ await deleteMlsState(this.config.dataDir, room.mlsGroupId);
65039
+ } catch {
65040
+ }
65041
+ }
65042
+ this._mlsGroups.delete(roomId);
65036
65043
  delete this._persisted.rooms[roomId];
65037
65044
  await this._persistState();
65038
65045
  this.emit("room_left", { roomId });
65039
65046
  }
65047
+ /**
65048
+ * #629 recurrence fix — reconcile local room state against the server on
65049
+ * connect.
65050
+ *
65051
+ * On disband the server marks each member ``"left"`` and sets
65052
+ * ``room.status="disbanded"`` but sends NO WS event to the agent's bridge, so a
65053
+ * disbanded/left room lingers in local state and replays dead MLS commit
65054
+ * history on every reconnect ("Desired gen in the past" / "invalid ghash tag").
65055
+ * ``GET /rooms`` returns ONLY active rooms the caller is a member of, so any
65056
+ * locally-persisted room ABSENT from it is terminal — prune its MLS state
65057
+ * (file + in-memory group, keyed by roomId), its sessions, and the room entry.
65058
+ *
65059
+ * Fail-safe: a failed fetch / non-ok / non-array response prunes NOTHING, so a
65060
+ * transient error can never over-prune a live room.
65061
+ */
65062
+ async _reconcileRoomsWithServer() {
65063
+ const local = this._persisted?.rooms;
65064
+ if (!local || Object.keys(local).length === 0) return;
65065
+ if (!this._deviceJwt) return;
65066
+ let activeIds;
65067
+ try {
65068
+ const res = await fetch(`${this.config.apiUrl}/api/v1/rooms`, {
65069
+ headers: { Authorization: `Bearer ${this._deviceJwt}` }
65070
+ });
65071
+ if (!res.ok) return;
65072
+ const list = await res.json();
65073
+ if (!Array.isArray(list)) return;
65074
+ activeIds = new Set(
65075
+ list.map((r2) => r2?.id).filter((id) => !!id)
65076
+ );
65077
+ } catch (err) {
65078
+ console.warn(
65079
+ `[SecureChannel] room reconcile skipped (fetch failed, pruning nothing): ${err instanceof Error ? err.message : String(err)}`
65080
+ );
65081
+ return;
65082
+ }
65083
+ const stale = Object.keys(local).filter((rid) => !activeIds.has(rid));
65084
+ if (stale.length === 0) return;
65085
+ for (const rid of stale) {
65086
+ const room = local[rid];
65087
+ if (room?.mlsGroupId) {
65088
+ try {
65089
+ await deleteMlsState(this.config.dataDir, room.mlsGroupId);
65090
+ } catch {
65091
+ }
65092
+ }
65093
+ this._mlsGroups.delete(rid);
65094
+ for (const convId of room?.conversationIds ?? []) {
65095
+ this._sessions.delete(convId);
65096
+ if (this._persisted?.sessions) delete this._persisted.sessions[convId];
65097
+ }
65098
+ delete local[rid];
65099
+ }
65100
+ await this._persistState();
65101
+ console.log(
65102
+ `[SecureChannel] Room reconcile: pruned ${stale.length} disbanded/left room(s) from local state (${stale.map((r2) => r2.slice(0, 8)).join(", ")})`
65103
+ );
65104
+ this.emit("rooms_reconciled", { pruned: stale });
65105
+ }
65040
65106
  /**
65041
65107
  * Return info for all joined rooms.
65042
65108
  */
@@ -66387,6 +66453,9 @@ var init_channel = __esm({
66387
66453
  await this._pullDrDeliveryQueue();
66388
66454
  await this._flushOutboundQueue();
66389
66455
  this._setState("ready");
66456
+ void this._reconcileRoomsWithServer().catch(
66457
+ (err) => console.warn(`[SecureChannel] room reconcile failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
66458
+ );
66390
66459
  if (this.config.enableScanning) {
66391
66460
  this._scanEngine = new ScanEngine();
66392
66461
  await this._fetchScanRules();
@@ -66409,7 +66478,7 @@ var init_channel = __esm({
66409
66478
  agentVersion: this.config.agentVersion ?? "0.0.0",
66410
66479
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
66411
66480
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
66412
- pluginVersion: true ? "0.23.7" : "0.0.0-dev"
66481
+ pluginVersion: true ? "0.23.9" : "0.0.0-dev"
66413
66482
  });
66414
66483
  this._telemetryReporter.startAutoFlush(3e4);
66415
66484
  }
@@ -66488,6 +66557,10 @@ var init_channel = __esm({
66488
66557
  await this._handleDeviceRevoked();
66489
66558
  return;
66490
66559
  }
66560
+ if (data.event === "connection_rejected") {
66561
+ await this._handleConnectionRejected(data);
66562
+ return;
66563
+ }
66491
66564
  if (data.event === "device_linked") {
66492
66565
  await this._handleDeviceLinked(data.data);
66493
66566
  return;
@@ -66661,6 +66734,7 @@ var init_channel = __esm({
66661
66734
  if (data.event === "arming_snapshot") {
66662
66735
  this.emit("arming_snapshot", {
66663
66736
  workAllowed: data.data?.work_allowed === true,
66737
+ gatesRemoved: data.data?.gates_removed === true,
66664
66738
  roomIds: data.data?.room_ids ?? []
66665
66739
  });
66666
66740
  }
@@ -66722,7 +66796,7 @@ var init_channel = __esm({
66722
66796
  agentVersion: this.config.agentVersion ?? "0.0.0",
66723
66797
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
66724
66798
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
66725
- pluginVersion: true ? "0.23.7" : "0.0.0-dev"
66799
+ pluginVersion: true ? "0.23.9" : "0.0.0-dev"
66726
66800
  });
66727
66801
  this._telemetryReporter.startAutoFlush(3e4);
66728
66802
  }
@@ -68032,7 +68106,12 @@ ${messageText}`;
68032
68106
  senderName: senderLabel,
68033
68107
  plaintext: messageText,
68034
68108
  messageType,
68035
- timestamp: data.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
68109
+ timestamp: data.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
68110
+ // Native bridge (claude-room-bridge) consumes this to decide reply
68111
+ // expectation: a human/owner speaking in a room always expects a reply,
68112
+ // an agent does not. Derived from the room roster (line ~5394) — the same
68113
+ // signal the OpenClaw mention-filter uses for loop prevention.
68114
+ senderIsAgent
68036
68115
  });
68037
68116
  const contextualMessage = senderIsAgent ? messageText : `[${senderLabel}]: ${messageText}`;
68038
68117
  Promise.resolve(this.config.onMessage?.(contextualMessage, metadata)).catch((err) => {
@@ -69909,6 +69988,37 @@ ${messageText}`;
69909
69988
  * re-enrollment, which SHOULD wipe creds, goes through setup --force /
69910
69989
  * _forceReEnroll in start().
69911
69990
  */
69991
+ /**
69992
+ * Handle a WS `connection_rejected` event.
69993
+ *
69994
+ * The server accepts the WS upgrade, then — for a device it will not serve —
69995
+ * emits `{event:"connection_rejected", reason, retryable}` and closes with
69996
+ * code 4403. `retryable:false` (e.g. `device_revoked`) is a TERMINAL
69997
+ * credential failure: retrying can only reconnect, get rejected, and close
69998
+ * again. Left unhandled, that generic close drove the reconnect classifier
69999
+ * into "reconnect loop detected", which the bridge treats as a restartable
70000
+ * exit — so launchd/systemd relaunched us every ~30s to hammer the backend
70001
+ * with the same rejected handshake (empirically ~1150 rejects / 2h).
70002
+ *
70003
+ * So we route a non-retryable rejection to the SAME `auth_failed` surface the
70004
+ * proactive/reactive reissue paths use (see channel.ts emit sites). The
70005
+ * bridge wires `auth_failed` to a clean exit(0) (packages/claude-room-bridge
70006
+ * /src/bridge.ts), so launchd stops relaunching. Mirrors those emitters:
70007
+ * emit only + mark the session; the consumer decides how to stop. A
70008
+ * `retryable` rejection is transient — fall through to a normal reconnect.
70009
+ */
70010
+ async _handleConnectionRejected(data) {
70011
+ if (data?.retryable === true) {
70012
+ this._scheduleReconnect();
70013
+ return;
70014
+ }
70015
+ this._authFailedThisSession = true;
70016
+ const authReason = data?.reason === "device_jwt_expired" ? "device_jwt_expired" : "device_revoked";
70017
+ console.warn(
70018
+ `[SecureChannel] connection_rejected (reason=${data?.reason ?? "unknown"}, retryable=false) \u2014 terminal; surfacing auth_failed`
70019
+ );
70020
+ this.emit("auth_failed", { reason: authReason });
70021
+ }
69912
70022
  async _handleDeviceRevoked() {
69913
70023
  const now = Date.now();
69914
70024
  this._revokeRecoveries = this._revokeRecoveries.filter(
@@ -96900,7 +97010,7 @@ var init_index = __esm({
96900
97010
  init_skill_invoker();
96901
97011
  await init_skill_telemetry();
96902
97012
  await init_policy_enforcer();
96903
- VERSION = true ? "0.23.7" : "0.0.0-dev";
97013
+ VERSION = true ? "0.23.9" : "0.0.0-dev";
96904
97014
  }
96905
97015
  });
96906
97016
  await init_index();