ragent-cli 1.11.15 → 1.11.17

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
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "ragent-cli",
34
- version: "1.11.15",
34
+ version: "1.11.17",
35
35
  description: "CLI agent for rAgent Live \u2014 browser-first terminal control plane for AI coding agents",
36
36
  main: "dist/index.js",
37
37
  bin: {
@@ -435,6 +435,10 @@ var blockCounter = 0;
435
435
  function genBlockId(prefix) {
436
436
  return `${prefix}-${Date.now()}-${(++blockCounter).toString(36)}`;
437
437
  }
438
+ function makeStableBlockIdGen(turnId) {
439
+ let n = 0;
440
+ return (prefix) => `${turnId}-${prefix}-${n++}`;
441
+ }
438
442
  function now() {
439
443
  return (/* @__PURE__ */ new Date()).toISOString();
440
444
  }
@@ -1422,6 +1426,7 @@ var GeminiCliParserV2 = class {
1422
1426
  return this.parseLegacySchemaItem(item, turnId, timestamp, pendingLegacyCalls);
1423
1427
  }
1424
1428
  parseNewSchemaItem(item, turnId, timestamp) {
1429
+ const nextBlockId = makeStableBlockIdGen(turnId);
1425
1430
  const statusLevel = geminiStatusLevelFromType(item.type);
1426
1431
  if (statusLevel) {
1427
1432
  const text2 = geminiTextFromNewItem(item);
@@ -1440,7 +1445,7 @@ var GeminiCliParserV2 = class {
1440
1445
  {
1441
1446
  op: "insert_block",
1442
1447
  turnId,
1443
- block: makeStatusBlock(genBlockId("status"), "notice", text2, statusLevel)
1448
+ block: makeStatusBlock(nextBlockId("status"), "notice", text2, statusLevel)
1444
1449
  },
1445
1450
  {
1446
1451
  op: "complete_turn",
@@ -1466,7 +1471,7 @@ ${description}` : subject || description;
1466
1471
  op: "insert_block",
1467
1472
  turnId,
1468
1473
  block: makeReasoningBlock(
1469
- genBlockId("reason"),
1474
+ nextBlockId("reason"),
1470
1475
  reasoningText,
1471
1476
  "summary"
1472
1477
  )
@@ -1477,14 +1482,14 @@ ${description}` : subject || description;
1477
1482
  blocks.push({
1478
1483
  op: "insert_block",
1479
1484
  turnId,
1480
- block: makeTextBlock(genBlockId("txt"), text, true)
1485
+ block: makeTextBlock(nextBlockId("txt"), text, true)
1481
1486
  });
1482
1487
  }
1483
1488
  if (role === "assistant" && Array.isArray(item.toolCalls)) {
1484
1489
  for (const tc of item.toolCalls) {
1485
1490
  if (!tc.name) continue;
1486
- const callId = tc.id ?? genBlockId("call");
1487
- const blockId = genBlockId("tool");
1491
+ const callId = tc.id ?? nextBlockId("call");
1492
+ const blockId = nextBlockId("tool");
1488
1493
  const state = mapGeminiToolStatus(tc.status);
1489
1494
  const isComplete = state === "completed" || state === "error" || state === "cancelled";
1490
1495
  blocks.push({
@@ -1527,6 +1532,7 @@ ${description}` : subject || description;
1527
1532
  parseLegacySchemaItem(item, turnId, timestamp, pendingLegacyCalls) {
1528
1533
  const role = geminiRoleFromLegacy(item.role);
1529
1534
  if (!role) return [];
1535
+ const nextBlockId = makeStableBlockIdGen(turnId);
1530
1536
  const parts = geminiPartsFromLegacyItem(item);
1531
1537
  const currentTurnBlocks = [];
1532
1538
  const priorTurnUpdates = [];
@@ -1536,12 +1542,12 @@ ${description}` : subject || description;
1536
1542
  currentTurnBlocks.push({
1537
1543
  op: "insert_block",
1538
1544
  turnId,
1539
- block: makeTextBlock(genBlockId("txt"), text, true)
1545
+ block: makeTextBlock(nextBlockId("txt"), text, true)
1540
1546
  });
1541
1547
  }
1542
1548
  if (part.functionCall?.name) {
1543
- const callId = genBlockId("call");
1544
- const blockId = genBlockId("tool");
1549
+ const callId = nextBlockId("call");
1550
+ const blockId = nextBlockId("tool");
1545
1551
  pendingLegacyCalls.set(part.functionCall.name, { turnId, blockId });
1546
1552
  currentTurnBlocks.push({
1547
1553
  op: "insert_block",
@@ -1577,13 +1583,13 @@ ${description}` : subject || description;
1577
1583
  });
1578
1584
  }
1579
1585
  } else {
1580
- const blockId = genBlockId("tool");
1586
+ const blockId = nextBlockId("tool");
1581
1587
  currentTurnBlocks.push({
1582
1588
  op: "insert_block",
1583
1589
  turnId,
1584
1590
  block: makeToolBlock(
1585
1591
  blockId,
1586
- genBlockId("call"),
1592
+ nextBlockId("call"),
1587
1593
  part.functionResponse.name,
1588
1594
  "completed",
1589
1595
  void 0,
@@ -1640,7 +1646,8 @@ var AntigravityCliParserV2 = class {
1640
1646
  if (!obj.source || !obj.type) return [];
1641
1647
  const ops = [];
1642
1648
  const timestamp = obj.created_at || now();
1643
- const turnId = `step-${obj.step_index}-${timestamp}`;
1649
+ const turnId = `step-${obj.step_index}`;
1650
+ const nextBlockId = makeStableBlockIdGen(turnId);
1644
1651
  const normalizedType = obj.type.toLowerCase().replace(/[^a-z0-9]/g, "");
1645
1652
  const pendingIndex = this.pendingTools.findIndex(
1646
1653
  (t) => t.normalizedName === normalizedType
@@ -1689,7 +1696,7 @@ var AntigravityCliParserV2 = class {
1689
1696
  ops.push({
1690
1697
  op: "insert_block",
1691
1698
  turnId,
1692
- block: makeTextBlock(genBlockId("txt"), obj.content, true)
1699
+ block: makeTextBlock(nextBlockId("txt"), obj.content, true)
1693
1700
  });
1694
1701
  }
1695
1702
  ops.push({
@@ -1713,13 +1720,13 @@ var AntigravityCliParserV2 = class {
1713
1720
  ops.push({
1714
1721
  op: "insert_block",
1715
1722
  turnId,
1716
- block: makeTextBlock(genBlockId("txt"), obj.content, true)
1723
+ block: makeTextBlock(nextBlockId("txt"), obj.content, true)
1717
1724
  });
1718
1725
  }
1719
1726
  if (obj.tool_calls && obj.tool_calls.length > 0) {
1720
1727
  for (const tc of obj.tool_calls) {
1721
- const callId = genBlockId("call");
1722
- const blockId = genBlockId("tool");
1728
+ const callId = nextBlockId("call");
1729
+ const blockId = nextBlockId("tool");
1723
1730
  const normalizedName = tc.name.toLowerCase().replace(/[^a-z0-9]/g, "");
1724
1731
  this.pendingTools.push({
1725
1732
  blockId,
@@ -1760,7 +1767,7 @@ var AntigravityCliParserV2 = class {
1760
1767
  ops.push({
1761
1768
  op: "insert_block",
1762
1769
  turnId,
1763
- block: makeTextBlock(genBlockId("txt"), obj.content, true)
1770
+ block: makeTextBlock(nextBlockId("txt"), obj.content, true)
1764
1771
  });
1765
1772
  }
1766
1773
  ops.push({
@@ -1776,6 +1783,7 @@ var AntigravityCliParserV2 = class {
1776
1783
  return ops;
1777
1784
  }
1778
1785
  parseDocument(document) {
1786
+ this.pendingTools = [];
1779
1787
  const ops = [];
1780
1788
  for (const line of document.split("\n")) {
1781
1789
  const trimmed = line.trim();
@@ -1992,8 +2000,10 @@ function setBlock(turns, turnId, blockId, block) {
1992
2000
  }
1993
2001
  var MAX_SNAPSHOT_TURNS = 1e3;
1994
2002
  var MAX_SNAPSHOT_SIZE_CHARS = 45e4;
2003
+ var SNAPSHOT_FIELD_RESERVE = 64;
1995
2004
  function trimSnapshot(snapshot, maxTurns = MAX_SNAPSHOT_TURNS) {
1996
2005
  let currentSnapshot = snapshot;
2006
+ const originalLength = snapshot.turnOrder.length;
1997
2007
  if (currentSnapshot.turnOrder.length > maxTurns) {
1998
2008
  const keptOrder = currentSnapshot.turnOrder.slice(-maxTurns);
1999
2009
  const keptTurns = {};
@@ -2009,10 +2019,8 @@ function trimSnapshot(snapshot, maxTurns = MAX_SNAPSHOT_TURNS) {
2009
2019
  };
2010
2020
  }
2011
2021
  let serialized = JSON.stringify(currentSnapshot);
2012
- if (serialized.length <= MAX_SNAPSHOT_SIZE_CHARS) {
2013
- return currentSnapshot;
2014
- }
2015
- while (currentSnapshot.turnOrder.length > 1 && serialized.length > MAX_SNAPSHOT_SIZE_CHARS) {
2022
+ const sizeBudget = MAX_SNAPSHOT_SIZE_CHARS - SNAPSHOT_FIELD_RESERVE;
2023
+ while (currentSnapshot.turnOrder.length > 1 && serialized.length > sizeBudget) {
2016
2024
  const keptOrder = currentSnapshot.turnOrder.slice(1);
2017
2025
  const keptTurns = {};
2018
2026
  for (const id of keptOrder) {
@@ -2027,6 +2035,11 @@ function trimSnapshot(snapshot, maxTurns = MAX_SNAPSHOT_TURNS) {
2027
2035
  };
2028
2036
  serialized = JSON.stringify(currentSnapshot);
2029
2037
  }
2038
+ const droppedThisCall = originalLength - currentSnapshot.turnOrder.length;
2039
+ const truncatedTurns = (snapshot.truncatedTurns ?? 0) + droppedThisCall;
2040
+ if (truncatedTurns > 0 && currentSnapshot.truncatedTurns !== truncatedTurns) {
2041
+ currentSnapshot = { ...currentSnapshot, truncatedTurns };
2042
+ }
2030
2043
  return currentSnapshot;
2031
2044
  }
2032
2045
 
@@ -2562,6 +2575,155 @@ function releaseSessionSource(sessionId) {
2562
2575
  sessionSourceLocks.delete(sessionId);
2563
2576
  }
2564
2577
 
2578
+ // src/websocket.ts
2579
+ var import_ws = __toESM(require("ws"));
2580
+ var BACKPRESSURE_HIGH_WATER = 256 * 1024;
2581
+ var BACKPRESSURE_LOW_WATER = 64 * 1024;
2582
+ var MAX_PENDING_QUEUE = 500;
2583
+ var DRAIN_INTERVAL_MS = 50;
2584
+ function sanitizeForJson(str) {
2585
+ return str.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
2586
+ }
2587
+ var DANGEROUS_OSC_RE = /\x1b\](?:52|7|8);[^\x07\x1b]*(?:\x07|\x1b\\)/g;
2588
+ function stripDangerousEscapes(str) {
2589
+ return str.replace(DANGEROUS_OSC_RE, "");
2590
+ }
2591
+ var SECRET_PATTERNS = [
2592
+ /AKIA[0-9A-Z]{16}/g,
2593
+ // AWS access key
2594
+ /gh[ps]_[A-Za-z0-9_]{36,}/g,
2595
+ // GitHub token
2596
+ /sk_(?:live|test)_[A-Za-z0-9]{24,}/g,
2597
+ // Stripe secret key
2598
+ /eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g
2599
+ // JWT
2600
+ ];
2601
+ var redactionEnabled = true;
2602
+ function setRedactionEnabled(enabled) {
2603
+ redactionEnabled = enabled;
2604
+ }
2605
+ function redactSecrets(str) {
2606
+ if (!redactionEnabled) return str;
2607
+ let result = str;
2608
+ for (const pattern of SECRET_PATTERNS) {
2609
+ pattern.lastIndex = 0;
2610
+ result = result.replace(pattern, "[REDACTED]");
2611
+ }
2612
+ return result;
2613
+ }
2614
+ var streamingTails = /* @__PURE__ */ new Map();
2615
+ var MAX_STREAMING_TAIL = 1024;
2616
+ var SECRET_PREFIX_RE = /(?:AKIA[0-9A-Z]*|gh[ps]_[A-Za-z0-9_]*|sk_(?:live|test)_[A-Za-z0-9]*|eyJ[A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]*){0,2})$/;
2617
+ function redactStreamingChunk(sessionId, chunk) {
2618
+ if (!redactionEnabled) return chunk;
2619
+ const prevTail = streamingTails.get(sessionId) ?? "";
2620
+ const combined = prevTail + chunk;
2621
+ const redacted = redactSecrets(combined);
2622
+ const prefix = SECRET_PREFIX_RE.exec(redacted);
2623
+ if (prefix && prefix.index + prefix[0].length === redacted.length && redacted.length - prefix.index <= MAX_STREAMING_TAIL) {
2624
+ streamingTails.set(sessionId, redacted.slice(prefix.index));
2625
+ return redacted.slice(0, prefix.index);
2626
+ }
2627
+ streamingTails.delete(sessionId);
2628
+ return redacted;
2629
+ }
2630
+ function flushStreamingRedaction(sessionId) {
2631
+ const tail = streamingTails.get(sessionId) ?? "";
2632
+ streamingTails.delete(sessionId);
2633
+ return redactionEnabled ? redactSecrets(tail) : tail;
2634
+ }
2635
+ var pendingQueue = [];
2636
+ var drainTimer = null;
2637
+ var drainWs = null;
2638
+ var droppedFrames = 0;
2639
+ var PRESSURE_HIGH = 0.75;
2640
+ var PRESSURE_LOW = 0.25;
2641
+ var pressureIsHigh = false;
2642
+ function getQueuePressure() {
2643
+ const level = pendingQueue.length / MAX_PENDING_QUEUE;
2644
+ if (level >= PRESSURE_HIGH) pressureIsHigh = true;
2645
+ else if (level < PRESSURE_LOW) pressureIsHigh = false;
2646
+ return { level, isHigh: pressureIsHigh };
2647
+ }
2648
+ function getDroppedFrameCount() {
2649
+ return droppedFrames;
2650
+ }
2651
+ function drainQueue() {
2652
+ if (!drainWs || drainWs.readyState !== import_ws.default.OPEN) {
2653
+ stopDrainTimer();
2654
+ return;
2655
+ }
2656
+ while (pendingQueue.length > 0 && drainWs.bufferedAmount < BACKPRESSURE_LOW_WATER) {
2657
+ const frame = pendingQueue.shift();
2658
+ drainWs.send(frame);
2659
+ }
2660
+ if (pendingQueue.length === 0) {
2661
+ stopDrainTimer();
2662
+ }
2663
+ }
2664
+ function startDrainTimer(ws) {
2665
+ drainWs = ws;
2666
+ if (!drainTimer) {
2667
+ drainTimer = setInterval(drainQueue, DRAIN_INTERVAL_MS);
2668
+ }
2669
+ }
2670
+ function stopDrainTimer() {
2671
+ if (drainTimer) {
2672
+ clearInterval(drainTimer);
2673
+ drainTimer = null;
2674
+ }
2675
+ drainWs = null;
2676
+ }
2677
+ function sendToGroup(ws, group, data) {
2678
+ if (!group || ws.readyState !== import_ws.default.OPEN) return;
2679
+ const sanitized = sanitizePayload(data);
2680
+ const frame = JSON.stringify({
2681
+ type: "sendToGroup",
2682
+ group,
2683
+ dataType: "json",
2684
+ data: sanitized,
2685
+ noEcho: true
2686
+ });
2687
+ if (ws.bufferedAmount > BACKPRESSURE_HIGH_WATER) {
2688
+ if (pendingQueue.length >= MAX_PENDING_QUEUE) {
2689
+ pendingQueue.shift();
2690
+ droppedFrames++;
2691
+ if (droppedFrames % 100 === 1) {
2692
+ console.warn(`[rAgent] Backpressure: dropped ${droppedFrames} frames (queue full at ${MAX_PENDING_QUEUE})`);
2693
+ }
2694
+ }
2695
+ pendingQueue.push(frame);
2696
+ startDrainTimer(ws);
2697
+ return;
2698
+ }
2699
+ if (pendingQueue.length > 0) {
2700
+ pendingQueue.push(frame);
2701
+ drainWs = ws;
2702
+ drainQueue();
2703
+ return;
2704
+ }
2705
+ ws.send(frame);
2706
+ }
2707
+ function sanitizeValue(value) {
2708
+ if (typeof value === "string") {
2709
+ return redactSecrets(stripDangerousEscapes(sanitizeForJson(value)));
2710
+ }
2711
+ if (Array.isArray(value)) {
2712
+ return value.map((item) => sanitizeValue(item));
2713
+ }
2714
+ if (value !== null && typeof value === "object") {
2715
+ return sanitizePayload(value);
2716
+ }
2717
+ return value;
2718
+ }
2719
+ function sanitizePayload(obj) {
2720
+ const result = {};
2721
+ for (const [key, value] of Object.entries(obj)) {
2722
+ result[key] = sanitizeValue(value);
2723
+ }
2724
+ return result;
2725
+ }
2726
+
2565
2727
  // src/transcript-watcher.ts
2566
2728
  var log7 = createLogger("transcript-watcher");
2567
2729
  var cachedTicksPerSecond = null;
@@ -3056,7 +3218,7 @@ function getNewestPath(paths, options) {
3056
3218
  } catch {
3057
3219
  return null;
3058
3220
  }
3059
- }).filter((entry) => entry !== null).filter((entry) => options?.minMtimeMs === void 0 || entry.mtimeMs >= options.minMtimeMs).sort((a, b) => b.mtimeMs - a.mtimeMs);
3221
+ }).filter((entry) => entry !== null).filter((entry) => options?.minMtimeMs === void 0 || entry.mtimeMs >= options.minMtimeMs).filter((entry) => !options?.excludePaths?.has(entry.target)).sort((a, b) => b.mtimeMs - a.mtimeMs);
3060
3222
  return ranked[0]?.target ?? null;
3061
3223
  }
3062
3224
  function discoverTranscriptForPid(pid, agentType) {
@@ -3204,10 +3366,10 @@ function discoverViaProc(rootPid, agentType) {
3204
3366
  }
3205
3367
  return getNewestPath(candidates);
3206
3368
  }
3207
- function discoverViaProcessCwd(pid, minMtimeMs) {
3369
+ function discoverViaProcessCwd(pid, minMtimeMs, excludePaths) {
3208
3370
  try {
3209
3371
  const processCwd = fs2.readlinkSync(`/proc/${pid}/cwd`);
3210
- return processCwd ? discoverViaCwd(processCwd, pid, minMtimeMs) : null;
3372
+ return processCwd ? discoverViaCwd(processCwd, pid, minMtimeMs, excludePaths) : null;
3211
3373
  } catch {
3212
3374
  return null;
3213
3375
  }
@@ -3235,7 +3397,7 @@ function resolveUserHome(pid) {
3235
3397
  return null;
3236
3398
  }
3237
3399
  }
3238
- function discoverCodexTranscriptFromHome(pid, minMtimeMs) {
3400
+ function discoverCodexTranscriptFromHome(pid, minMtimeMs, excludePaths) {
3239
3401
  const userHome = resolveUserHome(pid);
3240
3402
  if (!userHome) return null;
3241
3403
  const sessionsDir = path2.join(userHome, ".codex", "sessions");
@@ -3262,9 +3424,9 @@ function discoverCodexTranscriptFromHome(pid, minMtimeMs) {
3262
3424
  }
3263
3425
  }
3264
3426
  }
3265
- return getNewestPath(matches, { minMtimeMs });
3427
+ return getNewestPath(matches, { minMtimeMs, excludePaths });
3266
3428
  }
3267
- function discoverGeminiTranscriptFromHome(pid) {
3429
+ function discoverGeminiTranscriptFromHome(pid, minMtimeMs, excludePaths) {
3268
3430
  const userHome = resolveUserHome(pid);
3269
3431
  if (!userHome) return null;
3270
3432
  const tmpDir = path2.join(userHome, ".gemini", "tmp");
@@ -3291,9 +3453,9 @@ function discoverGeminiTranscriptFromHome(pid) {
3291
3453
  }
3292
3454
  }
3293
3455
  }
3294
- return getNewestPath(matches);
3456
+ return getNewestPath(matches, { minMtimeMs, excludePaths });
3295
3457
  }
3296
- function discoverAntigravityTranscriptFromHome(pid, minMtimeMs) {
3458
+ function discoverAntigravityTranscriptFromHome(pid, minMtimeMs, excludePaths) {
3297
3459
  const userHome = resolveUserHome(pid);
3298
3460
  if (!userHome) return null;
3299
3461
  const brainDir = path2.join(userHome, ".gemini", "antigravity-cli", "brain");
@@ -3311,9 +3473,9 @@ function discoverAntigravityTranscriptFromHome(pid, minMtimeMs) {
3311
3473
  }
3312
3474
  } catch {
3313
3475
  }
3314
- return getNewestPath(matches, { minMtimeMs });
3476
+ return getNewestPath(matches, { minMtimeMs, excludePaths });
3315
3477
  }
3316
- function discoverAntigravityViaCwd(paneCwd, pid) {
3478
+ function discoverAntigravityViaCwd(paneCwd, pid, excludePaths) {
3317
3479
  const userHome = resolveUserHome(pid);
3318
3480
  if (!userHome) return null;
3319
3481
  const historyFile = path2.join(userHome, ".gemini", "antigravity-cli", "history.jsonl");
@@ -3346,7 +3508,7 @@ function discoverAntigravityViaCwd(paneCwd, pid) {
3346
3508
  "logs",
3347
3509
  "transcript.jsonl"
3348
3510
  );
3349
- if (fs2.existsSync(transcriptPath)) {
3511
+ if (fs2.existsSync(transcriptPath) && !excludePaths?.has(transcriptPath)) {
3350
3512
  return transcriptPath;
3351
3513
  }
3352
3514
  }
@@ -3359,7 +3521,7 @@ function discoverAntigravityViaCwd(paneCwd, pid) {
3359
3521
  }
3360
3522
  return null;
3361
3523
  }
3362
- function discoverViaCwd(paneCwd, pid, minMtimeMs) {
3524
+ function discoverViaCwd(paneCwd, pid, minMtimeMs, excludePaths) {
3363
3525
  const userHome = resolveUserHome(pid);
3364
3526
  if (!userHome) return null;
3365
3527
  const claudeProjectsDir = path2.join(userHome, ".claude", "projects");
@@ -3373,60 +3535,67 @@ function discoverViaCwd(paneCwd, pid, minMtimeMs) {
3373
3535
  if (projectDirs.length === 0) return null;
3374
3536
  try {
3375
3537
  const jsonlFiles = projectDirs.flatMap((projectDir) => fs2.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => path2.join(projectDir, f)));
3376
- return getNewestPath(jsonlFiles, { minMtimeMs });
3538
+ return getNewestPath(jsonlFiles, { minMtimeMs, excludePaths });
3377
3539
  } catch {
3378
3540
  return null;
3379
3541
  }
3380
3542
  }
3381
- function discoverTranscriptFile(sessionId, agentType, forceRefresh = false) {
3543
+ function discoverTranscriptFile(sessionId, agentType, forceRefresh = false, excludePaths) {
3382
3544
  const now2 = Date.now();
3383
3545
  const cacheKey = `${sessionId}:${agentType || ""}`;
3384
- if (!forceRefresh) {
3546
+ const exclusionScoped = Boolean(excludePaths && excludePaths.size > 0);
3547
+ if (!forceRefresh && !exclusionScoped) {
3385
3548
  const cached = transcriptDiscoveryCache.get(cacheKey);
3386
3549
  if (cached && cached.expiresAt > now2) {
3387
3550
  return cached.value;
3388
3551
  }
3389
3552
  }
3390
- const result = discoverTranscriptFileImpl(sessionId, agentType);
3553
+ const { path: result, viaProc } = discoverTranscriptFileImpl(sessionId, agentType, excludePaths);
3391
3554
  const isProcessSession = sessionId.startsWith("process:");
3392
- const ttl = result ? isProcessSession ? 3600 * 1e3 : 5e3 : 5e3;
3393
- transcriptDiscoveryCache.set(cacheKey, {
3394
- value: result,
3395
- expiresAt: Date.now() + ttl
3396
- });
3555
+ const ttl = result ? isProcessSession && viaProc ? 3600 * 1e3 : 5e3 : 5e3;
3556
+ if (!exclusionScoped) {
3557
+ transcriptDiscoveryCache.set(cacheKey, {
3558
+ value: result,
3559
+ expiresAt: Date.now() + ttl
3560
+ });
3561
+ }
3397
3562
  return result;
3398
3563
  }
3399
- function discoverTranscriptFileImpl(sessionId, agentType) {
3564
+ function discoverTranscriptFileDetailed(sessionId, agentType, excludePaths) {
3565
+ return discoverTranscriptFileImpl(sessionId, agentType, excludePaths);
3566
+ }
3567
+ function discoverTranscriptFileImpl(sessionId, agentType, excludePaths) {
3400
3568
  const processPid = parseProcessPid(sessionId);
3401
3569
  if (processPid) {
3402
3570
  const t0 = performance.now();
3403
3571
  const procResult = discoverViaProc(processPid, agentType);
3404
3572
  if (procResult) {
3405
3573
  console.log(`[DISCOVER] discoverViaProc found file for PID ${processPid} in ${(performance.now() - t0).toFixed(2)} ms`);
3406
- return procResult;
3574
+ return { path: procResult, viaProc: true };
3407
3575
  }
3408
3576
  if (agentType === "Claude Code") {
3409
3577
  const t1 = performance.now();
3410
3578
  const startMs = getProcessStartEpochMs(processPid);
3411
- const cwdResult = discoverViaProcessCwd(processPid, startMs ? startMs - 6e4 : void 0);
3579
+ const cwdResult = discoverViaProcessCwd(processPid, startMs ? startMs - 6e4 : void 0, excludePaths);
3412
3580
  console.log(`[DISCOVER] Claude Code discovery for PID ${processPid} took ${(performance.now() - t1).toFixed(2)} ms`);
3413
- if (cwdResult) return cwdResult;
3581
+ if (cwdResult) return { path: cwdResult, viaProc: false };
3414
3582
  }
3415
3583
  if (agentType === "Codex CLI") {
3416
3584
  const t1 = performance.now();
3417
3585
  const startMs = getProcessStartEpochMs(processPid);
3418
3586
  const command = getProcessCommand(processPid);
3419
3587
  if (commandMatchesAgent(command, agentType)) {
3420
- const codexResult = discoverCodexTranscriptFromHome(processPid, startMs ? startMs - 6e4 : void 0);
3588
+ const codexResult = discoverCodexTranscriptFromHome(processPid, startMs ? startMs - 6e4 : void 0, excludePaths);
3421
3589
  console.log(`[DISCOVER] Codex CLI discovery for PID ${processPid} took ${(performance.now() - t1).toFixed(2)} ms`);
3422
- if (codexResult) return codexResult;
3590
+ if (codexResult) return { path: codexResult, viaProc: false };
3423
3591
  }
3424
3592
  }
3425
3593
  if (agentType === "Gemini CLI") {
3426
3594
  const t1 = performance.now();
3427
- const geminiResult = discoverGeminiTranscriptFromHome(processPid);
3595
+ const startMs = getProcessStartEpochMs(processPid);
3596
+ const geminiResult = discoverGeminiTranscriptFromHome(processPid, startMs ? startMs - 6e4 : void 0, excludePaths);
3428
3597
  console.log(`[DISCOVER] Gemini CLI discovery for PID ${processPid} took ${(performance.now() - t1).toFixed(2)} ms`);
3429
- if (geminiResult) return geminiResult;
3598
+ if (geminiResult) return { path: geminiResult, viaProc: false };
3430
3599
  }
3431
3600
  if (agentType === "Antigravity CLI") {
3432
3601
  const t1 = performance.now();
@@ -3434,23 +3603,23 @@ function discoverTranscriptFileImpl(sessionId, agentType) {
3434
3603
  try {
3435
3604
  const processCwd = fs2.readlinkSync(`/proc/${processPid}/cwd`);
3436
3605
  if (processCwd) {
3437
- const cwdResult = discoverAntigravityViaCwd(processCwd, processPid);
3606
+ const cwdResult = discoverAntigravityViaCwd(processCwd, processPid, excludePaths);
3438
3607
  if (cwdResult) {
3439
3608
  console.log(`[DISCOVER] Antigravity CLI discovery (CWD path) for PID ${processPid} took ${(performance.now() - t1).toFixed(2)} ms`);
3440
- return cwdResult;
3609
+ return { path: cwdResult, viaProc: false };
3441
3610
  }
3442
3611
  }
3443
3612
  } catch {
3444
3613
  }
3445
- const antigravityResult = discoverAntigravityTranscriptFromHome(processPid, startMs ? startMs - 6e4 : void 0);
3614
+ const antigravityResult = discoverAntigravityTranscriptFromHome(processPid, startMs ? startMs - 6e4 : void 0, excludePaths);
3446
3615
  console.log(`[DISCOVER] Antigravity CLI discovery (home path) for PID ${processPid} took ${(performance.now() - t1).toFixed(2)} ms`);
3447
- if (antigravityResult) return antigravityResult;
3616
+ if (antigravityResult) return { path: antigravityResult, viaProc: false };
3448
3617
  }
3449
- return null;
3618
+ return { path: null, viaProc: false };
3450
3619
  }
3451
3620
  if (sessionId.startsWith("tmux:")) {
3452
3621
  const paneTarget = sessionId.slice("tmux:".length);
3453
- if (!paneTarget) return null;
3622
+ if (!paneTarget) return { path: null, viaProc: false };
3454
3623
  const cleanEnv = { ...process.env };
3455
3624
  delete cleanEnv.TMUX;
3456
3625
  delete cleanEnv.TMUX_PANE;
@@ -3465,7 +3634,7 @@ function discoverTranscriptFileImpl(sessionId, agentType) {
3465
3634
  if (parsedPanePid > 0) {
3466
3635
  panePid = parsedPanePid;
3467
3636
  const procResult = discoverViaProc(panePid, agentType);
3468
- if (procResult) return procResult;
3637
+ if (procResult) return { path: procResult, viaProc: true };
3469
3638
  }
3470
3639
  } catch {
3471
3640
  }
@@ -3476,9 +3645,10 @@ function discoverTranscriptFileImpl(sessionId, agentType) {
3476
3645
  const cwdResult = discoverViaCwd(
3477
3646
  info.cwd,
3478
3647
  info.pid,
3479
- info.startEpochMs ? info.startEpochMs - 6e4 : void 0
3648
+ info.startEpochMs ? info.startEpochMs - 6e4 : void 0,
3649
+ excludePaths
3480
3650
  );
3481
- if (cwdResult) return cwdResult;
3651
+ if (cwdResult) return { path: cwdResult, viaProc: false };
3482
3652
  }
3483
3653
  try {
3484
3654
  const paneCwd = (0, import_child_process2.execFileSync)(
@@ -3486,7 +3656,10 @@ function discoverTranscriptFileImpl(sessionId, agentType) {
3486
3656
  ["display-message", "-t", paneTarget, "-p", "#{pane_current_path}"],
3487
3657
  { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
3488
3658
  ).trim();
3489
- if (paneCwd) return discoverViaCwd(paneCwd, panePid ?? void 0);
3659
+ if (paneCwd) {
3660
+ const cwdResult = discoverViaCwd(paneCwd, panePid ?? void 0, void 0, excludePaths);
3661
+ if (cwdResult) return { path: cwdResult, viaProc: false };
3662
+ }
3490
3663
  } catch {
3491
3664
  }
3492
3665
  }
@@ -3495,20 +3668,23 @@ function discoverTranscriptFileImpl(sessionId, agentType) {
3495
3668
  const newestForAgent = getNewestPath(
3496
3669
  agentInfos.map((info) => discoverCodexTranscriptFromHome(
3497
3670
  info.pid,
3498
- info.startEpochMs ? info.startEpochMs - 6e4 : void 0
3499
- )).filter((value) => Boolean(value))
3671
+ info.startEpochMs ? info.startEpochMs - 6e4 : void 0,
3672
+ excludePaths
3673
+ )).filter((value) => Boolean(value)),
3674
+ { excludePaths }
3500
3675
  );
3501
- if (newestForAgent) return newestForAgent;
3676
+ if (newestForAgent) return { path: newestForAgent, viaProc: false };
3502
3677
  }
3503
3678
  if (agentType === "Gemini CLI") {
3504
- return discoverGeminiTranscriptFromHome(panePid ?? void 0);
3679
+ const geminiResult = discoverGeminiTranscriptFromHome(panePid ?? void 0, void 0, excludePaths);
3680
+ if (geminiResult) return { path: geminiResult, viaProc: false };
3505
3681
  }
3506
3682
  if (agentType === "Antigravity CLI") {
3507
3683
  const agentInfos = panePid ? findAgentProcessInfos(panePid, agentType) : [];
3508
3684
  for (const info of agentInfos) {
3509
3685
  if (info.cwd) {
3510
- const cwdResult = discoverAntigravityViaCwd(info.cwd, info.pid);
3511
- if (cwdResult) return cwdResult;
3686
+ const cwdResult = discoverAntigravityViaCwd(info.cwd, info.pid, excludePaths);
3687
+ if (cwdResult) return { path: cwdResult, viaProc: false };
3512
3688
  }
3513
3689
  }
3514
3690
  try {
@@ -3518,28 +3694,31 @@ function discoverTranscriptFileImpl(sessionId, agentType) {
3518
3694
  { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
3519
3695
  ).trim();
3520
3696
  if (paneCwd) {
3521
- const cwdResult = discoverAntigravityViaCwd(paneCwd, panePid ?? void 0);
3522
- if (cwdResult) return cwdResult;
3697
+ const cwdResult = discoverAntigravityViaCwd(paneCwd, panePid ?? void 0, excludePaths);
3698
+ if (cwdResult) return { path: cwdResult, viaProc: false };
3523
3699
  }
3524
3700
  } catch {
3525
3701
  }
3526
3702
  const newestForAgent = getNewestPath(
3527
3703
  agentInfos.map((info) => discoverAntigravityTranscriptFromHome(
3528
3704
  info.pid,
3529
- info.startEpochMs ? info.startEpochMs - 6e4 : void 0
3530
- )).filter((value) => Boolean(value))
3705
+ info.startEpochMs ? info.startEpochMs - 6e4 : void 0,
3706
+ excludePaths
3707
+ )).filter((value) => Boolean(value)),
3708
+ { excludePaths }
3531
3709
  );
3532
- if (newestForAgent) return newestForAgent;
3710
+ if (newestForAgent) return { path: newestForAgent, viaProc: false };
3533
3711
  }
3534
- return null;
3712
+ return { path: null, viaProc: false };
3535
3713
  }
3536
- return null;
3714
+ return { path: null, viaProc: false };
3537
3715
  }
3538
3716
  var MAX_PARTIAL_BUFFER = 256 * 1024;
3539
3717
  var MAX_REPLAY_TURNS = 1e3;
3540
3718
  var POLL_INTERVAL_MS = 800;
3541
3719
  var DOC_READ_DEBOUNCE_MS = 200;
3542
3720
  var REDISCOVERY_INTERVAL_MS = 5e3;
3721
+ var MARKDOWN_RESYNC_DRAIN_MS = 300;
3543
3722
  var ENABLE_RETRY_DELAYS_MS = [1e3, 2e3, 5e3];
3544
3723
  var TranscriptWatcher = class {
3545
3724
  filePath;
@@ -3662,6 +3841,9 @@ var TranscriptWatcher = class {
3662
3841
  return;
3663
3842
  }
3664
3843
  const ops = this.parserV2.parseDocument(content);
3844
+ if (ops.length === 0 && this.snapshot.turnOrder.length > 0) {
3845
+ return;
3846
+ }
3665
3847
  let nextSnapshot = createEmptySnapshot(this.snapshot.sessionId);
3666
3848
  if (ops.length > 0) {
3667
3849
  nextSnapshot = applyOps(nextSnapshot, ops);
@@ -3752,6 +3934,12 @@ var TranscriptWatcher = class {
3752
3934
  }
3753
3935
  this.documentFingerprint = fingerprint;
3754
3936
  const parsedTurns = this.parser.parseDocument?.(content) ?? [];
3937
+ const v2Ops = this.parserV2?.parseDocument?.(content) ?? [];
3938
+ const parsedNothing = parsedTurns.length === 0 && v2Ops.length === 0;
3939
+ const haveContent = this.turns.length > 0 || this.snapshot.turnOrder.length > 0;
3940
+ if (parsedNothing && haveContent) {
3941
+ return;
3942
+ }
3755
3943
  this.seq++;
3756
3944
  this.turns = parsedTurns.slice(-MAX_REPLAY_TURNS).map((turn) => ({
3757
3945
  turnId: turn.turnId,
@@ -3762,10 +3950,9 @@ var TranscriptWatcher = class {
3762
3950
  }));
3763
3951
  this.callbacks.onTurnsSnapshot?.(this.turns, this.seq);
3764
3952
  if (this.parserV2?.parseDocument && this.callbacks.onSnapshot) {
3765
- const ops = this.parserV2.parseDocument(content);
3766
3953
  let nextSnapshot = createEmptySnapshot(this.snapshot.sessionId);
3767
- if (ops.length > 0) {
3768
- nextSnapshot = applyOps(nextSnapshot, ops);
3954
+ if (v2Ops.length > 0) {
3955
+ nextSnapshot = applyOps(nextSnapshot, v2Ops);
3769
3956
  }
3770
3957
  this.snapshot = trimSnapshot(nextSnapshot);
3771
3958
  this.callbacks.onSnapshot(this.snapshot, this.seq);
@@ -3825,6 +4012,9 @@ function getParser(agentType) {
3825
4012
  var TranscriptWatcherManager = class {
3826
4013
  active = /* @__PURE__ */ new Map();
3827
4014
  rediscoveryTimers = /* @__PURE__ */ new Map();
4015
+ /** #542: sessions awaiting a markdown snapshot re-emit after a frame drop. */
4016
+ pendingMarkdownResync = /* @__PURE__ */ new Set();
4017
+ markdownResyncTimer;
3828
4018
  /** Pending retry timer per session for #483 (no_transcript / watch_failed). */
3829
4019
  enableRetryTimers = /* @__PURE__ */ new Map();
3830
4020
  /** Index into ENABLE_RETRY_DELAYS_MS for the next scheduled attempt. */
@@ -3865,7 +4055,11 @@ var TranscriptWatcherManager = class {
3865
4055
  const existing = this.active.get(sessionId);
3866
4056
  let replacingExisting = false;
3867
4057
  if (existing) {
3868
- const newPath = discoverTranscriptFile(sessionId, agentType, true);
4058
+ const newPath = discoverTranscriptFileDetailed(
4059
+ sessionId,
4060
+ agentType,
4061
+ this.ownedPathsExcept(sessionId)
4062
+ ).path;
3869
4063
  const agentChanged = existing.agentType !== requestedAgentType;
3870
4064
  const pathChanged = Boolean(newPath && newPath !== existing.filePath);
3871
4065
  const existingPathGone = !fs2.existsSync(existing.filePath);
@@ -3890,7 +4084,11 @@ var TranscriptWatcherManager = class {
3890
4084
  this.cancelEnableRetry(sessionId);
3891
4085
  return { ok: false, reason: "unsupported_agent" };
3892
4086
  }
3893
- const filePath = discoverTranscriptFile(sessionId, agentType, isRetry || replacingExisting);
4087
+ const filePath = discoverTranscriptFileDetailed(
4088
+ sessionId,
4089
+ agentType,
4090
+ this.ownedPathsExcept(sessionId)
4091
+ ).path;
3894
4092
  if (!filePath) {
3895
4093
  log7.info("no transcript file found", { sessionId });
3896
4094
  this.armEnableRetry(sessionId, agentType);
@@ -3990,6 +4188,22 @@ var TranscriptWatcherManager = class {
3990
4188
  this.sendV2SnapshotFn(sessionId, createEmptySnapshot(sessionId), 0);
3991
4189
  }
3992
4190
  }
4191
+ /**
4192
+ * Paths currently bound to OTHER live sessions. Passed to discovery so two
4193
+ * sessions in one folder (or two same-tool sessions) never resolve to the
4194
+ * same transcript via newest-mtime. The session's own file is never excluded
4195
+ * (so it can keep/rebind it); /proc fd discovery ignores this set entirely.
4196
+ * #540.
4197
+ */
4198
+ ownedPathsExcept(sessionId) {
4199
+ const owned = /* @__PURE__ */ new Set();
4200
+ for (const [sid, session] of this.active) {
4201
+ if (sid !== sessionId && session.filePath) {
4202
+ owned.add(session.filePath);
4203
+ }
4204
+ }
4205
+ return owned;
4206
+ }
3993
4207
  /** Disable markdown streaming for a session. */
3994
4208
  disableMarkdown(sessionId) {
3995
4209
  this.cancelEnableRetry(sessionId);
@@ -4005,6 +4219,7 @@ var TranscriptWatcherManager = class {
4005
4219
  }
4006
4220
  this.active.delete(sessionId);
4007
4221
  this.stopRediscovery(sessionId);
4222
+ this.pendingMarkdownResync.delete(sessionId);
4008
4223
  log7.info("stopped watching transcript", { sessionId });
4009
4224
  }
4010
4225
  /** Periodically check if the transcript file has changed (new conversation). */
@@ -4016,7 +4231,11 @@ var TranscriptWatcherManager = class {
4016
4231
  this.stopRediscovery(sessionId);
4017
4232
  return;
4018
4233
  }
4019
- const newPath = discoverTranscriptFile(sessionId, agentType, true);
4234
+ const { path: newPath } = discoverTranscriptFileDetailed(
4235
+ sessionId,
4236
+ agentType,
4237
+ this.ownedPathsExcept(sessionId)
4238
+ );
4020
4239
  if (!newPath || newPath === session.filePath) return;
4021
4240
  log7.info("transcript file changed", { sessionId, newPath });
4022
4241
  session.watcher.stop();
@@ -4078,8 +4297,62 @@ var TranscriptWatcherManager = class {
4078
4297
  this.sendV2SnapshotFn(sessionId, snapshot, session.watcher.currentSeq);
4079
4298
  }
4080
4299
  }
4300
+ /**
4301
+ * #542: re-emit markdown snapshots after the WS dropped frames under
4302
+ * backpressure. The large V2 snapshot is sent as a single un-chunked frame
4303
+ * and is the prime drop victim, but — unlike tmux-pipe terminal streams,
4304
+ * which resync via capture-pane — the markdown channel had no recovery path,
4305
+ * so a dropped snapshot left the read view permanently partial until a
4306
+ * reconnect. Re-emit the V2 snapshot for every session that currently holds
4307
+ * content, draining one per tick so the (large) snapshots don't immediately
4308
+ * re-saturate the queue.
4309
+ */
4310
+ resyncMarkdownAfterDrop() {
4311
+ if (!this.sendV2SnapshotFn) return;
4312
+ let queued = false;
4313
+ for (const [sessionId, session] of this.active) {
4314
+ if (session.watcher.getSnapshot().turnOrder.length > 0) {
4315
+ this.pendingMarkdownResync.add(sessionId);
4316
+ queued = true;
4317
+ }
4318
+ }
4319
+ if (!queued || this.markdownResyncTimer) return;
4320
+ this.markdownResyncTimer = setInterval(() => {
4321
+ if (this.pendingMarkdownResync.size === 0) {
4322
+ this.stopMarkdownResync();
4323
+ return;
4324
+ }
4325
+ if (getQueuePressure().isHigh) return;
4326
+ const next = this.pendingMarkdownResync.values().next();
4327
+ if (next.done) {
4328
+ this.stopMarkdownResync();
4329
+ return;
4330
+ }
4331
+ const sessionId = next.value;
4332
+ this.pendingMarkdownResync.delete(sessionId);
4333
+ const session = this.active.get(sessionId);
4334
+ if (session && this.sendV2SnapshotFn) {
4335
+ this.sendV2SnapshotFn(
4336
+ sessionId,
4337
+ session.watcher.getSnapshot(),
4338
+ session.watcher.currentSeq
4339
+ );
4340
+ }
4341
+ }, MARKDOWN_RESYNC_DRAIN_MS);
4342
+ if (typeof this.markdownResyncTimer.unref === "function") {
4343
+ this.markdownResyncTimer.unref();
4344
+ }
4345
+ }
4346
+ stopMarkdownResync() {
4347
+ if (this.markdownResyncTimer) {
4348
+ clearInterval(this.markdownResyncTimer);
4349
+ this.markdownResyncTimer = void 0;
4350
+ }
4351
+ this.pendingMarkdownResync.clear();
4352
+ }
4081
4353
  /** Stop all watchers. */
4082
4354
  stopAll() {
4355
+ this.stopMarkdownResync();
4083
4356
  for (const [sessionId, session] of this.active) {
4084
4357
  session.watcher.stop();
4085
4358
  this.stopRediscovery(sessionId);
@@ -10575,155 +10848,6 @@ var OutputBuffer = class {
10575
10848
  }
10576
10849
  };
10577
10850
 
10578
- // src/websocket.ts
10579
- var import_ws = __toESM(require("ws"));
10580
- var BACKPRESSURE_HIGH_WATER = 256 * 1024;
10581
- var BACKPRESSURE_LOW_WATER = 64 * 1024;
10582
- var MAX_PENDING_QUEUE = 500;
10583
- var DRAIN_INTERVAL_MS = 50;
10584
- function sanitizeForJson(str) {
10585
- return str.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
10586
- }
10587
- var DANGEROUS_OSC_RE = /\x1b\](?:52|7|8);[^\x07\x1b]*(?:\x07|\x1b\\)/g;
10588
- function stripDangerousEscapes(str) {
10589
- return str.replace(DANGEROUS_OSC_RE, "");
10590
- }
10591
- var SECRET_PATTERNS = [
10592
- /AKIA[0-9A-Z]{16}/g,
10593
- // AWS access key
10594
- /gh[ps]_[A-Za-z0-9_]{36,}/g,
10595
- // GitHub token
10596
- /sk_(?:live|test)_[A-Za-z0-9]{24,}/g,
10597
- // Stripe secret key
10598
- /eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g
10599
- // JWT
10600
- ];
10601
- var redactionEnabled = true;
10602
- function setRedactionEnabled(enabled) {
10603
- redactionEnabled = enabled;
10604
- }
10605
- function redactSecrets(str) {
10606
- if (!redactionEnabled) return str;
10607
- let result = str;
10608
- for (const pattern of SECRET_PATTERNS) {
10609
- pattern.lastIndex = 0;
10610
- result = result.replace(pattern, "[REDACTED]");
10611
- }
10612
- return result;
10613
- }
10614
- var streamingTails = /* @__PURE__ */ new Map();
10615
- var MAX_STREAMING_TAIL = 1024;
10616
- var SECRET_PREFIX_RE = /(?:AKIA[0-9A-Z]*|gh[ps]_[A-Za-z0-9_]*|sk_(?:live|test)_[A-Za-z0-9]*|eyJ[A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]*){0,2})$/;
10617
- function redactStreamingChunk(sessionId, chunk) {
10618
- if (!redactionEnabled) return chunk;
10619
- const prevTail = streamingTails.get(sessionId) ?? "";
10620
- const combined = prevTail + chunk;
10621
- const redacted = redactSecrets(combined);
10622
- const prefix = SECRET_PREFIX_RE.exec(redacted);
10623
- if (prefix && prefix.index + prefix[0].length === redacted.length && redacted.length - prefix.index <= MAX_STREAMING_TAIL) {
10624
- streamingTails.set(sessionId, redacted.slice(prefix.index));
10625
- return redacted.slice(0, prefix.index);
10626
- }
10627
- streamingTails.delete(sessionId);
10628
- return redacted;
10629
- }
10630
- function flushStreamingRedaction(sessionId) {
10631
- const tail = streamingTails.get(sessionId) ?? "";
10632
- streamingTails.delete(sessionId);
10633
- return redactionEnabled ? redactSecrets(tail) : tail;
10634
- }
10635
- var pendingQueue = [];
10636
- var drainTimer = null;
10637
- var drainWs = null;
10638
- var droppedFrames = 0;
10639
- var PRESSURE_HIGH = 0.75;
10640
- var PRESSURE_LOW = 0.25;
10641
- var pressureIsHigh = false;
10642
- function getQueuePressure() {
10643
- const level = pendingQueue.length / MAX_PENDING_QUEUE;
10644
- if (level >= PRESSURE_HIGH) pressureIsHigh = true;
10645
- else if (level < PRESSURE_LOW) pressureIsHigh = false;
10646
- return { level, isHigh: pressureIsHigh };
10647
- }
10648
- function getDroppedFrameCount() {
10649
- return droppedFrames;
10650
- }
10651
- function drainQueue() {
10652
- if (!drainWs || drainWs.readyState !== import_ws.default.OPEN) {
10653
- stopDrainTimer();
10654
- return;
10655
- }
10656
- while (pendingQueue.length > 0 && drainWs.bufferedAmount < BACKPRESSURE_LOW_WATER) {
10657
- const frame = pendingQueue.shift();
10658
- drainWs.send(frame);
10659
- }
10660
- if (pendingQueue.length === 0) {
10661
- stopDrainTimer();
10662
- }
10663
- }
10664
- function startDrainTimer(ws) {
10665
- drainWs = ws;
10666
- if (!drainTimer) {
10667
- drainTimer = setInterval(drainQueue, DRAIN_INTERVAL_MS);
10668
- }
10669
- }
10670
- function stopDrainTimer() {
10671
- if (drainTimer) {
10672
- clearInterval(drainTimer);
10673
- drainTimer = null;
10674
- }
10675
- drainWs = null;
10676
- }
10677
- function sendToGroup(ws, group, data) {
10678
- if (!group || ws.readyState !== import_ws.default.OPEN) return;
10679
- const sanitized = sanitizePayload(data);
10680
- const frame = JSON.stringify({
10681
- type: "sendToGroup",
10682
- group,
10683
- dataType: "json",
10684
- data: sanitized,
10685
- noEcho: true
10686
- });
10687
- if (ws.bufferedAmount > BACKPRESSURE_HIGH_WATER) {
10688
- if (pendingQueue.length >= MAX_PENDING_QUEUE) {
10689
- pendingQueue.shift();
10690
- droppedFrames++;
10691
- if (droppedFrames % 100 === 1) {
10692
- console.warn(`[rAgent] Backpressure: dropped ${droppedFrames} frames (queue full at ${MAX_PENDING_QUEUE})`);
10693
- }
10694
- }
10695
- pendingQueue.push(frame);
10696
- startDrainTimer(ws);
10697
- return;
10698
- }
10699
- if (pendingQueue.length > 0) {
10700
- pendingQueue.push(frame);
10701
- drainWs = ws;
10702
- drainQueue();
10703
- return;
10704
- }
10705
- ws.send(frame);
10706
- }
10707
- function sanitizeValue(value) {
10708
- if (typeof value === "string") {
10709
- return redactSecrets(stripDangerousEscapes(sanitizeForJson(value)));
10710
- }
10711
- if (Array.isArray(value)) {
10712
- return value.map((item) => sanitizeValue(item));
10713
- }
10714
- if (value !== null && typeof value === "object") {
10715
- return sanitizePayload(value);
10716
- }
10717
- return value;
10718
- }
10719
- function sanitizePayload(obj) {
10720
- const result = {};
10721
- for (const [key, value] of Object.entries(obj)) {
10722
- result[key] = sanitizeValue(value);
10723
- }
10724
- return result;
10725
- }
10726
-
10727
10851
  // src/session-streamer.ts
10728
10852
  var import_node_child_process2 = require("child_process");
10729
10853
  var import_node_fs = require("fs");
@@ -10838,6 +10962,10 @@ var SessionStreamer = class {
10838
10962
  sendFn;
10839
10963
  onStreamStopped;
10840
10964
  sendResetFn;
10965
+ /** #542: invoked once when a new backpressure frame-drop is detected, so the
10966
+ * markdown channel can recover (the terminal channel resyncs via tmux-pipe
10967
+ * capture, but the markdown snapshot had no recovery path). */
10968
+ onFramesDropped;
10841
10969
  /**
10842
10970
  * Per-session ring buffer for output captured while the WebSocket is disconnected.
10843
10971
  * Each entry is a string chunk; oldest entries are evicted when the total byte
@@ -10851,10 +10979,11 @@ var SessionStreamer = class {
10851
10979
  lastSeenDroppedFrames = 0;
10852
10980
  /** Sessions whose viewers need a capture-pane resync after frame drops. */
10853
10981
  pendingDropResync = /* @__PURE__ */ new Set();
10854
- constructor(sendFn, onStreamStopped, sendResetFn) {
10982
+ constructor(sendFn, onStreamStopped, sendResetFn, onFramesDropped) {
10855
10983
  this.sendFn = sendFn;
10856
10984
  this.onStreamStopped = onStreamStopped;
10857
10985
  this.sendResetFn = sendResetFn;
10986
+ this.onFramesDropped = onFramesDropped;
10858
10987
  this.cleanupStaleStreams();
10859
10988
  this.startBackpressureResumeTimer();
10860
10989
  }
@@ -10898,6 +11027,7 @@ var SessionStreamer = class {
10898
11027
  droppedTotal: dropped,
10899
11028
  streams: this.pendingDropResync.size
10900
11029
  });
11030
+ this.onFramesDropped?.();
10901
11031
  }
10902
11032
  const next = this.pendingDropResync.values().next();
10903
11033
  if (!next.done) {
@@ -11136,6 +11266,33 @@ var SessionStreamer = class {
11136
11266
  this.disconnectBufferBytes.delete(sessionId);
11137
11267
  return result;
11138
11268
  }
11269
+ /**
11270
+ * #556: recover active streams after a relay reconnect.
11271
+ *
11272
+ * For tmux-pipe streams the disconnect-buffered deltas are stale AND
11273
+ * redundant: the portal requests a capture-pane resync on reconnect, and the
11274
+ * capture reflects everything tmux applied to the pane during the disconnect.
11275
+ * Replaying the buffered deltas instead (a) paints stale bytes over the fresh
11276
+ * capture → interleaved garble, and (b) re-floods the relay immediately after
11277
+ * a reconnect, helping trigger Azure's next close — the reconnect churn loop
11278
+ * behind the terminal artifacting. So we DISCARD the buffer and resync.
11279
+ *
11280
+ * pty-attach / process-trace streams have no capture-pane resync, so their
11281
+ * buffered output IS replayed via `replay`.
11282
+ */
11283
+ recoverStreamsAfterReconnect(replay) {
11284
+ for (const stream of this.active.values()) {
11285
+ if (stream.stopped) continue;
11286
+ if (stream.streamType === "tmux-pipe") {
11287
+ this.drainBuffer(stream.sessionId);
11288
+ this.resyncStream(stream.sessionId);
11289
+ } else {
11290
+ for (const chunk of this.drainBuffer(stream.sessionId)) {
11291
+ replay(stream.sessionId, chunk);
11292
+ }
11293
+ }
11294
+ }
11295
+ }
11139
11296
  /**
11140
11297
  * Re-send capture-pane data for an already-active tmux stream.
11141
11298
  * Used when a viewer missed the initial burst (e.g. joinGroup race),
@@ -12121,6 +12278,7 @@ var InventoryManager = class {
12121
12278
 
12122
12279
  // src/connection-manager.ts
12123
12280
  var import_ws3 = __toESM(require("ws"));
12281
+ var import_node_crypto2 = require("crypto");
12124
12282
  var log21 = createLogger("connection");
12125
12283
  var OPPORTUNISTIC_INVENTORY_SYNC_MS = 1200;
12126
12284
  var MIN_INVENTORY_SYNC_GAP_MS = 1500;
@@ -12131,6 +12289,16 @@ var ConnectionManager = class {
12131
12289
  reconnectDelay = DEFAULT_RECONNECT_DELAY_MS;
12132
12290
  /** Outbound frame counter; reset on every new handshake. See #310. */
12133
12291
  txSeq = new SequenceTracker();
12292
+ /**
12293
+ * Per-process stream epoch, announced in the `register` message. The
12294
+ * sessionKey is derived from the (stable) hostId, so it does NOT change when
12295
+ * the CLI process restarts — but `txSeq` resets to 1 on a fresh process. A
12296
+ * portal holding a high lastSeq would then reject every restarted frame as
12297
+ * out-of-order and silently drop it forever. The portal resets its inbound
12298
+ * trackers when this epoch changes, recovering the stream without a manual
12299
+ * refresh. A new value per ConnectionManager == per CLI process. See #543.
12300
+ */
12301
+ streamEpoch = (0, import_node_crypto2.randomUUID)();
12134
12302
  /**
12135
12303
  * Inbound frame trackers, keyed by viewerId. The portal's Terminal.tsx
12136
12304
  * initializes a fresh txSeq (starting at 1) on every component mount, so a
@@ -13816,7 +13984,7 @@ function validateProvisionRequest(payload) {
13816
13984
  }
13817
13985
 
13818
13986
  // src/approval-policy.ts
13819
- var import_node_crypto2 = require("crypto");
13987
+ var import_node_crypto3 = require("crypto");
13820
13988
  function hasToken(cmd, token) {
13821
13989
  const lower = cmd.toLowerCase();
13822
13990
  const t = token.toLowerCase();
@@ -13923,7 +14091,7 @@ function matchPolicy(command, _policy = DEFAULT_APPROVAL_POLICY, hostId = "", se
13923
14091
  for (const matcher of MATCHERS) {
13924
14092
  if (matcher.test(command)) {
13925
14093
  return {
13926
- requestId: (0, import_node_crypto2.randomUUID)(),
14094
+ requestId: (0, import_node_crypto3.randomUUID)(),
13927
14095
  hostId,
13928
14096
  sessionId,
13929
14097
  action: matcher.action,
@@ -14373,6 +14541,9 @@ async function runAgent(rawOptions) {
14373
14541
  mode,
14374
14542
  ...viewerId ? { viewerId } : {}
14375
14543
  });
14544
+ },
14545
+ () => {
14546
+ transcriptWatcher.resyncMarkdownAfterDrop();
14376
14547
  }
14377
14548
  );
14378
14549
  const conn = new ConnectionManager(sessionStreamer, outputBuffer);
@@ -14579,7 +14750,11 @@ async function runAgent(rawOptions) {
14579
14750
  sendToGroup(ws, groups.privateGroup, {
14580
14751
  type: "register",
14581
14752
  hostName: options.hostName,
14582
- environment: os10.platform()
14753
+ environment: os10.platform(),
14754
+ // #543: lets the portal detect a CLI process restart (txSeq reset
14755
+ // to 1 under a host-stable sessionKey) and reset its inbound
14756
+ // sequence trackers so restarted frames aren't dropped forever.
14757
+ streamEpoch: conn.streamEpoch
14583
14758
  });
14584
14759
  conn.replayBufferedOutput((chunk) => {
14585
14760
  for (const outputChunk of splitTransportChunks(chunk)) {
@@ -14601,19 +14776,16 @@ async function runAgent(rawOptions) {
14601
14776
  }
14602
14777
  }
14603
14778
  });
14604
- for (const [sessionId] of sessionStreamer.activeStreams()) {
14605
- const buffered = sessionStreamer.drainBuffer(sessionId);
14606
- for (const chunk of buffered) {
14607
- for (const outputChunk of splitTransportChunks(chunk)) {
14608
- if (conn.sessionKey) {
14609
- const { enc, iv, seq } = encryptPayload(outputChunk, conn.sessionKey, conn.txSeq.next());
14610
- sendToGroup(ws, groups.privateGroup, { type: "output", enc, iv, seq, sessionId });
14611
- } else {
14612
- sendToGroup(ws, groups.privateGroup, { type: "output", data: outputChunk, sessionId });
14613
- }
14779
+ sessionStreamer.recoverStreamsAfterReconnect((sessionId, chunk) => {
14780
+ for (const outputChunk of splitTransportChunks(chunk)) {
14781
+ if (conn.sessionKey) {
14782
+ const { enc, iv, seq } = encryptPayload(outputChunk, conn.sessionKey, conn.txSeq.next());
14783
+ sendToGroup(ws, groups.privateGroup, { type: "output", enc, iv, seq, sessionId });
14784
+ } else {
14785
+ sendToGroup(ws, groups.privateGroup, { type: "output", data: outputChunk, sessionId });
14614
14786
  }
14615
14787
  }
14616
- }
14788
+ });
14617
14789
  await inventory.syncInventory(ws, groups, true);
14618
14790
  conn.startTimers(
14619
14791
  () => inventory.announceToRegistry(ws, groups.registryGroup, "heartbeat"),
package/dist/sbom.json CHANGED
@@ -1299,8 +1299,8 @@
1299
1299
  {
1300
1300
  "type": "library",
1301
1301
  "name": "ragent-cli",
1302
- "version": "1.11.15",
1303
- "bom-ref": "ragent-live|ragent-cli@1.11.15",
1302
+ "version": "1.11.17",
1303
+ "bom-ref": "ragent-live|ragent-cli@1.11.17",
1304
1304
  "author": "Intellimetrics",
1305
1305
  "description": "CLI agent for rAgent Live — browser-first terminal control plane for AI coding agents",
1306
1306
  "licenses": [
@@ -1311,7 +1311,7 @@
1311
1311
  }
1312
1312
  }
1313
1313
  ],
1314
- "purl": "pkg:npm/ragent-cli@1.11.15",
1314
+ "purl": "pkg:npm/ragent-cli@1.11.17",
1315
1315
  "externalReferences": [
1316
1316
  {
1317
1317
  "url": "https://github.com/chadlindell/ragent-live/issues",
@@ -1436,7 +1436,7 @@
1436
1436
  "ragent-live|@emnapi/wasi-threads@1.2.1",
1437
1437
  "ragent-live|@pkgjs/parseargs@0.11.0",
1438
1438
  "ragent-live|@tybys/wasm-util@0.10.1",
1439
- "ragent-live|ragent-cli@1.11.15"
1439
+ "ragent-live|ragent-cli@1.11.17"
1440
1440
  ]
1441
1441
  },
1442
1442
  {
@@ -1584,7 +1584,7 @@
1584
1584
  ]
1585
1585
  },
1586
1586
  {
1587
- "ref": "ragent-live|ragent-cli@1.11.15",
1587
+ "ref": "ragent-live|ragent-cli@1.11.17",
1588
1588
  "dependsOn": [
1589
1589
  "ragent-live|@azure/web-pubsub-client@1.0.4",
1590
1590
  "ragent-live|@openai/codex-sdk@0.130.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ragent-cli",
3
- "version": "1.11.15",
3
+ "version": "1.11.17",
4
4
  "description": "CLI agent for rAgent Live — browser-first terminal control plane for AI coding agents",
5
5
  "main": "dist/index.js",
6
6
  "bin": {