@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.
Files changed (2) hide show
  1. package/dist/cli.js +109 -32
  2. 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 mentions = messages.filter((m) => {
861
- if (m.seq <= opts.deliveredMaxSeq) return false;
860
+ const inWindow = messages.filter((m) => {
862
861
  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);
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: messages, mentions: capped };
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, deliveredMaxSeq) {
1125
+ scheduleCatchUp(channelId, afterSeq) {
1127
1126
  const run = () => {
1128
1127
  this.activeCatchUps++;
1129
- void this.catchUp(channelId, afterSeq, deliveredMaxSeq).finally(() => {
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, deliveredMaxSeq) {
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 deliveredMax = this.cursorOf(ch);
1297
- if (!this.caughtUp.has(ch) && deliveredMax > 0) {
1294
+ const floor = this.cursorOf(ch);
1295
+ if (!this.caughtUp.has(ch) && floor > 0) {
1298
1296
  this.caughtUp.add(ch);
1299
- const rebuildFloor = Math.max(0, deliveredMax - REBUILD_LOOKBACK_SEQS);
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 (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)
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 dirname2, join as join3, resolve as resolve2 } from "path";
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
- const session = await this.guard(
2092
- this.conn.newSession({ cwd: this.launch.cwd, mcpServers: [] })
2093
- );
2094
- this._sessionId = session.sessionId;
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 = dirname2(process.execPath);
2267
+ const nodeDir = dirname3(process.execPath);
2212
2268
  for (const name of ["npx", "npx.cmd"]) {
2213
- const candidate = join3(nodeDir, name);
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 = join3(dir, cmd);
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
- this.client = this.makeClient(this.launch);
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
- return cfg.agentMode === "acp" ? new AcpAdapter(cfg.agent, void 0, void 0, policy) : new ClaudeAdapter(policy);
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.4",
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",