@agentvault/muse-bridge 0.1.32 → 0.1.34

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
@@ -92,6 +92,22 @@ export interface ReplyChannel {
92
92
  groupId?: string;
93
93
  }): Promise<T>;
94
94
  }
95
+ /**
96
+ * "Recent problems" (2026-10-26): every say delivery is one
97
+ * `ai.agent.tool.invoke` span (tool `say`) on the channel's existing telemetry
98
+ * pipe. Only the outcome, a machine error code, the duration and the
99
+ * conversation it answers — never the text or the error message. Never throws.
100
+ */
101
+ /**
102
+ * #1467: a Muse turn failed. One `ai.agent.tool.invoke` error span (tool
103
+ * `model_turn`): `model_rate_limited` when Meta answered 429, else
104
+ * `model_error`. No conversation and no text. Never throws.
105
+ */
106
+ export declare function reportTurnFailure(channel: {
107
+ telemetry?: ToolSpanSink | null;
108
+ } | undefined, failure: {
109
+ upstreamStatus?: number;
110
+ }): void;
95
111
  export declare class ActiveTarget {
96
112
  private target;
97
113
  setRoom(roomId: string): void;
package/dist/index.js CHANGED
@@ -11799,11 +11799,11 @@ async function joinWithFirstMatchingKp(welcomeBytes, candidates) {
11799
11799
  return null;
11800
11800
  }
11801
11801
  function ownIdentity() {
11802
- const v2 = true ? "0.23.60" : FALLBACK;
11802
+ const v2 = true ? "0.23.61" : FALLBACK;
11803
11803
  return `${PACKAGE}@${v2}`;
11804
11804
  }
11805
11805
  function buildSha() {
11806
- const raw = true ? "9599d8e" : "";
11806
+ const raw = true ? "129e537" : "";
11807
11807
  const cleaned = (raw ?? "").trim();
11808
11808
  return !cleaned || cleaned === "unknown" ? null : cleaned;
11809
11809
  }
@@ -12658,6 +12658,10 @@ function buildToolInvokeSpan(report) {
12658
12658
  "tool.duration_ms": durationMs
12659
12659
  };
12660
12660
  if (isError) attributes["tool.error_kind"] = safeErrorKind(report.errorKind);
12661
+ const resets = report.limitResetsAt;
12662
+ if (attributes["tool.error_kind"] === "plan_limit_reached" && typeof resets === "number" && Number.isInteger(resets) && resets > 0 && resets < 1e11) {
12663
+ attributes["av.limit_resets_at"] = resets;
12664
+ }
12661
12665
  if (kind !== "unknown" && typeof conv.id === "string" && conv.id) {
12662
12666
  attributes["av.conversation_id"] = conv.id.slice(0, 64);
12663
12667
  }
@@ -71487,6 +71491,11 @@ var init_dist = __esm({
71487
71491
  "ambiguous_target",
71488
71492
  "no_receipt",
71489
71493
  "invalid_request",
71494
+ // #1467: the owner's own AI plan refused the turn (a bridge on a subscription).
71495
+ "plan_limit_reached",
71496
+ // #1467: a Muse turn failed — Meta answered 429, or anything else.
71497
+ "model_rate_limited",
71498
+ "model_error",
71490
71499
  UNCLASSIFIED_ERROR
71491
71500
  ];
71492
71501
  KNOWN_CODES = new Set(KNOWN_TOOL_ERROR_CODES);
@@ -71698,6 +71707,17 @@ var init_dist = __esm({
71698
71707
  * self-heal rather than be logged and dropped. */
71699
71708
  _mlsCommitFailCounts = /* @__PURE__ */ new Map();
71700
71709
  static MAX_MLS_DECRYPT_FAILS = 3;
71710
+ /**
71711
+ * #1518: ids of MLS application frames this process has already decrypted
71712
+ * (server-assigned `mls_messages.id`, the same on the live delivery pull and
71713
+ * on a `/sync` / reconnect replay). A replayed frame cannot be decrypted a
71714
+ * second time — forward secrecy deleted its key — so without this every
71715
+ * reconnect after a backend deploy fed already-read messages into the
71716
+ * decrypt-failure counter and re-admitted the agent (+2 epochs). Insertion
71717
+ * ordered; the oldest ids fall out past DECRYPTED_ID_CAP.
71718
+ */
71719
+ _decryptedMessageIds = /* @__PURE__ */ new Map();
71720
+ static DECRYPTED_ID_CAP = 5e3;
71701
71721
  /**
71702
71722
  * #1084: consecutive resyncs that found NOTHING to apply, per group.
71703
71723
  *
@@ -73903,7 +73923,7 @@ ${text}`,
73903
73923
  */
73904
73924
  sendActivitySpan(spanData) {
73905
73925
  if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
73906
- const pluginVersion = true ? "0.23.60" : "0.0.0-dev";
73926
+ const pluginVersion = true ? "0.23.61" : "0.0.0-dev";
73907
73927
  const agentName = this.config.agentName ?? "Agent";
73908
73928
  const resource = {
73909
73929
  "service.name": "agentvault-agent",
@@ -76113,7 +76133,7 @@ ${text}`,
76113
76133
  agentVersion: this.config.agentVersion ?? "0.0.0",
76114
76134
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
76115
76135
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
76116
- pluginVersion: true ? "0.23.60" : "0.0.0-dev"
76136
+ pluginVersion: true ? "0.23.61" : "0.0.0-dev"
76117
76137
  });
76118
76138
  this._telemetryReporter.startAutoFlush(3e4);
76119
76139
  }
@@ -76445,7 +76465,7 @@ ${text}`,
76445
76465
  agentVersion: this.config.agentVersion ?? "0.0.0",
76446
76466
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
76447
76467
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
76448
- pluginVersion: true ? "0.23.60" : "0.0.0-dev"
76468
+ pluginVersion: true ? "0.23.61" : "0.0.0-dev"
76449
76469
  });
76450
76470
  this._telemetryReporter.startAutoFlush(3e4);
76451
76471
  }
@@ -76881,9 +76901,14 @@ ${text}`,
76881
76901
  if (groupId) void this._requestWelcomeSelfHeal(groupId);
76882
76902
  return;
76883
76903
  }
76904
+ if (this._alreadyDecrypted(data.message_id)) {
76905
+ console.debug(`[SecureChannel] MLS 1:1 frame ${String(data.message_id).slice(0, 8)} already read \u2014 replay skipped`);
76906
+ return;
76907
+ }
76884
76908
  try {
76885
76909
  const cipherBytes = new Uint8Array(Buffer.from(data.payload, "hex"));
76886
76910
  const result = await mgr.decrypt(cipherBytes);
76911
+ this._rememberDecrypted(data.message_id);
76887
76912
  if (mgrKey) this._mlsDecryptFailCounts.delete(mgrKey);
76888
76913
  const mlsGroupId = (convGroupId ? this._persisted?.mlsGroups?.[convGroupId]?.mlsGroupId : null) ?? this._persisted?.mlsConversations?.[convId]?.mlsGroupId ?? groupId;
76889
76914
  if (mlsGroupId) {
@@ -77906,9 +77931,14 @@ ${baseText}`;
77906
77931
  }
77907
77932
  return;
77908
77933
  }
77934
+ if (this._alreadyDecrypted(data.message_id)) {
77935
+ console.debug(`[SecureChannel] MLS room frame ${String(data.message_id).slice(0, 8)} already read \u2014 replay skipped`);
77936
+ return;
77937
+ }
77909
77938
  try {
77910
77939
  const ciphertext = Buffer.from(data.payload, "hex");
77911
77940
  const result = await mlsGroup.decrypt(new Uint8Array(ciphertext));
77941
+ this._rememberDecrypted(data.message_id);
77912
77942
  this._mlsDecryptFailCounts.delete(resolvedRoomId);
77913
77943
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
77914
77944
  if (result.isHandshake || !result.plaintext) {
@@ -79086,16 +79116,57 @@ ${baseText}`;
79086
79116
  }
79087
79117
  return await mlsGroup.encrypt(plaintextBytes);
79088
79118
  }
79119
+ /** #1518: is the server answering at all? Bounded; any failure is "no". */
79120
+ async _serverReachable() {
79121
+ try {
79122
+ const res = await fetch(`${this.config.apiUrl}/health`, { signal: AbortSignal.timeout(5e3) });
79123
+ return res.ok;
79124
+ } catch {
79125
+ return false;
79126
+ }
79127
+ }
79128
+ /** #1518: the server is down — don't re-key (that needs the same server).
79129
+ * Clear the run so the next failures ask again. Reported as handled. */
79130
+ _deferResync(groupKey, groupId, why) {
79131
+ this._mlsDecryptFailCounts.delete(groupKey);
79132
+ console.warn(
79133
+ `[SecureChannel] Resync for ${groupId.slice(0, 8)}: server unreachable (${why}) \u2014 NOT re-keying; will retry (#1518)`
79134
+ );
79135
+ return true;
79136
+ }
79137
+ /** #1518: has this process already decrypted the frame with this id? */
79138
+ _alreadyDecrypted(messageId) {
79139
+ return typeof messageId === "string" && messageId !== "" && this._decryptedMessageIds.has(messageId);
79140
+ }
79141
+ /** #1518: remember a decrypted frame's id (bounded; oldest dropped). */
79142
+ _rememberDecrypted(messageId) {
79143
+ if (typeof messageId !== "string" || messageId === "") return;
79144
+ this._decryptedMessageIds.set(messageId, true);
79145
+ if (this._decryptedMessageIds.size > _SecureChannel.DECRYPTED_ID_CAP) {
79146
+ const oldest = this._decryptedMessageIds.keys().next().value;
79147
+ if (oldest !== void 0) this._decryptedMessageIds.delete(oldest);
79148
+ }
79149
+ }
79089
79150
  async _resyncOnDivergence(groupKey, groupId) {
79090
79151
  const mgr = this._mlsGroups.get(groupKey);
79091
79152
  if (!mgr?.isInitialized) return false;
79092
79153
  const fromEpoch = Number(mgr.epoch);
79093
79154
  try {
79094
- const res = await fetch(
79095
- `${this.config.apiUrl}/api/v1/mls/groups/${groupId}/sync?last_epoch=${fromEpoch}`,
79096
- { headers: { Authorization: `Bearer ${this._deviceJwt}` } }
79097
- );
79155
+ let res;
79156
+ try {
79157
+ res = await fetch(
79158
+ `${this.config.apiUrl}/api/v1/mls/groups/${groupId}/sync?last_epoch=${fromEpoch}`,
79159
+ { headers: { Authorization: `Bearer ${this._deviceJwt}` } }
79160
+ );
79161
+ } catch (netErr) {
79162
+ if (!await this._serverReachable()) return this._deferResync(groupKey, groupId, String(netErr));
79163
+ console.warn(`[SecureChannel] Resync-on-divergence failed for ${groupId.slice(0, 8)}:`, netErr);
79164
+ return false;
79165
+ }
79098
79166
  if (!res.ok) {
79167
+ if ((res.status >= 500 || res.status === 429) && !await this._serverReachable()) {
79168
+ return this._deferResync(groupKey, groupId, `HTTP ${res.status}`);
79169
+ }
79099
79170
  console.warn(`[SecureChannel] Resync /sync failed for ${groupId.slice(0, 8)}: ${res.status}`);
79100
79171
  return false;
79101
79172
  }
@@ -80210,9 +80281,14 @@ ${baseText}`;
80210
80281
  console.warn(`[SecureChannel] MLS group not loaded for A2A ${a2aChannelId.slice(0, 8)}`);
80211
80282
  return;
80212
80283
  }
80284
+ if (this._alreadyDecrypted(data.message_id)) {
80285
+ console.debug(`[SecureChannel] MLS A2A frame ${String(data.message_id).slice(0, 8)} already read \u2014 replay skipped`);
80286
+ return;
80287
+ }
80213
80288
  try {
80214
80289
  const ciphertext = new Uint8Array(Buffer.from(data.payload, "hex"));
80215
80290
  const result = await mlsGroup.decrypt(ciphertext);
80291
+ this._rememberDecrypted(data.message_id);
80216
80292
  this._mlsDecryptFailCounts.delete(`a2a:${a2aChannelId}`);
80217
80293
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
80218
80294
  if (result.isHandshake || !result.plaintext) {
@@ -102863,7 +102939,7 @@ data:
102863
102939
  await init_policy_enforcer();
102864
102940
  init_room_protocol();
102865
102941
  init_tool_telemetry();
102866
- VERSION = true ? "0.23.60" : "0.0.0-dev";
102942
+ VERSION = true ? "0.23.61" : "0.0.0-dev";
102867
102943
  }
102868
102944
  });
102869
102945
  await init_index();
@@ -103567,6 +103643,17 @@ import { chmodSync as chmodSync5, mkdirSync as mkdirSync6 } from "node:fs";
103567
103643
  import { join as join17 } from "node:path";
103568
103644
 
103569
103645
  // src/muse-session.ts
103646
+ var ERROR_TYPE = /\(([a-z][a-z_]{2,40})\)\s*$/;
103647
+ function describeTurnFailure(err) {
103648
+ const message = err instanceof Error ? err.message : "";
103649
+ if (message === "muse turn stopped") return null;
103650
+ const status = err?.upstreamStatus;
103651
+ const type = ERROR_TYPE.exec(message.slice(-200))?.[1];
103652
+ return {
103653
+ ...typeof status === "number" && Number.isInteger(status) && status >= 400 && status <= 599 ? { upstreamStatus: status } : {},
103654
+ ...type ? { errorType: type } : {}
103655
+ };
103656
+ }
103570
103657
  var MuseSession = class {
103571
103658
  constructor(driver, opts = {}) {
103572
103659
  this.driver = driver;
@@ -103638,8 +103725,17 @@ var MuseSession = class {
103638
103725
  ${item.text}` : item.text;
103639
103726
  const result = await this.driver.runTurn(prompt, { ownerTurn: item.ownerTurn });
103640
103727
  await this.maybeFallback(item, result);
103641
- } catch {
103642
- this.log("turn failed reason=driver");
103728
+ } catch (err) {
103729
+ const failure2 = describeTurnFailure(err);
103730
+ if (failure2) {
103731
+ this.log(`turn failed reason=driver${failure2.upstreamStatus ? ` upstream_status=${failure2.upstreamStatus}` : ""}${failure2.errorType ? ` error_type=${failure2.errorType}` : ""}`);
103732
+ try {
103733
+ this.opts.onTurnFailed?.(failure2);
103734
+ } catch {
103735
+ }
103736
+ } else {
103737
+ this.log("turn failed reason=stopped");
103738
+ }
103643
103739
  await this.failureNotice(item);
103644
103740
  } finally {
103645
103741
  this.current = null;
@@ -103767,6 +103863,16 @@ var META_RESPONSES_URL = "https://api.meta.ai/v1/responses";
103767
103863
  var BODY_LIMIT = 16 * 1024 * 1024;
103768
103864
  var ERROR_LIMIT = 64 * 1024;
103769
103865
  var HOP_HEADERS = ["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade"];
103866
+ var LIMIT_HEADER = /^(?:retry-after|x-ratelimit-[a-z0-9-]{1,40}|ratelimit(?:-[a-z0-9-]{1,40})?)$/;
103867
+ var LIMIT_VALUE = /^[0-9][0-9.smh]{0,15}$/;
103868
+ function limitHeaderNote(headers) {
103869
+ const parts = [];
103870
+ headers.forEach((value, name) => {
103871
+ if (parts.length >= 8 || !LIMIT_HEADER.test(name)) return;
103872
+ parts.push(`${name}=${LIMIT_VALUE.test(value.trim()) ? value.trim() : "present"}`);
103873
+ });
103874
+ return parts.length ? ` ${parts.join(" ")}` : "";
103875
+ }
103770
103876
  function record2(value) {
103771
103877
  return value !== null && typeof value === "object" && !Array.isArray(value);
103772
103878
  }
@@ -103885,6 +103991,7 @@ async function createMuseProviderAdapter(opts) {
103885
103991
  } catch {
103886
103992
  }
103887
103993
  };
103994
+ let lastStatus;
103888
103995
  const server = createServer2((req, res) => {
103889
103996
  const abort = new AbortController();
103890
103997
  const stop = () => abort.abort();
@@ -103963,6 +104070,10 @@ async function createMuseProviderAdapter(opts) {
103963
104070
  }
103964
104071
  }
103965
104072
  }
104073
+ if (!response.ok) {
104074
+ lastStatus = response.status;
104075
+ log(`upstream_error status=${response.status}${limitHeaderNote(response.headers)}`);
104076
+ }
103966
104077
  await forward(response, res, abort.signal, inspected);
103967
104078
  })().catch(() => {
103968
104079
  log("request_failed reason=transport");
@@ -104004,7 +104115,11 @@ async function createMuseProviderAdapter(opts) {
104004
104115
  await close();
104005
104116
  throw new Error("provider adapter unavailable");
104006
104117
  }
104007
- return { baseUrl: `http://127.0.0.1:${address.port}${route.slice(0, -"/responses".length)}`, close };
104118
+ return {
104119
+ baseUrl: `http://127.0.0.1:${address.port}${route.slice(0, -"/responses".length)}`,
104120
+ close,
104121
+ upstreamStatus: () => lastStatus
104122
+ };
104008
104123
  }
104009
104124
 
104010
104125
  // src/muse-driver.ts
@@ -104101,6 +104216,12 @@ var MuseDriver = class {
104101
104216
  if (this.current?.cancelled) throw new Error("muse turn stopped");
104102
104217
  spawn2.args.splice(2, 0, "--base-url", adapter.baseUrl);
104103
104218
  return await this.execJsonl(spawn2);
104219
+ } catch (err) {
104220
+ const status = adapter.upstreamStatus?.();
104221
+ if (typeof status === "number" && err instanceof Error) {
104222
+ err.upstreamStatus = status;
104223
+ }
104224
+ throw err;
104104
104225
  } finally {
104105
104226
  await adapter.close();
104106
104227
  if (this.adapter === adapter) this.adapter = null;
@@ -104724,6 +104845,18 @@ var RoomHushState = class {
104724
104845
  return t2 !== void 0 && now < t2;
104725
104846
  }
104726
104847
  };
104848
+ function reportTurnFailure(channel, failure2) {
104849
+ try {
104850
+ reportToolInvoke(channel?.telemetry, {
104851
+ toolName: "model_turn",
104852
+ outcome: "error",
104853
+ errorKind: failure2.upstreamStatus === 429 ? "model_rate_limited" : "model_error",
104854
+ durationMs: 0,
104855
+ conversation: { kind: "unknown" }
104856
+ });
104857
+ } catch {
104858
+ }
104859
+ }
104727
104860
  function reportSay(channel, t2, startedAt, failure2) {
104728
104861
  try {
104729
104862
  reportToolInvoke(channel.telemetry, {
@@ -105004,7 +105137,7 @@ function mkdir700(path2) {
105004
105137
  chmodSync5(path2, 448);
105005
105138
  }
105006
105139
  function packageVersion() {
105007
- return true ? "0.1.32" : void 0;
105140
+ return true ? "0.1.34" : void 0;
105008
105141
  }
105009
105142
  function helpText(version2) {
105010
105143
  return [
@@ -105129,7 +105262,13 @@ async function main(opts = {}) {
105129
105262
  spawnFn: opts.spawnFn
105130
105263
  });
105131
105264
  const identity = identityPrompt(cfg.agentName, SAY_TOOL_NAME);
105132
- session = new MuseSession(driver, { log: (m2) => logLine(m2), identity });
105265
+ session = new MuseSession(driver, {
105266
+ log: (m2) => logLine(m2),
105267
+ identity,
105268
+ // #1467: every failed turn reaches Recent problems (and Admin) — `channel`
105269
+ // is read when the failure happens, after it is connected.
105270
+ onTurnFailed: (failure2) => reportTurnFailure(channel, failure2)
105271
+ });
105133
105272
  try {
105134
105273
  await driver.start();
105135
105274
  } catch (err) {
@@ -1,7 +1,10 @@
1
1
  export interface MuseProviderAdapter {
2
2
  baseUrl: string;
3
3
  close(): Promise<void>;
4
+ /** #1467: the last error status Meta returned this turn; undefined if none. */
5
+ upstreamStatus?(): number | undefined;
4
6
  }
7
+ export declare function limitHeaderNote(headers: Headers): string;
5
8
  export interface MuseProviderAdapterOptions {
6
9
  timeoutMs: number;
7
10
  log?: (message: string) => void;
@@ -20,6 +20,16 @@ export interface DriverLike {
20
20
  * cannot redirect a private 1:1 reply, because the newer message only becomes
21
21
  * current when its own turn starts.
22
22
  */
23
+ /** #1467: what is safe to keep about a failed turn — never its message text. */
24
+ export interface TurnFailure {
25
+ /** The error status Meta's API returned this turn, when it returned one. */
26
+ upstreamStatus?: number;
27
+ /** The provider's error type, e.g. "invalid_request_error" — only a short
28
+ * lower-case token in parentheses at the end of Muse's reason. */
29
+ errorType?: string;
30
+ }
31
+ /** A failed turn → what to report, or null when the turn was stopped on purpose. */
32
+ export declare function describeTurnFailure(err: unknown): TurnFailure | null;
23
33
  export declare class MuseSession implements RoomSession {
24
34
  private readonly driver;
25
35
  private readonly opts;
@@ -32,6 +42,8 @@ export declare class MuseSession implements RoomSession {
32
42
  onSay?: (text: string) => Promise<void>;
33
43
  log?: (m: string) => void;
34
44
  identity?: string;
45
+ /** #1467: a turn failed (not stopped). Fire-and-forget; a throw is swallowed. */
46
+ onTurnFailed?: (failure: TurnFailure) => void;
35
47
  });
36
48
  push(text: string, reply?: (text: string) => Promise<void>, opts?: {
37
49
  autoReplyOnText?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/muse-bridge",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "description": "AgentVault Muse Bridge — daemon bridging a Muse agent into E2E-encrypted AgentVault direct messages and rooms.",
6
6
  "main": "dist/index.js",