@securecoms/agent-bridge 0.19.0 → 0.21.0

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
@@ -5172,8 +5172,11 @@ var RestMlsBackend = class {
5172
5172
  queue_ids: queueIds
5173
5173
  });
5174
5174
  }
5175
- async nackDelivery(_deviceId, queueIds) {
5176
- await this.post("/api/v1/mls/delivery/nack", { queue_ids: queueIds });
5175
+ async nackDelivery(deviceId, queueIds) {
5176
+ await this.post("/api/v1/mls/delivery/nack", {
5177
+ device_id: deviceId,
5178
+ queue_ids: queueIds
5179
+ });
5177
5180
  }
5178
5181
  async listGroupMessages(deviceId, groupId, opts) {
5179
5182
  const params = new URLSearchParams({ device_id: deviceId, limit: String(opts?.limit ?? 100) });
@@ -5681,7 +5684,7 @@ import { DatabaseSync } from "node:sqlite";
5681
5684
  import { MLSGroupManager as MLSGroupManager3, bytesToHex as bytesToHex4, hexToBytes as hexToBytes3 } from "@agentvault/crypto";
5682
5685
 
5683
5686
  // ../connect-mls-store/dist/schema.js
5684
- var SCHEMA_VERSION = 6;
5687
+ var SCHEMA_VERSION = 7;
5685
5688
  function openSchema(db) {
5686
5689
  db.exec("PRAGMA journal_mode=WAL");
5687
5690
  const row = db.prepare("PRAGMA user_version").get();
@@ -5773,6 +5776,17 @@ function openSchema(db) {
5773
5776
  );
5774
5777
  `);
5775
5778
  }
5779
+ if (row.user_version < 7) {
5780
+ db.exec(`
5781
+ CREATE TABLE IF NOT EXISTS agent_memory_fact_assignment (
5782
+ identity_id TEXT NOT NULL,
5783
+ assignment_key TEXT NOT NULL,
5784
+ conversation_id TEXT NOT NULL,
5785
+ applied_at INTEGER NOT NULL,
5786
+ PRIMARY KEY (identity_id, assignment_key)
5787
+ );
5788
+ `);
5789
+ }
5776
5790
  db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
5777
5791
  }
5778
5792
 
@@ -5812,6 +5826,10 @@ function decodeBundle(bytes) {
5812
5826
  }
5813
5827
  };
5814
5828
  }
5829
+ var DAY_MS = 24 * 60 * 60 * 1e3;
5830
+ var DEFAULT_MEMORY_RETENTION_MS = 30 * DAY_MS;
5831
+ var DEFAULT_MEMORY_FACT_RETENTION_K = 200;
5832
+ var DEFAULT_MEMORY_FACT_RETENTION_MS = 90 * DAY_MS;
5815
5833
  var SqliteMlsStateStore = class {
5816
5834
  db;
5817
5835
  id;
@@ -5825,6 +5843,10 @@ var SqliteMlsStateStore = class {
5825
5843
  bundles = /* @__PURE__ */ new Map();
5826
5844
  lastKpIds = /* @__PURE__ */ new Map();
5827
5845
  memoryRetentionK;
5846
+ memoryRetentionMs;
5847
+ memoryFactRetentionK;
5848
+ memoryFactRetentionMs;
5849
+ memoryNow;
5828
5850
  // M0(b)/F4-1 + C1 + H2: in-memory ONLY for now — the daemon side of FileSafe
5829
5851
  // (this store) isn't wired into the multi-member vault yet (Task 5's capture
5830
5852
  // sites are all connect-web/browser owner paths). Kept here purely so
@@ -5839,6 +5861,10 @@ var SqliteMlsStateStore = class {
5839
5861
  this.id = opts.identityId;
5840
5862
  this.cipher = opts.cipherFor(opts.identityId);
5841
5863
  this.memoryRetentionK = opts.memoryRetentionK ?? 500;
5864
+ this.memoryRetentionMs = opts.memoryRetentionMs ?? DEFAULT_MEMORY_RETENTION_MS;
5865
+ this.memoryFactRetentionK = opts.memoryFactRetentionK ?? DEFAULT_MEMORY_FACT_RETENTION_K;
5866
+ this.memoryFactRetentionMs = opts.memoryFactRetentionMs ?? DEFAULT_MEMORY_FACT_RETENTION_MS;
5867
+ this.memoryNow = opts.memoryNow ?? Date.now;
5842
5868
  try {
5843
5869
  this.db = new DatabaseSync(opts.dbPath);
5844
5870
  } catch (e) {
@@ -6056,23 +6082,24 @@ var SqliteMlsStateStore = class {
6056
6082
  const dedupKey = input.messageId ?? input.idempotencyKey ?? null;
6057
6083
  if (!dedupKey)
6058
6084
  throw new Error("recordMemory requires message_id or idempotency_key");
6085
+ const now = this.memoryNow();
6086
+ this.pruneMemory(input.conversationId, now);
6059
6087
  const sealed = await this.cipher.seal(new TextEncoder().encode(input.text));
6060
6088
  const info = this.db.prepare(`INSERT OR IGNORE INTO agent_memory
6061
6089
  (identity_id, conversation_id, seq, role, peer_id, sealed_text, modality, message_id, idempotency_key, dedup_key, recorded_at)
6062
6090
  VALUES (?, ?,
6063
6091
  (SELECT COALESCE(MAX(seq),0)+1 FROM agent_memory WHERE identity_id=? AND conversation_id=?),
6064
- ?, ?, ?, ?, ?, ?, ?, ?)`).run(this.id, input.conversationId, this.id, input.conversationId, input.role, input.peerId ?? null, sealed, input.modality, input.messageId ?? null, input.idempotencyKey ?? null, dedupKey, Date.now());
6092
+ ?, ?, ?, ?, ?, ?, ?, ?)`).run(this.id, input.conversationId, this.id, input.conversationId, input.role, input.peerId ?? null, sealed, input.modality, input.messageId ?? null, input.idempotencyKey ?? null, dedupKey, now);
6065
6093
  if (info.changes === 0) {
6066
6094
  const row2 = this.db.prepare("SELECT seq FROM agent_memory WHERE identity_id=? AND conversation_id=? AND dedup_key=?").get(this.id, input.conversationId, dedupKey);
6067
6095
  return { seq: row2.seq, deduped: true };
6068
6096
  }
6069
6097
  const row = this.db.prepare("SELECT MAX(seq) AS seq FROM agent_memory WHERE identity_id=? AND conversation_id=?").get(this.id, input.conversationId);
6070
- if (this.memoryRetentionK > 0) {
6071
- this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq <= ?").run(this.id, input.conversationId, row.seq - this.memoryRetentionK);
6072
- }
6098
+ this.pruneMemoryTurns(input.conversationId, now);
6073
6099
  return { seq: row.seq, deduped: false };
6074
6100
  }
6075
6101
  async memoryContext(conversationId, limit = 50) {
6102
+ this.pruneMemory(conversationId, this.memoryNow());
6076
6103
  const rows = this.db.prepare(`SELECT seq, role, peer_id, sealed_text, recorded_at FROM agent_memory
6077
6104
  WHERE identity_id=? AND conversation_id=? ORDER BY seq DESC LIMIT ?`).all(this.id, conversationId, limit);
6078
6105
  const recent = [];
@@ -6087,9 +6114,94 @@ var SqliteMlsStateStore = class {
6087
6114
  return { recent, facts };
6088
6115
  }
6089
6116
  async recordMemoryFact(conversationId, fact) {
6117
+ const now = this.memoryNow();
6090
6118
  const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
6091
6119
  this.db.prepare(`INSERT INTO agent_memory_fact (identity_id, conversation_id, fact_id, sealed_fact, added_at)
6092
- VALUES (?, ?, (SELECT COALESCE(MAX(fact_id),0)+1 FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?), ?, ?)`).run(this.id, conversationId, this.id, conversationId, sealed, Date.now());
6120
+ VALUES (?, ?, (SELECT COALESCE(MAX(fact_id),0)+1 FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?), ?, ?)`).run(this.id, conversationId, this.id, conversationId, sealed, now);
6121
+ this.pruneMemoryFacts(conversationId, now);
6122
+ }
6123
+ /**
6124
+ * Import one owner-approved legacy fact exactly once. `assignmentKey` is
6125
+ * content-free (`source-id:line-hash`) and intentionally excludes the target
6126
+ * channel: the same legacy fact can therefore never be replayed into a
6127
+ * second audience. The ledger and sealed fact commit atomically.
6128
+ */
6129
+ async recordImportedMemoryFact(conversationId, fact, assignmentKey) {
6130
+ if (!/^[0-9a-f]{64}:[0-9a-f]{64}$/.test(assignmentKey)) {
6131
+ throw new Error("invalid legacy memory assignment key");
6132
+ }
6133
+ const existing = this.db.prepare(`SELECT conversation_id FROM agent_memory_fact_assignment
6134
+ WHERE identity_id=? AND assignment_key=?`).get(this.id, assignmentKey);
6135
+ if (existing) {
6136
+ if (existing.conversation_id !== conversationId) {
6137
+ throw new Error("legacy memory fact is already assigned to another channel");
6138
+ }
6139
+ return { factId: 0, deduped: true };
6140
+ }
6141
+ const now = this.memoryNow();
6142
+ const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
6143
+ this.db.exec("BEGIN IMMEDIATE");
6144
+ try {
6145
+ const raced = this.db.prepare(`SELECT conversation_id FROM agent_memory_fact_assignment
6146
+ WHERE identity_id=? AND assignment_key=?`).get(this.id, assignmentKey);
6147
+ if (raced) {
6148
+ if (raced.conversation_id !== conversationId) {
6149
+ throw new Error("legacy memory fact is already assigned to another channel");
6150
+ }
6151
+ this.db.exec("COMMIT");
6152
+ return { factId: 0, deduped: true };
6153
+ }
6154
+ const row = this.db.prepare("SELECT COALESCE(MAX(fact_id),0)+1 AS fact_id FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?").get(this.id, conversationId);
6155
+ this.db.prepare(`INSERT INTO agent_memory_fact (identity_id, conversation_id, fact_id, sealed_fact, added_at)
6156
+ VALUES (?, ?, ?, ?, ?)`).run(this.id, conversationId, row.fact_id, sealed, now);
6157
+ this.db.prepare(`INSERT INTO agent_memory_fact_assignment (identity_id, assignment_key, conversation_id, applied_at)
6158
+ VALUES (?, ?, ?, ?)`).run(this.id, assignmentKey, conversationId, now);
6159
+ this.pruneMemoryFacts(conversationId, now);
6160
+ this.db.exec("COMMIT");
6161
+ return { factId: row.fact_id, deduped: false };
6162
+ } catch (error) {
6163
+ this.db.exec("ROLLBACK");
6164
+ throw error;
6165
+ }
6166
+ }
6167
+ /** Remove every durable turn and fact in one channel-scoped namespace. */
6168
+ purgeMemory(conversationId) {
6169
+ this.db.exec("BEGIN IMMEDIATE");
6170
+ try {
6171
+ this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
6172
+ this.db.prepare("DELETE FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
6173
+ this.db.prepare("DELETE FROM agent_memory_fact_assignment WHERE identity_id=? AND conversation_id=?").run(this.id, conversationId);
6174
+ this.db.exec("COMMIT");
6175
+ } catch (error) {
6176
+ this.db.exec("ROLLBACK");
6177
+ throw error;
6178
+ }
6179
+ }
6180
+ pruneMemory(conversationId, now) {
6181
+ this.pruneMemoryTurns(conversationId, now);
6182
+ this.pruneMemoryFacts(conversationId, now);
6183
+ }
6184
+ pruneMemoryTurns(conversationId, now) {
6185
+ if (this.memoryRetentionMs > 0) {
6186
+ this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND recorded_at < ?").run(this.id, conversationId, now - this.memoryRetentionMs);
6187
+ }
6188
+ if (this.memoryRetentionK > 0) {
6189
+ const row = this.db.prepare("SELECT MAX(seq) AS seq FROM agent_memory WHERE identity_id=? AND conversation_id=?").get(this.id, conversationId);
6190
+ if (row.seq != null) {
6191
+ this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq <= ?").run(this.id, conversationId, row.seq - this.memoryRetentionK);
6192
+ }
6193
+ }
6194
+ }
6195
+ pruneMemoryFacts(conversationId, now) {
6196
+ if (this.memoryFactRetentionMs > 0) {
6197
+ this.db.prepare("DELETE FROM agent_memory_fact WHERE identity_id=? AND conversation_id=? AND added_at < ?").run(this.id, conversationId, now - this.memoryFactRetentionMs);
6198
+ }
6199
+ if (this.memoryFactRetentionK > 0) {
6200
+ const row = this.db.prepare("SELECT MAX(fact_id) AS fact_id FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?").get(this.id, conversationId);
6201
+ if (row.fact_id != null) {
6202
+ this.db.prepare("DELETE FROM agent_memory_fact WHERE identity_id=? AND conversation_id=? AND fact_id <= ?").run(this.id, conversationId, row.fact_id - this.memoryFactRetentionK);
6203
+ }
6204
+ }
6093
6205
  }
6094
6206
  /** @internal test-only */
6095
6207
  _rawMainForTest(identityId, channelId) {
@@ -7316,6 +7428,7 @@ import { createHash, randomUUID } from "node:crypto";
7316
7428
  import {
7317
7429
  chmodSync,
7318
7430
  existsSync,
7431
+ lstatSync,
7319
7432
  mkdirSync,
7320
7433
  readFileSync as readFileSync5,
7321
7434
  renameSync,
@@ -7323,7 +7436,7 @@ import {
7323
7436
  unlinkSync,
7324
7437
  writeFileSync
7325
7438
  } from "node:fs";
7326
- import { basename as basename2, join as join6 } from "node:path";
7439
+ import { basename as basename2, isAbsolute, join as join6 } from "node:path";
7327
7440
  var MEMORY_FILE = "MEMORY.md";
7328
7441
  var REAL_FS = {
7329
7442
  mkdir: (p, mode) => mkdirSync(p, { recursive: true, mode }),
@@ -7398,6 +7511,54 @@ function lineHash(line) {
7398
7511
  return createHash("sha256").update(line.trim(), "utf8").digest("hex");
7399
7512
  }
7400
7513
  var LOCAL_REPORT_FILE = "local-config.json";
7514
+ var LEGACY_MEMORY_SOURCE_MAX_BYTES = 4 * 1024 * 1024;
7515
+ var LEGACY_MEMORY_REPORT_MAX_BYTES = 4 * 1024 * 1024;
7516
+ var LEGACY_MEMORY_REPORT_MAX_SOURCES = 100;
7517
+ var LEGACY_MEMORY_FACT_MAX_BYTES = 1024;
7518
+ var LEGACY_MEMORY_FACT_DECISION_MAX = 500;
7519
+ function legacySourceId(source, bytes) {
7520
+ return createHash("sha256").update(source.kind).update("\0").update(source.channel_id ?? "").update("\0").update(source.path).update("\0").update(bytes).digest("hex");
7521
+ }
7522
+ function localLegacySources(doc) {
7523
+ if (!Array.isArray(doc.legacy_memory)) return [];
7524
+ const out = [];
7525
+ let contentBudget = LEGACY_MEMORY_REPORT_MAX_BYTES;
7526
+ for (const raw of doc.legacy_memory) {
7527
+ if (out.length >= LEGACY_MEMORY_REPORT_MAX_SOURCES) break;
7528
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
7529
+ const source = raw;
7530
+ const kind = source.kind;
7531
+ const path = source.path;
7532
+ const channelId = source.channelId;
7533
+ if (kind !== "global-memory" && kind !== "conversation-history" && kind !== "captured-memory" || typeof path !== "string" || !isAbsolute(path) || channelId !== void 0 && typeof channelId !== "string") continue;
7534
+ try {
7535
+ const st = lstatSync(path);
7536
+ if (!st.isFile() || st.isSymbolicLink()) continue;
7537
+ if (st.size <= LEGACY_MEMORY_SOURCE_MAX_BYTES && st.size > contentBudget) continue;
7538
+ const oversize = st.size > LEGACY_MEMORY_SOURCE_MAX_BYTES;
7539
+ const bytes = oversize ? Buffer.from(`oversize:${st.size}:${st.mtimeMs}:${st.ctimeMs}`, "utf8") : Buffer.from(readFileSync5(path));
7540
+ if (!oversize && (bytes.byteLength > LEGACY_MEMORY_SOURCE_MAX_BYTES || bytes.byteLength > contentBudget)) {
7541
+ continue;
7542
+ }
7543
+ const exportable = !oversize;
7544
+ const candidate = {
7545
+ id: "",
7546
+ kind,
7547
+ ...typeof channelId === "string" && channelId ? { channel_id: channelId } : {},
7548
+ path,
7549
+ content: exportable ? bytes.toString("utf8") : null,
7550
+ bytes: exportable ? bytes.byteLength : st.size,
7551
+ exportable
7552
+ };
7553
+ candidate.id = legacySourceId(candidate, bytes);
7554
+ if (source.id !== candidate.id) continue;
7555
+ out.push(candidate);
7556
+ if (exportable) contentBudget -= bytes.byteLength;
7557
+ } catch {
7558
+ }
7559
+ }
7560
+ return out;
7561
+ }
7401
7562
  function readLocalReport(home, identityId) {
7402
7563
  const p = join6(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
7403
7564
  if (!existsSync(p)) return null;
@@ -7409,11 +7570,98 @@ function readLocalReport(home, identityId) {
7409
7570
  }
7410
7571
  if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
7411
7572
  const hasSearchKey = doc.has_search_key === true;
7573
+ const hasLlmKey = doc.has_llm_key === true;
7412
7574
  const raw = doc.persona;
7413
7575
  const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
7414
7576
  Object.entries(raw).filter(([, v]) => typeof v === "string")
7415
7577
  ) : {};
7416
- return { persona, has_search_key: hasSearchKey };
7578
+ return {
7579
+ persona,
7580
+ has_search_key: hasSearchKey,
7581
+ has_llm_key: hasLlmKey,
7582
+ legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source)
7583
+ };
7584
+ }
7585
+ function deleteLegacyMemorySources(home, identityId, ids) {
7586
+ if (ids.length === 0) return 0;
7587
+ const p = join6(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
7588
+ if (!existsSync(p)) return 0;
7589
+ let doc;
7590
+ try {
7591
+ doc = JSON.parse(readFileSync5(p, "utf8"));
7592
+ } catch {
7593
+ return 0;
7594
+ }
7595
+ const wanted = new Set(ids);
7596
+ let deleted = 0;
7597
+ for (const source of localLegacySources(doc)) {
7598
+ if (!wanted.has(source.id)) continue;
7599
+ try {
7600
+ unlinkSync(source.path);
7601
+ deleted += 1;
7602
+ } catch {
7603
+ }
7604
+ }
7605
+ return deleted;
7606
+ }
7607
+ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, recordFact) {
7608
+ if (!Array.isArray(rawDecisions) || rawDecisions.length > LEGACY_MEMORY_FACT_DECISION_MAX) {
7609
+ return { sourcesDeleted: 0, factsAssigned: 0 };
7610
+ }
7611
+ const bySource = /* @__PURE__ */ new Map();
7612
+ for (const raw of rawDecisions) {
7613
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
7614
+ return { sourcesDeleted: 0, factsAssigned: 0 };
7615
+ }
7616
+ const d = raw;
7617
+ if (typeof d.source_id !== "string" || !/^[0-9a-f]{64}$/.test(d.source_id) || typeof d.fact_hash !== "string" || !/^[0-9a-f]{64}$/.test(d.fact_hash) || d.action !== "assign" && d.action !== "delete" || d.action === "assign" && (typeof d.channel_id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(d.channel_id)) || d.action === "delete" && d.channel_id !== void 0) return { sourcesDeleted: 0, factsAssigned: 0 };
7618
+ const decision = d;
7619
+ const current = bySource.get(decision.source_id);
7620
+ if (current === null) continue;
7621
+ const group = current ?? /* @__PURE__ */ new Map();
7622
+ const prior = group.get(decision.fact_hash);
7623
+ if (prior && (prior.action !== decision.action || prior.channel_id !== decision.channel_id)) {
7624
+ bySource.set(decision.source_id, null);
7625
+ continue;
7626
+ }
7627
+ group.set(decision.fact_hash, decision);
7628
+ bySource.set(decision.source_id, group);
7629
+ }
7630
+ const reportPath = join6(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
7631
+ if (!existsSync(reportPath)) return { sourcesDeleted: 0, factsAssigned: 0 };
7632
+ let doc;
7633
+ try {
7634
+ doc = JSON.parse(readFileSync5(reportPath, "utf8"));
7635
+ } catch {
7636
+ return { sourcesDeleted: 0, factsAssigned: 0 };
7637
+ }
7638
+ let sourcesDeleted = 0;
7639
+ let factsAssigned = 0;
7640
+ for (const source of localLegacySources(doc)) {
7641
+ const decisions = bySource.get(source.id);
7642
+ if (!decisions || source.kind !== "global-memory" || source.content === null) continue;
7643
+ const facts = source.content.split("\n").map((line) => line.trim()).filter(Boolean);
7644
+ const currentHashes = new Set(facts.map(lineHash));
7645
+ if (currentHashes.size === 0 || decisions.size !== currentHashes.size || [...currentHashes].some((hash) => !decisions.has(hash)) || [...decisions].some(([hash]) => !currentHashes.has(hash))) continue;
7646
+ const uniqueFacts = /* @__PURE__ */ new Map();
7647
+ for (const fact of facts) uniqueFacts.set(lineHash(fact), fact);
7648
+ const assignments = [...decisions].filter(([, decision]) => decision.action === "assign");
7649
+ if (assignments.some(([hash]) => Buffer.byteLength(uniqueFacts.get(hash) ?? "", "utf8") > LEGACY_MEMORY_FACT_MAX_BYTES)) {
7650
+ continue;
7651
+ }
7652
+ try {
7653
+ for (const [hash, decision] of assignments) {
7654
+ await recordFact(decision.channel_id, uniqueFacts.get(hash), `${source.id}:${hash}`);
7655
+ factsAssigned += 1;
7656
+ }
7657
+ const stillCurrent = localLegacySources(doc).some((candidate) => candidate.id === source.id);
7658
+ if (!stillCurrent) continue;
7659
+ unlinkSync(source.path);
7660
+ sourcesDeleted += 1;
7661
+ } catch {
7662
+ }
7663
+ }
7664
+ return { sourcesDeleted, factsAssigned };
7417
7665
  }
7418
7666
  function readManagedMemory(home, identityId) {
7419
7667
  const p = join6(runtimeConfigDir(home, identityId), MEMORY_FILE);
@@ -7442,6 +7690,51 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
7442
7690
 
7443
7691
  // src/bridge-agent.ts
7444
7692
  import { homedir as homedir2 } from "node:os";
7693
+
7694
+ // src/reply-receipts.ts
7695
+ var DEFAULT_TTL_MS = 15 * 60 * 1e3;
7696
+ var DEFAULT_CAPACITY2 = 256;
7697
+ var RuntimeReplyReceiptLedger = class {
7698
+ receipts = /* @__PURE__ */ new Map();
7699
+ ttlMs;
7700
+ capacity;
7701
+ now;
7702
+ constructor(opts = {}) {
7703
+ this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
7704
+ this.capacity = opts.capacity ?? DEFAULT_CAPACITY2;
7705
+ this.now = opts.now ?? Date.now;
7706
+ if (!Number.isFinite(this.ttlMs) || this.ttlMs <= 0) throw new Error("reply receipt ttlMs must be positive");
7707
+ if (!Number.isInteger(this.capacity) || this.capacity <= 0) throw new Error("reply receipt capacity must be positive");
7708
+ }
7709
+ record(inReplyTo, channelId, replyMessageId) {
7710
+ const receipt = Object.freeze({
7711
+ in_reply_to: inReplyTo,
7712
+ reply_message_id: replyMessageId,
7713
+ channel_id: channelId,
7714
+ replied_at: this.now()
7715
+ });
7716
+ this.prune(receipt.replied_at);
7717
+ this.receipts.delete(inReplyTo);
7718
+ this.receipts.set(inReplyTo, receipt);
7719
+ while (this.receipts.size > this.capacity) {
7720
+ const oldest = this.receipts.keys().next().value;
7721
+ if (oldest === void 0) break;
7722
+ this.receipts.delete(oldest);
7723
+ }
7724
+ return receipt;
7725
+ }
7726
+ get(inReplyTo) {
7727
+ this.prune(this.now());
7728
+ return this.receipts.get(inReplyTo) ?? null;
7729
+ }
7730
+ prune(now) {
7731
+ for (const [key, receipt] of this.receipts) {
7732
+ if (now - receipt.replied_at >= this.ttlMs) this.receipts.delete(key);
7733
+ }
7734
+ }
7735
+ };
7736
+
7737
+ // src/bridge-agent.ts
7445
7738
  var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
7446
7739
  var FAST_PATH_SPOKEN_GUIDANCE = "A one-sentence spoken acknowledgement has already been sent for this turn. Reply only with information it did not contain, in one or two short sentences; if there is nothing to add, send nothing.";
7447
7740
  var ROOM_VOICE_GUIDANCE_MENTION_ONLY = "You are in a group voice room. Reply ONLY when a message @mentions your display name. Keep replies to one short sentence. When you should not reply, send nothing.";
@@ -7711,6 +8004,12 @@ var BridgeAgent = class _BridgeAgent {
7711
8004
  channelOwner = /* @__PURE__ */ new Map();
7712
8005
  buffer = [];
7713
8006
  maxBuffer;
8007
+ /**
8008
+ * Short-lived, content-free runtime reply correlations. The attached runtime
8009
+ * supplies the inbound message id only when an outbound send is its reply;
8010
+ * ws-server/http-server record the receipt after send() publishes.
8011
+ */
8012
+ runtimeReplyReceipts = new RuntimeReplyReceiptLedger();
7714
8013
  /** Cumulative count of messages shed by the buffer cap (surfaced on health()/metrics()). */
7715
8014
  droppedMessages = 0;
7716
8015
  /**
@@ -7936,6 +8235,7 @@ var BridgeAgent = class _BridgeAgent {
7936
8235
  waiters = [];
7937
8236
  pingHandle;
7938
8237
  safetyTimer;
8238
+ runtimeConfigTimer;
7939
8239
  listeners = /* @__PURE__ */ new Set();
7940
8240
  startedAt = 0;
7941
8241
  store;
@@ -8045,25 +8345,65 @@ var BridgeAgent = class _BridgeAgent {
8045
8345
  });
8046
8346
  if (!res.ok) throw new Error(`runtime-config ${res.status}`);
8047
8347
  const payload = await res.json();
8048
- if (!payload || !payload.version) {
8348
+ if (!payload) {
8349
+ process.stderr.write("[bridge] runtime config not applied (empty response)\n");
8350
+ return;
8351
+ }
8352
+ if (typeof payload.version !== "number") {
8049
8353
  process.stderr.write(
8050
- `[bridge] runtime config not applied (version ${JSON.stringify(payload?.version ?? null)} \u2014 no saved config)
8354
+ `[bridge] runtime config not applied (version ${JSON.stringify(payload.version ?? null)} \u2014 invalid response)
8051
8355
  `
8052
8356
  );
8053
8357
  return;
8054
8358
  }
8359
+ const version = payload.version;
8360
+ if (!version) {
8361
+ writeManagedFiles(this.home, this.cfg.identityId, {});
8362
+ process.stderr.write(
8363
+ `[bridge] runtime config not applied (version ${JSON.stringify(payload?.version ?? null)} \u2014 no saved config)
8364
+ `
8365
+ );
8366
+ }
8055
8367
  const skillsJson = payload.skills_json ?? {};
8056
- writeManagedFiles(this.home, this.cfg.identityId, {
8057
- ...payload.files ?? {},
8058
- "skills.json": `${JSON.stringify(skillsJson, null, 2)}
8368
+ if (version) {
8369
+ writeManagedFiles(this.home, this.cfg.identityId, {
8370
+ ...payload.files ?? {},
8371
+ "skills.json": `${JSON.stringify(skillsJson, null, 2)}
8059
8372
  `
8060
- });
8373
+ });
8374
+ }
8061
8375
  if (payload.memory_prunes?.length) {
8062
8376
  pruneManagedMemory(this.home, this.cfg.identityId, payload.memory_prunes);
8063
8377
  }
8378
+ if (payload.legacy_memory_deletes?.length) {
8379
+ deleteLegacyMemorySources(this.home, this.cfg.identityId, payload.legacy_memory_deletes);
8380
+ }
8381
+ if (payload.legacy_memory_fact_decisions?.length) {
8382
+ const provider = this.cfg.memoryProvider ?? "securecoms";
8383
+ const result = await applyLegacyMemoryFactDecisions(
8384
+ this.home,
8385
+ this.cfg.identityId,
8386
+ payload.legacy_memory_fact_decisions,
8387
+ async (channelId, fact, assignmentKey) => {
8388
+ if (provider === "none" || provider === "hermes") {
8389
+ throw new Error("configured memory provider has no protected channel-fact import path");
8390
+ }
8391
+ if (provider === "openclaw" && !await this.mayReadOpenClawMemory(channelId)) {
8392
+ throw new Error("OpenClaw legacy fact target is not the active owner DM");
8393
+ }
8394
+ await this.store.recordImportedMemoryFact(channelId, fact, assignmentKey);
8395
+ }
8396
+ );
8397
+ if (result.sourcesDeleted > 0) {
8398
+ process.stderr.write(
8399
+ `[bridge] legacy-memory disposition applied (sources=${result.sourcesDeleted}, assigned=${result.factsAssigned})
8400
+ `
8401
+ );
8402
+ }
8403
+ }
8064
8404
  const local = readLocalReport(this.home, this.cfg.identityId);
8065
8405
  const report = {
8066
- applied_version: payload.version,
8406
+ applied_version: version,
8067
8407
  paths: checkConfiguredPaths(skillsJson),
8068
8408
  // Read AFTER the prunes above, so the owner's next view of memory is
8069
8409
  // the file as it now stands, not the one they just pruned from.
@@ -8075,7 +8415,18 @@ var BridgeAgent = class _BridgeAgent {
8075
8415
  // is an environment value on that process, invisible to the daemon and
8076
8416
  // to the backend. The backend needs it to generate a truthful tool
8077
8417
  // paragraph, so it rides along here.
8078
- has_search_key: local?.has_search_key ?? false
8418
+ has_search_key: local?.has_search_key ?? false,
8419
+ // Same idea, for the BYOK LLM key file: only the runtime can see
8420
+ // whether one is configured in its own environment.
8421
+ has_llm_key: local?.has_llm_key ?? false,
8422
+ // The backend uses this only to expose target channels compatible with
8423
+ // the actual protected-memory adapter. It is an enum, never a path or
8424
+ // provider credential.
8425
+ memory_provider: this.cfg.memoryProvider ?? "securecoms",
8426
+ // Inert legacy files only. `readLocalReport` revalidates the current
8427
+ // regular-file bytes and strips every local path before this leaves the
8428
+ // machine.
8429
+ legacy_memory: local?.legacy_memory ?? []
8079
8430
  };
8080
8431
  const posted = await this.fetchImpl(`${base}/report`, {
8081
8432
  method: "POST",
@@ -8087,13 +8438,16 @@ var BridgeAgent = class _BridgeAgent {
8087
8438
  });
8088
8439
  if (!posted.ok) {
8089
8440
  process.stderr.write(
8090
- `[bridge] runtime config v${payload.version} written, but reporting it failed (${posted.status}) \u2014 the console will keep showing it as not applied until the next cycle
8441
+ version ? `[bridge] runtime config v${version} written, but reporting it failed (${posted.status}) \u2014 the console will keep showing it as not applied until the next cycle
8442
+ ` : `[bridge] legacy-memory inventory report failed (${posted.status}); no saved runtime config was applied
8091
8443
  `
8092
8444
  );
8093
8445
  return;
8094
8446
  }
8095
- process.stderr.write(`[bridge] runtime config v${payload.version} applied
8096
- `);
8447
+ process.stderr.write(
8448
+ version ? `[bridge] runtime config v${version} applied
8449
+ ` : "[bridge] legacy-memory inventory reported; no saved runtime config was applied\n"
8450
+ );
8097
8451
  } catch (e) {
8098
8452
  process.stderr.write(
8099
8453
  `[bridge] runtime config refetch failed: ${e?.message ?? e}
@@ -8129,6 +8483,10 @@ var BridgeAgent = class _BridgeAgent {
8129
8483
  await this.provider.publishKeyPackage();
8130
8484
  await this.refetchVoiceSettings();
8131
8485
  await this.refetchRuntimeConfig();
8486
+ this.runtimeConfigTimer = setInterval(() => {
8487
+ void this.refetchRuntimeConfig();
8488
+ }, 6 * 60 * 60 * 1e3);
8489
+ this.runtimeConfigTimer.unref?.();
8132
8490
  this.startReceivePump();
8133
8491
  if (this.selfHealSweepEnabled) {
8134
8492
  this.bootSweepTimer = setTimeout(
@@ -8931,7 +9289,47 @@ var BridgeAgent = class _BridgeAgent {
8931
9289
  async recordMemory(input) {
8932
9290
  return this.memory.record(input);
8933
9291
  }
9292
+ /**
9293
+ * OpenClaw collapses text history and workspace facts into the owner's
9294
+ * session. Resolve the channel from fresh, authenticated server metadata
9295
+ * before exposing that provider through the local bridge API. A cold or
9296
+ * stale cache is refreshed once; any failure remains a fail-closed denial.
9297
+ */
9298
+ async mayReadOpenClawMemory(channelId) {
9299
+ if (this.cfg.memoryProvider !== "openclaw") return true;
9300
+ if (!this.freshMeta(channelId)) await this.refreshChannelMeta();
9301
+ if (!this.freshRoster(channelId)) await this.refreshChannelRoster(channelId);
9302
+ const meta = this.freshMeta(channelId);
9303
+ const roster = this.freshRoster(channelId);
9304
+ if (meta?.is_dm !== true || !roster) return false;
9305
+ const humanCounterparts = [...roster.entries()].filter(
9306
+ ([identityId, member]) => identityId !== this.cfg.identityId && !member.is_agent
9307
+ );
9308
+ return humanCounterparts.length === 1 && humanCounterparts[0][1].is_agent_owner;
9309
+ }
9310
+ /** An inbound turn may receive OpenClaw memory only from an active owner DM. */
9311
+ mayReadOpenClawMemoryForTurn(authorization) {
9312
+ return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
9313
+ }
9314
+ /**
9315
+ * Resolve the prior-turn snapshot carried to the native runtime. OpenClaw
9316
+ * stays owner-DM-only; every provider failure degrades to empty context and
9317
+ * never falls back to the runtime's retired plaintext JSON files.
9318
+ */
9319
+ async memoryForTurn(channelId, authorization, limit = 50) {
9320
+ if (!this.mayReadOpenClawMemoryForTurn(authorization)) {
9321
+ return { recent: [], facts: [] };
9322
+ }
9323
+ try {
9324
+ return await this.memory.context(channelId, limit);
9325
+ } catch {
9326
+ return { recent: [], facts: [] };
9327
+ }
9328
+ }
8934
9329
  async memoryContext(conversationId, limit) {
9330
+ if (!await this.mayReadOpenClawMemory(conversationId)) {
9331
+ return { recent: [], facts: [] };
9332
+ }
8935
9333
  return this.memory.context(conversationId, limit);
8936
9334
  }
8937
9335
  /**
@@ -8975,10 +9373,14 @@ var BridgeAgent = class _BridgeAgent {
8975
9373
  }
8976
9374
  try {
8977
9375
  let memory;
8978
- try {
8979
- memory = await this.memory.context(m.channelId, 30);
8980
- } catch {
9376
+ if (!this.mayReadOpenClawMemoryForTurn(m.authorization)) {
8981
9377
  memory = { recent: [], facts: [] };
9378
+ } else {
9379
+ try {
9380
+ memory = await this.memory.context(m.channelId, 30);
9381
+ } catch {
9382
+ memory = { recent: [], facts: [] };
9383
+ }
8982
9384
  }
8983
9385
  const messages = buildFastMessages({
8984
9386
  persona: this.cfg.voice.persona,
@@ -9231,13 +9633,42 @@ var BridgeAgent = class _BridgeAgent {
9231
9633
  const rows = await res.json();
9232
9634
  const members = /* @__PURE__ */ new Map();
9233
9635
  for (const r of rows) {
9234
- members.set(r.identity_id, { name: r.display_name, is_agent: r.is_agent });
9636
+ members.set(r.identity_id, {
9637
+ name: r.display_name,
9638
+ is_agent: r.is_agent,
9639
+ is_agent_owner: r.is_agent_owner === true,
9640
+ is_partner: r.is_partner === true
9641
+ });
9235
9642
  }
9236
9643
  this.channelRoster.set(channelId, { members, at: Date.now() });
9237
9644
  await this.resolveAndPinOwner(channelId, rows);
9238
9645
  } catch {
9239
9646
  }
9240
9647
  }
9648
+ /**
9649
+ * Classify the sender only from server-authenticated channel/roster data.
9650
+ * Missing or stale metadata is UNKNOWN with zero grants, never a permissive
9651
+ * guess. The returned object and grant array are frozen before they cross the
9652
+ * daemon boundary so one consumer cannot widen a later execution check.
9653
+ */
9654
+ authorizationFor(channelId, senderId) {
9655
+ const meta = this.freshMeta(channelId);
9656
+ const sender = this.freshRoster(channelId)?.get(senderId);
9657
+ let audience = "unknown";
9658
+ if (meta && sender) {
9659
+ if (sender.is_agent) audience = "agent_to_agent";
9660
+ else if (meta.is_dm && sender.is_agent_owner) audience = "owner_dm";
9661
+ else if (meta.is_dm) audience = "other_human_dm";
9662
+ else if (sender.is_partner) audience = "partner_room";
9663
+ else audience = "team_room";
9664
+ }
9665
+ return Object.freeze({
9666
+ version: 1,
9667
+ audience,
9668
+ sender_membership: sender ? "active" : "unknown",
9669
+ tool_grants: Object.freeze([])
9670
+ });
9671
+ }
9241
9672
  /**
9242
9673
  * Task 8 (owner-attested Reset Secure Session): STRUCTURAL owner selection —
9243
9674
  * NEVER read the server's `role` field to pick the owner. In a DM the owner
@@ -9751,6 +10182,7 @@ var BridgeAgent = class _BridgeAgent {
9751
10182
  sender_id: m.envelope.sender_id,
9752
10183
  text: typeof parsed.text === "string" ? parsed.text : "",
9753
10184
  ts: m.receivedAt,
10185
+ authorization: this.authorizationFor(m.channelId, m.envelope.sender_id),
9754
10186
  sender_name: senderName,
9755
10187
  sender_is_agent: senderIsAgent,
9756
10188
  attachments: [attachment],
@@ -9789,6 +10221,11 @@ var BridgeAgent = class _BridgeAgent {
9789
10221
  const senderName = this.freshRoster(m.channelId)?.get(
9790
10222
  m.envelope.sender_id
9791
10223
  )?.name;
10224
+ const turnAuthorization = this.authorizationFor(
10225
+ m.channelId,
10226
+ m.envelope.sender_id
10227
+ );
10228
+ const runtimeMemory = this.memoryForTurn(m.channelId, turnAuthorization);
9792
10229
  const speaker = parsed.relay === true && typeof parsed.speaker_identity_id === "string" && typeof parsed.speaker_display_name === "string" ? {
9793
10230
  identityId: parsed.speaker_identity_id,
9794
10231
  displayName: parsed.speaker_display_name
@@ -9805,12 +10242,14 @@ var BridgeAgent = class _BridgeAgent {
9805
10242
  const turnText = parsed.text;
9806
10243
  const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
9807
10244
  const fastGreeting = isCallStart && fastPathFiring;
9808
- const runtimeEntry = (ackSent) => ({
10245
+ const runtimeEntry = (ackSent, memory) => ({
9809
10246
  channel_id: m.channelId,
9810
10247
  message_id: m.messageId,
9811
10248
  sender_id: m.envelope.sender_id,
9812
10249
  text: turnText,
9813
10250
  ts: m.receivedAt,
10251
+ authorization: turnAuthorization,
10252
+ memory,
9814
10253
  // A relay-attributed turn is the SPEAKER's message to the runtime:
9815
10254
  // name it after them and never flag it as agent-authored.
9816
10255
  sender_name: speaker?.displayName ?? senderName,
@@ -9824,18 +10263,23 @@ var BridgeAgent = class _BridgeAgent {
9824
10263
  if (fastPathFiring) {
9825
10264
  const fastSpeaker = speaker ?? (senderName ? { identityId: m.envelope.sender_id, displayName: senderName } : void 0);
9826
10265
  void this.runFastPath(
9827
- { channelId: m.channelId, messageId: m.messageId, senderId: m.envelope.sender_id },
10266
+ {
10267
+ channelId: m.channelId,
10268
+ messageId: m.messageId,
10269
+ senderId: m.envelope.sender_id,
10270
+ authorization: turnAuthorization
10271
+ },
9828
10272
  fastGreeting ? greetingTurnFor(this.voiceSettings ?? DEFAULT_VOICE_SETTINGS, fastSpeaker?.displayName) : parsed.text,
9829
10273
  voiceGuidance,
9830
10274
  fastSpeaker
9831
- ).then((outcome) => {
10275
+ ).then(async (outcome) => {
9832
10276
  if (outcome === "answered") return;
9833
- this.buffer.push(runtimeEntry(outcome === "deferred"));
10277
+ this.buffer.push(runtimeEntry(outcome === "deferred", await runtimeMemory));
9834
10278
  this.capBuffer();
9835
10279
  this.wakeListeners();
9836
10280
  });
9837
10281
  } else {
9838
- this.buffer.push(runtimeEntry(false));
10282
+ this.buffer.push(runtimeEntry(false, await runtimeMemory));
9839
10283
  }
9840
10284
  this.capBuffer();
9841
10285
  void recordInboundMemory(
@@ -9901,6 +10345,12 @@ var BridgeAgent = class _BridgeAgent {
9901
10345
  this.buffer.push(...keep);
9902
10346
  return out;
9903
10347
  }
10348
+ recordRuntimeReply(inReplyTo, channelId, replyMessageId) {
10349
+ return this.runtimeReplyReceipts.record(inReplyTo, channelId, replyMessageId);
10350
+ }
10351
+ runtimeReplyReceipt(inReplyTo) {
10352
+ return this.runtimeReplyReceipts.get(inReplyTo);
10353
+ }
9904
10354
  /**
9905
10355
  * Put messages back at the front (SSE/disconnect write-failure recovery).
9906
10356
  *
@@ -10052,6 +10502,7 @@ var BridgeAgent = class _BridgeAgent {
10052
10502
  this.closingDown = true;
10053
10503
  this.pingHandle?.close();
10054
10504
  if (this.safetyTimer) clearInterval(this.safetyTimer);
10505
+ if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
10055
10506
  if (this.bootSweepTimer) clearTimeout(this.bootSweepTimer);
10056
10507
  if (this.selfHealTimer) clearTimeout(this.selfHealTimer);
10057
10508
  await this.provider.drainInFlight(_BridgeAgent.GRACEFUL_DRAIN_MS);
@@ -10285,6 +10736,7 @@ var sendSchema = z3.object({
10285
10736
  channel_id: z3.string().min(1),
10286
10737
  text: z3.string().optional(),
10287
10738
  idempotency_key: z3.string().optional(),
10739
+ in_reply_to: z3.string().min(1).max(200).optional(),
10288
10740
  a2a: a2aSchema.optional()
10289
10741
  }).refine((d) => typeof d.text === "string" || d.a2a !== void 0, {
10290
10742
  message: "send requires either text or a2a"
@@ -10401,10 +10853,19 @@ function createHttpServer(agent, opts) {
10401
10853
  if (method === "GET" && path === "/v1/metrics") {
10402
10854
  return json(res, 200, agent.metrics());
10403
10855
  }
10856
+ if (method === "GET" && path === "/v1/reply-receipt") {
10857
+ const inReplyTo = url.searchParams.get("in_reply_to");
10858
+ if (!inReplyTo || inReplyTo.length > 200) return jsonError(res, "invalid_request");
10859
+ const receipt = agent.runtimeReplyReceipt(inReplyTo);
10860
+ return json(res, 200, receipt ? { found: true, receipt } : { found: false });
10861
+ }
10404
10862
  if (method === "POST" && path === "/v1/send") {
10405
10863
  const parsed = sendSchema.safeParse(JSON.parse(await readBody(req, SEND_CAP)));
10406
10864
  if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
10407
10865
  const r = parsed.data.a2a ? await agent.sendA2A(parsed.data.channel_id, parsed.data.a2a) : await agent.send(parsed.data.channel_id, parsed.data.text);
10866
+ if (parsed.data.in_reply_to) {
10867
+ agent.recordRuntimeReply(parsed.data.in_reply_to, parsed.data.channel_id, r.message_id);
10868
+ }
10408
10869
  return json(res, 200, r);
10409
10870
  }
10410
10871
  if (method === "POST" && path === "/v1/send-attachment") {
@@ -10660,6 +11121,10 @@ async function handleInbound(sock, agent, data, inflight) {
10660
11121
  sendJson(sock, wsErrorFrame("invalid_request"));
10661
11122
  return;
10662
11123
  }
11124
+ if (m.in_reply_to !== void 0 && (typeof m.in_reply_to !== "string" || m.in_reply_to.length === 0 || m.in_reply_to.length > 200)) {
11125
+ sendJson(sock, wsErrorFrame("invalid_request"));
11126
+ return;
11127
+ }
10663
11128
  if (m.a2a && typeof m.a2a === "object") {
10664
11129
  const a = m.a2a;
10665
11130
  if (typeof a.a2a_version !== "string" || !a.a2a_version || a.kind !== "message" && a.kind !== "task" && a.kind !== "artifact" || typeof a.object !== "object" || a.object === null) {
@@ -10668,6 +11133,9 @@ async function handleInbound(sock, agent, data, inflight) {
10668
11133
  }
10669
11134
  try {
10670
11135
  const r = await agent.sendA2A(m.channel_id, { a2a_version: a.a2a_version, kind: a.kind, object: a.object });
11136
+ if (typeof m.in_reply_to === "string") {
11137
+ agent.recordRuntimeReply(m.in_reply_to, m.channel_id, r.message_id);
11138
+ }
10671
11139
  sendJson(sock, { type: "sent", message_id: r.message_id });
10672
11140
  } catch (e) {
10673
11141
  if (e instanceof ChannelPendingError) {
@@ -10686,6 +11154,9 @@ async function handleInbound(sock, agent, data, inflight) {
10686
11154
  }
10687
11155
  try {
10688
11156
  const r = await agent.send(m.channel_id, m.text);
11157
+ if (typeof m.in_reply_to === "string") {
11158
+ agent.recordRuntimeReply(m.in_reply_to, m.channel_id, r.message_id);
11159
+ }
10689
11160
  sendJson(sock, { type: "sent", message_id: r.message_id });
10690
11161
  } catch (e) {
10691
11162
  if (e instanceof ChannelPendingError) {