@agentvault/agentvault 0.23.10 → 0.23.12

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
@@ -63912,6 +63912,10 @@ var init_channel = __esm({
63912
63912
  _sessionGroupIds = /* @__PURE__ */ new Map();
63913
63913
  /** Consecutive MLS decrypt failure count per group key for epoch recovery. */
63914
63914
  _mlsDecryptFailCounts = /* @__PURE__ */ new Map();
63915
+ /** #474: per-group consecutive processCommit failures. Mirrors the decrypt
63916
+ * counter above — a commit that fails is divergence, and divergence must
63917
+ * self-heal rather than be logged and dropped. */
63918
+ _mlsCommitFailCounts = /* @__PURE__ */ new Map();
63915
63919
  static MAX_MLS_DECRYPT_FAILS = 3;
63916
63920
  /** Cached MLS KeyPackage bundle for this device (regenerated on each connect). */
63917
63921
  _mlsKeyPackage = null;
@@ -64792,7 +64796,7 @@ var init_channel = __esm({
64792
64796
  */
64793
64797
  sendActivitySpan(spanData) {
64794
64798
  if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
64795
- const pluginVersion = true ? "0.23.10" : "0.0.0-dev";
64799
+ const pluginVersion = true ? "0.23.12" : "0.0.0-dev";
64796
64800
  const agentName = this.config.agentName ?? "Agent";
64797
64801
  const resource = {
64798
64802
  "service.name": "agentvault-agent",
@@ -64826,7 +64830,16 @@ var init_channel = __esm({
64826
64830
  this._ws.send(
64827
64831
  JSON.stringify({
64828
64832
  event: "worker_heartbeat",
64829
- data: { worker_capable: h2.workerCapable, armed_rooms: h2.armedRooms }
64833
+ data: {
64834
+ worker_capable: h2.workerCapable,
64835
+ armed_rooms: h2.armedRooms,
64836
+ // #20: OPTIONAL on the way in so an older bridge against a newer
64837
+ // plugin still emits a shape the backend can read. `=== true` means
64838
+ // absent/malformed becomes false — the fail-closed direction, and it
64839
+ // never emits `undefined` (which would drop the key entirely).
64840
+ host_trusted: h2.hostTrusted === true,
64841
+ gates_effective: h2.gatesEffective === true
64842
+ }
64830
64843
  })
64831
64844
  );
64832
64845
  }
@@ -66522,7 +66535,7 @@ var init_channel = __esm({
66522
66535
  agentVersion: this.config.agentVersion ?? "0.0.0",
66523
66536
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
66524
66537
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
66525
- pluginVersion: true ? "0.23.10" : "0.0.0-dev"
66538
+ pluginVersion: true ? "0.23.12" : "0.0.0-dev"
66526
66539
  });
66527
66540
  this._telemetryReporter.startAutoFlush(3e4);
66528
66541
  }
@@ -66840,7 +66853,7 @@ var init_channel = __esm({
66840
66853
  agentVersion: this.config.agentVersion ?? "0.0.0",
66841
66854
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
66842
66855
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
66843
- pluginVersion: true ? "0.23.10" : "0.0.0-dev"
66856
+ pluginVersion: true ? "0.23.12" : "0.0.0-dev"
66844
66857
  });
66845
66858
  this._telemetryReporter.startAutoFlush(3e4);
66846
66859
  }
@@ -68236,14 +68249,9 @@ ${messageText}`;
68236
68249
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
68237
68250
  console.log(`[SecureChannel] MLS commit processed for room ${roomId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
68238
68251
  } catch (err) {
68239
- const isOldEpoch = err?.message?.includes("epoch") || err?.message?.includes("old") || err?.message?.includes("Validation");
68240
- if (isOldEpoch) {
68241
- console.log(`[SecureChannel] Discarding old-epoch commit for room ${roomId.slice(0, 8)}`);
68242
- } else {
68243
- console.error(`[SecureChannel] MLS commit failed for room ${roomId.slice(0, 8)}:`, err);
68244
- throw err;
68245
- }
68252
+ await this._onCommitFailure(roomId, groupId, err, `room ${roomId.slice(0, 8)}`);
68246
68253
  }
68254
+ this._mlsCommitFailCounts.delete(roomId);
68247
68255
  } else {
68248
68256
  this._bufferMlsCommit(groupId, epoch, data);
68249
68257
  console.log(`[SecureChannel] Buffered MLS commit for room ${roomId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
@@ -68261,14 +68269,9 @@ ${messageText}`;
68261
68269
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
68262
68270
  console.log(`[SecureChannel] MLS commit processed for A2A ${chId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
68263
68271
  } catch (err) {
68264
- const isOldEpoch = err?.message?.includes("epoch") || err?.message?.includes("old") || err?.message?.includes("Validation");
68265
- if (isOldEpoch) {
68266
- console.log(`[SecureChannel] Discarding old-epoch commit for A2A ${chId.slice(0, 8)}`);
68267
- } else {
68268
- console.error(`[SecureChannel] MLS commit failed for A2A ${chId.slice(0, 8)}:`, err);
68269
- throw err;
68270
- }
68272
+ await this._onCommitFailure(`a2a:${chId}`, groupId, err, `A2A ${chId.slice(0, 8)}`);
68271
68273
  }
68274
+ this._mlsCommitFailCounts.delete(`a2a:${chId}`);
68272
68275
  } else {
68273
68276
  this._bufferMlsCommit(groupId, epoch, data);
68274
68277
  console.log(`[SecureChannel] Buffered MLS commit for A2A ${chId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
@@ -68325,6 +68328,52 @@ ${messageText}`;
68325
68328
  * sends us one. Deduped to a 30s window per group so a burst of undecryptable
68326
68329
  * rows doesn't spam request-welcome. Fire-and-forget.
68327
68330
  */
68331
+ /**
68332
+ * #474 — a failed `processCommit` is DIVERGENCE, and divergence must
68333
+ * self-heal. It previously did neither: the room stayed broken forever.
68334
+ *
68335
+ * Two swallow paths existed, and only the second was ever reported:
68336
+ *
68337
+ * 1. `isOldEpoch` substring-matched the error text for "epoch" / "old" /
68338
+ * "Validation" and discarded deliberately. `ValidationError: Desired gen
68339
+ * in the past` contains "Validation", so the LOUDEST production failure
68340
+ * was filed as benign — 1,439 occurrences on one agent, all thrown away
68341
+ * by a string match.
68342
+ * 2. Everything else re-threw to the WS dispatcher, which logs and drops.
68343
+ *
68344
+ * Result: ~1,600 commit failures across 13 rooms and ZERO resyncs. Classifying
68345
+ * divergence by grepping an error message is the root of it — error text is
68346
+ * not a protocol, and the one case that must never be silent was the one that
68347
+ * happened to contain the magic word.
68348
+ *
68349
+ * So: no classification. COUNT, and after MAX_MLS_DECRYPT_FAILS consecutive
68350
+ * failures run the same ladder the decrypt path has used since #440 — replay
68351
+ * the missed commits via /sync, and fall back to re-join only if that cannot
68352
+ * catch up. Counting rather than resyncing on first failure matters: a
68353
+ * genuinely stale commit is normal, and resyncing on each one would have
68354
+ * fired ~1,600 Welcome requests.
68355
+ *
68356
+ * Never re-throws. The caller's only handler logs and drops, so throwing here
68357
+ * is indistinguishable from swallowing — it just moves the swallow one frame up.
68358
+ */
68359
+ async _onCommitFailure(groupKey, mlsGroupId, err, label) {
68360
+ const count = (this._mlsCommitFailCounts.get(groupKey) ?? 0) + 1;
68361
+ this._mlsCommitFailCounts.set(groupKey, count);
68362
+ console.error(
68363
+ `[SecureChannel] MLS commit failed (${count}/${_SecureChannel.MAX_MLS_DECRYPT_FAILS}) for ${label}:`,
68364
+ err
68365
+ );
68366
+ if (count < _SecureChannel.MAX_MLS_DECRYPT_FAILS) return;
68367
+ console.warn(`[SecureChannel] MLS commit resync triggered for ${label} \u2014 ${count} consecutive failures`);
68368
+ try {
68369
+ if (!await this._resyncOnDivergence(groupKey, mlsGroupId)) {
68370
+ await this._triggerMlsEpochRecovery(groupKey, mlsGroupId);
68371
+ }
68372
+ this._mlsCommitFailCounts.delete(groupKey);
68373
+ } catch (recoverErr) {
68374
+ console.error(`[SecureChannel] MLS commit resync FAILED for ${label}:`, recoverErr);
68375
+ }
68376
+ }
68328
68377
  async _requestWelcomeSelfHeal(groupId) {
68329
68378
  const now = Date.now();
68330
68379
  const last = this._welcomeSelfHealInflight.get(groupId) ?? 0;
@@ -69126,9 +69175,43 @@ ${messageText}`;
69126
69175
  const errMsg = addErr instanceof Error ? addErr.message : String(addErr);
69127
69176
  if (errMsg.includes("already in the group")) {
69128
69177
  console.log(`[SecureChannel] A2A member already in group \u2014 removing and re-adding ${req.requesting_device_id.slice(0, 8)}`);
69178
+ const preRemoveState = JSON.stringify(mlsGroup.exportState());
69179
+ let removeCommitPublished = false;
69129
69180
  try {
69130
69181
  const identityBytes = new TextEncoder().encode(req.requesting_device_id);
69131
- await mlsGroup.removeMember(identityBytes);
69182
+ const leafIndex = mlsGroup.memberLeafIndex(identityBytes);
69183
+ if (leafIndex === null) {
69184
+ console.log(`[SecureChannel] A2A re-add: no leaf index for ${req.requesting_device_id.slice(0, 8)} \u2014 cannot remove`);
69185
+ continue;
69186
+ }
69187
+ const removeResult = await mlsGroup.removeMember(leafIndex);
69188
+ try {
69189
+ const removeResp = await fetch(
69190
+ `${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
69191
+ {
69192
+ method: "POST",
69193
+ headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
69194
+ body: JSON.stringify({
69195
+ group_id: chState.mlsGroupId,
69196
+ device_id: this._deviceId,
69197
+ commit: Buffer.from(removeResult.commit).toString("hex"),
69198
+ commit_epoch: Number(mlsGroup.epoch)
69199
+ })
69200
+ }
69201
+ );
69202
+ if (removeResp.ok) {
69203
+ removeCommitPublished = true;
69204
+ } else {
69205
+ const detail = await removeResp.text().catch(() => "");
69206
+ console.warn(`[SecureChannel] A2A remove-commit distribute failed (${removeResp.status}): ${detail}`);
69207
+ }
69208
+ } catch (distErr) {
69209
+ console.warn(`[SecureChannel] A2A remove-commit distribute network error:`, distErr);
69210
+ }
69211
+ if (!removeCommitPublished) {
69212
+ mlsGroup.importState(JSON.parse(preRemoveState));
69213
+ continue;
69214
+ }
69132
69215
  const kpRes2 = await fetch(
69133
69216
  `${this.config.apiUrl}/api/v1/mls/key-packages/batch?device_ids=${req.requesting_device_id}`,
69134
69217
  { headers: { Authorization: `Bearer ${this._deviceJwt}` } }
@@ -69152,6 +69235,12 @@ ${messageText}`;
69152
69235
  }
69153
69236
  } catch (readdErr) {
69154
69237
  console.log(`[SecureChannel] A2A remove+re-add FAILED: ${readdErr instanceof Error ? readdErr.message : String(readdErr)}`);
69238
+ if (!removeCommitPublished) {
69239
+ try {
69240
+ mlsGroup.importState(JSON.parse(preRemoveState));
69241
+ } catch {
69242
+ }
69243
+ }
69155
69244
  continue;
69156
69245
  }
69157
69246
  } else {
@@ -97055,7 +97144,7 @@ var init_index = __esm({
97055
97144
  init_skill_invoker();
97056
97145
  await init_skill_telemetry();
97057
97146
  await init_policy_enforcer();
97058
- VERSION = true ? "0.23.10" : "0.0.0-dev";
97147
+ VERSION = true ? "0.23.12" : "0.0.0-dev";
97059
97148
  }
97060
97149
  });
97061
97150
  await init_index();