@agentvault/agentvault 0.23.19 → 0.23.21

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
@@ -63011,6 +63011,35 @@ async function deleteMlsState(dataDir, groupId) {
63011
63011
  } catch {
63012
63012
  }
63013
63013
  }
63014
+ async function quarantineOrphanedMlsState(dataDir, isStillMember, opts = {}) {
63015
+ const max = opts.max ?? 20;
63016
+ const quarantined = [];
63017
+ let names;
63018
+ try {
63019
+ names = await readdir(dataDir);
63020
+ } catch {
63021
+ return quarantined;
63022
+ }
63023
+ for (const name of names) {
63024
+ if (quarantined.length >= max) break;
63025
+ const m2 = /^mls-(?!kp-pending-)([A-Za-z0-9_-]+)\.json$/.exec(name);
63026
+ if (!m2) continue;
63027
+ const groupId = m2[1];
63028
+ let member;
63029
+ try {
63030
+ member = await isStillMember(groupId);
63031
+ } catch {
63032
+ continue;
63033
+ }
63034
+ if (member === null || member === true) continue;
63035
+ try {
63036
+ await rename(join(dataDir, name), join(dataDir, `${name}.orphaned`));
63037
+ quarantined.push(groupId);
63038
+ } catch {
63039
+ }
63040
+ }
63041
+ return quarantined;
63042
+ }
63014
63043
  var SYNC_LOCK_STALE_MS, pendingKpPath;
63015
63044
  var init_mls_state = __esm({
63016
63045
  "src/mls-state.ts"() {
@@ -63355,6 +63384,45 @@ var init_transport2 = __esm({
63355
63384
  }
63356
63385
  });
63357
63386
 
63387
+ // src/a2a-log-gate.ts
63388
+ function a2aChannelSignature(channels) {
63389
+ return JSON.stringify(
63390
+ channels.map((c2) => ({
63391
+ id: c2.channelId?.slice(0, 8),
63392
+ status: c2.status,
63393
+ participants: c2.participant_count
63394
+ })).sort((a2, b2) => (a2.id ?? "").localeCompare(b2.id ?? ""))
63395
+ );
63396
+ }
63397
+ var A2ALogGate;
63398
+ var init_a2a_log_gate = __esm({
63399
+ "src/a2a-log-gate.ts"() {
63400
+ "use strict";
63401
+ A2ALogGate = class {
63402
+ last = null;
63403
+ /** True when this state differs from the last reported one. Records it. */
63404
+ shouldReport(channels) {
63405
+ const sig = a2aChannelSignature(channels);
63406
+ if (sig === this.last) return false;
63407
+ this.last = sig;
63408
+ return true;
63409
+ }
63410
+ };
63411
+ }
63412
+ });
63413
+
63414
+ // src/mls-membership-probe.ts
63415
+ function mlsMembershipFromStatus(status) {
63416
+ if (status === 200) return true;
63417
+ if (status === 403 || status === 404) return false;
63418
+ return null;
63419
+ }
63420
+ var init_mls_membership_probe = __esm({
63421
+ "src/mls-membership-probe.ts"() {
63422
+ "use strict";
63423
+ }
63424
+ });
63425
+
63358
63426
  // src/openclaw-compat.ts
63359
63427
  var openclaw_compat_exports = {};
63360
63428
  __export(openclaw_compat_exports, {
@@ -63917,6 +63985,8 @@ var init_channel = __esm({
63917
63985
  await init_crypto_helpers();
63918
63986
  await init_state();
63919
63987
  init_transport2();
63988
+ init_a2a_log_gate();
63989
+ init_mls_membership_probe();
63920
63990
  ROOM_AGENT_TYPES = /* @__PURE__ */ new Set([
63921
63991
  "message",
63922
63992
  "text",
@@ -63982,6 +64052,11 @@ var init_channel = __esm({
63982
64052
  _heartbeatTimer = null;
63983
64053
  _heartbeatCallback = null;
63984
64054
  _heartbeatIntervalSeconds = 0;
64055
+ /** #798: report the A2A channel set only when it CHANGES. This line polls
64056
+ * every 30s and produced 90.2% of bridge.log (90,841/100,702 lines) and a
64057
+ * 27.9MB gateway.log on wren, every one reading `channels=0`. Not deleted —
64058
+ * #794's A2A watch needs the transitions. */
64059
+ _a2aLogGate = new A2ALogGate();
63985
64060
  _wakeDetectorTimer = null;
63986
64061
  _lastWakeTick = Date.now();
63987
64062
  _trustToken = null;
@@ -63991,6 +64066,17 @@ var init_channel = __esm({
63991
64066
  _pendingPollTimer = null;
63992
64067
  _syncMessageIds = null;
63993
64068
  _deliveryHeartbeat = null;
64069
+ /**
64070
+ * How often to re-check the KeyPackage pool against the server (#826).
64071
+ *
64072
+ * KeyPackages expire after 7 days and, before this, only a reconnect
64073
+ * republished them — so an agent that simply stayed up ran to zero usable
64074
+ * and silently stopped accepting new MLS 1:1 conversations while looking
64075
+ * perfectly healthy. 6h gives ~28 attempts inside one lifetime, so a run of
64076
+ * transient failures cannot age the pool out. Cost is one GET per interval.
64077
+ */
64078
+ static _KP_REPLENISH_INTERVAL_MS = 6 * 60 * 60 * 1e3;
64079
+ _kpReplenishTimer = null;
63994
64080
  _a2aReconcileTimer = null;
63995
64081
  _deliveryPulling = false;
63996
64082
  _drDeliveryPulling = false;
@@ -64955,7 +65041,7 @@ var init_channel = __esm({
64955
65041
  */
64956
65042
  sendActivitySpan(spanData) {
64957
65043
  if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
64958
- const pluginVersion = true ? "0.23.19" : "0.0.0-dev";
65044
+ const pluginVersion = true ? "0.23.21" : "0.0.0-dev";
64959
65045
  const agentName = this.config.agentName ?? "Agent";
64960
65046
  const resource = {
64961
65047
  "service.name": "agentvault-agent",
@@ -65275,6 +65361,47 @@ var init_channel = __esm({
65275
65361
  );
65276
65362
  this.emit("rooms_reconciled", { pruned: stale });
65277
65363
  }
65364
+ /**
65365
+ * #629 — quarantine MLS state for groups this device is no longer in.
65366
+ *
65367
+ * `_reconcileRoomsWithServer` above covers ROOMS: anything absent from
65368
+ * `GET /rooms` is pruned. It cannot see a group that was never a room — a 1:1
65369
+ * conversation group or an A2A channel group — so that state lingered with
65370
+ * nothing to reconcile it. Measured on loopita 2026-08-08: one group-state
65371
+ * file whose group had no server-side row at all.
65372
+ *
65373
+ * `quarantineOrphanedMlsState` was written for exactly this and shipped with
65374
+ * 11 passing tests, but nothing ever called it. This is that call site.
65375
+ *
65376
+ * Runs AFTER the room reconcile so rooms are already gone and this only sees
65377
+ * genuine non-room orphans.
65378
+ *
65379
+ * Quarantine RENAMES to `.orphaned` rather than deleting: if the verdict is
65380
+ * ever wrong the state is recoverable by hand, which deletion would not be.
65381
+ */
65382
+ async _quarantineOrphanedMlsGroups() {
65383
+ if (!this._deviceJwt) return;
65384
+ const quarantined = await quarantineOrphanedMlsState(
65385
+ this.config.dataDir,
65386
+ async (groupId) => {
65387
+ try {
65388
+ const res = await fetch(
65389
+ `${this.config.apiUrl}/api/v1/mls/groups/${encodeURIComponent(groupId)}`,
65390
+ { headers: { Authorization: `Bearer ${this._deviceJwt}` } }
65391
+ );
65392
+ return mlsMembershipFromStatus(res.status);
65393
+ } catch {
65394
+ return null;
65395
+ }
65396
+ }
65397
+ );
65398
+ if (quarantined.length > 0) {
65399
+ console.log(
65400
+ `[SecureChannel] Quarantined ${quarantined.length} orphaned MLS group state file(s): ` + quarantined.map((g2) => g2.slice(0, 8)).join(", ")
65401
+ );
65402
+ this.emit("mls_state_quarantined", { groupIds: quarantined });
65403
+ }
65404
+ }
65278
65405
  /**
65279
65406
  * Handle an in-band `{event:"error"}` frame from the server.
65280
65407
  *
@@ -66435,7 +66562,9 @@ var init_channel = __esm({
66435
66562
  }
66436
66563
  const myHub = this._persisted.hubAddress || "";
66437
66564
  const myHubId = this._persisted.hubId || "";
66438
- console.log(`[listA2AChannels] myHub=${myHub}, channels=${channels.length}, raw=${JSON.stringify(channels.map((c2) => ({ id: c2.channelId?.slice(0, 8), status: c2.status, participants: c2.participant_count })))}`);
66565
+ if (this._a2aLogGate.shouldReport(channels)) {
66566
+ console.log(`[listA2AChannels] myHub=${myHub}, channels=${channels.length}, raw=${JSON.stringify(channels.map((c2) => ({ id: c2.channelId?.slice(0, 8), status: c2.status, participants: c2.participant_count })))}`);
66567
+ }
66439
66568
  for (const ch of channels) {
66440
66569
  if (ch.status === "active" || ch.status === "approved") {
66441
66570
  const myParticipant = ch.participants?.find(
@@ -66797,6 +66926,8 @@ var init_channel = __esm({
66797
66926
  this._setState("ready");
66798
66927
  void this._reconcileRoomsWithServer().catch(
66799
66928
  (err) => console.warn(`[SecureChannel] room reconcile failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
66929
+ ).then(() => this._quarantineOrphanedMlsGroups()).catch(
66930
+ (err) => console.warn(`[SecureChannel] MLS orphan quarantine failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
66800
66931
  );
66801
66932
  if (this.config.enableScanning) {
66802
66933
  this._scanEngine = new ScanEngine();
@@ -66820,7 +66951,7 @@ var init_channel = __esm({
66820
66951
  agentVersion: this.config.agentVersion ?? "0.0.0",
66821
66952
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
66822
66953
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
66823
- pluginVersion: true ? "0.23.19" : "0.0.0-dev"
66954
+ pluginVersion: true ? "0.23.21" : "0.0.0-dev"
66824
66955
  });
66825
66956
  this._telemetryReporter.startAutoFlush(3e4);
66826
66957
  }
@@ -66829,6 +66960,7 @@ var init_channel = __esm({
66829
66960
  console.warn("[SecureChannel] A2A channel sync on connect failed:", err);
66830
66961
  });
66831
66962
  await this._ensureKpPoolPublished();
66963
+ this._startKpReplenishTimer();
66832
66964
  this._pullDeliveryQueue().catch((err) => {
66833
66965
  console.warn("[SecureChannel] Initial delivery pull failed:", err);
66834
66966
  });
@@ -67143,7 +67275,7 @@ var init_channel = __esm({
67143
67275
  agentVersion: this.config.agentVersion ?? "0.0.0",
67144
67276
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67145
67277
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67146
- pluginVersion: true ? "0.23.19" : "0.0.0-dev"
67278
+ pluginVersion: true ? "0.23.21" : "0.0.0-dev"
67147
67279
  });
67148
67280
  this._telemetryReporter.startAutoFlush(3e4);
67149
67281
  }
@@ -67486,6 +67618,7 @@ var init_channel = __esm({
67486
67618
  this._deliveryHeartbeat = null;
67487
67619
  }
67488
67620
  this._stopA2aReconcileTimer();
67621
+ this._stopKpReplenishTimer();
67489
67622
  if (this._stopped) return;
67490
67623
  this._setState("disconnected");
67491
67624
  this._scheduleReconnect();
@@ -68879,6 +69012,103 @@ ${messageText}`;
68879
69012
  this._kpPoolFilling = false;
68880
69013
  }
68881
69014
  }
69015
+ /** Start the periodic KeyPackage replenish (#826). Idempotent. */
69016
+ _startKpReplenishTimer() {
69017
+ this._stopKpReplenishTimer();
69018
+ this._kpReplenishTimer = setInterval(() => {
69019
+ void this._replenishKpPoolFromServer();
69020
+ }, _SecureChannel._KP_REPLENISH_INTERVAL_MS);
69021
+ }
69022
+ _stopKpReplenishTimer() {
69023
+ if (this._kpReplenishTimer) {
69024
+ clearInterval(this._kpReplenishTimer);
69025
+ this._kpReplenishTimer = null;
69026
+ }
69027
+ }
69028
+ /**
69029
+ * The server's EXPIRY-AWARE usable KeyPackage count, or null when unknown.
69030
+ *
69031
+ * `count_unconsumed` filters `expires_at > utcnow()`, which is the only count
69032
+ * that reflects what a peer can actually fetch. Returns null (not 0) on any
69033
+ * failure so callers can tell "server says empty" from "we could not ask" —
69034
+ * guessing 0 here would publish a fresh pool on every transient 503.
69035
+ */
69036
+ async _fetchServerKpCount() {
69037
+ try {
69038
+ const res = await fetch(
69039
+ `${this.config.apiUrl}/api/v1/mls/key-packages/count?device_id=${this._deviceId}`,
69040
+ { headers: { Authorization: `Bearer ${this._deviceJwt}` } }
69041
+ );
69042
+ if (!res.ok) return null;
69043
+ const body = await res.json();
69044
+ const n2 = body?.count;
69045
+ return typeof n2 === "number" && Number.isFinite(n2) ? n2 : null;
69046
+ } catch {
69047
+ return null;
69048
+ }
69049
+ }
69050
+ /**
69051
+ * Top the pool up against the SERVER's count (#826).
69052
+ *
69053
+ * ⚠️ DO NOT replace this with a periodic call to `_ensureKpPoolPublished()`.
69054
+ * That function early-returns on `_pendingKpBundles.length >= _KP_POOL_TARGET`,
69055
+ * and that array holds PRIVATE bundles which never expire locally — so on an
69056
+ * agent that has been connected longer than the KeyPackage lifetime it is
69057
+ * still full while the server has zero usable. Measured on prod 2026-08-09:
69058
+ * cortina and eclaude were online and green with 10 unconsumed / 0 usable,
69059
+ * unable to accept any new MLS 1:1. A timer over the local guard would have
69060
+ * run forever and changed nothing.
69061
+ *
69062
+ * Publishes only the shortfall, and never throws — this runs on a timer and
69063
+ * must not be able to take the channel down.
69064
+ */
69065
+ async _replenishKpPoolFromServer() {
69066
+ try {
69067
+ if (this._kpPoolFilling) return;
69068
+ const usable = await this._fetchServerKpCount();
69069
+ if (usable === null) return;
69070
+ const shortfall = _SecureChannel._KP_POOL_TARGET - usable;
69071
+ if (shortfall <= 0) return;
69072
+ this._kpPoolFilling = true;
69073
+ try {
69074
+ const identity = new TextEncoder().encode(this._deviceId);
69075
+ let published = 0;
69076
+ for (let i2 = 0; i2 < shortfall; i2++) {
69077
+ const mlsMgr = new MLSGroupManager();
69078
+ const kp = await mlsMgr.generateKeyPackage(identity);
69079
+ const kpHex = Buffer.from(
69080
+ MLSGroupManager.serializeKeyPackage(kp.publicPackage)
69081
+ ).toString("hex");
69082
+ const res = await fetch(`${this.config.apiUrl}/api/v1/mls/key-packages`, {
69083
+ method: "POST",
69084
+ headers: {
69085
+ Authorization: `Bearer ${this._deviceJwt}`,
69086
+ "Content-Type": "application/json"
69087
+ },
69088
+ body: JSON.stringify({ key_package: kpHex })
69089
+ });
69090
+ if (!res.ok) {
69091
+ console.warn(
69092
+ `[SecureChannel] KeyPackage replenish rejected (status ${res.status}) after ${published}/${shortfall}`
69093
+ );
69094
+ break;
69095
+ }
69096
+ this._recordPublishedKp(kp);
69097
+ if (!this._mlsKeyPackage) this._mlsKeyPackage = kp;
69098
+ published++;
69099
+ }
69100
+ if (published > 0) {
69101
+ console.log(
69102
+ `[SecureChannel] MLS KeyPackage pool replenished (${published} new; server had ${usable}/${_SecureChannel._KP_POOL_TARGET} usable)`
69103
+ );
69104
+ }
69105
+ } finally {
69106
+ this._kpPoolFilling = false;
69107
+ }
69108
+ } catch (err) {
69109
+ console.warn("[SecureChannel] KeyPackage replenish failed:", err);
69110
+ }
69111
+ }
68882
69112
  async _handleMlsWelcome(data) {
68883
69113
  const groupId = data.group_id;
68884
69114
  const conversationId = data.conversation_id;
@@ -97472,7 +97702,7 @@ var init_index = __esm({
97472
97702
  init_skill_invoker();
97473
97703
  await init_skill_telemetry();
97474
97704
  await init_policy_enforcer();
97475
- VERSION = true ? "0.23.19" : "0.0.0-dev";
97705
+ VERSION = true ? "0.23.21" : "0.0.0-dev";
97476
97706
  }
97477
97707
  });
97478
97708
  await init_index();