@snowyroad/arp 0.3.4 → 0.3.6
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 +109 -32
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -857,15 +857,15 @@ function isAddressed(content, agentName) {
|
|
|
857
857
|
return false;
|
|
858
858
|
}
|
|
859
859
|
function classifyCatchUp(messages, agentName, nowMs, opts) {
|
|
860
|
-
const
|
|
861
|
-
if (m.seq <= opts.deliveredMaxSeq) return false;
|
|
860
|
+
const inWindow = messages.filter((m) => {
|
|
862
861
|
const t = Date.parse(rawUntrusted(m.createdAt));
|
|
863
|
-
|
|
864
|
-
if (!withinTtl) return false;
|
|
865
|
-
return !sameText(m.senderName, agentName) && isAddressed(m.content, agentName);
|
|
862
|
+
return Number.isFinite(t) ? t >= nowMs - opts.ttlMs : true;
|
|
866
863
|
});
|
|
864
|
+
const mentions = inWindow.filter(
|
|
865
|
+
(m) => !sameText(m.senderName, agentName) && isAddressed(m.content, agentName)
|
|
866
|
+
);
|
|
867
867
|
const capped = mentions.slice(-opts.maxMentions);
|
|
868
|
-
return { context:
|
|
868
|
+
return { context: inWindow, mentions: capped };
|
|
869
869
|
}
|
|
870
870
|
|
|
871
871
|
// src/relayClient.ts
|
|
@@ -883,7 +883,6 @@ var MAX_BACKFILL_CHARS_PER_CATCHUP = 2e6;
|
|
|
883
883
|
var MAX_BACKFILL_CHARS_PER_CONNECTION = 8e6;
|
|
884
884
|
var MAX_CONCURRENT_CATCHUPS = 3;
|
|
885
885
|
var GAP_RESUME_MIN_INTERVAL_MS = 5e3;
|
|
886
|
-
var REBUILD_LOOKBACK_SEQS = 100;
|
|
887
886
|
function clampContent(s) {
|
|
888
887
|
if (s.length <= MAX_MESSAGE_CONTENT_CHARS) return s;
|
|
889
888
|
return `${s.slice(0, MAX_MESSAGE_CONTENT_CHARS)}
|
|
@@ -1123,10 +1122,10 @@ var RelayClient = class {
|
|
|
1123
1122
|
}
|
|
1124
1123
|
/** Run a catch-up with bounded concurrency (BRIDGE-12): at most
|
|
1125
1124
|
* MAX_CONCURRENT_CATCHUPS paginated backfill loops in flight; extras queue FIFO. */
|
|
1126
|
-
scheduleCatchUp(channelId, afterSeq
|
|
1125
|
+
scheduleCatchUp(channelId, afterSeq) {
|
|
1127
1126
|
const run = () => {
|
|
1128
1127
|
this.activeCatchUps++;
|
|
1129
|
-
void this.catchUp(channelId, afterSeq
|
|
1128
|
+
void this.catchUp(channelId, afterSeq).finally(() => {
|
|
1130
1129
|
this.activeCatchUps--;
|
|
1131
1130
|
const next = this.catchUpWaiters.shift();
|
|
1132
1131
|
if (next) next();
|
|
@@ -1141,15 +1140,14 @@ var RelayClient = class {
|
|
|
1141
1140
|
}
|
|
1142
1141
|
/** Offline-rejoin catch-up: classify the missed window and hand it to onCatchUp once.
|
|
1143
1142
|
* Does NOT route through emitInbound/onInbound to avoid per-message passive submits. */
|
|
1144
|
-
async catchUp(channelId, afterSeq
|
|
1143
|
+
async catchUp(channelId, afterSeq) {
|
|
1145
1144
|
const missed = await this.fetchAfterSeq(channelId, afterSeq);
|
|
1146
1145
|
if (missed.length === 0) return;
|
|
1147
1146
|
for (const m of missed) if (m.id) this.markSeen(channelId, m.id);
|
|
1148
1147
|
this.bumpCursor(channelId, missed[missed.length - 1].seq);
|
|
1149
1148
|
const result = classifyCatchUp(missed, this.cfg.agentName, Date.now(), {
|
|
1150
1149
|
ttlMs: this.cfg.catchUpTtlMs,
|
|
1151
|
-
maxMentions: this.cfg.catchUpMaxMentions
|
|
1152
|
-
deliveredMaxSeq
|
|
1150
|
+
maxMentions: this.cfg.catchUpMaxMentions
|
|
1153
1151
|
});
|
|
1154
1152
|
this.catchUpCbs.forEach((cb) => cb(channelId, result));
|
|
1155
1153
|
}
|
|
@@ -1293,11 +1291,10 @@ var RelayClient = class {
|
|
|
1293
1291
|
for (const [ch, seqRaw] of Object.entries(resume)) {
|
|
1294
1292
|
const seq = Number(seqRaw);
|
|
1295
1293
|
if (Number.isFinite(seq) && seq > this.cursorOf(ch)) this.cursors.set(ch, seq);
|
|
1296
|
-
const
|
|
1297
|
-
if (!this.caughtUp.has(ch) &&
|
|
1294
|
+
const floor = this.cursorOf(ch);
|
|
1295
|
+
if (!this.caughtUp.has(ch) && floor > 0) {
|
|
1298
1296
|
this.caughtUp.add(ch);
|
|
1299
|
-
|
|
1300
|
-
this.scheduleCatchUp(ch, rebuildFloor, deliveredMax);
|
|
1297
|
+
this.scheduleCatchUp(ch, floor);
|
|
1301
1298
|
}
|
|
1302
1299
|
}
|
|
1303
1300
|
}
|
|
@@ -1801,8 +1798,8 @@ ${fence("channel message", msg.content)}
|
|
|
1801
1798
|
this.beacon?.begin();
|
|
1802
1799
|
try {
|
|
1803
1800
|
await this.session.converseLocal(capPrompt(
|
|
1804
|
-
this.promptHead() + `You just
|
|
1805
|
-
` + fence("
|
|
1801
|
+
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):
|
|
1802
|
+
` + fence("missed channel messages", transcript)
|
|
1806
1803
|
));
|
|
1807
1804
|
} finally {
|
|
1808
1805
|
this.beacon?.end();
|
|
@@ -1870,7 +1867,7 @@ var ActivityBeacon = class {
|
|
|
1870
1867
|
// src/adapter.ts
|
|
1871
1868
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1872
1869
|
import { accessSync, constants, existsSync as existsSync2, statSync } from "fs";
|
|
1873
|
-
import { delimiter, dirname as
|
|
1870
|
+
import { delimiter, dirname as dirname3, join as join4, resolve as resolve2 } from "path";
|
|
1874
1871
|
|
|
1875
1872
|
// src/acp/client.ts
|
|
1876
1873
|
import { spawn } from "child_process";
|
|
@@ -1970,6 +1967,7 @@ var AcpClient = class {
|
|
|
1970
1967
|
child = null;
|
|
1971
1968
|
conn = null;
|
|
1972
1969
|
_sessionId = null;
|
|
1970
|
+
loadSupported = false;
|
|
1973
1971
|
/**
|
|
1974
1972
|
* The currently-running turn's reply accumulator. Set for the duration of one
|
|
1975
1973
|
* turn so agent_message_chunk text lands in THIS turn's buffer only. Because
|
|
@@ -2078,7 +2076,7 @@ var AcpClient = class {
|
|
|
2078
2076
|
}
|
|
2079
2077
|
};
|
|
2080
2078
|
this.conn = new ClientSideConnection(() => client, stream);
|
|
2081
|
-
await this.guard(
|
|
2079
|
+
const init = await this.guard(
|
|
2082
2080
|
this.conn.initialize({
|
|
2083
2081
|
protocolVersion: 1,
|
|
2084
2082
|
clientCapabilities: {
|
|
@@ -2088,10 +2086,29 @@ var AcpClient = class {
|
|
|
2088
2086
|
clientInfo: { name: "arp-bridge", version: "0.1.0" }
|
|
2089
2087
|
})
|
|
2090
2088
|
);
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
this.
|
|
2089
|
+
this.loadSupported = init.agentCapabilities?.loadSession === true;
|
|
2090
|
+
const candidateId = this._sessionId ?? this.launch.session?.persistedId ?? null;
|
|
2091
|
+
let liveId = null;
|
|
2092
|
+
if (candidateId && this.loadSupported) {
|
|
2093
|
+
try {
|
|
2094
|
+
await this.guard(
|
|
2095
|
+
this.conn.loadSession({ sessionId: candidateId, cwd: this.launch.cwd, mcpServers: [] })
|
|
2096
|
+
);
|
|
2097
|
+
liveId = candidateId;
|
|
2098
|
+
} catch (err) {
|
|
2099
|
+
console.warn(
|
|
2100
|
+
`[arp-bridge] session/load failed; starting a fresh session: ${sanitizeForTty(String(err?.message ?? err))}`
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
if (!liveId) {
|
|
2105
|
+
const session = await this.guard(
|
|
2106
|
+
this.conn.newSession({ cwd: this.launch.cwd, mcpServers: [] })
|
|
2107
|
+
);
|
|
2108
|
+
liveId = session.sessionId;
|
|
2109
|
+
}
|
|
2110
|
+
this._sessionId = liveId;
|
|
2111
|
+
this.launch.session?.save(liveId);
|
|
2095
2112
|
}
|
|
2096
2113
|
/**
|
|
2097
2114
|
* Send one user turn. Resolves with the full assembled reply text once the
|
|
@@ -2192,6 +2209,45 @@ var AcpClient = class {
|
|
|
2192
2209
|
}
|
|
2193
2210
|
};
|
|
2194
2211
|
|
|
2212
|
+
// src/sessionStore.ts
|
|
2213
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, chmodSync as chmodSync2 } from "fs";
|
|
2214
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
2215
|
+
function safe(s) {
|
|
2216
|
+
return s.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2217
|
+
}
|
|
2218
|
+
function sessionFilePath(dir, relayUrl, agentName, channelId) {
|
|
2219
|
+
return join3(dir, "sessions", relayHostSegment(relayUrl), `${safe(agentName)}__${safe(channelId)}.json`);
|
|
2220
|
+
}
|
|
2221
|
+
function loadStoredSession(dir, relayUrl, agentName, channelId) {
|
|
2222
|
+
const file = sessionFilePath(dir, relayUrl, agentName, channelId);
|
|
2223
|
+
let raw;
|
|
2224
|
+
try {
|
|
2225
|
+
raw = readFileSync2(file, "utf8");
|
|
2226
|
+
} catch {
|
|
2227
|
+
return null;
|
|
2228
|
+
}
|
|
2229
|
+
try {
|
|
2230
|
+
const p = JSON.parse(raw);
|
|
2231
|
+
if (typeof p.sessionId === "string" && p.sessionId.trim() !== "" && typeof p.cwd === "string" && p.cwd.trim() !== "") {
|
|
2232
|
+
return { sessionId: p.sessionId, cwd: p.cwd };
|
|
2233
|
+
}
|
|
2234
|
+
return null;
|
|
2235
|
+
} catch {
|
|
2236
|
+
return null;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
function saveStoredSession(dir, relayUrl, agentName, channelId, rec) {
|
|
2240
|
+
const file = sessionFilePath(dir, relayUrl, agentName, channelId);
|
|
2241
|
+
mkdirSync2(dirname2(file), { recursive: true, mode: 448 });
|
|
2242
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
2243
|
+
writeFileSync2(tmp, JSON.stringify(rec, null, 2) + "\n", { mode: 384 });
|
|
2244
|
+
renameSync2(tmp, file);
|
|
2245
|
+
try {
|
|
2246
|
+
chmodSync2(file, 384);
|
|
2247
|
+
} catch {
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2195
2251
|
// src/adapter.ts
|
|
2196
2252
|
function defaultToolPolicy() {
|
|
2197
2253
|
return { mode: "readonly", configDirAbs: resolve2(configDir(process.env)) };
|
|
@@ -2208,9 +2264,9 @@ function pinned(pkg) {
|
|
|
2208
2264
|
var npxBinaryAbs = null;
|
|
2209
2265
|
function resolveNpxBinary() {
|
|
2210
2266
|
if (npxBinaryAbs) return npxBinaryAbs;
|
|
2211
|
-
const nodeDir =
|
|
2267
|
+
const nodeDir = dirname3(process.execPath);
|
|
2212
2268
|
for (const name of ["npx", "npx.cmd"]) {
|
|
2213
|
-
const candidate =
|
|
2269
|
+
const candidate = join4(nodeDir, name);
|
|
2214
2270
|
if (existsSync2(candidate)) {
|
|
2215
2271
|
npxBinaryAbs = candidate;
|
|
2216
2272
|
return candidate;
|
|
@@ -2223,7 +2279,7 @@ function resolveNpxBinary() {
|
|
|
2223
2279
|
function which(cmd, pathEnv = process.env.PATH ?? "") {
|
|
2224
2280
|
for (const dir of pathEnv.split(delimiter)) {
|
|
2225
2281
|
if (!dir) continue;
|
|
2226
|
-
const candidate =
|
|
2282
|
+
const candidate = join4(dir, cmd);
|
|
2227
2283
|
try {
|
|
2228
2284
|
accessSync(candidate, constants.X_OK);
|
|
2229
2285
|
if (statSync(candidate).isFile()) return resolve2(candidate);
|
|
@@ -2277,13 +2333,15 @@ var defaultAcpClientFactory = (launch) => new AcpClient(launch);
|
|
|
2277
2333
|
var MAX_CONSECUTIVE_RESTARTS = 3;
|
|
2278
2334
|
var RESTART_BACKOFF_MS = 250;
|
|
2279
2335
|
var AcpAdapter = class {
|
|
2280
|
-
constructor(agent, makeClient = defaultAcpClientFactory, backoffMs = RESTART_BACKOFF_MS, policy = defaultToolPolicy()) {
|
|
2336
|
+
constructor(agent, makeClient = defaultAcpClientFactory, backoffMs = RESTART_BACKOFF_MS, policy = defaultToolPolicy(), session) {
|
|
2281
2337
|
this.makeClient = makeClient;
|
|
2282
2338
|
this.backoffMs = backoffMs;
|
|
2339
|
+
this.session = session;
|
|
2283
2340
|
this.launch = { ...launchSpecFor(agent), policy };
|
|
2284
2341
|
}
|
|
2285
2342
|
makeClient;
|
|
2286
2343
|
backoffMs;
|
|
2344
|
+
session;
|
|
2287
2345
|
launch;
|
|
2288
2346
|
// --- supervised live state (set in start()) ---
|
|
2289
2347
|
client = null;
|
|
@@ -2297,7 +2355,17 @@ var AcpAdapter = class {
|
|
|
2297
2355
|
/** Latched once the loop guard trips, so we stop trying (and stop retrying turns). */
|
|
2298
2356
|
gaveUp = false;
|
|
2299
2357
|
async start(_opts) {
|
|
2300
|
-
|
|
2358
|
+
const rec = this.session?.load() ?? null;
|
|
2359
|
+
const cwd = rec?.cwd ?? this.launch.cwd;
|
|
2360
|
+
const launch = {
|
|
2361
|
+
...this.launch,
|
|
2362
|
+
cwd,
|
|
2363
|
+
session: this.session ? {
|
|
2364
|
+
persistedId: rec?.sessionId ?? null,
|
|
2365
|
+
save: (id) => this.session.save({ sessionId: id, cwd })
|
|
2366
|
+
} : void 0
|
|
2367
|
+
};
|
|
2368
|
+
this.client = this.makeClient(launch);
|
|
2301
2369
|
await this.client.start();
|
|
2302
2370
|
return {
|
|
2303
2371
|
submit: (text) => {
|
|
@@ -2523,14 +2591,23 @@ var ClaudeAdapter = class {
|
|
|
2523
2591
|
};
|
|
2524
2592
|
}
|
|
2525
2593
|
};
|
|
2526
|
-
function createAdapter(cfg) {
|
|
2594
|
+
function createAdapter(cfg, channelId) {
|
|
2527
2595
|
const policy = {
|
|
2528
2596
|
mode: cfg.toolMode,
|
|
2529
2597
|
configDirAbs: resolve2(configDir(process.env)),
|
|
2530
2598
|
agentName: cfg.agentName
|
|
2531
2599
|
// for the once-per-process "arp tools full <name>" denial hint
|
|
2532
2600
|
};
|
|
2533
|
-
|
|
2601
|
+
if (cfg.agentMode !== "acp") return new ClaudeAdapter(policy);
|
|
2602
|
+
const session = channelId ? makeSessionPersistence(cfg, channelId) : void 0;
|
|
2603
|
+
return new AcpAdapter(cfg.agent, void 0, void 0, policy, session);
|
|
2604
|
+
}
|
|
2605
|
+
function makeSessionPersistence(cfg, channelId) {
|
|
2606
|
+
const dir = configDir(process.env);
|
|
2607
|
+
return {
|
|
2608
|
+
load: () => loadStoredSession(dir, cfg.relayWsUrl, cfg.agentName, channelId),
|
|
2609
|
+
save: (rec) => saveStoredSession(dir, cfg.relayWsUrl, cfg.agentName, channelId, rec)
|
|
2610
|
+
};
|
|
2534
2611
|
}
|
|
2535
2612
|
|
|
2536
2613
|
// src/elicit.ts
|
|
@@ -2615,7 +2692,7 @@ async function startBridge(cfg, relay, deps) {
|
|
|
2615
2692
|
const inFlight = pending.get(channelId);
|
|
2616
2693
|
if (inFlight) return inFlight;
|
|
2617
2694
|
const p = (async () => {
|
|
2618
|
-
const adapter = deps.makeAdapter(cfg);
|
|
2695
|
+
const adapter = deps.makeAdapter(cfg, channelId);
|
|
2619
2696
|
const beacon = new ActivityBeacon((state) => relay.sendActivity(channelId, state));
|
|
2620
2697
|
const session = new ChannelSession(
|
|
2621
2698
|
adapter,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snowyroad/arp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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",
|