@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.
- package/dist/cli.js +113 -65
- 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
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
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
|
|
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
|
|
835
|
-
|
|
836
|
-
|
|
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:
|
|
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
|
-
/**
|
|
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)
|
|
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
|
-
|
|
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
|
|
1259
|
-
if (!this.caughtUp.has(ch) &&
|
|
1296
|
+
const deliveredMax = this.cursorOf(ch);
|
|
1297
|
+
if (!this.caughtUp.has(ch) && deliveredMax > 0) {
|
|
1260
1298
|
this.caughtUp.add(ch);
|
|
1261
|
-
|
|
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 (
|
|
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) => ({
|
|
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
|
|
1602
|
+
const lines = entries.map((e) => utext`${firstNonEmpty([e.agentName], "someone")}: ${e.content}`);
|
|
1561
1603
|
return `DISCUSSION HISTORY:
|
|
1562
|
-
${fence("flow discussion history", lines
|
|
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 ?? []
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
1760
|
-
` + fence("
|
|
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 =
|
|
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
|
|
2617
|
-
if (
|
|
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
|
+
"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.
|
|
43
|
-
"@anthropic-ai/claude-agent-sdk": "0.
|
|
44
|
-
"
|
|
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": "^
|
|
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": "^
|
|
55
|
+
"typescript": "^6.0.3",
|
|
52
56
|
"vitest": "^2.1.0"
|
|
53
57
|
}
|
|
54
58
|
}
|