chat-recall 0.7.0 → 0.7.2

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/watch.js CHANGED
@@ -6585,7 +6585,7 @@ var init_raw = __esm({
6585
6585
  // ../engine/dist/transcript/shadow.js
6586
6586
  import { gzipSync as gzipSync2, gunzipSync as gunzipSync2 } from "zlib";
6587
6587
  import { createHash as createHash2 } from "crypto";
6588
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync as renameSync2, statSync as statSync4 } from "fs";
6588
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync as renameSync2, statSync as statSync4, openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
6589
6589
  import { dirname as dirname7, join as join8 } from "path";
6590
6590
  function shadowRoot() {
6591
6591
  return join8(getDataDir(), "shadow");
@@ -6809,6 +6809,29 @@ function seedShadow(sessionId, container) {
6809
6809
  }
6810
6810
  return fullest;
6811
6811
  }
6812
+ function shadowUncompressedBytes(tool, rawId) {
6813
+ const path = shadowFileFor(tool, rawId);
6814
+ let fd = null;
6815
+ try {
6816
+ const size = statSync4(path).size;
6817
+ if (size < 18)
6818
+ return 0;
6819
+ fd = openSync2(path, "r");
6820
+ const tail = Buffer.alloc(4);
6821
+ if (readSync2(fd, tail, 0, 4, size - 4) !== 4)
6822
+ return 0;
6823
+ return tail.readUInt32LE(0);
6824
+ } catch {
6825
+ return 0;
6826
+ } finally {
6827
+ if (fd !== null) {
6828
+ try {
6829
+ closeSync2(fd);
6830
+ } catch {
6831
+ }
6832
+ }
6833
+ }
6834
+ }
6812
6835
  function shadowSizeOnDisk(tool, rawId) {
6813
6836
  try {
6814
6837
  return statSync4(shadowFileFor(tool, rawId)).size;
@@ -6829,7 +6852,7 @@ var init_shadow = __esm({
6829
6852
  });
6830
6853
 
6831
6854
  // ../engine/dist/core/live-session-scan.js
6832
- import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync4, statSync as statSync5, openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
6855
+ import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync4, statSync as statSync5, openSync as openSync3, readSync as readSync3, closeSync as closeSync3 } from "fs";
6833
6856
  import { join as join9, basename as basename5 } from "path";
6834
6857
  function detectTool(sessionId) {
6835
6858
  return getBackendForId(sessionId)?.id ?? "claude";
@@ -6856,6 +6879,15 @@ function sessionFileIndex() {
6856
6879
  return indexCache;
6857
6880
  const byId = /* @__PURE__ */ new Map();
6858
6881
  const seenRoots = /* @__PURE__ */ new Set();
6882
+ const projectPaths = /* @__PURE__ */ new Map();
6883
+ const projectPathOf = (dirName) => {
6884
+ let p = projectPaths.get(dirName);
6885
+ if (p === void 0) {
6886
+ p = resolveProjectDirName(dirName);
6887
+ projectPaths.set(dirName, p);
6888
+ }
6889
+ return p;
6890
+ };
6859
6891
  for (const root of claudeProjectDirs()) {
6860
6892
  if (seenRoots.has(root) || !existsSync7(root))
6861
6893
  continue;
@@ -6883,7 +6915,7 @@ function sessionFileIndex() {
6883
6915
  const located = {
6884
6916
  path: join9(dir, name),
6885
6917
  projectDir: entry.name,
6886
- projectPath: resolveProjectDirName(entry.name)
6918
+ projectPath: projectPathOf(entry.name)
6887
6919
  };
6888
6920
  const prior = byId.get(id);
6889
6921
  if (prior)
@@ -7565,7 +7597,7 @@ var init_flat_string = __esm({
7565
7597
  });
7566
7598
 
7567
7599
  // ../engine/dist/core/first-prompt.js
7568
- import { createReadStream as createReadStream2, existsSync as existsSync9, openSync as openSync3, readSync as readSync3, closeSync as closeSync3 } from "fs";
7600
+ import { createReadStream as createReadStream2, existsSync as existsSync9, openSync as openSync4, readSync as readSync4, closeSync as closeSync4 } from "fs";
7569
7601
  import { StringDecoder } from "string_decoder";
7570
7602
  function stripBanners2(s) {
7571
7603
  let out = s;
@@ -7619,7 +7651,7 @@ function extractFirstUserPromptSync(filePath, options = {}) {
7619
7651
  const maxBytes = options.maxBytes ?? 4 * 1024 * 1024;
7620
7652
  let fd;
7621
7653
  try {
7622
- fd = openSync3(filePath, "r");
7654
+ fd = openSync4(filePath, "r");
7623
7655
  } catch {
7624
7656
  return "";
7625
7657
  }
@@ -7631,7 +7663,7 @@ function extractFirstUserPromptSync(filePath, options = {}) {
7631
7663
  let pos = 0;
7632
7664
  let lineNo = 0;
7633
7665
  for (; ; ) {
7634
- const n = readSync3(fd, buf, 0, CHUNK, pos);
7666
+ const n = readSync4(fd, buf, 0, CHUNK, pos);
7635
7667
  if (n <= 0)
7636
7668
  break;
7637
7669
  pos += n;
@@ -7657,7 +7689,7 @@ function extractFirstUserPromptSync(filePath, options = {}) {
7657
7689
  return "";
7658
7690
  } finally {
7659
7691
  try {
7660
- closeSync3(fd);
7692
+ closeSync4(fd);
7661
7693
  } catch {
7662
7694
  }
7663
7695
  }
@@ -7676,8 +7708,8 @@ var init_first_prompt = __esm({
7676
7708
  });
7677
7709
 
7678
7710
  // ../engine/dist/core/backends/tail-read.js
7679
- import { openSync as openSync4, readSync as readSync4, closeSync as closeSync4, statSync as statSync6 } from "node:fs";
7680
- function readTailFromOffset(path, offset) {
7711
+ import { openSync as openSync5, readSync as readSync5, closeSync as closeSync5, statSync as statSync6 } from "node:fs";
7712
+ function readTailFromOffset(path, offset, maxBytes = TAIL_READ_MAX_BYTES) {
7681
7713
  let size = 0;
7682
7714
  try {
7683
7715
  size = statSync6(path).size;
@@ -7686,14 +7718,14 @@ function readTailFromOffset(path, offset) {
7686
7718
  }
7687
7719
  if (size <= offset)
7688
7720
  return { text: "", newOffset: offset };
7689
- const len = Math.min(size - offset, 16 * 1024 * 1024);
7721
+ const len = Math.min(size - offset, Math.max(1, maxBytes));
7690
7722
  const buf = Buffer.allocUnsafe(len);
7691
7723
  let fd = null;
7692
7724
  try {
7693
- fd = openSync4(path, "r");
7725
+ fd = openSync5(path, "r");
7694
7726
  let got = 0;
7695
7727
  while (got < len) {
7696
- const n = readSync4(fd, buf, got, len - got, offset + got);
7728
+ const n = readSync5(fd, buf, got, len - got, offset + got);
7697
7729
  if (n <= 0)
7698
7730
  break;
7699
7731
  got += n;
@@ -7702,25 +7734,47 @@ function readTailFromOffset(path, offset) {
7702
7734
  return { text: "", newOffset: offset };
7703
7735
  const slice = got < len ? buf.subarray(0, got) : buf;
7704
7736
  const lastNl = slice.lastIndexOf(10);
7705
- if (lastNl < 0) {
7706
- return { text: "", newOffset: offset };
7737
+ if (lastNl >= 0) {
7738
+ const text = slice.subarray(0, lastNl + 1).toString("utf-8");
7739
+ return { text, newOffset: offset + lastNl + 1 };
7707
7740
  }
7708
- const text = slice.subarray(0, lastNl + 1).toString("utf-8");
7709
- return { text, newOffset: offset + lastNl + 1 };
7741
+ if (offset + got >= size)
7742
+ return { text: "", newOffset: offset };
7743
+ const end = findNewline(fd, offset + got, size);
7744
+ if (end < 0)
7745
+ return { text: "", newOffset: offset };
7746
+ return { text: "", newOffset: end + 1, skippedBytes: end + 1 - offset };
7710
7747
  } catch {
7711
7748
  return { text: "", newOffset: offset };
7712
7749
  } finally {
7713
7750
  if (fd !== null) {
7714
7751
  try {
7715
- closeSync4(fd);
7752
+ closeSync5(fd);
7716
7753
  } catch {
7717
7754
  }
7718
7755
  }
7719
7756
  }
7720
7757
  }
7758
+ function findNewline(fd, from, size) {
7759
+ const block = Buffer.allocUnsafe(SKIP_SCAN_BYTES);
7760
+ let pos = from;
7761
+ while (pos < size) {
7762
+ const n = readSync5(fd, block, 0, Math.min(SKIP_SCAN_BYTES, size - pos), pos);
7763
+ if (n <= 0)
7764
+ return -1;
7765
+ const i = block.subarray(0, n).indexOf(10);
7766
+ if (i >= 0)
7767
+ return pos + i;
7768
+ pos += n;
7769
+ }
7770
+ return -1;
7771
+ }
7772
+ var TAIL_READ_MAX_BYTES, SKIP_SCAN_BYTES;
7721
7773
  var init_tail_read = __esm({
7722
7774
  "../engine/dist/core/backends/tail-read.js"() {
7723
7775
  "use strict";
7776
+ TAIL_READ_MAX_BYTES = 16 * 1024 * 1024;
7777
+ SKIP_SCAN_BYTES = 1024 * 1024;
7724
7778
  }
7725
7779
  });
7726
7780
 
@@ -7997,7 +8051,7 @@ var init_generic_engine = __esm({
7997
8051
  });
7998
8052
 
7999
8053
  // ../engine/dist/core/backends/claude.js
8000
- import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync5, statSync as statSync7, openSync as openSync5, readSync as readSync5, closeSync as closeSync5 } from "fs";
8054
+ import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync5, statSync as statSync7, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
8001
8055
  import { join as join10, basename as basename8 } from "path";
8002
8056
  function stripReminders(text) {
8003
8057
  return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
@@ -8094,13 +8148,13 @@ var init_claude2 = __esm({
8094
8148
  return null;
8095
8149
  let head;
8096
8150
  try {
8097
- const fd = openSync5(located.path, "r");
8151
+ const fd = openSync6(located.path, "r");
8098
8152
  try {
8099
8153
  const buf = Buffer.alloc(65536);
8100
- const n = readSync5(fd, buf, 0, buf.length, 0);
8154
+ const n = readSync6(fd, buf, 0, buf.length, 0);
8101
8155
  head = buf.toString("utf-8", 0, n);
8102
8156
  } finally {
8103
- closeSync5(fd);
8157
+ closeSync6(fd);
8104
8158
  }
8105
8159
  } catch {
8106
8160
  return null;
@@ -8145,12 +8199,12 @@ var init_claude2 = __esm({
8145
8199
  const seen = /* @__PURE__ */ new Set();
8146
8200
  const out = [];
8147
8201
  for (const root of claudeProjectDirs()) {
8148
- this.collectSessionsFromRoot(root, cutoff, filter, seen, out);
8202
+ this.collectSessionsFromRoot(root, cutoff, filter, seen, out, opts.previews !== false);
8149
8203
  }
8150
8204
  out.sort((a, b) => b.mtime - a.mtime);
8151
8205
  return opts.limit ? out.slice(0, opts.limit) : out;
8152
8206
  }
8153
- collectSessionsFromRoot(root, cutoff, filter, seen, out) {
8207
+ collectSessionsFromRoot(root, cutoff, filter, seen, out, previews) {
8154
8208
  if (!existsSync10(root))
8155
8209
  return;
8156
8210
  for (const proj of readdirSync7(root, { withFileTypes: true })) {
@@ -8209,7 +8263,7 @@ var init_claude2 = __esm({
8209
8263
  }
8210
8264
  if (stat3.mtimeMs < cutoff)
8211
8265
  continue;
8212
- const firstPrompt = extractFirstUserPromptSync(fullPath, { maxLength: 200 });
8266
+ const firstPrompt = previews ? extractFirstUserPromptSync(fullPath, { maxLength: 200 }) : "";
8213
8267
  out.push({
8214
8268
  toolId: "claude",
8215
8269
  rawId: sessionId,
@@ -8572,14 +8626,14 @@ var init_claude2 = __esm({
8572
8626
  spansMultipleSources(prefixedId) {
8573
8627
  return findSessionFiles(this.toRawId(prefixedId)).length > 1;
8574
8628
  }
8575
- async readFromOffset(prefixedId, offset) {
8629
+ async readFromOffset(prefixedId, offset, maxBytes) {
8576
8630
  const rawId = this.toRawId(prefixedId);
8577
8631
  const copies = findSessionFiles(rawId);
8578
8632
  if (copies.length === 0)
8579
8633
  return { text: "", newOffset: offset };
8580
8634
  if (copies.length > 1)
8581
8635
  return { text: "", newOffset: offset };
8582
- return readTailFromOffset(copies[0].path, offset);
8636
+ return readTailFromOffset(copies[0].path, offset, maxBytes);
8583
8637
  }
8584
8638
  };
8585
8639
  claudeBackend = new ClaudeBackend();
@@ -8807,23 +8861,8 @@ var init_opencode = __esm({
8807
8861
  const projectPath = row.project_path || row.directory || "";
8808
8862
  if (filter && !projectPath.toLowerCase().includes(filter))
8809
8863
  continue;
8810
- const firstUser = db.prepare(`
8811
- SELECT p.data
8812
- FROM part p JOIN message m ON m.id = p.message_id
8813
- WHERE p.session_id = ?
8814
- AND p.data LIKE '%"type":"text"%'
8815
- AND m.data LIKE '%"role":"user"%'
8816
- ORDER BY p.time_created ASC LIMIT 1
8817
- `).get(row.id);
8818
- let firstPrompt = "";
8819
- if (firstUser) {
8820
- try {
8821
- const parsed = JSON.parse(firstUser.data);
8822
- firstPrompt = flatString(String(parsed?.text || "").slice(0, 200));
8823
- } catch {
8824
- }
8825
- }
8826
- const messageCount = db.prepare("SELECT COUNT(*) as c FROM message WHERE session_id = ?").get(row.id)?.c ?? 0;
8864
+ const firstPrompt = opts.previews === false ? "" : this.firstUserPrompt(db, row.id);
8865
+ const messageCount = opts.previews === false ? 0 : db.prepare("SELECT COUNT(*) as c FROM message WHERE session_id = ?").get(row.id)?.c ?? 0;
8827
8866
  const created = new Date(row.time_created || row.time_updated).toISOString();
8828
8867
  const modified = new Date(row.time_updated).toISOString();
8829
8868
  out.push({
@@ -8847,6 +8886,45 @@ var init_opencode = __esm({
8847
8886
  db.close();
8848
8887
  }
8849
8888
  }
8889
+ /**
8890
+ * The first user text part of a session.
8891
+ *
8892
+ * Walks the session's user messages oldest first through the
8893
+ * (session_id, time_created, id) index and stops at the first one with a
8894
+ * text part. The role and the text are read by SQLite: a first message can
8895
+ * carry whole diffs in `summary` (107 MB on one row), and parsing that in JS
8896
+ * to read one field took the process to 510 MB. The earlier query filtered
8897
+ * every part and message with LIKE and then sorted, so it read all of a
8898
+ * session's data: 423 MB for a 236-session listing.
8899
+ */
8900
+ firstUserPrompt(db, sessionId) {
8901
+ try {
8902
+ const has = (table, col) => !!db.prepare(`SELECT 1 FROM pragma_table_info(?) WHERE name = ?`).get(table, col);
8903
+ const msgOrder = has("message", "time_created") ? "time_created, id" : "rowid";
8904
+ const partOrder = has("part", "time_created") ? "time_created, id" : "rowid";
8905
+ const nextUser = db.prepare(`SELECT id, ${msgOrder === "rowid" ? "rowid AS k1, 0 AS k2" : "time_created AS k1, id AS k2"} FROM message
8906
+ WHERE session_id = ? AND (${msgOrder === "rowid" ? "rowid, 0" : "time_created, id"}) > (?, ?)
8907
+ AND json_extract(data, '$.role') = 'user'
8908
+ ORDER BY ${msgOrder} LIMIT 1`);
8909
+ const firstText = db.prepare(`SELECT substr(json_extract(data, '$.text'), 1, 400) AS text FROM part
8910
+ WHERE message_id = ? AND json_extract(data, '$.type') = 'text'
8911
+ AND length(trim(json_extract(data, '$.text'))) > 0
8912
+ ORDER BY ${partOrder} LIMIT 1`);
8913
+ let k1 = Number.MIN_SAFE_INTEGER, k2 = msgOrder === "rowid" ? 0 : "";
8914
+ for (; ; ) {
8915
+ const m = nextUser.get(sessionId, k1, k2);
8916
+ if (!m)
8917
+ return "";
8918
+ k1 = m.k1;
8919
+ k2 = m.k2;
8920
+ const t = firstText.get(m.id);
8921
+ if (t?.text)
8922
+ return flatString(t.text.slice(0, 200));
8923
+ }
8924
+ } catch {
8925
+ return "";
8926
+ }
8927
+ }
8850
8928
  /** OpenCode auto-generates a session title (model-written, distinct from the
8851
8929
  * first prompt — e.g. "Replacing Claude with Gemini CLI"). It's a plain
8852
8930
  * column on the session row. */
@@ -9163,7 +9241,7 @@ var init_opencode = __esm({
9163
9241
  });
9164
9242
 
9165
9243
  // ../engine/dist/core/backends/codex.js
9166
- import { readdirSync as readdirSync8, readFileSync as readFileSync6, statSync as statSync8, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
9244
+ import { readdirSync as readdirSync8, readFileSync as readFileSync6, statSync as statSync8, openSync as openSync7, readSync as readSync7, closeSync as closeSync7 } from "fs";
9167
9245
  import { basename as basename9, dirname as dirname10, join as join12 } from "path";
9168
9246
  function parseShellWrites(cmd) {
9169
9247
  const out = [];
@@ -9409,28 +9487,29 @@ var init_codex2 = __esm({
9409
9487
  continue;
9410
9488
  let firstPrompt = "";
9411
9489
  let messageCount = 0;
9412
- try {
9413
- const lines = readFileSync6(filePath, "utf-8").split("\n");
9414
- for (const line of lines) {
9415
- if (!line.trim())
9416
- continue;
9417
- let ev;
9418
- try {
9419
- ev = JSON.parse(line);
9420
- } catch {
9421
- continue;
9422
- }
9423
- if (ev.type === "event_msg" && ev.payload?.type === "user_message") {
9424
- messageCount++;
9425
- if (!firstPrompt && ev.payload.message) {
9426
- firstPrompt = flatString(String(ev.payload.message).slice(0, 200));
9490
+ if (opts.previews !== false)
9491
+ try {
9492
+ const lines = readFileSync6(filePath, "utf-8").split("\n");
9493
+ for (const line of lines) {
9494
+ if (!line.trim())
9495
+ continue;
9496
+ let ev;
9497
+ try {
9498
+ ev = JSON.parse(line);
9499
+ } catch {
9500
+ continue;
9501
+ }
9502
+ if (ev.type === "event_msg" && ev.payload?.type === "user_message") {
9503
+ messageCount++;
9504
+ if (!firstPrompt && ev.payload.message) {
9505
+ firstPrompt = flatString(String(ev.payload.message).slice(0, 200));
9506
+ }
9507
+ } else if (ev.type === "response_item" && ev.payload?.type === "message") {
9508
+ messageCount++;
9427
9509
  }
9428
- } else if (ev.type === "response_item" && ev.payload?.type === "message") {
9429
- messageCount++;
9430
9510
  }
9511
+ } catch {
9431
9512
  }
9432
- } catch {
9433
- }
9434
9513
  const prior = seen.get(rawId);
9435
9514
  if (prior) {
9436
9515
  if (stat3.mtimeMs > prior.mtime) {
@@ -9837,10 +9916,10 @@ var init_codex2 = __esm({
9837
9916
  if (p === loc.path)
9838
9917
  continue;
9839
9918
  try {
9840
- const fd = openSync6(p, "r");
9919
+ const fd = openSync7(p, "r");
9841
9920
  const buf = Buffer.alloc(4096);
9842
- const n = readSync6(fd, buf, 0, 4096, 0);
9843
- closeSync6(fd);
9921
+ const n = readSync7(fd, buf, 0, 4096, 0);
9922
+ closeSync7(fd);
9844
9923
  const head = buf.toString("utf-8", 0, n);
9845
9924
  if (head.includes(parentId) && head.includes("parent_thread_id"))
9846
9925
  push(p, `subagents/${f}`);
@@ -9865,11 +9944,11 @@ var init_codex2 = __esm({
9865
9944
  return 0;
9866
9945
  }
9867
9946
  }
9868
- async readFromOffset(prefixedId, offset) {
9947
+ async readFromOffset(prefixedId, offset, maxBytes) {
9869
9948
  const loc = this.findSession(prefixedId);
9870
9949
  if (!loc)
9871
9950
  return { text: "", newOffset: offset };
9872
- return readTailFromOffset(loc.path, offset);
9951
+ return readTailFromOffset(loc.path, offset, maxBytes);
9873
9952
  }
9874
9953
  };
9875
9954
  codexBackend = new CodexBackend();
@@ -10120,21 +10199,22 @@ var init_agy = __esm({
10120
10199
  const projectDir = projectPath ? basename10(projectPath) : "";
10121
10200
  let firstPrompt = "";
10122
10201
  let messageCount = 0;
10123
- try {
10124
- const lines = readFileSync7(filePath, "utf-8").split("\n").filter(Boolean);
10125
- messageCount = lines.length;
10126
- for (const line of lines) {
10127
- const obj = JSON.parse(line);
10128
- if (obj.source === "USER_EXPLICIT" && obj.type === "USER_INPUT" && typeof obj.content === "string") {
10129
- const content = obj.content;
10130
- const match = content.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/);
10131
- const rawText = match ? match[1].trim() : content.trim();
10132
- firstPrompt = flatString(rawText.slice(0, 200));
10133
- break;
10202
+ if (opts.previews !== false)
10203
+ try {
10204
+ const lines = readFileSync7(filePath, "utf-8").split("\n").filter(Boolean);
10205
+ messageCount = lines.length;
10206
+ for (const line of lines) {
10207
+ const obj = JSON.parse(line);
10208
+ if (obj.source === "USER_EXPLICIT" && obj.type === "USER_INPUT" && typeof obj.content === "string") {
10209
+ const content = obj.content;
10210
+ const match = content.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/);
10211
+ const rawText = match ? match[1].trim() : content.trim();
10212
+ firstPrompt = flatString(rawText.slice(0, 200));
10213
+ break;
10214
+ }
10134
10215
  }
10216
+ } catch {
10135
10217
  }
10136
- } catch {
10137
- }
10138
10218
  if (seenIds.has(rawId))
10139
10219
  continue;
10140
10220
  seenIds.add(rawId);
@@ -10430,11 +10510,11 @@ var init_agy = __esm({
10430
10510
  return 0;
10431
10511
  }
10432
10512
  }
10433
- async readFromOffset(prefixedId, offset) {
10513
+ async readFromOffset(prefixedId, offset, maxBytes) {
10434
10514
  const loc = this.findSession(prefixedId);
10435
10515
  if (!loc)
10436
10516
  return { text: "", newOffset: offset };
10437
- return readTailFromOffset(loc.path, offset);
10517
+ return readTailFromOffset(loc.path, offset, maxBytes);
10438
10518
  }
10439
10519
  };
10440
10520
  agyBackend = new AgyBackend();
@@ -12895,10 +12975,10 @@ var init_types = __esm({
12895
12975
 
12896
12976
  // ../engine/dist/transcript/opencode.js
12897
12977
  async function parseOpenCodeSubagents(parentSessionId) {
12898
- const { existsSync: existsSync50 } = await import("fs");
12978
+ const { existsSync: existsSync51 } = await import("fs");
12899
12979
  const { opencodeBackend: opencodeBackend2 } = await Promise.resolve().then(() => (init_opencode(), opencode_exports));
12900
12980
  const dbPath = opencodeBackend2.dbPath();
12901
- if (!existsSync50(dbPath))
12981
+ if (!existsSync51(dbPath))
12902
12982
  return [];
12903
12983
  const realParent = parentSessionId.replace("opencode_", "");
12904
12984
  const db = openSqliteReadonlyOrThrow(dbPath);
@@ -12939,10 +13019,10 @@ async function parseOpenCodeSubagents(parentSessionId) {
12939
13019
  }
12940
13020
  }
12941
13021
  async function parseOpenCodeTranscript(sessionId) {
12942
- const { existsSync: existsSync50 } = await import("fs");
13022
+ const { existsSync: existsSync51 } = await import("fs");
12943
13023
  const { opencodeBackend: opencodeBackend2 } = await Promise.resolve().then(() => (init_opencode(), opencode_exports));
12944
13024
  const dbPath = opencodeBackend2.dbPath();
12945
- if (!existsSync50(dbPath))
13025
+ if (!existsSync51(dbPath))
12946
13026
  return [];
12947
13027
  const realId = sessionId.replace("opencode_", "");
12948
13028
  const db = openSqliteReadonlyOrThrow(dbPath);
@@ -13019,6 +13099,7 @@ __export(transcript_exports, {
13019
13099
  shadowFileFor: () => shadowFileFor,
13020
13100
  shadowRoot: () => shadowRoot,
13021
13101
  shadowSizeOnDisk: () => shadowSizeOnDisk,
13102
+ shadowUncompressedBytes: () => shadowUncompressedBytes,
13022
13103
  snapshotShadow: () => snapshotShadow,
13023
13104
  trimTranscriptForSync: () => trimTranscriptForSync,
13024
13105
  updateShadow: () => updateShadow,
@@ -13201,12 +13282,13 @@ function resolveProjectId(inputPath) {
13201
13282
  if (!isAbsolute(inputPath)) {
13202
13283
  return { id: `path:${inputPath}`, displayName: inputPath, source: "path" };
13203
13284
  }
13285
+ const known = inputCache.get(inputPath);
13286
+ if (known)
13287
+ return known;
13204
13288
  const realPath = safeRealpath(inputPath);
13205
- const cached = resolveCache.get(realPath);
13206
- if (cached)
13207
- return cached;
13208
- const result = doResolve(realPath);
13289
+ const result = resolveCache.get(realPath) ?? doResolve(realPath);
13209
13290
  resolveCache.set(realPath, result);
13291
+ inputCache.set(inputPath, result);
13210
13292
  return result;
13211
13293
  }
13212
13294
  function doResolve(realPath) {
@@ -13485,7 +13567,7 @@ function globToRegex(glob) {
13485
13567
  function escapeRe(s) {
13486
13568
  return s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
13487
13569
  }
13488
- var DEFAULT_AUTO_WORKSPACE_MIN_REPOS, BOT_WORKTREE_MARKERS, resolveCache, gitCache, workspaceCache, cachedConfig, GIT_TIMEOUT_MS, AUTO_WORKSPACE_MAX_DEPTH;
13570
+ var DEFAULT_AUTO_WORKSPACE_MIN_REPOS, BOT_WORKTREE_MARKERS, resolveCache, inputCache, gitCache, workspaceCache, cachedConfig, GIT_TIMEOUT_MS, AUTO_WORKSPACE_MAX_DEPTH;
13489
13571
  var init_project_resolver = __esm({
13490
13572
  "../engine/dist/core/project-resolver.js"() {
13491
13573
  "use strict";
@@ -13493,6 +13575,7 @@ var init_project_resolver = __esm({
13493
13575
  DEFAULT_AUTO_WORKSPACE_MIN_REPOS = 3;
13494
13576
  BOT_WORKTREE_MARKERS = [".claude-pr-bot"];
13495
13577
  resolveCache = /* @__PURE__ */ new Map();
13578
+ inputCache = /* @__PURE__ */ new Map();
13496
13579
  gitCache = /* @__PURE__ */ new Map();
13497
13580
  workspaceCache = /* @__PURE__ */ new Map();
13498
13581
  cachedConfig = null;
@@ -17875,7 +17958,7 @@ var init_sync_fields = __esm({
17875
17958
  });
17876
17959
 
17877
17960
  // ../engine/dist/core/index-lock.js
17878
- import { existsSync as existsSync39, mkdirSync as mkdirSync8, openSync as openSync7, writeFileSync as writeFileSync7, readFileSync as readFileSync31, rmSync as rmSync4, statSync as statSync35, closeSync as closeSync7 } from "fs";
17961
+ import { existsSync as existsSync39, mkdirSync as mkdirSync8, openSync as openSync8, writeFileSync as writeFileSync7, readFileSync as readFileSync31, rmSync as rmSync4, statSync as statSync35, closeSync as closeSync8 } from "fs";
17879
17962
  import { dirname as dirname17, join as join40 } from "path";
17880
17963
  function acquireIndexLock(opts) {
17881
17964
  const lockPath = lockFilePath();
@@ -17887,7 +17970,7 @@ function acquireIndexLock(opts) {
17887
17970
  }
17888
17971
  let fd;
17889
17972
  try {
17890
- fd = openSync7(lockPath, "wx", 384);
17973
+ fd = openSync8(lockPath, "wx", 384);
17891
17974
  } catch (err) {
17892
17975
  if (err.code === "EEXIST")
17893
17976
  return null;
@@ -17897,7 +17980,7 @@ function acquireIndexLock(opts) {
17897
17980
  try {
17898
17981
  writeFileSync7(fd, JSON.stringify(holder));
17899
17982
  } finally {
17900
- closeSync7(fd);
17983
+ closeSync8(fd);
17901
17984
  }
17902
17985
  let released = false;
17903
17986
  return {
@@ -17988,7 +18071,7 @@ var init_http = __esm({
17988
18071
  CLI_VERSION = JSON.parse(readFileSync32(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
17989
18072
  } catch {
17990
18073
  try {
17991
- if (true) CLI_VERSION = "0.7.0";
18074
+ if (true) CLI_VERSION = "0.7.2";
17992
18075
  } catch {
17993
18076
  }
17994
18077
  }
@@ -18460,16 +18543,16 @@ async function flush() {
18460
18543
  }
18461
18544
  function stableMark(value) {
18462
18545
  try {
18463
- const { createHash: createHash13 } = __require("node:crypto");
18464
- return createHash13("sha256").update(value).digest("hex").slice(0, 12);
18546
+ const { createHash: createHash14 } = __require("node:crypto");
18547
+ return createHash14("sha256").update(value).digest("hex").slice(0, 12);
18465
18548
  } catch {
18466
18549
  return "";
18467
18550
  }
18468
18551
  }
18469
18552
  function hashHost() {
18470
18553
  try {
18471
- const { createHash: createHash13 } = __require("node:crypto");
18472
- return createHash13("sha256").update(hostname()).digest("hex").slice(0, 12);
18554
+ const { createHash: createHash14 } = __require("node:crypto");
18555
+ return createHash14("sha256").update(hostname()).digest("hex").slice(0, 12);
18473
18556
  } catch {
18474
18557
  return "";
18475
18558
  }
@@ -18483,7 +18566,7 @@ var init_telemetry = __esm({
18483
18566
  init_sync_client();
18484
18567
  cliVersion = () => {
18485
18568
  try {
18486
- return true ? "0.7.0" : "dev";
18569
+ return true ? "0.7.2" : "dev";
18487
18570
  } catch {
18488
18571
  return "dev";
18489
18572
  }
@@ -18708,16 +18791,17 @@ __export(auto_update_exports, {
18708
18791
  noteUpdateFailure: () => noteUpdateFailure,
18709
18792
  planAutoUpdate: () => planAutoUpdate,
18710
18793
  readUpdateState: () => readUpdateState,
18794
+ resolveNpm: () => resolveNpm,
18711
18795
  runAutoUpdate: () => runAutoUpdate,
18712
18796
  runningPrefix: () => runningPrefix,
18713
18797
  sweepStaleStaging: () => sweepStaleStaging,
18714
18798
  tarballUrl: () => tarballUrl
18715
18799
  });
18716
- import { execSync as execSync2 } from "node:child_process";
18717
- import { readFileSync as readFileSync34 } from "node:fs";
18800
+ import { execSync as execSync2, execFileSync as execFileSync3 } from "node:child_process";
18801
+ import { readFileSync as readFileSync34, existsSync as existsSync41 } from "node:fs";
18718
18802
  import { fileURLToPath as fileURLToPath2 } from "node:url";
18719
18803
  import { writeFileSync as writeFileSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync5, readdirSync as readdirSync31, statSync as statSync36, mkdirSync as mkdirSync9 } from "node:fs";
18720
- import { join as join41, dirname as dirname18, sep as sep2 } from "node:path";
18804
+ import { join as join41, dirname as dirname18, sep as sep2, posix as pathPosix, win32 as pathWin32 } from "node:path";
18721
18805
  import { tmpdir as tmpdir2, homedir as homedir15 } from "node:os";
18722
18806
  import { createHash as createHash7 } from "node:crypto";
18723
18807
  function compareVersions(a, b) {
@@ -18832,25 +18916,46 @@ function runningPrefix(moduleUrl = import.meta.url) {
18832
18916
  const beforeNodeModules = here.slice(0, at);
18833
18917
  return beforeNodeModules.endsWith(`${sep2}lib`) ? beforeNodeModules.slice(0, -4) : beforeNodeModules;
18834
18918
  }
18835
- function npmInstall(cmd) {
18919
+ function npmInstall(npm, args) {
18836
18920
  try {
18837
- execSync2(cmd, { stdio: ["ignore", "ignore", "pipe"] });
18921
+ const opts = { stdio: ["ignore", "ignore", "pipe"], env: npm.env };
18922
+ if (npm.shell) execSync2([npm.file, ...npm.args, ...args].map((a) => `"${a}"`).join(" "), opts);
18923
+ else execFileSync3(npm.file, [...npm.args, ...args], opts);
18838
18924
  } catch (e) {
18839
18925
  const err = e;
18840
18926
  const detail = String(err.stderr ?? "").trim().split("\n").filter((l) => l.trim() && !/^npm (notice|warn)\b/i.test(l)).slice(-4).join(" | ").slice(0, 400);
18841
18927
  throw new Error(detail ? `${err.message ?? "install failed"} \u2014 ${detail}` : err.message ?? "install failed");
18842
18928
  }
18843
18929
  }
18930
+ function resolveNpm(loc) {
18931
+ const win = loc.platform === "win32";
18932
+ const p = win ? pathWin32 : pathPosix;
18933
+ const nodeDir = p.dirname(loc.execPath);
18934
+ const cliUnder = (root) => win ? p.join(root, "node_modules", "npm", "bin", "npm-cli.js") : p.join(root, "lib", "node_modules", "npm", "bin", "npm-cli.js");
18935
+ const candidates2 = [
18936
+ cliUnder(win ? nodeDir : p.join(nodeDir, "..")),
18937
+ ...loc.prefix ? [cliUnder(loc.prefix)] : []
18938
+ ];
18939
+ const pathKey = Object.keys(loc.env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
18940
+ const binDirs = [nodeDir, ...loc.prefix ? [win ? loc.prefix : p.join(loc.prefix, "bin")] : []];
18941
+ const delimiter3 = win ? ";" : ":";
18942
+ const current = loc.env[pathKey] ?? "";
18943
+ const env = { ...loc.env, [pathKey]: [...binDirs, ...current ? [current] : []].join(delimiter3) };
18944
+ const cli = candidates2.find((c) => loc.exists(c));
18945
+ if (cli) return { file: loc.execPath, args: [cli], env, shell: false };
18946
+ return { file: "npm", args: [], env, shell: true };
18947
+ }
18844
18948
  function realInstall(tgz) {
18845
18949
  const prefix = runningPrefix();
18950
+ const npm = resolveNpm({ execPath: process.execPath, prefix, platform: process.platform, env: process.env, exists: existsSync41 });
18846
18951
  if (prefix) {
18847
- npmInstall(`npm install -g --prefix "${prefix}" "${tgz}"`);
18952
+ npmInstall(npm, ["install", "-g", "--prefix", prefix, tgz]);
18848
18953
  return;
18849
18954
  }
18850
18955
  try {
18851
- npmInstall(`npm install -g "${tgz}"`);
18956
+ npmInstall(npm, ["install", "-g", tgz]);
18852
18957
  } catch {
18853
- npmInstall(`npm install -g --prefix "${process.env.HOME}/.local" "${tgz}"`);
18958
+ npmInstall(npm, ["install", "-g", "--prefix", join41(homedir15(), ".local"), tgz]);
18854
18959
  }
18855
18960
  }
18856
18961
  function realRestart(platform2) {
@@ -19434,7 +19539,7 @@ async function syncToTargetInScope(cred, opts = {}) {
19434
19539
  const listSinceMs = changedWalk ? Math.max(0, (opts.sinceMs ?? 0) - CHANGED_WALK_OVERLAP_MS) : opts.useLedger ? void 0 : opts.sinceMs;
19435
19540
  const refs = listAvailableBackends().flatMap((b) => {
19436
19541
  try {
19437
- return b.listSessions({ sinceMs: listSinceMs });
19542
+ return b.listSessions({ sinceMs: listSinceMs, previews: false });
19438
19543
  } catch {
19439
19544
  return [];
19440
19545
  }
@@ -19653,16 +19758,53 @@ async function syncToTargetInScope(cred, opts = {}) {
19653
19758
  clearTimeout(timer);
19654
19759
  }
19655
19760
  };
19761
+ const postChunkedHead = async (conv, fileSize) => {
19762
+ const headers = { "content-type": "application/json" };
19763
+ if (cred.token) headers.authorization = `Bearer ${cred.token}`;
19764
+ const ac = new AbortController();
19765
+ const timer = setTimeout(() => ac.abort(new Error(`upload timed out after ${UPLOAD_TIMEOUT_MS}ms`)), UPLOAD_TIMEOUT_MS);
19766
+ try {
19767
+ const res = await fetch(`${base2}/api/sync`, {
19768
+ method: "POST",
19769
+ headers,
19770
+ body: JSON.stringify({ conversations: [conv] }),
19771
+ signal: ac.signal
19772
+ });
19773
+ if (!res.ok) throw new Error(`chunked head sync failed: HTTP ${res.status} ${await res.text().catch(() => "")}`);
19774
+ const body = await res.json().catch(() => ({}));
19775
+ const id = conv.session_id;
19776
+ const guarded = (body.shrink_guarded ?? []).find((g) => g.session_id === id);
19777
+ const mtime = conv.mtime;
19778
+ try {
19779
+ if (!guarded) {
19780
+ markSynced(base2, [{ id, mtime, offset: conv.from_offset, size: conv.from_offset, acked: true }]);
19781
+ } else if (typeof guarded.o === "number" && guarded.o > 0 && guarded.o <= fileSize) {
19782
+ markSynced(base2, [{ id, mtime, offset: guarded.o, size: guarded.o, acked: true }]);
19783
+ } else {
19784
+ markSynced(base2, [{ id, mtime }]);
19785
+ }
19786
+ } catch {
19787
+ }
19788
+ appendResults.uploaded++;
19789
+ } finally {
19790
+ clearTimeout(timer);
19791
+ }
19792
+ };
19656
19793
  let appendBatch = [];
19794
+ let appendBatchBytes = 0;
19657
19795
  const APPEND_BATCH_SIZE = 50;
19796
+ const APPEND_BATCH_MAX_BYTES = SYNC_CHUNK_BYTES;
19658
19797
  const appendFlush = async () => {
19659
19798
  if (appendBatch.length === 0) return;
19660
19799
  const batch = appendBatch;
19661
19800
  appendBatch = [];
19801
+ appendBatchBytes = 0;
19662
19802
  await postAppend(batch);
19663
19803
  };
19664
- const appendAdd = async (conv) => {
19804
+ const appendAdd = async (conv, bytes = 0) => {
19805
+ if (appendBatch.length > 0 && appendBatchBytes + bytes > APPEND_BATCH_MAX_BYTES) await appendFlush();
19665
19806
  appendBatch.push(conv);
19807
+ appendBatchBytes += bytes;
19666
19808
  if (appendBatch.length >= APPEND_BATCH_SIZE) await appendFlush();
19667
19809
  };
19668
19810
  const itemsBatch = makeBatcher("items", 100);
@@ -19712,7 +19854,7 @@ async function syncToTargetInScope(cred, opts = {}) {
19712
19854
  cred,
19713
19855
  () => listAvailableBackends().flatMap((b) => {
19714
19856
  try {
19715
- return b.listSessions({});
19857
+ return b.listSessions({ previews: false });
19716
19858
  } catch {
19717
19859
  return [];
19718
19860
  }
@@ -19741,7 +19883,7 @@ async function syncToTargetInScope(cred, opts = {}) {
19741
19883
  let externalFindings = null;
19742
19884
  if (scanSecrets) {
19743
19885
  externalFindings = /* @__PURE__ */ new Map();
19744
- const toScan = slice.filter((ref) => modeOf(ref) === "full" && sessionFileBytes(ref) <= FULL_BUILD_MAX_BYTES);
19886
+ const toScan = slice.filter((ref) => modeOf(ref) === "full" && sessionBuildBytes(ref) <= FULL_BUILD_MAX_BYTES);
19745
19887
  trace?.(`batch secret scan start: ${toScan.length} session(s), \u2264${(BATCH_SCAN_MAX_BYTES / 1048576).toFixed(0)}MB per slice`);
19746
19888
  const batch = batchScanExternal(
19747
19889
  toScan.map((ref) => ({
@@ -19785,11 +19927,17 @@ async function syncToTargetInScope(cred, opts = {}) {
19785
19927
  const ack = ledger.get(ref.prefixedId);
19786
19928
  try {
19787
19929
  const tail = await buildConversationTail(ref, ack.o ?? 0, { mapPath, tenantRules });
19788
- if (tail && tail.findings.length > 0) {
19930
+ const chunked = sessionBuildBytes(ref) > FULL_BUILD_MAX_BYTES;
19931
+ if (tail && tail.findings.length > 0 && !chunked) {
19789
19932
  promotedForFindings = true;
19790
19933
  } else if (tail) {
19791
19934
  redactions += tail.redactions;
19792
- await appendAdd(tail.conv);
19935
+ await appendAdd(tail.conv, tail.bytes);
19936
+ if (tail.findings.length > 0) {
19937
+ await appendFlush();
19938
+ const all = await scanTranscriptFindingsChunked(ref, tenantRules);
19939
+ if (all.length > 0) await findingsBatch.add(all);
19940
+ }
19793
19941
  walked++;
19794
19942
  if (walked % 250 === 0) console.error(`[sync] ${walked}/${slice.length} sessions\u2026`);
19795
19943
  continue;
@@ -19837,12 +19985,13 @@ async function syncToTargetInScope(cred, opts = {}) {
19837
19985
  }
19838
19986
  redactions += built.redactions;
19839
19987
  if (built.srcHash) shippedHash.set(ref.prefixedId, built.srcHash);
19840
- await convBatch.add(built.conv);
19988
+ if (built.conv.chunked) await postChunkedHead(built.conv, sessionFileBytes(ref));
19989
+ else await convBatch.add(built.conv);
19841
19990
  if (built.findings.length > 0) await findingsBatch.add(built.findings);
19842
19991
  if (upload.sessionMeta) {
19843
19992
  await accumulateKg(extractEntities(built.kgText, { projectPath: built.projectPath, sourceType: "session", sessionId: ref.prefixedId, validFrom: ref.mtime ? new Date(ref.mtime).toISOString().slice(0, 10) : void 0 }), ref.prefixedId);
19844
19993
  }
19845
- if (upload.sessionMeta && sessionFileBytes(ref) <= FULL_BUILD_MAX_BYTES) {
19994
+ if (upload.sessionMeta && sessionFileBytes(ref) <= DERIVED_MAX_BYTES) {
19846
19995
  try {
19847
19996
  const count = { redactions: 0 };
19848
19997
  const offloaded = await scanPool().runDerived({
@@ -20022,35 +20171,70 @@ async function syncToTargetInScope(cred, opts = {}) {
20022
20171
  scanMs: Math.round(scanMs)
20023
20172
  };
20024
20173
  }
20174
+ async function scanTranscriptFindingsChunked(ref, tenantRules) {
20175
+ const backend = getBackendForId(ref.prefixedId);
20176
+ const findings = [];
20177
+ if (!backend?.readFromOffset) return findings;
20178
+ let offset = 0;
20179
+ let lineBase = 0;
20180
+ for (; ; ) {
20181
+ const chunk = await backend.readFromOffset(ref.prefixedId, offset, SYNC_CHUNK_BYTES);
20182
+ if (chunk.newOffset <= offset) break;
20183
+ offset = chunk.newOffset;
20184
+ if (chunk.skippedBytes) {
20185
+ lineBase += 1;
20186
+ continue;
20187
+ }
20188
+ try {
20189
+ for (const f of scanTextForFindings(chunk.text)) {
20190
+ findings.push({ session_id: ref.prefixedId, detector: "builtin", rule: f.rule, line: lineBase + f.line, preview: f.preview });
20191
+ }
20192
+ if (tenantRules && tenantRules.length > 0) {
20193
+ for (const f of scanTenantRules(chunk.text, tenantRules)) {
20194
+ findings.push({ session_id: ref.prefixedId, detector: f.detector, rule: f.rule, line: lineBase + f.line, preview: f.preview });
20195
+ }
20196
+ }
20197
+ } catch {
20198
+ }
20199
+ for (let i = chunk.text.indexOf("\n"); i >= 0; i = chunk.text.indexOf("\n", i + 1)) lineBase++;
20200
+ }
20201
+ return dropFuzzyFindings(findings, (f) => ({ detector: f.detector, rule: f.rule }));
20202
+ }
20025
20203
  function sessionFileBytes(ref) {
20026
20204
  const backend = getBackendForId(ref.prefixedId);
20027
20205
  if (!backend?.isAppendOnly?.()) return 0;
20028
20206
  return backend.fileSize?.(ref.prefixedId) ?? 0;
20029
20207
  }
20208
+ function sessionBuildBytes(ref) {
20209
+ const main = sessionFileBytes(ref);
20210
+ if (main === 0) return 0;
20211
+ return Math.max(main, shadowUncompressedBytes(ref.toolId, ref.rawId));
20212
+ }
20030
20213
  async function buildConversationSync(ref, mtime, opts = {}) {
20031
20214
  const mapPath = opts.mapPath ?? ((p) => p);
20032
20215
  let includeRaw = opts.includeRaw !== false;
20033
20216
  let includeMeta = opts.includeMeta !== false;
20034
20217
  let srcHash;
20035
- const fileBytes = sessionFileBytes(ref);
20036
- const oversized = fileBytes > FULL_BUILD_MAX_BYTES;
20037
- let tailText = "";
20038
- let tailOffsetEnd = 0;
20218
+ const oversized = sessionBuildBytes(ref) > FULL_BUILD_MAX_BYTES;
20219
+ let headOffsetEnd = 0;
20220
+ let oversizedFindings = null;
20039
20221
  let transcript = null;
20040
20222
  let fullestContainer = null;
20041
20223
  if (oversized) {
20042
20224
  const backend = getBackendForId(ref.prefixedId);
20043
20225
  if (!backend?.readFromOffset) return null;
20044
20226
  try {
20045
- const tail = await backend.readFromOffset(ref.prefixedId, fileBytes - FULL_BUILD_TAIL_BYTES);
20046
- tailText = tail.text;
20047
- tailOffsetEnd = tail.newOffset;
20048
- const messages = parseTailMessages(ref.toolId, tailText);
20227
+ let head = await backend.readFromOffset(ref.prefixedId, 0, SYNC_CHUNK_BYTES);
20228
+ for (let hops = 0; head.skippedBytes && hops < 8; hops++) {
20229
+ head = await backend.readFromOffset(ref.prefixedId, head.newOffset, SYNC_CHUNK_BYTES);
20230
+ }
20231
+ headOffsetEnd = head.newOffset;
20232
+ const messages = parseTailMessages(ref.toolId, head.text);
20049
20233
  if (messages.length > 0) transcript = { messages, subagents: [] };
20050
20234
  } catch {
20051
20235
  }
20052
20236
  includeRaw = false;
20053
- includeMeta = false;
20237
+ oversizedFindings = await scanTranscriptFindingsChunked(ref, opts.tenantRules);
20054
20238
  } else {
20055
20239
  try {
20056
20240
  const backend = getBackendForId(ref.prefixedId) ?? getBackend("claude");
@@ -20111,7 +20295,6 @@ async function buildConversationSync(ref, mtime, opts = {}) {
20111
20295
  );
20112
20296
  let projectPath = ref.projectPath;
20113
20297
  const meta = { messageCount: textMessages.length };
20114
- if (oversized) meta.truncated = true;
20115
20298
  if (textMessages.filter((m) => m.role === "user").length <= 1) meta.oneShot = true;
20116
20299
  try {
20117
20300
  const cwd = ref.fullPath ? readCwdFromJsonl(ref.fullPath) : "";
@@ -20225,21 +20408,7 @@ async function buildConversationSync(ref, mtime, opts = {}) {
20225
20408
  }
20226
20409
  scanMs = performance.now() - t0;
20227
20410
  }
20228
- if (oversized && tailText) {
20229
- const t0 = performance.now();
20230
- try {
20231
- for (const f of scanTextForFindings(tailText)) {
20232
- findings.push({ session_id: ref.prefixedId, detector: "builtin", rule: f.rule, line: f.line, preview: f.preview });
20233
- }
20234
- if (opts.tenantRules && opts.tenantRules.length > 0) {
20235
- for (const f of scanTenantRules(tailText, opts.tenantRules)) {
20236
- findings.push({ session_id: ref.prefixedId, detector: f.detector, rule: f.rule, line: f.line, preview: f.preview });
20237
- }
20238
- }
20239
- } catch {
20240
- }
20241
- scanMs = performance.now() - t0;
20242
- }
20411
+ if (oversizedFindings) findings.push(...oversizedFindings);
20243
20412
  const keptFindings = dropFuzzyFindings(findings, (f) => ({ detector: f.detector, rule: f.rule }));
20244
20413
  findings.length = 0;
20245
20414
  findings.push(...keptFindings);
@@ -20285,7 +20454,10 @@ async function buildConversationSync(ref, mtime, opts = {}) {
20285
20454
  // (convBatch onFlush uses this, not a recomputed size) so the next
20286
20455
  // append's base_offset matches exactly — no spurious continuity misses on
20287
20456
  // an actively-growing session. undefined for non-append-only backends.
20288
- from_offset: oversized ? tailOffsetEnd : getBackendForId(ref.prefixedId)?.fileSize?.(ref.rawId),
20457
+ from_offset: oversized ? headOffsetEnd : getBackendForId(ref.prefixedId)?.fileSize?.(ref.rawId),
20458
+ // The head of a chunked session. The server reports back when its shrink
20459
+ // guard keeps a fuller stored copy, so the client appends from there.
20460
+ ...oversized ? { chunked: true } : {},
20289
20461
  meta,
20290
20462
  mtime
20291
20463
  },
@@ -20307,12 +20479,30 @@ async function buildConversationTail(ref, fromOffset, opts = {}) {
20307
20479
  const backend = getBackendForId(ref.prefixedId);
20308
20480
  if (!backend?.readFromOffset) return null;
20309
20481
  const mapPath = opts.mapPath ?? ((p) => p);
20310
- const { text: tailText, newOffset } = await backend.readFromOffset(ref.prefixedId, fromOffset);
20311
- if (!tailText.trim()) return null;
20312
- const messages = parseTailMessages(ref.toolId, tailText);
20313
- if (messages.length === 0) return null;
20482
+ const { text: tailText, newOffset, skippedBytes } = await backend.readFromOffset(ref.prefixedId, fromOffset, SYNC_CHUNK_BYTES);
20483
+ if (newOffset <= fromOffset) return null;
20484
+ const messages = skippedBytes ? [] : parseTailMessages(ref.toolId, tailText);
20314
20485
  const textMessages = messages.filter((m) => m.role !== "summary" && m.content?.trim());
20315
- if (textMessages.length === 0 && !messages.some((m) => m.toolCalls?.length)) return null;
20486
+ const hasContent = textMessages.length > 0 || messages.some((m) => m.toolCalls?.length);
20487
+ if (!hasContent) {
20488
+ return {
20489
+ conv: {
20490
+ session_id: ref.prefixedId,
20491
+ tool: ref.toolId,
20492
+ project_path: mapPath(ref.projectPath),
20493
+ append: true,
20494
+ base_offset: fromOffset,
20495
+ from_offset: newOffset,
20496
+ redacted_text: "",
20497
+ envelope: { v: TRANSCRIPT_VERSION, messages: [], subagents: [] },
20498
+ mtime: Math.floor(ref.mtime) || 0
20499
+ },
20500
+ redactions: 0,
20501
+ newOffset,
20502
+ findings: [],
20503
+ bytes: 0
20504
+ };
20505
+ }
20316
20506
  const firstUser = textMessages.find((m) => m.role === "user");
20317
20507
  if (isInternalToolPrompt(firstUser?.content)) return null;
20318
20508
  const count = { redactions: 0 };
@@ -20355,7 +20545,8 @@ async function buildConversationTail(ref, fromOffset, opts = {}) {
20355
20545
  },
20356
20546
  redactions: count.redactions,
20357
20547
  newOffset,
20358
- findings
20548
+ findings,
20549
+ bytes: Buffer.byteLength(tailText)
20359
20550
  };
20360
20551
  }
20361
20552
  function isSyncSkip(r) {
@@ -20385,7 +20576,7 @@ async function syncIncremental(opts = {}) {
20385
20576
  lock.release();
20386
20577
  }
20387
20578
  }
20388
- var ownVersion, ERROR_CLASS, STANDING_FAILURE, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, SYNC_RULES_REFUSAL, hashPath, acceptedUploads, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
20579
+ var ownVersion, ERROR_CLASS, STANDING_FAILURE, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, SYNC_RULES_REFUSAL, hashPath, acceptedUploads, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, DERIVED_MAX_BYTES, SYNC_CHUNK_BYTES, BATCH_SCAN_MAX_BYTES;
20389
20580
  var init_sync_client = __esm({
20390
20581
  "src/sync-client.ts"() {
20391
20582
  "use strict";
@@ -20430,7 +20621,7 @@ var init_sync_client = __esm({
20430
20621
  ownVersion = JSON.parse(readFileSync35(join42(dir, "../package.json"), "utf-8")).version || ownVersion;
20431
20622
  } catch {
20432
20623
  try {
20433
- if (true) ownVersion = "0.7.0";
20624
+ if (true) ownVersion = "0.7.2";
20434
20625
  } catch {
20435
20626
  }
20436
20627
  }
@@ -20456,8 +20647,9 @@ var init_sync_client = __esm({
20456
20647
  capsOk = /* @__PURE__ */ new Map();
20457
20648
  advertisedIngestConc = /* @__PURE__ */ new Map();
20458
20649
  advertisedGzipBody = /* @__PURE__ */ new Set();
20459
- FULL_BUILD_MAX_BYTES = Math.max(8, parseInt(process.env.CHAT_RECALL_FULL_BUILD_MAX_MB || "64", 10)) * 1024 * 1024;
20460
- FULL_BUILD_TAIL_BYTES = 16 * 1024 * 1024;
20650
+ FULL_BUILD_MAX_BYTES = Math.max(8, parseInt(process.env.CHAT_RECALL_FULL_BUILD_MAX_MB || "24", 10)) * 1024 * 1024;
20651
+ DERIVED_MAX_BYTES = Math.max(8, parseInt(process.env.CHAT_RECALL_DERIVED_MAX_MB || "64", 10)) * 1024 * 1024;
20652
+ SYNC_CHUNK_BYTES = 8 * 1024 * 1024;
20461
20653
  BATCH_SCAN_MAX_BYTES = Math.max(1, parseInt(process.env.CHAT_RECALL_BATCHSCAN_MAX_MB || "64", 10)) * 1024 * 1024;
20462
20654
  }
20463
20655
  });
@@ -20606,7 +20798,7 @@ var init_artifact_codec = __esm({
20606
20798
  });
20607
20799
 
20608
20800
  // ../engine/dist/core/toolkit-sync.js
20609
- import { existsSync as existsSync41, mkdirSync as mkdirSync11, readFileSync as readFileSync37, writeFileSync as writeFileSync10, cpSync } from "fs";
20801
+ import { existsSync as existsSync42, mkdirSync as mkdirSync11, readFileSync as readFileSync37, writeFileSync as writeFileSync10, cpSync } from "fs";
20610
20802
  import { dirname as dirname20, join as join44, basename as basename22 } from "path";
20611
20803
  import { homedir as homedir17 } from "os";
20612
20804
  function rowFromMemoryItem(type, item) {
@@ -20640,11 +20832,11 @@ function skillsDirFor(tool) {
20640
20832
  }
20641
20833
  function copySkill(row, toTool) {
20642
20834
  const skillDir = row.extra.skillDir || dirname20(row.filePath);
20643
- if (!existsSync41(skillDir))
20835
+ if (!existsSync42(skillDir))
20644
20836
  return { ok: false, status: 404, error: `Source dir missing: ${skillDir}` };
20645
20837
  const name = row.extra.skillName || basename22(skillDir);
20646
20838
  const targetDir = join44(skillsDirFor(toTool), name);
20647
- if (existsSync41(targetDir))
20839
+ if (existsSync42(targetDir))
20648
20840
  return { ok: false, status: 409, error: `Already exists: ${targetDir}` };
20649
20841
  try {
20650
20842
  mkdirSync11(dirname20(targetDir), { recursive: true });
@@ -20676,7 +20868,7 @@ function readMcpEntry(tool, name) {
20676
20868
  return entry;
20677
20869
  }
20678
20870
  for (const t of tries) {
20679
- if (!existsSync41(t.path))
20871
+ if (!existsSync42(t.path))
20680
20872
  continue;
20681
20873
  try {
20682
20874
  const cfg = JSON.parse(readFileSync37(t.path, "utf-8"));
@@ -20689,7 +20881,7 @@ function readMcpEntry(tool, name) {
20689
20881
  return null;
20690
20882
  }
20691
20883
  function readCodexMcpEntry(path, target) {
20692
- if (!existsSync41(path))
20884
+ if (!existsSync42(path))
20693
20885
  return null;
20694
20886
  let content;
20695
20887
  try {
@@ -20743,7 +20935,7 @@ function readCodexMcpEntry(path, target) {
20743
20935
  }
20744
20936
  function writeCodexMcpEntry(path, name, entry) {
20745
20937
  let content = "";
20746
- if (existsSync41(path))
20938
+ if (existsSync42(path))
20747
20939
  content = readFileSync37(path, "utf-8");
20748
20940
  const header = `[mcp_servers.${name}]`;
20749
20941
  if (content.includes(header))
@@ -20801,7 +20993,7 @@ function writeMcpEntry(toTool, name, entry) {
20801
20993
  key2 = "mcp";
20802
20994
  }
20803
20995
  let cfg = {};
20804
- if (existsSync41(path)) {
20996
+ if (existsSync42(path)) {
20805
20997
  try {
20806
20998
  cfg = JSON.parse(readFileSync37(path, "utf-8"));
20807
20999
  } catch {
@@ -20865,7 +21057,7 @@ function copyCommandOrAgent(type, row, toTool) {
20865
21057
  return { ok: false, status: 404, error: `Source unreadable: ${e instanceof Error ? e.message : "failed"}` };
20866
21058
  }
20867
21059
  const out = emit(type, art, toTool);
20868
- if (existsSync41(out.path))
21060
+ if (existsSync42(out.path))
20869
21061
  return { ok: false, status: 409, error: `Already exists: ${out.path}` };
20870
21062
  try {
20871
21063
  mkdirSync11(dirname20(out.path), { recursive: true });
@@ -20885,7 +21077,7 @@ function copyInstructions(row, toTool) {
20885
21077
  return { ok: false, status: 404, error: `Source unreadable: ${e instanceof Error ? e.message : "failed"}` };
20886
21078
  }
20887
21079
  const out = emit("instructions", art, toTool, row.projectPath || void 0);
20888
- if (existsSync41(out.path))
21080
+ if (existsSync42(out.path))
20889
21081
  return { ok: false, status: 409, error: `Already exists: ${out.path}` };
20890
21082
  try {
20891
21083
  mkdirSync11(dirname20(out.path), { recursive: true });
@@ -21028,7 +21220,7 @@ var init_toolkit_sync = __esm({
21028
21220
  });
21029
21221
 
21030
21222
  // ../engine/dist/core/which.js
21031
- import { existsSync as existsSync43, statSync as statSync39, accessSync, constants } from "node:fs";
21223
+ import { existsSync as existsSync44, statSync as statSync39, accessSync, constants } from "node:fs";
21032
21224
  import { join as join46, isAbsolute as isAbsolute2, delimiter as delimiter2, sep as sep3 } from "node:path";
21033
21225
  function executable(p) {
21034
21226
  try {
@@ -21069,7 +21261,7 @@ function resolveOnPath(name) {
21069
21261
  continue;
21070
21262
  for (const c of candidates(name)) {
21071
21263
  const full = join46(dir, c);
21072
- if (existsSync43(full) && executable(full))
21264
+ if (existsSync44(full) && executable(full))
21073
21265
  return full;
21074
21266
  }
21075
21267
  }
@@ -21094,7 +21286,7 @@ __export(toolkit_pull_exports, {
21094
21286
  planPull: () => planPull,
21095
21287
  portableCommand: () => portableCommand
21096
21288
  });
21097
- import { existsSync as existsSync44, mkdirSync as mkdirSync13, statSync as statSync40, writeFileSync as writeFileSync12 } from "node:fs";
21289
+ import { existsSync as existsSync45, mkdirSync as mkdirSync13, statSync as statSync40, writeFileSync as writeFileSync12 } from "node:fs";
21098
21290
  import { dirname as dirname22, join as join47, basename as basename23, isAbsolute as isAbsolute3 } from "node:path";
21099
21291
  function isExecutableFile(p) {
21100
21292
  try {
@@ -21247,7 +21439,7 @@ function executePull(rows, opts = {}) {
21247
21439
  for (const tool of tools) {
21248
21440
  const dir = join47(skillsDirFor(tool), name);
21249
21441
  const file = join47(dir, "SKILL.md");
21250
- if (existsSync44(file)) {
21442
+ if (existsSync45(file)) {
21251
21443
  outcomes.push({ type: "skill", name, tool, status: "present" });
21252
21444
  continue;
21253
21445
  }
@@ -21292,7 +21484,7 @@ function executePull(rows, opts = {}) {
21292
21484
  outcomes.push({ type, name, tool, status: "failed", reason: e instanceof Error ? e.message : "emit failed" });
21293
21485
  continue;
21294
21486
  }
21295
- if (existsSync44(out.path)) {
21487
+ if (existsSync45(out.path)) {
21296
21488
  outcomes.push({ type, name, tool, status: "present" });
21297
21489
  continue;
21298
21490
  }
@@ -21468,6 +21660,9 @@ async function repairSession(id, opts = {}) {
21468
21660
  note: "archive count is a lower bound; the rebuild also unions the on-disk tail"
21469
21661
  };
21470
21662
  }
21663
+ if (!opts.force && endpoints.every((ep) => (currentOnServer.get(ep.url) ?? 0) >= fullest.messages)) {
21664
+ return { sessionId: id, status: "already-full", fullestMessages: fullest.messages, fullestSource: fullest.source, pushed: [] };
21665
+ }
21471
21666
  seedShadow(rawId, fullest.container);
21472
21667
  const located = backend.findSession(rawId);
21473
21668
  const projectPath = located?.projectPath || cwdFromContainer(fullest.container);
@@ -21510,7 +21705,7 @@ async function repairSession(id, opts = {}) {
21510
21705
  try {
21511
21706
  const res = await fetchWithTimeout(`${ep.url}/api/sync`, { method: "POST", headers, body: JSON.stringify({ conversations: [built.conv] }) }, 6e4);
21512
21707
  if (!res.ok) {
21513
- pushed.push({ server: ep.url, before, after: before });
21708
+ pushed.push({ server: ep.url, before, after: before, error: `HTTP ${res.status}` });
21514
21709
  log6(`push to ${ep.url} FAILED: HTTP ${res.status}`);
21515
21710
  continue;
21516
21711
  }
@@ -21519,10 +21714,21 @@ async function repairSession(id, opts = {}) {
21519
21714
  pushed.push({ server: ep.url, before, after: afterN });
21520
21715
  log6(`pushed to ${ep.url}: ${before} \u2192 ${afterN} msg(s)`);
21521
21716
  } catch (e) {
21522
- pushed.push({ server: ep.url, before, after: before });
21523
- log6(`push to ${ep.url} errored: ${e instanceof Error ? e.message : e}`);
21717
+ const error = e instanceof Error ? e.message : String(e);
21718
+ pushed.push({ server: ep.url, before, after: before, error });
21719
+ log6(`push to ${ep.url} errored: ${error}`);
21524
21720
  }
21525
21721
  }
21722
+ if (pushed.every((p) => p.error)) {
21723
+ return {
21724
+ sessionId: id,
21725
+ status: "error",
21726
+ fullestMessages: builtCount,
21727
+ fullestSource: fullest.source,
21728
+ pushed,
21729
+ note: `every push failed: ${pushed.map((p) => `${p.server}: ${p.error}`).join("; ")}`
21730
+ };
21731
+ }
21526
21732
  return { sessionId: id, status: "repaired", fullestMessages: builtCount, fullestSource: fullest.source, pushed };
21527
21733
  }
21528
21734
  async function repairSessions(ids, opts = {}) {
@@ -21592,7 +21798,7 @@ __export(shadow_prune_exports, {
21592
21798
  DEFAULT_MAX_TOTAL_BYTES: () => DEFAULT_MAX_TOTAL_BYTES,
21593
21799
  pruneShadow: () => pruneShadow
21594
21800
  });
21595
- import { readdirSync as readdirSync33, statSync as statSync42, unlinkSync as unlinkSync3 } from "fs";
21801
+ import { readdirSync as readdirSync33, statSync as statSync43, unlinkSync as unlinkSync3 } from "fs";
21596
21802
  import { join as join50 } from "path";
21597
21803
  function listShadows(isFullyAcked) {
21598
21804
  const out = [];
@@ -21617,7 +21823,7 @@ function listShadows(isFullyAcked) {
21617
21823
  const path = join50(dir, name);
21618
21824
  let st;
21619
21825
  try {
21620
- st = statSync42(path);
21826
+ st = statSync43(path);
21621
21827
  } catch {
21622
21828
  continue;
21623
21829
  }
@@ -21689,9 +21895,9 @@ var init_shadow_prune = __esm({
21689
21895
  });
21690
21896
 
21691
21897
  // ../engine/dist/types/code-intel.js
21692
- import { createHash as createHash11 } from "crypto";
21898
+ import { createHash as createHash12 } from "crypto";
21693
21899
  function shortHash(s) {
21694
- return createHash11("sha256").update(s).digest("hex").slice(0, 16);
21900
+ return createHash12("sha256").update(s).digest("hex").slice(0, 16);
21695
21901
  }
21696
21902
  function snippetKey(snippet) {
21697
21903
  return (snippet ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
@@ -21732,11 +21938,11 @@ var init_code_intel = __esm({
21732
21938
  });
21733
21939
 
21734
21940
  // ../engine/dist/core/companions.js
21735
- import { existsSync as existsSync47, mkdirSync as mkdirSync16, statSync as statSync43, chmodSync as chmodSync4, unlinkSync as unlinkSync4, renameSync as renameSync5, createWriteStream, readFileSync as readFileSync42, writeFileSync as writeFileSync15 } from "node:fs";
21941
+ import { existsSync as existsSync48, mkdirSync as mkdirSync16, statSync as statSync44, chmodSync as chmodSync4, unlinkSync as unlinkSync4, renameSync as renameSync5, createWriteStream, readFileSync as readFileSync42, writeFileSync as writeFileSync15 } from "node:fs";
21736
21942
  import { homedir as homedir19 } from "node:os";
21737
21943
  import { join as join51 } from "node:path";
21738
21944
  import { execSync as execSync3 } from "node:child_process";
21739
- import { createHash as createHash12 } from "node:crypto";
21945
+ import { createHash as createHash13 } from "node:crypto";
21740
21946
  function expectedCodeindexSha256(artifact) {
21741
21947
  return CODEINDEX_SHA256[artifact.replace(/^codeindex-/, "").replace(/\.exe$/, "")] ?? null;
21742
21948
  }
@@ -21783,7 +21989,7 @@ function checkCodeindexStatus() {
21783
21989
  status.installed = true;
21784
21990
  status.path = foundPath;
21785
21991
  try {
21786
- status.size = statSync43(foundPath).size;
21992
+ status.size = statSync44(foundPath).size;
21787
21993
  } catch {
21788
21994
  }
21789
21995
  try {
@@ -21844,7 +22050,7 @@ async function installCodeindex(opts = {}) {
21844
22050
  if (!expected) {
21845
22051
  throw new Error(`No pinned checksum for ${status.artifactName} \u2014 refusing to install an unverified binary. Add its sha256 to CODEINDEX_SHA256 in companions.ts.`);
21846
22052
  }
21847
- const actual = createHash12("sha256").update(readFileSync42(tmpPath)).digest("hex");
22053
+ const actual = createHash13("sha256").update(readFileSync42(tmpPath)).digest("hex");
21848
22054
  if (actual !== expected) {
21849
22055
  throw new Error(`Checksum mismatch for ${status.artifactName}.
21850
22056
  expected ${expected}
@@ -21855,7 +22061,7 @@ Refusing to install. The release may have been re-uploaded, or something is serv
21855
22061
  renameSync5(tmpPath, CODEINDEX_BIN_PATH);
21856
22062
  } catch (err) {
21857
22063
  try {
21858
- if (existsSync47(tmpPath))
22064
+ if (existsSync48(tmpPath))
21859
22065
  unlinkSync4(tmpPath);
21860
22066
  } catch {
21861
22067
  }
@@ -22008,7 +22214,10 @@ async function resolveCodeindexBin(autoInstall = true) {
22008
22214
  }
22009
22215
  function runMcp(binPath, workspace, calls, timeoutMs = 18e4) {
22010
22216
  return new Promise((resolve3, reject) => {
22011
- const p = spawn2(binPath, ["--mcp", "--workspace", workspace], { stdio: ["pipe", "pipe", "ignore"] });
22217
+ const p = spawn2(binPath, ["--mcp", "--workspace", workspace], {
22218
+ stdio: ["pipe", "pipe", "ignore"],
22219
+ env: { ...process.env, CODEINDEX_NO_DAEMON: "1" }
22220
+ });
22012
22221
  const res = /* @__PURE__ */ new Map();
22013
22222
  let pending = "";
22014
22223
  let failed = false;
@@ -22939,7 +23148,7 @@ __export(service_installer_exports, {
22939
23148
  uninstallService: () => uninstallService
22940
23149
  });
22941
23150
  import { execSync as execSync5 } from "child_process";
22942
- import { existsSync as existsSync48, mkdirSync as mkdirSync17, readFileSync as readFileSync44, rmSync as rmSync7, writeFileSync as writeFileSync16 } from "fs";
23151
+ import { existsSync as existsSync49, mkdirSync as mkdirSync17, readFileSync as readFileSync44, rmSync as rmSync7, writeFileSync as writeFileSync16 } from "fs";
22943
23152
  import { homedir as homedir20 } from "os";
22944
23153
  import { join as join53 } from "path";
22945
23154
  import { fileURLToPath as fileURLToPath5 } from "url";
@@ -22966,7 +23175,7 @@ function isServiceRunning() {
22966
23175
  return /com\.chat-recall\.watch/.test(tryCmd("launchctl list 2>/dev/null"));
22967
23176
  }
22968
23177
  if (process.platform === "linux") {
22969
- return /active/.test(tryCmd("systemctl --user is-active chat-recall-watch.service 2>/dev/null"));
23178
+ return /^active$/m.test(tryCmd("systemctl --user is-active chat-recall-watch.service 2>/dev/null"));
22970
23179
  }
22971
23180
  return false;
22972
23181
  }
@@ -22975,7 +23184,7 @@ function refreshServiceDefinition() {
22975
23184
  try {
22976
23185
  const { watchJs, node, logFile } = resolveDaemonPaths();
22977
23186
  const unitPath = join53(homedir20(), ".config", "systemd", "user", "chat-recall-watch.service");
22978
- if (!existsSync48(unitPath)) return false;
23187
+ if (!existsSync49(unitPath)) return false;
22979
23188
  const current = readFileSync44(unitPath, "utf8");
22980
23189
  if (!current.includes(watchJs)) return false;
22981
23190
  const wanted = renderSystemdUnit(watchJs, node, logFile);
@@ -23042,7 +23251,12 @@ function renderSystemdUnit(watchJs, node, logFile) {
23042
23251
  // sawtooth. 1536MB keeps GC honest (768 OOMed on the largest subagent-fanout session) with plenty of headroom for the
23043
23252
  // largest transcripts.
23044
23253
  `ExecStart=${node} --max-old-space-size=1536 ${watchJs}`,
23045
- "Restart=on-failure",
23254
+ // ALWAYS: the collector never ends its own work, so any exit systemd did
23255
+ // not ask for is a fault. Under on-failure a clean exit left it dead: on
23256
+ // 2026-09-16 the unit went "inactive (dead) … status=0/SUCCESS" and stayed
23257
+ // down for 8 days, so nothing synced and the auto-update tick, which runs
23258
+ // inside the daemon, never ran. `systemctl stop` still stops it.
23259
+ "Restart=always",
23046
23260
  "RestartSec=10",
23047
23261
  "Nice=10",
23048
23262
  // NEVER STOP RETRYING. systemd's default start limiter gives up after a
@@ -23138,7 +23352,7 @@ function uninstallService() {
23138
23352
  } catch {
23139
23353
  }
23140
23354
  const unitPath = join53(homedir20(), ".config", "systemd", "user", "chat-recall-watch.service");
23141
- if (existsSync48(unitPath)) {
23355
+ if (existsSync49(unitPath)) {
23142
23356
  rmSync7(unitPath, { force: true });
23143
23357
  removed = true;
23144
23358
  }
@@ -23155,7 +23369,7 @@ function uninstallService() {
23155
23369
  } catch {
23156
23370
  }
23157
23371
  let removed = false;
23158
- if (existsSync48(plistPath)) {
23372
+ if (existsSync49(plistPath)) {
23159
23373
  rmSync7(plistPath, { force: true });
23160
23374
  removed = true;
23161
23375
  }
@@ -23212,12 +23426,12 @@ init_codex2();
23212
23426
  init_cursor();
23213
23427
  init_opencode();
23214
23428
  init_agy();
23215
- import { existsSync as existsSync45, readFileSync as readFileSync39, writeFileSync as writeFileSync13, appendFileSync, mkdirSync as mkdirSync14 } from "node:fs";
23429
+ import { existsSync as existsSync46, readFileSync as readFileSync39, writeFileSync as writeFileSync13, appendFileSync, mkdirSync as mkdirSync14 } from "node:fs";
23216
23430
  import { join as join48 } from "node:path";
23217
23431
 
23218
23432
  // src/project-tasks.ts
23219
23433
  init_http();
23220
- import { existsSync as existsSync42, readFileSync as readFileSync38, writeFileSync as writeFileSync11, mkdirSync as mkdirSync12, statSync as statSync38 } from "node:fs";
23434
+ import { existsSync as existsSync43, readFileSync as readFileSync38, writeFileSync as writeFileSync11, mkdirSync as mkdirSync12, statSync as statSync38 } from "node:fs";
23221
23435
  import { join as join45, dirname as dirname21 } from "node:path";
23222
23436
  import { homedir as homedir18 } from "node:os";
23223
23437
 
@@ -23325,7 +23539,7 @@ async function pushProjectTaskStatuses(base2, authHeaders, opts = {}) {
23325
23539
  }
23326
23540
  for (const project of projects) {
23327
23541
  const file = join45(project, fk.filename);
23328
- if (!existsSync42(file)) continue;
23542
+ if (!existsSync43(file)) continue;
23329
23543
  let mtime;
23330
23544
  try {
23331
23545
  mtime = statSync38(file).mtimeMs;
@@ -23386,7 +23600,7 @@ function instructionTargets(rootPath, isGlobal) {
23386
23600
  const tools = installed.length ? installed : [INSTRUCTION_TOOLS[0]];
23387
23601
  if (isGlobal) return [...new Set(tools.map((t) => instructionsPath(t.id)))];
23388
23602
  if (!rootPath) return [];
23389
- const present = INSTRUCTION_FILENAMES.map((f) => join48(rootPath, f)).filter((p) => existsSync45(p));
23603
+ const present = INSTRUCTION_FILENAMES.map((f) => join48(rootPath, f)).filter((p) => existsSync46(p));
23390
23604
  if (present.length) return present;
23391
23605
  return [...new Set(tools.map((t) => join48(rootPath, instructionsFilename(t.id))))];
23392
23606
  }
@@ -23414,7 +23628,7 @@ function applyCodeRecommendation(intent) {
23414
23628
  mkdirSync14(dir, { recursive: true });
23415
23629
  } catch {
23416
23630
  }
23417
- const existing = existsSync45(file) ? readFileSync39(file, "utf8") : "";
23631
+ const existing = existsSync46(file) ? readFileSync39(file, "utf8") : "";
23418
23632
  if (existing.includes(text)) {
23419
23633
  skipped.push(file);
23420
23634
  continue;
@@ -23516,7 +23730,7 @@ try {
23516
23730
  ownVersion2 = JSON.parse(readFileSync39(join48(dir, "../package.json"), "utf-8")).version || ownVersion2;
23517
23731
  } catch {
23518
23732
  try {
23519
- if (true) ownVersion2 = "0.7.0";
23733
+ if (true) ownVersion2 = "0.7.2";
23520
23734
  } catch {
23521
23735
  }
23522
23736
  }
@@ -23581,7 +23795,9 @@ init_auto_update();
23581
23795
 
23582
23796
  // src/code-index-cursor.ts
23583
23797
  init_paths();
23584
- import { readFileSync as readFileSync40, writeFileSync as writeFileSync14, mkdirSync as mkdirSync15 } from "fs";
23798
+ import { readFileSync as readFileSync40, writeFileSync as writeFileSync14, mkdirSync as mkdirSync15, statSync as statSync41 } from "fs";
23799
+ import { execFileSync as execFileSync4 } from "child_process";
23800
+ import { createHash as createHash10 } from "crypto";
23585
23801
  import { join as join49, dirname as dirname24 } from "path";
23586
23802
  var cursorPath = () => join49(getDataDir(), "code-index-cursor.json");
23587
23803
  function readCursor() {
@@ -23590,7 +23806,11 @@ function readCursor() {
23590
23806
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
23591
23807
  const out = {};
23592
23808
  for (const [k, v] of Object.entries(raw)) {
23593
- if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
23809
+ if (typeof v === "number" && Number.isFinite(v)) out[k] = { at: v };
23810
+ else if (v && typeof v === "object" && typeof v.at === "number" && Number.isFinite(v.at)) {
23811
+ const fp = v.fp;
23812
+ out[k] = typeof fp === "string" ? { at: v.at, fp } : { at: v.at };
23813
+ }
23594
23814
  }
23595
23815
  return out;
23596
23816
  }
@@ -23606,13 +23826,44 @@ function writeCursor(c) {
23606
23826
  } catch {
23607
23827
  }
23608
23828
  }
23609
- function noteIndexed(workspace, now = Date.now()) {
23829
+ function noteIndexed(workspace, now = Date.now(), fp) {
23610
23830
  const c = readCursor();
23611
- c[workspace] = now;
23831
+ c[workspace] = fp ? { at: now, fp } : { at: now };
23612
23832
  writeCursor(c);
23613
23833
  }
23834
+ function workspaceFingerprint(workspace, collectorVersion) {
23835
+ try {
23836
+ const git2 = (args) => execFileSync4(
23837
+ "git",
23838
+ ["-C", workspace, ...args],
23839
+ { encoding: "utf-8", timeout: 1e4, maxBuffer: 16 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }
23840
+ );
23841
+ const head = git2(["rev-parse", "HEAD"]).trim();
23842
+ const status = git2(["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
23843
+ const h = createHash10("sha256").update(`v${collectorVersion}\0${head}\0`);
23844
+ for (const entry of status.split("\0")) {
23845
+ if (entry.length < 4) continue;
23846
+ const rel = entry.slice(3);
23847
+ let stamp2 = "gone";
23848
+ try {
23849
+ const s = statSync41(join49(workspace, rel));
23850
+ stamp2 = `${s.size}:${Math.floor(s.mtimeMs)}`;
23851
+ } catch {
23852
+ }
23853
+ h.update(`${entry.slice(0, 2)}\0${rel}\0${stamp2}\0`);
23854
+ }
23855
+ return h.digest("hex");
23856
+ } catch {
23857
+ return null;
23858
+ }
23859
+ }
23860
+ var UNCHANGED_RESEND_MS = 7 * 24 * 3600 * 1e3;
23861
+ function isUnchangedSinceIndexed(workspace, fp, cursor, now = Date.now()) {
23862
+ const e = cursor[workspace];
23863
+ return !!fp && !!e && e.fp === fp && now - e.at < UNCHANGED_RESEND_MS;
23864
+ }
23614
23865
  function orderByStaleness(workspaces, cursor = readCursor()) {
23615
- return workspaces.map((w, i) => ({ w, i, at: cursor[w] ?? -1 })).sort((a, b) => a.at - b.at || a.i - b.i).map((e) => e.w);
23866
+ return workspaces.map((w, i) => ({ w, i, at: cursor[w]?.at ?? -1 })).sort((a, b) => a.at - b.at || a.i - b.i).map((e) => e.w);
23616
23867
  }
23617
23868
  function pruneCursor(known) {
23618
23869
  const keep = new Set(known);
@@ -23732,7 +23983,7 @@ init_telemetry();
23732
23983
  // src/self-restart.ts
23733
23984
  import { spawn } from "child_process";
23734
23985
  import { readFileSync as readFileSync41 } from "fs";
23735
- import { createHash as createHash10 } from "crypto";
23986
+ import { createHash as createHash11 } from "crypto";
23736
23987
  var EXIT_CODE_CODE_CHANGED = 75;
23737
23988
  function codeFingerprint(entry = process.argv[1]) {
23738
23989
  if (!entry) return null;
@@ -23741,7 +23992,7 @@ function codeFingerprint(entry = process.argv[1]) {
23741
23992
  return {
23742
23993
  path: entry,
23743
23994
  size: buf.byteLength,
23744
- hash: createHash10("sha256").update(buf).digest("hex")
23995
+ hash: createHash11("sha256").update(buf).digest("hex")
23745
23996
  };
23746
23997
  } catch {
23747
23998
  return null;
@@ -23804,11 +24055,11 @@ function checkSelfRestart(opts) {
23804
24055
  }
23805
24056
 
23806
24057
  // src/log-rotate.ts
23807
- import { statSync as statSync41, copyFileSync as copyFileSync3, truncateSync } from "fs";
24058
+ import { statSync as statSync42, copyFileSync as copyFileSync3, truncateSync } from "fs";
23808
24059
  var LOG_MAX_BYTES = 32 * 1024 * 1024;
23809
24060
  function rotateLogIfLarge(logPath, maxBytes = LOG_MAX_BYTES) {
23810
24061
  try {
23811
- const size = statSync41(logPath).size;
24062
+ const size = statSync42(logPath).size;
23812
24063
  if (size < maxBytes) return { rotated: false, sizeBefore: 0 };
23813
24064
  copyFileSync3(logPath, `${logPath}.1`);
23814
24065
  truncateSync(logPath, 0);
@@ -23820,8 +24071,8 @@ function rotateLogIfLarge(logPath, maxBytes = LOG_MAX_BYTES) {
23820
24071
 
23821
24072
  // src/collector-migrate.ts
23822
24073
  init_http();
23823
- import { existsSync as existsSync46 } from "node:fs";
23824
- function selectProjectsToReindex(projects, current, existsFn = existsSync46) {
24074
+ import { existsSync as existsSync47 } from "node:fs";
24075
+ function selectProjectsToReindex(projects, current, existsFn = existsSync47) {
23825
24076
  const reindex = [];
23826
24077
  const missing = [];
23827
24078
  for (const p of projects) {
@@ -23863,8 +24114,8 @@ async function runCollectorMigration(opts) {
23863
24114
  // auto-indexer/indexer.ts
23864
24115
  init_sync_ledger();
23865
24116
  init_collector_health();
23866
- import { existsSync as existsSync49, readFileSync as readFileSync45, statSync as statSync44 } from "fs";
23867
- var CLI_VERSION3 = true ? "0.7.0" : "0.0.0";
24117
+ import { existsSync as existsSync50, readFileSync as readFileSync45, statSync as statSync45 } from "fs";
24118
+ var CLI_VERSION3 = true ? "0.7.2" : "0.0.0";
23868
24119
  var DEBOUNCE_MS = 5e3;
23869
24120
  var CLAUDE_DIR = claudeBackend.projectsDir();
23870
24121
  var PLANS_DIR = claudeBackend.plansDir();
@@ -23969,7 +24220,7 @@ var WATCH_WINDOW_MS = Math.max(1, Number(process.env.CHAT_RECALL_WATCH_WINDOW_DA
23969
24220
  var sessionWatcher = watchWithFallback("sessions", `${CLAUDE_DIR}/**/*.jsonl`, {
23970
24221
  ignored: (p, stats) => {
23971
24222
  if (/agent-/.test(p) || /^\./.test(basename25(p))) return true;
23972
- const st = stats ?? statSync44(p, { throwIfNoEntry: false });
24223
+ const st = stats ?? statSync45(p, { throwIfNoEntry: false });
23973
24224
  if (!st || st.isDirectory()) return false;
23974
24225
  return st.mtimeMs < Date.now() - WATCH_WINDOW_MS;
23975
24226
  },
@@ -24249,7 +24500,7 @@ function discoverWorkspaces() {
24249
24500
  for (const b of listAvailableBackends()) {
24250
24501
  let refs = [];
24251
24502
  try {
24252
- refs = b.listSessions({ sinceMs });
24503
+ refs = b.listSessions({ sinceMs, previews: false });
24253
24504
  } catch {
24254
24505
  }
24255
24506
  for (const r of refs) {
@@ -24269,7 +24520,7 @@ function discoverWorkspaces() {
24269
24520
  const isExcluded = (p) => excluded.some((x) => p.includes(x));
24270
24521
  const candidates2 = [...newest.entries()].filter(([p]) => !isExcluded(p)).filter(([p]) => !isPersonalPath(p, includeProjects)).filter(([p]) => {
24271
24522
  try {
24272
- return existsSync49(p);
24523
+ return existsSync50(p);
24273
24524
  } catch {
24274
24525
  return false;
24275
24526
  }
@@ -24301,7 +24552,7 @@ function isCodeWorkspace(p) {
24301
24552
  }
24302
24553
  return WORKSPACE_MARKERS.some((m) => {
24303
24554
  try {
24304
- return existsSync49(join54(p, m));
24555
+ return existsSync50(join54(p, m));
24305
24556
  } catch {
24306
24557
  return false;
24307
24558
  }
@@ -24317,7 +24568,7 @@ async function codeIndexTick() {
24317
24568
  if (discovered.length === 0) return;
24318
24569
  pruneCursor(discovered);
24319
24570
  const workspaces = orderByStaleness(discovered);
24320
- const { collectCode: collectCode2, resolveCodeindexBin: resolveCodeindexBin2 } = await Promise.resolve().then(() => (init_collector(), collector_exports));
24571
+ const { collectCode: collectCode2, resolveCodeindexBin: resolveCodeindexBin2, COLLECTOR_VERSION: COLLECTOR_VERSION2 } = await Promise.resolve().then(() => (init_collector(), collector_exports));
24321
24572
  let bin;
24322
24573
  try {
24323
24574
  bin = await resolveCodeindexBin2(true);
@@ -24325,8 +24576,14 @@ async function codeIndexTick() {
24325
24576
  daemonLog.error(`code intelligence skipped \u2014 codeindex unavailable: ${e instanceof Error ? e.message : e}`);
24326
24577
  return;
24327
24578
  }
24328
- let ok = 0;
24579
+ let ok = 0, unchanged = 0;
24580
+ const cursor = readCursor();
24329
24581
  for (const ws of workspaces) {
24582
+ const fp = workspaceFingerprint(ws, COLLECTOR_VERSION2);
24583
+ if (isUnchangedSinceIndexed(ws, fp, cursor)) {
24584
+ unchanged++;
24585
+ continue;
24586
+ }
24330
24587
  const t0 = Date.now();
24331
24588
  daemonLog.info(`code intelligence: indexing ${ws} \u2026`);
24332
24589
  let result;
@@ -24344,10 +24601,10 @@ async function codeIndexTick() {
24344
24601
  } catch {
24345
24602
  }
24346
24603
  }
24347
- noteIndexed(ws);
24604
+ noteIndexed(ws, Date.now(), fp);
24348
24605
  ok++;
24349
24606
  }
24350
- if (ok > 0) daemonLog.info(`code intelligence: indexed ${ok}/${workspaces.length} workspace(s)`);
24607
+ if (ok > 0 || unchanged > 0) daemonLog.info(`code intelligence: indexed ${ok}/${workspaces.length} workspace(s), ${unchanged} unchanged`);
24351
24608
  } finally {
24352
24609
  codeIndexInFlight = false;
24353
24610
  }