ragent-cli 1.11.14 → 1.11.16

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.14",
34
+ version: "1.11.16",
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");
@@ -10738,13 +10862,14 @@ var MAX_CONCURRENT_STREAMS = 20;
10738
10862
  var MAX_SESSION_BUFFER_BYTES = 64 * 1024;
10739
10863
  var PRIVATE_MODE_SEQUENCE_RE = /\x1b\[\?([0-9;]*)([hl])/g;
10740
10864
  var ALT_SCREEN_PARAMS = /* @__PURE__ */ new Set(["47", "1047", "1048", "1049"]);
10865
+ var MOUSE_MODE_PARAMS = /* @__PURE__ */ new Set(["9", "1000", "1002", "1003", "1005", "1006", "1015", "1016"]);
10741
10866
  function stripAlternateScreenModeSwitches(data) {
10742
10867
  return data.replace(
10743
10868
  PRIVATE_MODE_SEQUENCE_RE,
10744
10869
  (full, params, action) => {
10745
10870
  if (!params) return full;
10746
10871
  const parts = params.split(";");
10747
- const filtered = parts.filter((p) => !ALT_SCREEN_PARAMS.has(p));
10872
+ const filtered = parts.filter((p) => !ALT_SCREEN_PARAMS.has(p) && !MOUSE_MODE_PARAMS.has(p));
10748
10873
  if (filtered.length === parts.length) return full;
10749
10874
  if (filtered.length === 0) return "";
10750
10875
  return `\x1B[?${filtered.join(";")}${action}`;
@@ -10769,6 +10894,18 @@ var AlternateScreenModeStripper = class {
10769
10894
  return stripAlternateScreenModeSwitches(pending);
10770
10895
  }
10771
10896
  };
10897
+ function buildRelativeCursorSync(lineCount, x, y) {
10898
+ if (lineCount <= 0) return "";
10899
+ let seq = "";
10900
+ if (y <= lineCount - 1) {
10901
+ const up = lineCount - 1 - y;
10902
+ if (up > 0) seq += `\x1B[${up}A`;
10903
+ } else {
10904
+ seq += "\r\n".repeat(y - (lineCount - 1));
10905
+ }
10906
+ seq += `\x1B[${x + 1}G`;
10907
+ return seq;
10908
+ }
10772
10909
  function parsePaneTarget(sessionId) {
10773
10910
  if (!sessionId.startsWith("tmux:")) return null;
10774
10911
  const rest = sessionId.slice("tmux:".length);
@@ -10824,6 +10961,11 @@ var SessionStreamer = class {
10824
10961
  pendingStops = /* @__PURE__ */ new Map();
10825
10962
  sendFn;
10826
10963
  onStreamStopped;
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;
10827
10969
  /**
10828
10970
  * Per-session ring buffer for output captured while the WebSocket is disconnected.
10829
10971
  * Each entry is a string chunk; oldest entries are evicted when the total byte
@@ -10837,9 +10979,11 @@ var SessionStreamer = class {
10837
10979
  lastSeenDroppedFrames = 0;
10838
10980
  /** Sessions whose viewers need a capture-pane resync after frame drops. */
10839
10981
  pendingDropResync = /* @__PURE__ */ new Set();
10840
- constructor(sendFn, onStreamStopped) {
10982
+ constructor(sendFn, onStreamStopped, sendResetFn, onFramesDropped) {
10841
10983
  this.sendFn = sendFn;
10842
10984
  this.onStreamStopped = onStreamStopped;
10985
+ this.sendResetFn = sendResetFn;
10986
+ this.onFramesDropped = onFramesDropped;
10843
10987
  this.cleanupStaleStreams();
10844
10988
  this.startBackpressureResumeTimer();
10845
10989
  }
@@ -10883,6 +11027,7 @@ var SessionStreamer = class {
10883
11027
  droppedTotal: dropped,
10884
11028
  streams: this.pendingDropResync.size
10885
11029
  });
11030
+ this.onFramesDropped?.();
10886
11031
  }
10887
11032
  const next = this.pendingDropResync.values().next();
10888
11033
  if (!next.done) {
@@ -11123,84 +11268,141 @@ var SessionStreamer = class {
11123
11268
  }
11124
11269
  /**
11125
11270
  * Re-send capture-pane data for an already-active tmux stream.
11126
- * Used when a viewer missed the initial burst (e.g. joinGroup race)
11127
- * or reconnected ("Retry stream"). Does NOT restart pipe-pane.
11271
+ * Used when a viewer missed the initial burst (e.g. joinGroup race),
11272
+ * reconnected, resized, or after backpressure frame drops corrupted the
11273
+ * stream. Does NOT restart pipe-pane.
11274
+ *
11275
+ * Default is a LEAN repaint: visible pane + cursor only. Replaying
11276
+ * scrollback mid-session shoved thousands of lines of old output into
11277
+ * live viewers ("ghost text") on every resize/heal. Pass
11278
+ * `{ scrollback: true }` only for viewer (re)attach, where the replay is
11279
+ * prefixed with ED3 (`\x1b[3J`) so the viewer's existing scrollback is
11280
+ * cleared first instead of duplicated.
11128
11281
  */
11129
- resyncStream(sessionId, viewerId) {
11282
+ resyncStream(sessionId, viewerId, opts) {
11130
11283
  const stream = this.active.get(sessionId);
11131
11284
  if (!stream || stream.stopped || stream.streamType !== "tmux-pipe") return false;
11132
11285
  const { paneTarget, cleanEnv } = stream;
11133
11286
  if (!paneTarget || !cleanEnv) return false;
11134
11287
  const targetViewerId = viewerId?.trim() || void 0;
11135
- const send = (data) => {
11136
- if (targetViewerId) {
11137
- this.sendFn(sessionId, data, targetViewerId);
11138
- } else {
11139
- this.sendFn(sessionId, data);
11140
- }
11141
- };
11288
+ const withScrollback = opts?.scrollback === true;
11289
+ if (stream.initializing) {
11290
+ const pending = stream.pendingResyncs ??= [];
11291
+ const duplicate = pending.some(
11292
+ (p) => p.viewerId === targetViewerId && p.scrollback === withScrollback
11293
+ );
11294
+ if (!duplicate) pending.push({ viewerId: targetViewerId, scrollback: withScrollback });
11295
+ return true;
11296
+ }
11297
+ stream.initBuffer = [];
11142
11298
  stream.initializing = true;
11143
- try {
11144
- let alternateScreen = false;
11145
- try {
11146
- const altFlag = (0, import_node_child_process2.execFileSync)(
11147
- "tmux",
11148
- ["display-message", "-t", paneTarget, "-p", "#{alternate_on}"],
11149
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11150
- ).trim();
11151
- alternateScreen = altFlag === "1";
11152
- } catch {
11299
+ setImmediate(() => {
11300
+ if (stream.stopped) {
11301
+ stream.initializing = false;
11302
+ stream.initBuffer = [];
11303
+ return;
11153
11304
  }
11305
+ const send = (data) => {
11306
+ if (targetViewerId) {
11307
+ this.sendFn(sessionId, data, targetViewerId);
11308
+ } else {
11309
+ this.sendFn(sessionId, data);
11310
+ }
11311
+ };
11154
11312
  try {
11155
- const scrollback = (0, import_node_child_process2.execFileSync)(
11156
- "tmux",
11157
- ["capture-pane", "-t", paneTarget, "-p", "-e", "-S", "-5000", "-E", "-1"],
11158
- { env: cleanEnv, timeout: 1e4, encoding: "utf-8" }
11159
- );
11160
- if (scrollback && scrollback.length > 0) {
11161
- send(scrollback.replace(/\n/g, "\r\n"));
11313
+ this.sendResetFn?.(sessionId, withScrollback ? "full" : "repaint", targetViewerId);
11314
+ const alternateScreen = this.detectAlternateScreen(paneTarget, cleanEnv);
11315
+ if (withScrollback) {
11316
+ send("\x1B[3J");
11317
+ try {
11318
+ const scrollback = (0, import_node_child_process2.execFileSync)(
11319
+ "tmux",
11320
+ ["capture-pane", "-t", paneTarget, "-p", "-e", "-S", "-5000", "-E", "-1"],
11321
+ { env: cleanEnv, timeout: 1e4, encoding: "utf-8" }
11322
+ );
11323
+ if (scrollback && scrollback.length > 0) {
11324
+ send(scrollback.replace(/\n/g, "\r\n"));
11325
+ }
11326
+ } catch {
11327
+ }
11328
+ }
11329
+ send("\x1B[?1049l\x1B[0m\x1B[2J\x1B[H");
11330
+ const lineCount = this.sendVisiblePane(send, paneTarget, cleanEnv, alternateScreen);
11331
+ this.sendCursorSync(send, paneTarget, cleanEnv, lineCount);
11332
+ log17.info("resync capture", { sessionId, mode: withScrollback ? "full" : "repaint" });
11333
+ } catch (error) {
11334
+ const message = error instanceof Error ? error.message : String(error);
11335
+ log17.warn("failed to resync stream", { sessionId, error: message });
11336
+ } finally {
11337
+ stream.initializing = false;
11338
+ stream.initBuffer = [];
11339
+ const next = stream.pendingResyncs?.shift();
11340
+ if (next && !stream.stopped) {
11341
+ this.resyncStream(sessionId, next.viewerId, { scrollback: next.scrollback });
11162
11342
  }
11163
- } catch {
11164
11343
  }
11165
- send("\x1B[?1049l\x1B[0m\x1B[2J\x1B[H");
11166
- try {
11167
- const captureArgs = alternateScreen ? ["capture-pane", "-a", "-t", paneTarget, "-p", "-e"] : ["capture-pane", "-t", paneTarget, "-p", "-e"];
11168
- const initial = (0, import_node_child_process2.execFileSync)(
11169
- "tmux",
11170
- captureArgs,
11171
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11344
+ });
11345
+ return true;
11346
+ }
11347
+ /** Detect whether the pane is using the alternate screen buffer. */
11348
+ detectAlternateScreen(paneTarget, cleanEnv) {
11349
+ try {
11350
+ const altFlag = (0, import_node_child_process2.execFileSync)(
11351
+ "tmux",
11352
+ ["display-message", "-t", paneTarget, "-p", "#{alternate_on}"],
11353
+ { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11354
+ ).trim();
11355
+ return altFlag === "1";
11356
+ } catch {
11357
+ return false;
11358
+ }
11359
+ }
11360
+ /**
11361
+ * Capture and send the visible pane content. Converts bare \n to \r\n for
11362
+ * xterm.js and strips the trailing \r\n to prevent an extra scroll that
11363
+ * would shift the viewport by 1 row. Returns the number of lines sent
11364
+ * (0 when the capture failed or was empty) for relative cursor sync.
11365
+ */
11366
+ sendVisiblePane(send, paneTarget, cleanEnv, alternateScreen) {
11367
+ try {
11368
+ const captureArgs = alternateScreen ? ["capture-pane", "-a", "-t", paneTarget, "-p", "-e"] : ["capture-pane", "-t", paneTarget, "-p", "-e"];
11369
+ const initial = (0, import_node_child_process2.execFileSync)(
11370
+ "tmux",
11371
+ captureArgs,
11372
+ { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11373
+ );
11374
+ if (initial) {
11375
+ const converted = stripAlternateScreenModeSwitches(
11376
+ initial.replace(/\n/g, "\r\n").replace(/\r\n$/, "")
11172
11377
  );
11173
- if (initial) {
11174
- send(stripAlternateScreenModeSwitches(initial.replace(/\n/g, "\r\n").replace(/\r\n$/, "")));
11378
+ if (converted) {
11379
+ send(converted);
11380
+ return converted.split("\r\n").length;
11175
11381
  }
11176
- } catch {
11177
11382
  }
11178
- try {
11179
- const cursorInfo = (0, import_node_child_process2.execFileSync)(
11180
- "tmux",
11181
- ["display-message", "-t", paneTarget, "-p", "#{cursor_x} #{cursor_y}"],
11182
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11183
- ).trim();
11184
- const parts = cursorInfo.split(" ");
11185
- if (parts.length === 2) {
11186
- const x = parseInt(parts[0], 10);
11187
- const y = parseInt(parts[1], 10);
11188
- if (!isNaN(x) && !isNaN(y)) {
11189
- send(`\x1B[${y + 1};${x + 1}H`);
11190
- }
11383
+ } catch {
11384
+ }
11385
+ return 0;
11386
+ }
11387
+ /** Query the tmux cursor position and send a relative cursor sync. */
11388
+ sendCursorSync(send, paneTarget, cleanEnv, lineCount) {
11389
+ if (lineCount <= 0) return;
11390
+ try {
11391
+ const cursorInfo = (0, import_node_child_process2.execFileSync)(
11392
+ "tmux",
11393
+ ["display-message", "-t", paneTarget, "-p", "#{cursor_x} #{cursor_y}"],
11394
+ { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11395
+ ).trim();
11396
+ const parts = cursorInfo.split(" ");
11397
+ if (parts.length === 2) {
11398
+ const x = parseInt(parts[0], 10);
11399
+ const y = parseInt(parts[1], 10);
11400
+ if (!isNaN(x) && !isNaN(y)) {
11401
+ const seq = buildRelativeCursorSync(lineCount, x, y);
11402
+ if (seq) send(seq);
11191
11403
  }
11192
- } catch {
11193
11404
  }
11194
- stream.initializing = false;
11195
- stream.initBuffer = [];
11196
- log17.info("resync capture", { sessionId });
11197
- return true;
11198
- } catch (error) {
11199
- stream.initializing = false;
11200
- stream.initBuffer = [];
11201
- const message = error instanceof Error ? error.message : String(error);
11202
- log17.warn("failed to resync stream", { sessionId, error: message });
11203
- return false;
11405
+ } catch {
11204
11406
  }
11205
11407
  }
11206
11408
  // ---------------------------------------------------------------------------
@@ -11285,17 +11487,10 @@ var SessionStreamer = class {
11285
11487
  this.onStreamStopped?.(sessionId);
11286
11488
  }
11287
11489
  });
11288
- let alternateScreen = false;
11289
- try {
11290
- const altFlag = (0, import_node_child_process2.execFileSync)(
11291
- "tmux",
11292
- ["display-message", "-t", paneTarget, "-p", "#{alternate_on}"],
11293
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11294
- ).trim();
11295
- alternateScreen = altFlag === "1";
11296
- } catch {
11297
- }
11490
+ this.sendResetFn?.(sessionId, "full");
11491
+ const alternateScreen = this.detectAlternateScreen(paneTarget, cleanEnv);
11298
11492
  stream.alternateScreen = alternateScreen;
11493
+ this.sendFn(sessionId, "\x1B[3J");
11299
11494
  try {
11300
11495
  const scrollback = (0, import_node_child_process2.execFileSync)(
11301
11496
  "tmux",
@@ -11308,34 +11503,9 @@ var SessionStreamer = class {
11308
11503
  } catch {
11309
11504
  }
11310
11505
  this.sendFn(sessionId, "\x1B[?1049l\x1B[0m\x1B[2J\x1B[H");
11311
- try {
11312
- const captureArgs = alternateScreen ? ["capture-pane", "-a", "-t", paneTarget, "-p", "-e"] : ["capture-pane", "-t", paneTarget, "-p", "-e"];
11313
- const initial = (0, import_node_child_process2.execFileSync)(
11314
- "tmux",
11315
- captureArgs,
11316
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11317
- );
11318
- if (initial) {
11319
- this.sendFn(sessionId, stripAlternateScreenModeSwitches(initial.replace(/\n/g, "\r\n").replace(/\r\n$/, "")));
11320
- }
11321
- } catch {
11322
- }
11323
- try {
11324
- const cursorInfo = (0, import_node_child_process2.execFileSync)(
11325
- "tmux",
11326
- ["display-message", "-t", paneTarget, "-p", "#{cursor_x} #{cursor_y}"],
11327
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11328
- ).trim();
11329
- const parts = cursorInfo.split(" ");
11330
- if (parts.length === 2) {
11331
- const x = parseInt(parts[0], 10);
11332
- const y = parseInt(parts[1], 10);
11333
- if (!isNaN(x) && !isNaN(y)) {
11334
- this.sendFn(sessionId, `\x1B[${y + 1};${x + 1}H`);
11335
- }
11336
- }
11337
- } catch {
11338
- }
11506
+ const sendBroadcast = (data) => this.sendFn(sessionId, data);
11507
+ const lineCount = this.sendVisiblePane(sendBroadcast, paneTarget, cleanEnv, alternateScreen);
11508
+ this.sendCursorSync(sendBroadcast, paneTarget, cleanEnv, lineCount);
11339
11509
  stream.initializing = false;
11340
11510
  stream.initBuffer = [];
11341
11511
  log17.info("started streaming (tmux)", { sessionId, pane: paneTarget });
@@ -11697,6 +11867,11 @@ var ANSI_KEY_SEQUENCES = /* @__PURE__ */ new Map([
11697
11867
  ["\x1B[6~", "PageDown"]
11698
11868
  ]);
11699
11869
  var ANSI_KEY_ENTRIES = Array.from(ANSI_KEY_SEQUENCES.entries());
11870
+ var MOUSE_OR_FOCUS_REPORT_RE = /\x1b\[(?:<\d+;\d+;\d+[Mm]|\d+;\d+;\d+M|M[\x20-\xFF]{3}|[IO])/g;
11871
+ function sanitizeRemoteInput(data) {
11872
+ if (!data.includes("\x1B[")) return data;
11873
+ return data.replace(MOUSE_OR_FOCUS_REPORT_RE, "");
11874
+ }
11700
11875
  function controlKeyName(charCode) {
11701
11876
  if (charCode >= 1 && charCode <= 26) {
11702
11877
  return `C-${String.fromCharCode(charCode + 96)}`;
@@ -11727,7 +11902,8 @@ function appendLiteralSegment(segments, value) {
11727
11902
  segments.push({ kind: "literal", value });
11728
11903
  }
11729
11904
  }
11730
- function encodeTmuxInput(data) {
11905
+ function encodeTmuxInput(rawData) {
11906
+ const data = sanitizeRemoteInput(rawData);
11731
11907
  const segments = [];
11732
11908
  let literal = "";
11733
11909
  const flushLiteral = () => {
@@ -12075,6 +12251,7 @@ var InventoryManager = class {
12075
12251
 
12076
12252
  // src/connection-manager.ts
12077
12253
  var import_ws3 = __toESM(require("ws"));
12254
+ var import_node_crypto2 = require("crypto");
12078
12255
  var log21 = createLogger("connection");
12079
12256
  var OPPORTUNISTIC_INVENTORY_SYNC_MS = 1200;
12080
12257
  var MIN_INVENTORY_SYNC_GAP_MS = 1500;
@@ -12085,6 +12262,16 @@ var ConnectionManager = class {
12085
12262
  reconnectDelay = DEFAULT_RECONNECT_DELAY_MS;
12086
12263
  /** Outbound frame counter; reset on every new handshake. See #310. */
12087
12264
  txSeq = new SequenceTracker();
12265
+ /**
12266
+ * Per-process stream epoch, announced in the `register` message. The
12267
+ * sessionKey is derived from the (stable) hostId, so it does NOT change when
12268
+ * the CLI process restarts — but `txSeq` resets to 1 on a fresh process. A
12269
+ * portal holding a high lastSeq would then reject every restarted frame as
12270
+ * out-of-order and silently drop it forever. The portal resets its inbound
12271
+ * trackers when this epoch changes, recovering the stream without a manual
12272
+ * refresh. A new value per ConnectionManager == per CLI process. See #543.
12273
+ */
12274
+ streamEpoch = (0, import_node_crypto2.randomUUID)();
12088
12275
  /**
12089
12276
  * Inbound frame trackers, keyed by viewerId. The portal's Terminal.tsx
12090
12277
  * initializes a fresh txSeq (starting at 1) on every component mount, so a
@@ -13310,7 +13497,9 @@ var ControlDispatcher = class {
13310
13497
  this.handleInput(writtenPath, sessionId);
13311
13498
  }
13312
13499
  /** Handle input routing to the correct target. */
13313
- handleInput(data, sessionId) {
13500
+ handleInput(rawData, sessionId) {
13501
+ const data = sanitizeRemoteInput(rawData);
13502
+ if (!data) return;
13314
13503
  if (!sessionId || sessionId.startsWith("pty:")) {
13315
13504
  this.shell.write(data);
13316
13505
  } else if (sessionId.startsWith("tmux:")) {
@@ -13689,12 +13878,11 @@ var ControlDispatcher = class {
13689
13878
  const viewerKey = viewerId?.trim() || "";
13690
13879
  if (sessionId.startsWith("tmux:") || sessionId.startsWith("screen:") || sessionId.startsWith("zellij:") || sessionId.startsWith("process:")) {
13691
13880
  const alreadyStreaming = this.streamer.isStreaming(sessionId);
13692
- const viewerAlreadyAttached = alreadyStreaming && this.streamer.hasViewer(sessionId, viewerKey);
13693
13881
  const started = this.streamer.startStream(sessionId, viewerId ?? void 0);
13694
13882
  if (ws && ws.readyState === import_ws4.default.OPEN && group) {
13695
13883
  if (started) {
13696
- if (alreadyStreaming && !viewerAlreadyAttached) {
13697
- this.streamer.resyncStream(sessionId, viewerKey);
13884
+ if (alreadyStreaming) {
13885
+ this.streamer.resyncStream(sessionId, viewerKey, { scrollback: true });
13698
13886
  }
13699
13887
  const dims = this.queryPaneDimensions(sessionId);
13700
13888
  sendToGroup(ws, group, { type: "stream-started", sessionId, ...dims });
@@ -13769,7 +13957,7 @@ function validateProvisionRequest(payload) {
13769
13957
  }
13770
13958
 
13771
13959
  // src/approval-policy.ts
13772
- var import_node_crypto2 = require("crypto");
13960
+ var import_node_crypto3 = require("crypto");
13773
13961
  function hasToken(cmd, token) {
13774
13962
  const lower = cmd.toLowerCase();
13775
13963
  const t = token.toLowerCase();
@@ -13876,7 +14064,7 @@ function matchPolicy(command, _policy = DEFAULT_APPROVAL_POLICY, hostId = "", se
13876
14064
  for (const matcher of MATCHERS) {
13877
14065
  if (matcher.test(command)) {
13878
14066
  return {
13879
- requestId: (0, import_node_crypto2.randomUUID)(),
14067
+ requestId: (0, import_node_crypto3.randomUUID)(),
13880
14068
  hostId,
13881
14069
  sessionId,
13882
14070
  action: matcher.action,
@@ -14308,6 +14496,27 @@ async function runAgent(rawOptions) {
14308
14496
  }
14309
14497
  if (!conn.isReady()) return;
14310
14498
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "stream-stopped", sessionId });
14499
+ },
14500
+ (sessionId, mode, viewerId) => {
14501
+ if (!conn.isReady()) return;
14502
+ const tail = flushStreamingRedaction(sessionId);
14503
+ if (tail) {
14504
+ if (conn.sessionKey) {
14505
+ const { enc, iv, seq } = encryptPayload(tail, conn.sessionKey, conn.txSeq.next());
14506
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, seq, sessionId });
14507
+ } else {
14508
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", data: tail, sessionId });
14509
+ }
14510
+ }
14511
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, {
14512
+ type: "stream-reset",
14513
+ sessionId,
14514
+ mode,
14515
+ ...viewerId ? { viewerId } : {}
14516
+ });
14517
+ },
14518
+ () => {
14519
+ transcriptWatcher.resyncMarkdownAfterDrop();
14311
14520
  }
14312
14521
  );
14313
14522
  const conn = new ConnectionManager(sessionStreamer, outputBuffer);
@@ -14514,7 +14723,11 @@ async function runAgent(rawOptions) {
14514
14723
  sendToGroup(ws, groups.privateGroup, {
14515
14724
  type: "register",
14516
14725
  hostName: options.hostName,
14517
- environment: os10.platform()
14726
+ environment: os10.platform(),
14727
+ // #543: lets the portal detect a CLI process restart (txSeq reset
14728
+ // to 1 under a host-stable sessionKey) and reset its inbound
14729
+ // sequence trackers so restarted frames aren't dropped forever.
14730
+ streamEpoch: conn.streamEpoch
14518
14731
  });
14519
14732
  conn.replayBufferedOutput((chunk) => {
14520
14733
  for (const outputChunk of splitTransportChunks(chunk)) {