@agentvault/claude-bridge 0.7.1 → 0.7.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.
Files changed (2) hide show
  1. package/dist/index.js +100 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -64679,6 +64679,10 @@ var init_channel = __esm2({
64679
64679
  _sessionGroupIds = /* @__PURE__ */ new Map();
64680
64680
  /** Consecutive MLS decrypt failure count per group key for epoch recovery. */
64681
64681
  _mlsDecryptFailCounts = /* @__PURE__ */ new Map();
64682
+ /** #474: per-group consecutive processCommit failures. Mirrors the decrypt
64683
+ * counter above — a commit that fails is divergence, and divergence must
64684
+ * self-heal rather than be logged and dropped. */
64685
+ _mlsCommitFailCounts = /* @__PURE__ */ new Map();
64682
64686
  static MAX_MLS_DECRYPT_FAILS = 3;
64683
64687
  /** Cached MLS KeyPackage bundle for this device (regenerated on each connect). */
64684
64688
  _mlsKeyPackage = null;
@@ -65559,7 +65563,7 @@ var init_channel = __esm2({
65559
65563
  */
65560
65564
  sendActivitySpan(spanData) {
65561
65565
  if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
65562
- const pluginVersion = true ? "0.23.11" : "0.0.0-dev";
65566
+ const pluginVersion = true ? "0.23.12" : "0.0.0-dev";
65563
65567
  const agentName = this.config.agentName ?? "Agent";
65564
65568
  const resource = {
65565
65569
  "service.name": "agentvault-agent",
@@ -67298,7 +67302,7 @@ var init_channel = __esm2({
67298
67302
  agentVersion: this.config.agentVersion ?? "0.0.0",
67299
67303
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67300
67304
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67301
- pluginVersion: true ? "0.23.11" : "0.0.0-dev"
67305
+ pluginVersion: true ? "0.23.12" : "0.0.0-dev"
67302
67306
  });
67303
67307
  this._telemetryReporter.startAutoFlush(3e4);
67304
67308
  }
@@ -67616,7 +67620,7 @@ var init_channel = __esm2({
67616
67620
  agentVersion: this.config.agentVersion ?? "0.0.0",
67617
67621
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67618
67622
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67619
- pluginVersion: true ? "0.23.11" : "0.0.0-dev"
67623
+ pluginVersion: true ? "0.23.12" : "0.0.0-dev"
67620
67624
  });
67621
67625
  this._telemetryReporter.startAutoFlush(3e4);
67622
67626
  }
@@ -69012,14 +69016,9 @@ ${messageText}`;
69012
69016
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
69013
69017
  console.log(`[SecureChannel] MLS commit processed for room ${roomId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
69014
69018
  } catch (err) {
69015
- const isOldEpoch = err?.message?.includes("epoch") || err?.message?.includes("old") || err?.message?.includes("Validation");
69016
- if (isOldEpoch) {
69017
- console.log(`[SecureChannel] Discarding old-epoch commit for room ${roomId.slice(0, 8)}`);
69018
- } else {
69019
- console.error(`[SecureChannel] MLS commit failed for room ${roomId.slice(0, 8)}:`, err);
69020
- throw err;
69021
- }
69019
+ await this._onCommitFailure(roomId, groupId, err, `room ${roomId.slice(0, 8)}`);
69022
69020
  }
69021
+ this._mlsCommitFailCounts.delete(roomId);
69023
69022
  } else {
69024
69023
  this._bufferMlsCommit(groupId, epoch, data);
69025
69024
  console.log(`[SecureChannel] Buffered MLS commit for room ${roomId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
@@ -69037,14 +69036,9 @@ ${messageText}`;
69037
69036
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
69038
69037
  console.log(`[SecureChannel] MLS commit processed for A2A ${chId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
69039
69038
  } catch (err) {
69040
- const isOldEpoch = err?.message?.includes("epoch") || err?.message?.includes("old") || err?.message?.includes("Validation");
69041
- if (isOldEpoch) {
69042
- console.log(`[SecureChannel] Discarding old-epoch commit for A2A ${chId.slice(0, 8)}`);
69043
- } else {
69044
- console.error(`[SecureChannel] MLS commit failed for A2A ${chId.slice(0, 8)}:`, err);
69045
- throw err;
69046
- }
69039
+ await this._onCommitFailure(`a2a:${chId}`, groupId, err, `A2A ${chId.slice(0, 8)}`);
69047
69040
  }
69041
+ this._mlsCommitFailCounts.delete(`a2a:${chId}`);
69048
69042
  } else {
69049
69043
  this._bufferMlsCommit(groupId, epoch, data);
69050
69044
  console.log(`[SecureChannel] Buffered MLS commit for A2A ${chId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
@@ -69101,6 +69095,52 @@ ${messageText}`;
69101
69095
  * sends us one. Deduped to a 30s window per group so a burst of undecryptable
69102
69096
  * rows doesn't spam request-welcome. Fire-and-forget.
69103
69097
  */
69098
+ /**
69099
+ * #474 — a failed `processCommit` is DIVERGENCE, and divergence must
69100
+ * self-heal. It previously did neither: the room stayed broken forever.
69101
+ *
69102
+ * Two swallow paths existed, and only the second was ever reported:
69103
+ *
69104
+ * 1. `isOldEpoch` substring-matched the error text for "epoch" / "old" /
69105
+ * "Validation" and discarded deliberately. `ValidationError: Desired gen
69106
+ * in the past` contains "Validation", so the LOUDEST production failure
69107
+ * was filed as benign — 1,439 occurrences on one agent, all thrown away
69108
+ * by a string match.
69109
+ * 2. Everything else re-threw to the WS dispatcher, which logs and drops.
69110
+ *
69111
+ * Result: ~1,600 commit failures across 13 rooms and ZERO resyncs. Classifying
69112
+ * divergence by grepping an error message is the root of it — error text is
69113
+ * not a protocol, and the one case that must never be silent was the one that
69114
+ * happened to contain the magic word.
69115
+ *
69116
+ * So: no classification. COUNT, and after MAX_MLS_DECRYPT_FAILS consecutive
69117
+ * failures run the same ladder the decrypt path has used since #440 — replay
69118
+ * the missed commits via /sync, and fall back to re-join only if that cannot
69119
+ * catch up. Counting rather than resyncing on first failure matters: a
69120
+ * genuinely stale commit is normal, and resyncing on each one would have
69121
+ * fired ~1,600 Welcome requests.
69122
+ *
69123
+ * Never re-throws. The caller's only handler logs and drops, so throwing here
69124
+ * is indistinguishable from swallowing — it just moves the swallow one frame up.
69125
+ */
69126
+ async _onCommitFailure(groupKey, mlsGroupId, err, label) {
69127
+ const count = (this._mlsCommitFailCounts.get(groupKey) ?? 0) + 1;
69128
+ this._mlsCommitFailCounts.set(groupKey, count);
69129
+ console.error(
69130
+ `[SecureChannel] MLS commit failed (${count}/${_SecureChannel.MAX_MLS_DECRYPT_FAILS}) for ${label}:`,
69131
+ err
69132
+ );
69133
+ if (count < _SecureChannel.MAX_MLS_DECRYPT_FAILS) return;
69134
+ console.warn(`[SecureChannel] MLS commit resync triggered for ${label} \u2014 ${count} consecutive failures`);
69135
+ try {
69136
+ if (!await this._resyncOnDivergence(groupKey, mlsGroupId)) {
69137
+ await this._triggerMlsEpochRecovery(groupKey, mlsGroupId);
69138
+ }
69139
+ this._mlsCommitFailCounts.delete(groupKey);
69140
+ } catch (recoverErr) {
69141
+ console.error(`[SecureChannel] MLS commit resync FAILED for ${label}:`, recoverErr);
69142
+ }
69143
+ }
69104
69144
  async _requestWelcomeSelfHeal(groupId) {
69105
69145
  const now = Date.now();
69106
69146
  const last = this._welcomeSelfHealInflight.get(groupId) ?? 0;
@@ -69902,9 +69942,43 @@ ${messageText}`;
69902
69942
  const errMsg = addErr instanceof Error ? addErr.message : String(addErr);
69903
69943
  if (errMsg.includes("already in the group")) {
69904
69944
  console.log(`[SecureChannel] A2A member already in group \u2014 removing and re-adding ${req.requesting_device_id.slice(0, 8)}`);
69945
+ const preRemoveState = JSON.stringify(mlsGroup.exportState());
69946
+ let removeCommitPublished = false;
69905
69947
  try {
69906
69948
  const identityBytes = new TextEncoder().encode(req.requesting_device_id);
69907
- await mlsGroup.removeMember(identityBytes);
69949
+ const leafIndex = mlsGroup.memberLeafIndex(identityBytes);
69950
+ if (leafIndex === null) {
69951
+ console.log(`[SecureChannel] A2A re-add: no leaf index for ${req.requesting_device_id.slice(0, 8)} \u2014 cannot remove`);
69952
+ continue;
69953
+ }
69954
+ const removeResult = await mlsGroup.removeMember(leafIndex);
69955
+ try {
69956
+ const removeResp = await fetch(
69957
+ `${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
69958
+ {
69959
+ method: "POST",
69960
+ headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
69961
+ body: JSON.stringify({
69962
+ group_id: chState.mlsGroupId,
69963
+ device_id: this._deviceId,
69964
+ commit: Buffer.from(removeResult.commit).toString("hex"),
69965
+ commit_epoch: Number(mlsGroup.epoch)
69966
+ })
69967
+ }
69968
+ );
69969
+ if (removeResp.ok) {
69970
+ removeCommitPublished = true;
69971
+ } else {
69972
+ const detail = await removeResp.text().catch(() => "");
69973
+ console.warn(`[SecureChannel] A2A remove-commit distribute failed (${removeResp.status}): ${detail}`);
69974
+ }
69975
+ } catch (distErr) {
69976
+ console.warn(`[SecureChannel] A2A remove-commit distribute network error:`, distErr);
69977
+ }
69978
+ if (!removeCommitPublished) {
69979
+ mlsGroup.importState(JSON.parse(preRemoveState));
69980
+ continue;
69981
+ }
69908
69982
  const kpRes2 = await fetch(
69909
69983
  `${this.config.apiUrl}/api/v1/mls/key-packages/batch?device_ids=${req.requesting_device_id}`,
69910
69984
  { headers: { Authorization: `Bearer ${this._deviceJwt}` } }
@@ -69928,6 +70002,12 @@ ${messageText}`;
69928
70002
  }
69929
70003
  } catch (readdErr) {
69930
70004
  console.log(`[SecureChannel] A2A remove+re-add FAILED: ${readdErr instanceof Error ? readdErr.message : String(readdErr)}`);
70005
+ if (!removeCommitPublished) {
70006
+ try {
70007
+ mlsGroup.importState(JSON.parse(preRemoveState));
70008
+ } catch {
70009
+ }
70010
+ }
69931
70011
  continue;
69932
70012
  }
69933
70013
  } else {
@@ -97395,7 +97475,7 @@ var init_index = __esm2({
97395
97475
  init_skill_invoker();
97396
97476
  await init_skill_telemetry();
97397
97477
  await init_policy_enforcer();
97398
- VERSION = true ? "0.23.11" : "0.0.0-dev";
97478
+ VERSION = true ? "0.23.12" : "0.0.0-dev";
97399
97479
  }
97400
97480
  });
97401
97481
  await init_index();
@@ -133651,7 +133731,7 @@ async function main() {
133651
133731
  "[bridge] warning: passing the invite token on the command line is visible to other local users via 'ps'. Prefer: AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge"
133652
133732
  );
133653
133733
  }
133654
- console.error(`[bridge] version: ${true ? "0.7.1" : "dev"}`);
133734
+ console.error(`[bridge] version: ${true ? "0.7.2" : "dev"}`);
133655
133735
  console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
133656
133736
  console.error(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
133657
133737
  if (cfg.armRoom) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.7.1",
3
+ "version": "0.7.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",