forge-remote 2.2.0 → 2.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-remote",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Desktop relay for Forge Remote — monitor and control Claude Code sessions from your phone",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -44,6 +44,16 @@ const TOOL_RESULT_TRUNCATE_BYTES = 8 * 1024;
44
44
  // outputs occasionally exceed Firestore limits; truncate with a marker.
45
45
  const TEXT_TRUNCATE_BYTES = 64 * 1024;
46
46
 
47
+ // First-sight backfill cap. When mirroring is enabled on a project that
48
+ // already has a multi-MB session.jsonl on disk (e.g., a long-running
49
+ // conversation), we don't want to dump the entire history into Firestore —
50
+ // that's slow, expensive, and almost certainly not what the user wants.
51
+ // Instead, jump near the end of the file and ingest only the last chunk
52
+ // so the user gets a few hundred recent events for context. New sessions
53
+ // (file size <= the threshold) are processed from byte 0 as normal.
54
+ const INITIAL_BACKFILL_THRESHOLD_BYTES = 256 * 1024;
55
+ const INITIAL_BACKFILL_TAIL_BYTES = 64 * 1024;
56
+
47
57
  // File-event types in the .jsonl that aren't user-visible — skip silently.
48
58
  const SKIP_TYPES = new Set([
49
59
  "queue-operation",
@@ -353,6 +363,11 @@ async function processFile(absPath) {
353
363
  offset: 0,
354
364
  idleTimer: null,
355
365
  sessionDocReady: false,
366
+ // True once we've decided where to start reading. Stays false until
367
+ // the user has opted in AND we've applied the initial-backfill rule
368
+ // below — that way the bootstrap only runs once, on the first real
369
+ // read attempt, not on every "add" event for a non-mirrored project.
370
+ bootstrapped: false,
356
371
  };
357
372
  fileState.set(absPath, state);
358
373
 
@@ -361,6 +376,7 @@ async function processFile(absPath) {
361
376
  const saved = cursors[absPath];
362
377
  if (saved && typeof saved.offset === "number") {
363
378
  state.offset = saved.offset;
379
+ state.bootstrapped = true; // saved cursor wins over backfill heuristic
364
380
  }
365
381
  }
366
382
 
@@ -373,6 +389,24 @@ async function processFile(absPath) {
373
389
  } catch {
374
390
  return;
375
391
  }
392
+
393
+ // First-sight bootstrap: if we don't have a saved cursor and the file is
394
+ // already larger than the threshold (because Claude has been writing to
395
+ // it before mirroring was enabled), jump close to the end so we ingest
396
+ // only the most recent chunk. Without this, opting in on a long-running
397
+ // session would dump the entire history into Firestore.
398
+ if (!state.bootstrapped) {
399
+ if (stat.size > INITIAL_BACKFILL_THRESHOLD_BYTES) {
400
+ state.offset = stat.size - INITIAL_BACKFILL_TAIL_BYTES;
401
+ log.info(
402
+ `claude-session-watcher: ${state.sessionId.slice(0, 8)} ` +
403
+ `large existing file (${(stat.size / 1024 / 1024).toFixed(1)}MB) — ` +
404
+ `tail-only from byte ${state.offset}`,
405
+ );
406
+ }
407
+ state.bootstrapped = true;
408
+ }
409
+
376
410
  if (stat.size <= state.offset) return;
377
411
 
378
412
  // If the file shrunk (rotation? unlikely for jsonl but possible), reset.