@snowyroad/arp 0.3.3 → 0.3.4

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/cli.js +113 -65
  2. package/package.json +10 -6
package/dist/cli.js CHANGED
@@ -680,6 +680,33 @@ function redactConfig(cfg) {
680
680
  import { randomUUID } from "crypto";
681
681
 
682
682
  // src/untrusted.ts
683
+ function untrusted(s) {
684
+ return s;
685
+ }
686
+ function rawUntrusted(u) {
687
+ return u;
688
+ }
689
+ function utext(strings, ...values) {
690
+ let out = strings[0];
691
+ for (let i = 0; i < values.length; i++) out += String(values[i]) + strings[i + 1];
692
+ return out;
693
+ }
694
+ function joinUntrusted(parts, sep2) {
695
+ return parts.join(sep2);
696
+ }
697
+ function firstNonEmpty(parts, fallback) {
698
+ for (const p of parts) if (p !== "") return p;
699
+ return fallback;
700
+ }
701
+ function hasText(u) {
702
+ return u !== "";
703
+ }
704
+ function isBlankText(u) {
705
+ return u.trim() === "";
706
+ }
707
+ function sameText(u, s) {
708
+ return u === s;
709
+ }
683
710
  var MARKER_RE = /<<<(?=[\s\u200B-\u200D\u2060\uFEFF]*(?:END[\s\u200B-\u200D\u2060\uFEFF]+)?UNTRUSTED)/gi;
684
711
  function neutralizeMarkers(content) {
685
712
  return content.replace(MARKER_RE, "<<\\<");
@@ -690,7 +717,7 @@ function sanitizeLabel(label) {
690
717
  function fence(label, content) {
691
718
  const l = sanitizeLabel(label);
692
719
  return `<<<UNTRUSTED ${l}>>>
693
- ${neutralizeMarkers(content)}
720
+ ${neutralizeMarkers(rawUntrusted(content))}
694
721
  <<<END UNTRUSTED ${l}>>>`;
695
722
  }
696
723
  function untrustedPreamble(mode) {
@@ -774,24 +801,24 @@ function normalizeRosterEntry(name, memberDescription, card) {
774
801
  if (typeof card.description === "string" && card.description) description = card.description;
775
802
  if (Array.isArray(card.skills)) skills = card.skills.map((s) => s && typeof s.name === "string" ? s.name : "").filter(Boolean);
776
803
  }
777
- return { name, description, skills };
804
+ return { name: untrusted(name), description: untrusted(description), skills: skills.map(untrusted) };
778
805
  }
779
806
  function assembleRosterFacts(entries, selfName) {
780
- const peers = entries.filter((e) => e.name !== selfName);
807
+ const peers = entries.filter((e) => !sameText(e.name, selfName));
781
808
  if (peers.length === 0) return "";
782
809
  const lines = peers.map((p) => {
783
- const desc = p.description ? `: ${p.description}` : "";
784
- const skills = p.skills.length ? ` [skills: ${p.skills.join(", ")}]` : "";
785
- return `- ${p.name}${desc}${skills}`;
810
+ const desc = hasText(p.description) ? utext`: ${p.description}` : "";
811
+ const skills = p.skills.length ? utext` [skills: ${joinUntrusted(p.skills, ", ")}]` : "";
812
+ return utext`- ${p.name}${desc}${skills}`;
786
813
  });
787
814
  return `Also in this channel:
788
- ${fence("peer roster", lines.join("\n"))}`;
815
+ ${fence("peer roster", joinUntrusted(lines, "\n"))}`;
789
816
  }
790
817
 
791
818
  // src/channelContext.ts
792
819
  function buildChannelContext(input) {
793
820
  let out = "";
794
- if (input.memory.trim()) {
821
+ if (!isBlankText(input.memory)) {
795
822
  out += `## Channel Memory (shared context for this channel)
796
823
  ${fence("channel memory", input.memory)}
797
824
  ---
@@ -799,8 +826,7 @@ ${fence("channel memory", input.memory)}
799
826
  `;
800
827
  }
801
828
  if (input.pins.length > 0) {
802
- const sections = input.pins.map((p) => fence("pinned file", `\u{1F4CC} ${p.label}
803
- ${p.content}`));
829
+ const sections = input.pins.map((p) => fence("pinned file", utext`📌 ${p.label}\n${p.content}`));
804
830
  out += `## Pinned Files (from GitHub)
805
831
  ${sections.join("\n\n")}
806
832
  ---
@@ -810,10 +836,10 @@ ${sections.join("\n\n")}
810
836
  if (input.topics.length > 0) {
811
837
  const lines = input.topics.map((t) => {
812
838
  const count = t.count != null ? ` (${t.count} messages)` : "";
813
- return `- ${t.title}${count}`;
839
+ return utext`- ${t.title}${count}`;
814
840
  });
815
841
  out += `## Channel Topics
816
- ${fence("channel topic titles", lines.join("\n"))}
842
+ ${fence("channel topic titles", joinUntrusted(lines, "\n"))}
817
843
  ---
818
844
 
819
845
  `;
@@ -825,21 +851,21 @@ ${fence("channel topic titles", lines.join("\n"))}
825
851
  function isAddressed(content, agentName) {
826
852
  const name = agentName.trim();
827
853
  if (!name) return false;
828
- const c = content.toLowerCase();
854
+ const c = rawUntrusted(content).toLowerCase();
829
855
  const forms = /* @__PURE__ */ new Set([name.toLowerCase(), name.toLowerCase().replace(/\s+/g, "_")]);
830
856
  for (const f of forms) if (c.includes("@" + f)) return true;
831
857
  return false;
832
858
  }
833
859
  function classifyCatchUp(messages, agentName, nowMs, opts) {
834
- const inWindow = messages.filter((m) => {
835
- const t = Date.parse(m.createdAt);
836
- return Number.isFinite(t) ? t >= nowMs - opts.ttlMs : true;
860
+ const mentions = messages.filter((m) => {
861
+ if (m.seq <= opts.deliveredMaxSeq) return false;
862
+ const t = Date.parse(rawUntrusted(m.createdAt));
863
+ const withinTtl = Number.isFinite(t) ? t >= nowMs - opts.ttlMs : true;
864
+ if (!withinTtl) return false;
865
+ return !sameText(m.senderName, agentName) && isAddressed(m.content, agentName);
837
866
  });
838
- const mentions = inWindow.filter(
839
- (m) => m.senderName !== agentName && isAddressed(m.content, agentName)
840
- );
841
867
  const capped = mentions.slice(-opts.maxMentions);
842
- return { context: inWindow, mentions: capped };
868
+ return { context: messages, mentions: capped };
843
869
  }
844
870
 
845
871
  // src/relayClient.ts
@@ -857,6 +883,7 @@ var MAX_BACKFILL_CHARS_PER_CATCHUP = 2e6;
857
883
  var MAX_BACKFILL_CHARS_PER_CONNECTION = 8e6;
858
884
  var MAX_CONCURRENT_CATCHUPS = 3;
859
885
  var GAP_RESUME_MIN_INTERVAL_MS = 5e3;
886
+ var REBUILD_LOOKBACK_SEQS = 100;
860
887
  function clampContent(s) {
861
888
  if (s.length <= MAX_MESSAGE_CONTENT_CHARS) return s;
862
889
  return `${s.slice(0, MAX_MESSAGE_CONTENT_CHARS)}
@@ -904,6 +931,8 @@ var RelayClient = class {
904
931
  graceTimer = null;
905
932
  confirmed = false;
906
933
  // single-shot: onReady has fired (relay accepted the agent)
934
+ reconnectAnnounced = false;
935
+ // per-connection: "reconnected" printed for this connection
907
936
  catchUpCbs = [];
908
937
  caughtUp = /* @__PURE__ */ new Set();
909
938
  // channels caught up this connection
@@ -995,6 +1024,7 @@ var RelayClient = class {
995
1024
  this.caughtUp.clear();
996
1025
  this.backfillCharsThisConn = 0;
997
1026
  this.connectedAt = Date.now();
1027
+ this.reconnectAnnounced = false;
998
1028
  this.heartbeatTimer = setInterval(() => this.send({ type: "ping" }), HEARTBEAT_MS);
999
1029
  this.armWatchdog();
1000
1030
  this.stableTimer = setTimeout(() => {
@@ -1005,13 +1035,20 @@ var RelayClient = class {
1005
1035
  this.graceTimer = setTimeout(() => this.confirmReady(), AUTH_GRACE_MS);
1006
1036
  this.onConnected();
1007
1037
  }
1008
- /** Single-shot: fires onReady at most once, on the FIRST successful connect. */
1038
+ /** Fires onReady at most once, on the FIRST successful connect; on later
1039
+ * connections it announces the reconnect instead (once per connection). */
1009
1040
  confirmReady() {
1010
1041
  if (this.graceTimer) {
1011
1042
  clearTimeout(this.graceTimer);
1012
1043
  this.graceTimer = null;
1013
1044
  }
1014
- if (this.confirmed) return;
1045
+ if (this.confirmed) {
1046
+ if (!this.reconnectAnnounced) {
1047
+ this.reconnectAnnounced = true;
1048
+ console.log("[arp-bridge] reconnected");
1049
+ }
1050
+ return;
1051
+ }
1015
1052
  this.confirmed = true;
1016
1053
  this.readyCb?.();
1017
1054
  }
@@ -1064,12 +1101,12 @@ var RelayClient = class {
1064
1101
  id: String(m.id ?? ""),
1065
1102
  seq,
1066
1103
  channelId,
1067
- content,
1068
- senderId: String(m.agentId ?? ""),
1069
- senderName: String(m.agentName ?? ""),
1104
+ content: untrusted(content),
1105
+ senderId: untrusted(String(m.agentId ?? "")),
1106
+ senderName: untrusted(String(m.agentName ?? "")),
1070
1107
  senderType: String(m.messageType ?? m.type ?? ""),
1071
1108
  // relay live/resume key is messageType; history path uses type
1072
- createdAt: String(m.createdAt ?? ""),
1109
+ createdAt: untrusted(String(m.createdAt ?? "")),
1073
1110
  isHistory: false
1074
1111
  // shape parity with live messages; the caller decides how to handle them
1075
1112
  });
@@ -1086,10 +1123,10 @@ var RelayClient = class {
1086
1123
  }
1087
1124
  /** Run a catch-up with bounded concurrency (BRIDGE-12): at most
1088
1125
  * MAX_CONCURRENT_CATCHUPS paginated backfill loops in flight; extras queue FIFO. */
1089
- scheduleCatchUp(channelId, afterSeq) {
1126
+ scheduleCatchUp(channelId, afterSeq, deliveredMaxSeq) {
1090
1127
  const run = () => {
1091
1128
  this.activeCatchUps++;
1092
- void this.catchUp(channelId, afterSeq).finally(() => {
1129
+ void this.catchUp(channelId, afterSeq, deliveredMaxSeq).finally(() => {
1093
1130
  this.activeCatchUps--;
1094
1131
  const next = this.catchUpWaiters.shift();
1095
1132
  if (next) next();
@@ -1104,14 +1141,15 @@ var RelayClient = class {
1104
1141
  }
1105
1142
  /** Offline-rejoin catch-up: classify the missed window and hand it to onCatchUp once.
1106
1143
  * Does NOT route through emitInbound/onInbound to avoid per-message passive submits. */
1107
- async catchUp(channelId, afterSeq) {
1144
+ async catchUp(channelId, afterSeq, deliveredMaxSeq) {
1108
1145
  const missed = await this.fetchAfterSeq(channelId, afterSeq);
1109
1146
  if (missed.length === 0) return;
1110
1147
  for (const m of missed) if (m.id) this.markSeen(channelId, m.id);
1111
1148
  this.bumpCursor(channelId, missed[missed.length - 1].seq);
1112
1149
  const result = classifyCatchUp(missed, this.cfg.agentName, Date.now(), {
1113
1150
  ttlMs: this.cfg.catchUpTtlMs,
1114
- maxMentions: this.cfg.catchUpMaxMentions
1151
+ maxMentions: this.cfg.catchUpMaxMentions,
1152
+ deliveredMaxSeq
1115
1153
  });
1116
1154
  this.catchUpCbs.forEach((cb) => cb(channelId, result));
1117
1155
  }
@@ -1189,7 +1227,7 @@ var RelayClient = class {
1189
1227
  }
1190
1228
  onMessage(raw) {
1191
1229
  this.armWatchdog();
1192
- if (!this.confirmed) this.confirmReady();
1230
+ this.confirmReady();
1193
1231
  let msg;
1194
1232
  try {
1195
1233
  msg = JSON.parse(raw);
@@ -1255,10 +1293,11 @@ var RelayClient = class {
1255
1293
  for (const [ch, seqRaw] of Object.entries(resume)) {
1256
1294
  const seq = Number(seqRaw);
1257
1295
  if (Number.isFinite(seq) && seq > this.cursorOf(ch)) this.cursors.set(ch, seq);
1258
- const floor = this.cursorOf(ch);
1259
- if (!this.caughtUp.has(ch) && floor > 0) {
1296
+ const deliveredMax = this.cursorOf(ch);
1297
+ if (!this.caughtUp.has(ch) && deliveredMax > 0) {
1260
1298
  this.caughtUp.add(ch);
1261
- this.scheduleCatchUp(ch, floor);
1299
+ const rebuildFloor = Math.max(0, deliveredMax - REBUILD_LOOKBACK_SEQS);
1300
+ this.scheduleCatchUp(ch, rebuildFloor, deliveredMax);
1262
1301
  }
1263
1302
  }
1264
1303
  }
@@ -1279,13 +1318,13 @@ var RelayClient = class {
1279
1318
  id: String(m.id ?? ""),
1280
1319
  seq: Number(m.seq ?? 0),
1281
1320
  channelId,
1282
- content: clampContent(String(m.content ?? "")),
1321
+ content: untrusted(clampContent(String(m.content ?? ""))),
1283
1322
  // bound per-message memory/prompt size (BRIDGE-09)
1284
- senderId: String(m.agentId ?? ""),
1285
- senderName: String(m.agentName ?? ""),
1323
+ senderId: untrusted(String(m.agentId ?? "")),
1324
+ senderName: untrusted(String(m.agentName ?? "")),
1286
1325
  senderType: String(m.messageType ?? m.type ?? ""),
1287
1326
  // relay live/resume key is messageType; history path uses type
1288
- createdAt: String(m.createdAt ?? ""),
1327
+ createdAt: untrusted(String(m.createdAt ?? "")),
1289
1328
  isHistory: Boolean(msg.isHistory)
1290
1329
  };
1291
1330
  const gapDetected = !inbound.isHistory && this.cursorOf(channelId) > 0 && inbound.seq > this.cursorOf(channelId) + 1;
@@ -1305,8 +1344,8 @@ var RelayClient = class {
1305
1344
  if (!flowId) return;
1306
1345
  const rawHistory = kind === "synthesis" || kind === "direction" ? msg.flowHistory ?? msg.messages ?? [] : msg.recentMessages ?? [];
1307
1346
  const history = (Array.isArray(rawHistory) ? rawHistory : []).map((e) => ({
1308
- agentName: String(e.agentName ?? ""),
1309
- content: clampContent(String(e.content ?? "")),
1347
+ agentName: untrusted(String(e.agentName ?? "")),
1348
+ content: untrusted(clampContent(String(e.content ?? ""))),
1310
1349
  // bound per-entry size (BRIDGE-09)
1311
1350
  messageType: e.messageType ?? e.type,
1312
1351
  turnNumber: e.turnNumber,
@@ -1316,11 +1355,11 @@ var RelayClient = class {
1316
1355
  kind,
1317
1356
  flowId,
1318
1357
  channelId: String(msg.channelId ?? ""),
1319
- topic: String(msg.topic ?? ""),
1320
- rolePrompt: typeof msg.rolePrompt === "string" ? msg.rolePrompt : void 0,
1321
- contextPrompt: typeof msg.contextPrompt === "string" ? msg.contextPrompt : void 0,
1322
- synthesisPrompt: typeof msg.synthesisPrompt === "string" ? msg.synthesisPrompt : void 0,
1323
- candidates: Array.isArray(msg.candidates) ? msg.candidates : void 0,
1358
+ topic: untrusted(String(msg.topic ?? "")),
1359
+ rolePrompt: typeof msg.rolePrompt === "string" ? untrusted(msg.rolePrompt) : void 0,
1360
+ contextPrompt: typeof msg.contextPrompt === "string" ? untrusted(msg.contextPrompt) : void 0,
1361
+ synthesisPrompt: typeof msg.synthesisPrompt === "string" ? untrusted(msg.synthesisPrompt) : void 0,
1362
+ candidates: Array.isArray(msg.candidates) ? msg.candidates.map((c) => untrusted(String(c ?? ""))) : void 0,
1324
1363
  history
1325
1364
  };
1326
1365
  this.flowCbs.forEach((cb) => cb(signal));
@@ -1455,22 +1494,22 @@ var RelayClient = class {
1455
1494
  console.warn("[arp-bridge] flow post failed:", sanitizeForTty(String(err)));
1456
1495
  }
1457
1496
  }
1458
- /** Channel memory text ("" if none or on error — never throws). */
1497
+ /** Channel memory text (empty if none or on error — never throws). Branded (H2-4). */
1459
1498
  async fetchChannelMemory(channelId) {
1460
1499
  const ch = this.pathId(channelId, "channelId");
1461
- if (!ch) return "";
1500
+ if (!ch) return untrusted("");
1462
1501
  const url = `${this.cfg.relayHttpUrl}/channels/${ch}/memory`;
1463
1502
  try {
1464
1503
  const res = await this.deps.fetchFn(url, { headers: { Authorization: `Bearer ${this.cfg.token}` } });
1465
1504
  if (!res.ok) {
1466
1505
  console.warn("[arp-bridge] memory HTTP", res.status);
1467
- return "";
1506
+ return untrusted("");
1468
1507
  }
1469
1508
  const data = await res.json();
1470
- return typeof data?.content === "string" ? data.content : "";
1509
+ return untrusted(typeof data?.content === "string" ? data.content : "");
1471
1510
  } catch (err) {
1472
1511
  console.warn("[arp-bridge] memory fetch failed:", sanitizeForTty(String(err)));
1473
- return "";
1512
+ return untrusted("");
1474
1513
  }
1475
1514
  }
1476
1515
  /** Channel topics with message counts ([] if none or on error). The relay returns
@@ -1488,7 +1527,7 @@ var RelayClient = class {
1488
1527
  const data = await res.json();
1489
1528
  const topics = data?.topics ?? [];
1490
1529
  const counts = data?.messageCounts ?? {};
1491
- return topics.filter((t) => typeof t?.title === "string").map((t) => ({ title: t.title, count: typeof counts[t.id] === "number" ? counts[t.id] : null }));
1530
+ return topics.filter((t) => typeof t?.title === "string").map((t) => ({ title: untrusted(t.title), count: typeof counts[t.id] === "number" ? counts[t.id] : null }));
1492
1531
  } catch (err) {
1493
1532
  console.warn("[arp-bridge] topics fetch failed:", sanitizeForTty(String(err)));
1494
1533
  return [];
@@ -1507,7 +1546,10 @@ var RelayClient = class {
1507
1546
  }
1508
1547
  const data = await res.json();
1509
1548
  const pins = data?.pins ?? [];
1510
- return pins.filter((p) => p?.injectContext && typeof p.cachedContent === "string" && p.cachedContent.trim()).map((p) => ({ label: p.displayName || `${p.repoUrl ?? ""}/${p.filePath ?? ""}`, content: p.cachedContent }));
1549
+ return pins.filter((p) => p?.injectContext && typeof p.cachedContent === "string" && p.cachedContent.trim()).map((p) => ({
1550
+ label: untrusted(p.displayName || `${p.repoUrl ?? ""}/${p.filePath ?? ""}`),
1551
+ content: untrusted(p.cachedContent)
1552
+ }));
1511
1553
  } catch (err) {
1512
1554
  console.warn("[arp-bridge] pins fetch failed:", sanitizeForTty(String(err)));
1513
1555
  return [];
@@ -1541,8 +1583,8 @@ var RelayClient = class {
1541
1583
  const data = await res.json();
1542
1584
  const list = Array.isArray(data) ? data : data.messages ?? [];
1543
1585
  return list.map((e) => ({
1544
- agentName: String(e.agentName ?? ""),
1545
- content: String(e.content ?? ""),
1586
+ agentName: untrusted(String(e.agentName ?? "")),
1587
+ content: untrusted(String(e.content ?? "")),
1546
1588
  messageType: e.messageType ?? e.type,
1547
1589
  turnNumber: e.turnNumber,
1548
1590
  createdAt: e.createdAt
@@ -1557,15 +1599,15 @@ var RelayClient = class {
1557
1599
  // src/flow.ts
1558
1600
  function renderFlowHistory(entries) {
1559
1601
  if (entries.length === 0) return "";
1560
- const lines = entries.map((e) => `${e.agentName || "someone"}: ${e.content}`);
1602
+ const lines = entries.map((e) => utext`${firstNonEmpty([e.agentName], "someone")}: ${e.content}`);
1561
1603
  return `DISCUSSION HISTORY:
1562
- ${fence("flow discussion history", lines.join("\n"))}
1604
+ ${fence("flow discussion history", joinUntrusted(lines, "\n"))}
1563
1605
 
1564
1606
  `;
1565
1607
  }
1566
1608
  function buildFlowPrompt(signal, agentName, channelId, toolMode = "readonly") {
1567
1609
  if (signal.kind === "direction") {
1568
- const candidates = (signal.candidates ?? []).join(", ");
1610
+ const candidates = joinUntrusted(signal.candidates ?? [], ", ");
1569
1611
  const history2 = renderFlowHistory(signal.history);
1570
1612
  const hasHistory = history2 !== "";
1571
1613
  const preamble = hasHistory ? [``, history2.trimEnd(), ``, `Read the conversation above and decide who should speak next.`] : [``, `No turns have been taken yet \u2014 choose who should speak FIRST.`];
@@ -1577,7 +1619,7 @@ function buildFlowPrompt(signal, agentName, channelId, toolMode = "readonly") {
1577
1619
  fence("flow topic", signal.topic),
1578
1620
  ...preamble,
1579
1621
  `Available participants:`,
1580
- fence("flow participant names", candidates || "(none online)"),
1622
+ fence("flow participant names", firstNonEmpty([candidates], "(none online)")),
1581
1623
  ``,
1582
1624
  `Reply with ONLY the name of the single participant who should speak next,`,
1583
1625
  `or reply with ONLY the word END to conclude the discussion and move to synthesis.`,
@@ -1679,7 +1721,7 @@ ${toolStatusLine(this.toolMode)}
1679
1721
  */
1680
1722
  async submit(msg) {
1681
1723
  if (!this.session) throw new Error("ChannelSession not started");
1682
- const who = msg.senderName || msg.senderId || "someone";
1724
+ const who = firstNonEmpty([msg.senderName, msg.senderId], "someone");
1683
1725
  const facts = assembleRosterFacts(this.roster, this.agentName);
1684
1726
  const rosterBlock = facts ? `${facts}
1685
1727
 
@@ -1750,14 +1792,17 @@ ${fence("channel message", msg.content)}
1750
1792
  async submitCatchUp(result) {
1751
1793
  if (!this.session) throw new Error("ChannelSession not started");
1752
1794
  if (result.context.length === 0) return;
1753
- const transcript = result.context.map((m) => `[${m.createdAt}] ${m.senderName || m.senderId || "someone"}: ${m.content}`).join("\n");
1795
+ const transcript = joinUntrusted(
1796
+ result.context.map((m) => utext`[${m.createdAt}] ${firstNonEmpty([m.senderName, m.senderId], "someone")}: ${m.content}`),
1797
+ "\n"
1798
+ );
1754
1799
  if (result.mentions.length === 0) {
1755
1800
  if (!this.session.converseLocal) return;
1756
1801
  this.beacon?.begin();
1757
1802
  try {
1758
1803
  await this.session.converseLocal(capPrompt(
1759
- this.promptHead() + `You just reconnected to ARP channel ${this.channelId} after being away. Here is what you missed (context only, do NOT reply to it):
1760
- ` + fence("missed channel messages", transcript)
1804
+ this.promptHead() + `You just (re)connected to ARP channel ${this.channelId}. Here is recent channel history for context \u2014 you may have already seen some of it. Absorb it so you can follow back-references in later messages; do NOT reply to it:
1805
+ ` + fence("recent channel history", transcript)
1761
1806
  ));
1762
1807
  } finally {
1763
1808
  this.beacon?.end();
@@ -1765,7 +1810,10 @@ ${fence("channel message", msg.content)}
1765
1810
  return;
1766
1811
  }
1767
1812
  const channelContext = this.fetchContext ? await this.fetchContext() : "";
1768
- const addressed = result.mentions.map((m) => `[${m.createdAt}] ${m.senderName || m.senderId || "someone"}: ${m.content}`).join("\n");
1813
+ const addressed = joinUntrusted(
1814
+ result.mentions.map((m) => utext`[${m.createdAt}] ${firstNonEmpty([m.senderName, m.senderId], "someone")}: ${m.content}`),
1815
+ "\n"
1816
+ );
1769
1817
  const head = this.promptHead() + channelContext + `You are ${this.agentName}. You just reconnected to ARP channel ${this.channelId} after being away. While you were gone, the channel said (context):
1770
1818
  ${fence("missed channel messages", transcript)}
1771
1819
 
@@ -2613,8 +2661,8 @@ async function startBridge(cfg, relay, deps) {
2613
2661
  }
2614
2662
  relay.onInbound((m) => {
2615
2663
  if (m.isHistory) return;
2616
- if (m.senderId && m.senderId === cfg.agentUuid || m.senderName && m.senderName === cfg.agentName) return;
2617
- if (!m.content.trim()) return;
2664
+ if (hasText(m.senderId) && sameText(m.senderId, cfg.agentUuid) || hasText(m.senderName) && sameText(m.senderName, cfg.agentName)) return;
2665
+ if (isBlankText(m.content)) return;
2618
2666
  ensureSession(m.channelId).then((s) => s.submit(m)).catch((e) => console.warn(`[arp-bridge] inbound routing failed for channel ${sanitizeForTty(m.channelId)}:`, sanitizeForTty(String(e))));
2619
2667
  });
2620
2668
  relay.onFlowSignal((signal) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowyroad/arp",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Connect your own coding agent (Claude Code, Codex, Gemini, Grok) to an Agent Relay Protocol channel and collaborate with other agents and humans.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "SnowyRoad",
@@ -20,6 +20,7 @@
20
20
  "engines": {
21
21
  "node": ">=20"
22
22
  },
23
+ "packageManager": "pnpm@9.15.4",
23
24
  "bin": {
24
25
  "arp": "dist/cli.js"
25
26
  },
@@ -39,16 +40,19 @@
39
40
  "typecheck": "tsc --noEmit"
40
41
  },
41
42
  "dependencies": {
42
- "@agentclientprotocol/sdk": "0.24.0",
43
- "@anthropic-ai/claude-agent-sdk": "0.1.77",
44
- "ws": "8.21.0"
43
+ "@agentclientprotocol/sdk": "0.25.1",
44
+ "@anthropic-ai/claude-agent-sdk": "0.3.177",
45
+ "@anthropic-ai/sdk": "0.104.1",
46
+ "@modelcontextprotocol/sdk": "1.29.0",
47
+ "ws": "8.21.0",
48
+ "zod": "4.4.3"
45
49
  },
46
50
  "devDependencies": {
47
- "@types/node": "^22.0.0",
51
+ "@types/node": "^25.9.3",
48
52
  "@types/ws": "^8.5.12",
49
53
  "tsup": "^8.5.1",
50
54
  "tsx": "^4.19.0",
51
- "typescript": "^5.6.0",
55
+ "typescript": "^6.0.3",
52
56
  "vitest": "^2.1.0"
53
57
  }
54
58
  }