chat-recall 0.7.17 → 0.7.19

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.
@@ -4524,6 +4524,7 @@ function defaultSourcesEnabled() {
4524
4524
  commands: true
4525
4525
  },
4526
4526
  cursor: { sessions: true, skills: true, agents: true, commands: true },
4527
+ hermes: { sessions: true },
4527
4528
  shared: { skills: true },
4528
4529
  common: { mcps: true, agentMd: true }
4529
4530
  };
@@ -4594,6 +4595,7 @@ function mergeSources(base, partial) {
4594
4595
  codex: { ...base.enabled.codex, ...partial.enabled?.codex ?? {} },
4595
4596
  agy: { ...base.enabled.agy, ...inherited, ...partial.enabled?.agy ?? {} },
4596
4597
  cursor: { ...base.enabled.cursor, ...partial.enabled?.cursor ?? {} },
4598
+ hermes: { ...base.enabled.hermes, ...partial.enabled?.hermes ?? {} },
4597
4599
  shared: { ...base.enabled.shared, ...partial.enabled?.shared ?? {} },
4598
4600
  common: { ...base.enabled.common, ...partial.enabled?.common ?? {} }
4599
4601
  };
@@ -4605,6 +4607,7 @@ function mergeSources(base, partial) {
4605
4607
  agyHome: partial.agyHome ?? base.agyHome,
4606
4608
  cursorHome: partial.cursorHome ?? base.cursorHome,
4607
4609
  cursorIdeHome: partial.cursorIdeHome ?? base.cursorIdeHome,
4610
+ hermesHome: partial.hermesHome ?? base.hermesHome,
4608
4611
  extraClaudeHomes: partial.extraClaudeHomes ?? base.extraClaudeHomes,
4609
4612
  approvedHomes: partial.approvedHomes ?? base.approvedHomes,
4610
4613
  declinedHomes: partial.declinedHomes ?? base.declinedHomes,
@@ -5284,6 +5287,8 @@ function declaredOrigin(value) {
5284
5287
  function userRecordOrigin(obj, text) {
5285
5288
  if (obj.isSidechain === true)
5286
5289
  return "subagent";
5290
+ if (obj.isCompactSummary === true)
5291
+ return "compact-summary";
5287
5292
  const declared = declaredOrigin(obj.origin);
5288
5293
  if (declared && declared !== "human")
5289
5294
  return declared;
@@ -5430,6 +5435,7 @@ function redactContainer(c, redact) {
5430
5435
  }))
5431
5436
  };
5432
5437
  }
5438
+ var RAW_PARSE_MAX_BYTES;
5433
5439
  var init_raw = __esm({
5434
5440
  "../engine/dist/transcript/raw.js"() {
5435
5441
  "use strict";
@@ -5437,6 +5443,7 @@ var init_raw = __esm({
5437
5443
  init_claude();
5438
5444
  init_codex();
5439
5445
  init_from_events();
5446
+ RAW_PARSE_MAX_BYTES = 40 * 1024 * 1024;
5440
5447
  }
5441
5448
  });
5442
5449
 
@@ -5524,6 +5531,13 @@ function grandfatherLegacyHomes() {
5524
5531
  }
5525
5532
  return { seeded };
5526
5533
  }
5534
+ function defaultHermesHome() {
5535
+ if (process.platform === "win32") {
5536
+ const local = process.env.LOCALAPPDATA?.trim() || join3(homedir2(), "AppData", "Local");
5537
+ return join3(local, "hermes");
5538
+ }
5539
+ return join3(homedir2(), ".hermes");
5540
+ }
5527
5541
  function primaryHomes() {
5528
5542
  const home = homedir2();
5529
5543
  const env = process.env;
@@ -5540,7 +5554,8 @@ function primaryHomes() {
5540
5554
  env.CHAT_RECALL_CODEX_HOME || s?.codexHome || join3(home, ".codex"),
5541
5555
  env.CHAT_RECALL_AGY_HOME || s?.agyHome || join3(home, ".gemini", "antigravity-cli"),
5542
5556
  env.CHAT_RECALL_CURSOR_HOME || s?.cursorHome || join3(home, ".cursor"),
5543
- dirOf(opencodeDb)
5557
+ dirOf(opencodeDb),
5558
+ env.CHAT_RECALL_HERMES_HOME || s?.hermesHome || env.HERMES_HOME?.trim() || defaultHermesHome()
5544
5559
  ];
5545
5560
  return candidates.map(normalizeHomePath);
5546
5561
  }
@@ -5619,6 +5634,9 @@ function geminiHomeDir() {
5619
5634
  function opencodeDbPath() {
5620
5635
  return process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath || join4(homedir3(), ".local", "share", "opencode", "opencode.db");
5621
5636
  }
5637
+ function hermesHomeDir() {
5638
+ return process.env.CHAT_RECALL_HERMES_HOME || sources().hermesHome || process.env.HERMES_HOME?.trim() || defaultHermesHome();
5639
+ }
5622
5640
  function codexHomeDir() {
5623
5641
  return process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome || join4(homedir3(), ".codex");
5624
5642
  }
@@ -5724,6 +5742,22 @@ function opencodeDbPaths(opts = {}) {
5724
5742
  dbs.push(primary);
5725
5743
  return applyExclusions(dbs, opts.includeExcluded);
5726
5744
  }
5745
+ function hermesDbPaths(opts = {}) {
5746
+ const root = hermesHomeDir();
5747
+ const dbs = [join4(root, "state.db")];
5748
+ try {
5749
+ const profiles = join4(root, "profiles");
5750
+ for (const entry of readdirSync2(profiles, { withFileTypes: true })) {
5751
+ if (!entry.isDirectory())
5752
+ continue;
5753
+ const p = join4(profiles, entry.name, "state.db");
5754
+ if (existsSync3(p))
5755
+ dbs.push(p);
5756
+ }
5757
+ } catch {
5758
+ }
5759
+ return applyExclusions(dbs, opts.includeExcluded);
5760
+ }
5727
5761
  var sources, _sourceExclusionFilter;
5728
5762
  var init_tool_paths = __esm({
5729
5763
  "../engine/dist/core/tool-paths.js"() {
@@ -7001,6 +7035,71 @@ function readTailFromOffset(path, offset, maxBytes = TAIL_READ_MAX_BYTES) {
7001
7035
  }
7002
7036
  }
7003
7037
  }
7038
+ function readSourcesFromOffset(paths, offset, maxBytes = TAIL_READ_MAX_BYTES) {
7039
+ let start = 0;
7040
+ let linesBefore = 0;
7041
+ for (let i = 0; i < paths.length; i++) {
7042
+ const path = paths[i];
7043
+ let size = 0;
7044
+ try {
7045
+ size = statSync3(path).size;
7046
+ } catch {
7047
+ size = 0;
7048
+ }
7049
+ const last = i === paths.length - 1;
7050
+ if (!last && offset >= start + size) {
7051
+ const lines = linesBeforeOffset(path, size);
7052
+ if (lines === void 0)
7053
+ return { text: "", newOffset: offset };
7054
+ linesBefore += lines + (size > 0 && !endsWithNewline(path, size) ? 1 : 0);
7055
+ start += size;
7056
+ continue;
7057
+ }
7058
+ const local = offset - start;
7059
+ const read = readTailFromOffset(path, local, maxBytes);
7060
+ const shift = (r) => ({
7061
+ ...r,
7062
+ newOffset: start + r.newOffset,
7063
+ ...r.baseLine !== void 0 ? { baseLine: linesBefore + r.baseLine } : {}
7064
+ });
7065
+ if (last || read.newOffset > local || local >= size)
7066
+ return shift(read);
7067
+ if (size - local > maxBytes)
7068
+ return { text: "", newOffset: start + size, skippedBytes: size - local };
7069
+ let fd = null;
7070
+ try {
7071
+ fd = openSync(path, "r");
7072
+ const text = readAt(fd, local, size - local).toString("utf-8");
7073
+ return shift({ text, newOffset: size, baseLine: linesBeforeOffset(path, local) });
7074
+ } catch {
7075
+ return { text: "", newOffset: offset };
7076
+ } finally {
7077
+ if (fd !== null) {
7078
+ try {
7079
+ closeSync(fd);
7080
+ } catch {
7081
+ }
7082
+ }
7083
+ }
7084
+ }
7085
+ return { text: "", newOffset: offset };
7086
+ }
7087
+ function endsWithNewline(path, size) {
7088
+ let fd = null;
7089
+ try {
7090
+ fd = openSync(path, "r");
7091
+ return readAt(fd, size - 1, 1)[0] === 10;
7092
+ } catch {
7093
+ return true;
7094
+ } finally {
7095
+ if (fd !== null) {
7096
+ try {
7097
+ closeSync(fd);
7098
+ } catch {
7099
+ }
7100
+ }
7101
+ }
7102
+ }
7004
7103
  function findNewline(fd, from, size) {
7005
7104
  const block = Buffer.allocUnsafe(SKIP_SCAN_BYTES);
7006
7105
  let pos = from;
@@ -9479,6 +9578,473 @@ var init_cursor = __esm({
9479
9578
  }
9480
9579
  });
9481
9580
 
9581
+ // ../engine/dist/core/backends/hermes.js
9582
+ import { existsSync as existsSync10 } from "fs";
9583
+ function firstUserPrompt(db, sessionId) {
9584
+ try {
9585
+ const row = db.prepare(`
9586
+ SELECT substr(content, 1, 400) AS text FROM messages
9587
+ WHERE session_id = ? AND role = 'user' AND active = 1 AND _compressed_summary = 0
9588
+ AND (display_kind IS NULL OR display_kind = 'steer')
9589
+ AND length(trim(content)) > 0
9590
+ ORDER BY id LIMIT 1
9591
+ `).get(sessionId);
9592
+ return row?.text ? flatString(row.text.slice(0, 200)) : "";
9593
+ } catch {
9594
+ return "";
9595
+ }
9596
+ }
9597
+ function messageRowsToEvents(rows) {
9598
+ const events = [];
9599
+ let line = 0;
9600
+ for (const r of rows) {
9601
+ line++;
9602
+ if (r.active === 0)
9603
+ continue;
9604
+ if (r.display_kind === "failed_turn" || r.display_kind === "hidden")
9605
+ continue;
9606
+ const ts = Math.round((r.timestamp || 0) * 1e3);
9607
+ const tsIso = ts ? new Date(ts).toISOString() : void 0;
9608
+ const text = (r.content ?? "").trim();
9609
+ if (r._compressed_summary === 1) {
9610
+ if (text)
9611
+ events.push({ kind: "summary", ts, tsIso, line, text });
9612
+ continue;
9613
+ }
9614
+ if (r.role === "user") {
9615
+ if (!text)
9616
+ continue;
9617
+ events.push({
9618
+ kind: "user",
9619
+ ts,
9620
+ tsIso,
9621
+ line,
9622
+ text,
9623
+ ...r.display_kind === "process_complete" ? { origin: "task-notification" } : {}
9624
+ });
9625
+ continue;
9626
+ }
9627
+ if (r.role === "assistant") {
9628
+ if (text)
9629
+ events.push({ kind: "assistant_text", ts, tsIso, line, text });
9630
+ for (const call of parseToolCalls(r.tool_calls)) {
9631
+ const toolName = call.function?.name ?? "";
9632
+ const toolInput = parseArguments(call.function?.arguments);
9633
+ const command = toolName === "terminal" && typeof toolInput?.command === "string" ? toolInput.command : void 0;
9634
+ events.push({
9635
+ kind: "tool_use",
9636
+ ts,
9637
+ tsIso,
9638
+ line,
9639
+ toolName,
9640
+ toolUseId: call.id || call.call_id || `${r.id}:${toolName}`,
9641
+ toolInput,
9642
+ ...command ? { command } : {}
9643
+ });
9644
+ }
9645
+ continue;
9646
+ }
9647
+ if (r.role === "tool") {
9648
+ const body = r.content ?? "";
9649
+ const { exitCode, isError } = toolResultStatus(body);
9650
+ events.push({
9651
+ kind: "tool_result",
9652
+ ts,
9653
+ tsIso,
9654
+ line,
9655
+ toolUseId: r.tool_call_id ?? void 0,
9656
+ toolName: r.tool_name ?? void 0,
9657
+ resultBody: body,
9658
+ resultIsError: isError,
9659
+ ...exitCode !== void 0 ? { resultExitCode: exitCode } : {},
9660
+ resultBytes: Buffer.byteLength(body)
9661
+ });
9662
+ }
9663
+ }
9664
+ return events;
9665
+ }
9666
+ function parseToolCalls(json) {
9667
+ if (!json)
9668
+ return [];
9669
+ try {
9670
+ const calls = JSON.parse(json);
9671
+ return Array.isArray(calls) ? calls : [];
9672
+ } catch {
9673
+ return [];
9674
+ }
9675
+ }
9676
+ function parseArguments(args) {
9677
+ if (typeof args !== "string")
9678
+ return args ?? {};
9679
+ try {
9680
+ return JSON.parse(args);
9681
+ } catch {
9682
+ return { raw: args };
9683
+ }
9684
+ }
9685
+ function toolResultStatus(body) {
9686
+ if (!body.startsWith("{"))
9687
+ return { isError: false };
9688
+ try {
9689
+ const r = JSON.parse(body);
9690
+ const exitCode = typeof r.exit_code === "number" ? r.exit_code : void 0;
9691
+ const hasError = r.error !== void 0 && r.error !== null && r.error !== "";
9692
+ return { exitCode, isError: hasError || exitCode !== void 0 && exitCode !== 0 };
9693
+ } catch {
9694
+ return { isError: false };
9695
+ }
9696
+ }
9697
+ var PREFIX5, MESSAGE_COLUMNS, ARCHIVED_SESSION_COLUMNS, SESSION_MTIME_SQL, HermesBackend, hermesBackend;
9698
+ var init_hermes = __esm({
9699
+ "../engine/dist/core/backends/hermes.js"() {
9700
+ "use strict";
9701
+ init_sqlite_reader();
9702
+ init_tool_paths();
9703
+ init_session_outcome();
9704
+ init_session_git();
9705
+ init_generic_engine();
9706
+ init_flat_string();
9707
+ PREFIX5 = "hermes_";
9708
+ MESSAGE_COLUMNS = "id, role, content, tool_calls, tool_call_id, tool_name, timestamp, display_kind, active, _compressed_summary";
9709
+ ARCHIVED_SESSION_COLUMNS = [
9710
+ "id",
9711
+ "source",
9712
+ "title",
9713
+ "model",
9714
+ "cwd",
9715
+ "git_branch",
9716
+ "git_repo_root",
9717
+ "parent_session_id",
9718
+ "started_at",
9719
+ "ended_at",
9720
+ "end_reason",
9721
+ "message_count",
9722
+ "tool_call_count",
9723
+ "input_tokens",
9724
+ "output_tokens",
9725
+ "cache_read_tokens",
9726
+ "cache_write_tokens",
9727
+ "reasoning_tokens",
9728
+ "estimated_cost_usd",
9729
+ "actual_cost_usd"
9730
+ ];
9731
+ SESSION_MTIME_SQL = `MAX(
9732
+ s.started_at,
9733
+ COALESCE(s.ended_at, 0),
9734
+ COALESCE(s.last_activity_at, 0),
9735
+ COALESCE((SELECT MAX(m.timestamp) FROM messages m WHERE m.session_id = s.id), 0)
9736
+ )`;
9737
+ HermesBackend = class {
9738
+ id = "hermes";
9739
+ idPrefix = PREFIX5;
9740
+ displayName = "Hermes";
9741
+ homeDir() {
9742
+ return hermesHomeDir();
9743
+ }
9744
+ /** Every Hermes database on this machine, the root profile first. */
9745
+ dbPaths() {
9746
+ return hermesDbPaths();
9747
+ }
9748
+ isAvailable() {
9749
+ return this.dbPaths().some((p) => existsSync10(p));
9750
+ }
9751
+ /** The database that holds this session. A session id is unique within a
9752
+ * database, and each named profile has its own. */
9753
+ dbPathFor(rawId) {
9754
+ const cached = this._dbForSession.get(rawId);
9755
+ if (cached)
9756
+ return cached;
9757
+ for (const path of this.dbPaths()) {
9758
+ const db = openSqliteReadonly(path);
9759
+ if (!db)
9760
+ continue;
9761
+ try {
9762
+ if (db.prepare("SELECT 1 AS ok FROM sessions WHERE id = ?").get(rawId)) {
9763
+ this._dbForSession.set(rawId, path);
9764
+ return path;
9765
+ }
9766
+ } catch {
9767
+ } finally {
9768
+ db.close();
9769
+ }
9770
+ }
9771
+ return null;
9772
+ }
9773
+ _dbForSession = /* @__PURE__ */ new Map();
9774
+ /** Tests, and anything that adds a profile mid-process. */
9775
+ _clearDbRouting() {
9776
+ this._dbForSession.clear();
9777
+ }
9778
+ // ── ID handling ────────────────────────────────────────────────
9779
+ matchesId(id) {
9780
+ return id.startsWith(PREFIX5);
9781
+ }
9782
+ toRawId(id) {
9783
+ return id.startsWith(PREFIX5) ? id.slice(PREFIX5.length) : id;
9784
+ }
9785
+ toPrefixedId(rawId) {
9786
+ return rawId.startsWith(PREFIX5) ? rawId : PREFIX5 + rawId;
9787
+ }
9788
+ // ── Location ───────────────────────────────────────────────────
9789
+ findSession(id) {
9790
+ const rawId = this.toRawId(id);
9791
+ const path = this.dbPathFor(rawId);
9792
+ if (!path)
9793
+ return null;
9794
+ const db = openSqliteReadonly(path);
9795
+ if (!db)
9796
+ return null;
9797
+ try {
9798
+ const row = db.prepare(`
9799
+ SELECT s.cwd, s.git_repo_root, ${SESSION_MTIME_SQL} AS mtime
9800
+ FROM sessions s WHERE s.id = ?
9801
+ `).get(rawId);
9802
+ if (!row)
9803
+ return null;
9804
+ return {
9805
+ path,
9806
+ format: "sqlite",
9807
+ projectDir: "",
9808
+ projectPath: row.git_repo_root || row.cwd || "",
9809
+ mtime: Math.round((row.mtime || 0) * 1e3)
9810
+ };
9811
+ } catch {
9812
+ return null;
9813
+ } finally {
9814
+ db.close();
9815
+ }
9816
+ }
9817
+ listSessions(opts = {}) {
9818
+ const byId = /* @__PURE__ */ new Map();
9819
+ for (const dbPath of this.dbPaths()) {
9820
+ if (!existsSync10(dbPath))
9821
+ continue;
9822
+ for (const ref of this.listSessionsInDb(dbPath, opts)) {
9823
+ const prior = byId.get(ref.rawId);
9824
+ if (!prior || ref.mtime > prior.mtime)
9825
+ byId.set(ref.rawId, ref);
9826
+ }
9827
+ }
9828
+ const all = [...byId.values()].sort((a, b) => b.mtime - a.mtime);
9829
+ return opts.limit ? all.slice(0, opts.limit) : all;
9830
+ }
9831
+ listSessionsInDb(dbPath, opts) {
9832
+ const cutoffSec = (opts.sinceMs ?? 0) / 1e3;
9833
+ const filter = opts.projectFilter?.toLowerCase();
9834
+ const db = openSqliteReadonly(dbPath);
9835
+ if (!db)
9836
+ return [];
9837
+ try {
9838
+ const rows = db.prepare(`
9839
+ SELECT * FROM (
9840
+ SELECT s.id, s.cwd, s.git_repo_root, s.started_at, s.message_count,
9841
+ ${SESSION_MTIME_SQL} AS mtime
9842
+ FROM sessions s
9843
+ ) WHERE mtime >= ? ORDER BY mtime DESC
9844
+ `).all(cutoffSec);
9845
+ const out = [];
9846
+ for (const row of rows) {
9847
+ const projectPath = row.git_repo_root || row.cwd || "";
9848
+ if (filter && !projectPath.toLowerCase().includes(filter))
9849
+ continue;
9850
+ const mtime = Math.round(row.mtime * 1e3);
9851
+ this._dbForSession.set(row.id, dbPath);
9852
+ out.push({
9853
+ toolId: "hermes",
9854
+ rawId: row.id,
9855
+ prefixedId: this.toPrefixedId(row.id),
9856
+ projectPath,
9857
+ projectDir: "",
9858
+ fullPath: dbPath,
9859
+ created: new Date(Math.round(row.started_at * 1e3)).toISOString(),
9860
+ modified: new Date(mtime).toISOString(),
9861
+ mtime,
9862
+ firstPrompt: opts.previews === false ? "" : firstUserPrompt(db, row.id),
9863
+ messageCount: opts.previews === false ? 0 : row.message_count ?? 0
9864
+ });
9865
+ if (opts.limit && out.length >= opts.limit)
9866
+ break;
9867
+ }
9868
+ return out;
9869
+ } catch {
9870
+ return [];
9871
+ } finally {
9872
+ db.close();
9873
+ }
9874
+ }
9875
+ /** Hermes writes a title for each session (a model-written one, or the
9876
+ * first prompt for a one-shot run). */
9877
+ getNativeTitle(rawId) {
9878
+ const path = this.dbPathFor(this.toRawId(rawId));
9879
+ if (!path)
9880
+ return null;
9881
+ const db = openSqliteReadonly(path);
9882
+ if (!db)
9883
+ return null;
9884
+ try {
9885
+ const row = db.prepare("SELECT title FROM sessions WHERE id = ?").get(this.toRawId(rawId));
9886
+ const t = row?.title?.trim();
9887
+ return t ? t.slice(0, 200) : null;
9888
+ } catch {
9889
+ return null;
9890
+ } finally {
9891
+ db.close();
9892
+ }
9893
+ }
9894
+ // ── Generic-engine inputs ───────────────────────────────────────
9895
+ fileToolMap = {
9896
+ write_file: "write",
9897
+ patch: "edit",
9898
+ read_file: "read"
9899
+ };
9900
+ /**
9901
+ * `write_file` carries `{ path, content }`. `patch` carries
9902
+ * `{ mode: 'replace', path, old_string, new_string }`; its other mode,
9903
+ * `patch`, is a multi-file V4A patch with no single before and after.
9904
+ */
9905
+ extractEditDelta(toolName, input) {
9906
+ if (input == null || typeof input !== "object")
9907
+ return null;
9908
+ const inp = input;
9909
+ if (toolName === "write_file") {
9910
+ return { before: "", after: typeof inp.content === "string" ? inp.content : null };
9911
+ }
9912
+ if (toolName === "patch" && (inp.mode === void 0 || inp.mode === "replace")) {
9913
+ const before = typeof inp.old_string === "string" ? inp.old_string : null;
9914
+ const after = typeof inp.new_string === "string" ? inp.new_string : null;
9915
+ if (before === null && after === null)
9916
+ return null;
9917
+ return { before, after };
9918
+ }
9919
+ return null;
9920
+ }
9921
+ readEvents(rawId) {
9922
+ const id = this.toRawId(rawId);
9923
+ const path = this.dbPathFor(id);
9924
+ if (!path)
9925
+ return [];
9926
+ const db = openSqliteReadonly(path);
9927
+ if (!db)
9928
+ return [];
9929
+ try {
9930
+ const rows = db.prepare(`SELECT ${MESSAGE_COLUMNS} FROM messages WHERE session_id = ? ORDER BY id`).all(id);
9931
+ return messageRowsToEvents(rows);
9932
+ } catch {
9933
+ return [];
9934
+ } finally {
9935
+ db.close();
9936
+ }
9937
+ }
9938
+ /**
9939
+ * Events from the dump `exportRawSession` writes, for a session rebuilt from
9940
+ * the server's archive. The archive joins the lines of every dump it got, so
9941
+ * a row that changed (a rewind sets `active` to 0) is there more than once.
9942
+ * The last copy of each row id is the newest.
9943
+ */
9944
+ readEventsFromText(text) {
9945
+ const rows = /* @__PURE__ */ new Map();
9946
+ for (const line of text.split("\n")) {
9947
+ if (!line.trim())
9948
+ continue;
9949
+ try {
9950
+ const rec = JSON.parse(line);
9951
+ if (rec.kind === "message" && rec.row) {
9952
+ rows.delete(rec.row.id);
9953
+ rows.set(rec.row.id, rec.row);
9954
+ }
9955
+ } catch {
9956
+ }
9957
+ }
9958
+ return messageRowsToEvents([...rows.values()].sort((a, b) => a.id - b.id));
9959
+ }
9960
+ // ── Per-session operations — all delegate to the generic engine ─
9961
+ extractTurns(id, opts = {}) {
9962
+ return extractTurnsFromEvents(this.toPrefixedId(id), this.readEvents(id), opts);
9963
+ }
9964
+ liveScanEdits(id) {
9965
+ const located = this.findSession(id);
9966
+ if (!located) {
9967
+ return { found: false, projectPath: "", projectDir: "", edits: [], fileMtime: 0, tool: "hermes" };
9968
+ }
9969
+ return liveScanEditsFromEvents(this.readEvents(id), this.fileToolMap, {
9970
+ sessionId: this.toPrefixedId(id),
9971
+ tool: "hermes",
9972
+ projectPath: located.projectPath,
9973
+ projectDir: located.projectDir,
9974
+ fileMtime: located.mtime,
9975
+ found: true
9976
+ });
9977
+ }
9978
+ replay(id) {
9979
+ const located = this.findSession(id);
9980
+ if (!located) {
9981
+ return { sessionId: this.toPrefixedId(id), found: false, projectPath: "", files: [], totalLinesAdded: 0, totalLinesRemoved: 0 };
9982
+ }
9983
+ return replayFromEvents(this.toPrefixedId(id), this.readEvents(id), this.fileToolMap, this.extractEditDelta.bind(this), {
9984
+ projectPath: located.projectPath,
9985
+ found: true
9986
+ });
9987
+ }
9988
+ computeOutcome(id, opts) {
9989
+ return computeOutcome(this.toPrefixedId(id), opts);
9990
+ }
9991
+ /** File edits since `sinceMs` across the sessions active since then. */
9992
+ collectRecentEdits(opts) {
9993
+ const refs = this.listSessions({ sinceMs: opts.sinceMs, projectFilter: opts.projectFilter, previews: false });
9994
+ const edits = [];
9995
+ for (const ref of opts.limitSessions ? refs.slice(0, opts.limitSessions) : refs) {
9996
+ for (const e of this.liveScanEdits(ref.rawId).edits) {
9997
+ if (e.ts >= opts.sinceMs)
9998
+ edits.push(e);
9999
+ }
10000
+ }
10001
+ return edits.sort((a, b) => b.ts - a.ts);
10002
+ }
10003
+ getCommits(id, files, startMs, endMs, bufferMinutes) {
10004
+ return getSessionCommits(this.toPrefixedId(id), files, startMs, endMs, bufferMinutes);
10005
+ }
10006
+ /**
10007
+ * The session row and its messages as JSONL, one row per line in id order.
10008
+ * Hermes rewrites and compacts its database, so the archive keeps the rows
10009
+ * as they were synced.
10010
+ */
10011
+ exportRawSession(id) {
10012
+ const rawId = this.toRawId(id);
10013
+ const path = this.dbPathFor(rawId);
10014
+ if (!path)
10015
+ return null;
10016
+ const db = openSqliteReadonly(path);
10017
+ if (!db)
10018
+ return null;
10019
+ try {
10020
+ const have = new Set(db.prepare(`SELECT name FROM pragma_table_info('sessions')`).all().map((c) => c.name));
10021
+ const cols = ARCHIVED_SESSION_COLUMNS.filter((c) => have.has(c)).map((c) => `s.${c}`);
10022
+ const session = db.prepare(`
10023
+ SELECT ${[...cols, `${SESSION_MTIME_SQL} AS mtime`].join(", ")}
10024
+ FROM sessions s WHERE s.id = ?
10025
+ `).get(rawId);
10026
+ if (!session)
10027
+ return null;
10028
+ const rows = db.prepare(`SELECT ${MESSAGE_COLUMNS} FROM messages WHERE session_id = ? ORDER BY id`).all(rawId);
10029
+ const lines = [JSON.stringify({ kind: "session", row: session })];
10030
+ for (const r of rows)
10031
+ lines.push(JSON.stringify({ kind: "message", row: r }));
10032
+ return {
10033
+ tool: "hermes",
10034
+ mtime: Math.round(Number(session.mtime || 0) * 1e3),
10035
+ files: [{ name: `${rawId}.dump.jsonl`, bytes: Buffer.from(lines.join("\n") + "\n", "utf-8") }]
10036
+ };
10037
+ } catch {
10038
+ return null;
10039
+ } finally {
10040
+ db.close();
10041
+ }
10042
+ }
10043
+ };
10044
+ hermesBackend = new HermesBackend();
10045
+ }
10046
+ });
10047
+
9482
10048
  // ../engine/dist/core/backends/index.js
9483
10049
  function bootstrapBackends() {
9484
10050
  registerBackend(claudeBackend);
@@ -9486,6 +10052,7 @@ function bootstrapBackends() {
9486
10052
  registerBackend(codexBackend);
9487
10053
  registerBackend(agyBackend);
9488
10054
  registerBackend(cursorBackend);
10055
+ registerBackend(hermesBackend);
9489
10056
  }
9490
10057
  var init_backends = __esm({
9491
10058
  "../engine/dist/core/backends/index.js"() {
@@ -9496,6 +10063,7 @@ var init_backends = __esm({
9496
10063
  init_codex2();
9497
10064
  init_agy();
9498
10065
  init_cursor();
10066
+ init_hermes();
9499
10067
  init_tool_backend();
9500
10068
  _setRegistryBootstrapper(bootstrapBackends);
9501
10069
  }
@@ -9708,7 +10276,7 @@ var init_session_outcome = __esm({
9708
10276
  });
9709
10277
 
9710
10278
  // ../engine/dist/core/first-prompt.js
9711
- import { createReadStream, existsSync as existsSync10, openSync as openSync3, readSync as readSync3, closeSync as closeSync3 } from "fs";
10279
+ import { createReadStream, existsSync as existsSync11, openSync as openSync3, readSync as readSync3, closeSync as closeSync3 } from "fs";
9712
10280
  import { StringDecoder } from "string_decoder";
9713
10281
  function stripBanners(s) {
9714
10282
  let out = s;
@@ -9877,7 +10445,7 @@ var init_project_dir_name = __esm({
9877
10445
  });
9878
10446
 
9879
10447
  // ../engine/dist/core/backends/claude.js
9880
- import { existsSync as existsSync11, readdirSync as readdirSync8, readFileSync as readFileSync7, statSync as statSync9, openSync as openSync4, readSync as readSync4, closeSync as closeSync4 } from "fs";
10448
+ import { existsSync as existsSync12, readdirSync as readdirSync8, readFileSync as readFileSync7, statSync as statSync9, openSync as openSync4, readSync as readSync4, closeSync as closeSync4 } from "fs";
9881
10449
  import { join as join11, basename as basename6 } from "path";
9882
10450
  function stripReminders(text) {
9883
10451
  return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
@@ -9947,7 +10515,7 @@ var init_claude2 = __esm({
9947
10515
  return join11(this.homeDir(), "history.jsonl");
9948
10516
  }
9949
10517
  isAvailable() {
9950
- return existsSync11(this.projectsDir());
10518
+ return existsSync12(this.projectsDir());
9951
10519
  }
9952
10520
  // ── ID handling ────────────────────────────────────────────────
9953
10521
  /**
@@ -10053,7 +10621,7 @@ var init_claude2 = __esm({
10053
10621
  prior.modified = ref.modified;
10054
10622
  }
10055
10623
  };
10056
- if (!existsSync11(root))
10624
+ if (!existsSync12(root))
10057
10625
  return;
10058
10626
  for (const proj of readdirSync8(root, { withFileTypes: true })) {
10059
10627
  if (!proj.isDirectory())
@@ -10064,12 +10632,12 @@ var init_claude2 = __esm({
10064
10632
  const projectPath = resolveProjectDirName(proj.name);
10065
10633
  const indexPath = join11(projPath, "sessions-index.json");
10066
10634
  const indexed = /* @__PURE__ */ new Set();
10067
- if (existsSync11(indexPath)) {
10635
+ if (existsSync12(indexPath)) {
10068
10636
  try {
10069
10637
  const indexData = JSON.parse(readFileSync7(indexPath, "utf-8"));
10070
10638
  const entries = indexData.entries || [];
10071
10639
  for (const entry of entries) {
10072
- if (!entry.fullPath || !existsSync11(entry.fullPath))
10640
+ if (!entry.fullPath || !existsSync12(entry.fullPath))
10073
10641
  continue;
10074
10642
  indexed.add(entry.sessionId);
10075
10643
  add({
@@ -10394,7 +10962,7 @@ var init_claude2 = __esm({
10394
10962
  collectRecentEdits(opts) {
10395
10963
  const candidates = [];
10396
10964
  for (const root of claudeProjectDirs()) {
10397
- if (!existsSync11(root))
10965
+ if (!existsSync12(root))
10398
10966
  continue;
10399
10967
  for (const projEntry of readdirSync8(root, { withFileTypes: true })) {
10400
10968
  if (!projEntry.isDirectory())
@@ -10416,7 +10984,7 @@ var init_claude2 = __esm({
10416
10984
  const st = statSync9(fp);
10417
10985
  let mtime = st.mtimeMs;
10418
10986
  const subDir = join11(projPath, f.slice(0, -6), "subagents");
10419
- if (existsSync11(subDir)) {
10987
+ if (existsSync12(subDir)) {
10420
10988
  for (const sub of readdirSync8(subDir)) {
10421
10989
  try {
10422
10990
  const subSt = statSync9(join11(subDir, sub));
@@ -10493,27 +11061,45 @@ var init_claude2 = __esm({
10493
11061
  /**
10494
11062
  * True when this session exists in MORE THAN ONE home.
10495
11063
  *
10496
- * Such a session cannot be tail-appended: `fileSize()` is the SUM across
10497
- * copies while a byte offset addresses ONE file, so the two disagree by
10498
- * construction and the "tail" is either empty or the wrong bytes. The caller
10499
- * uses this to force a FULL sync, which ships the unioned container and is
10500
- * correct regardless of how the halves are arranged.
10501
- *
10502
11064
  * Measured on ec05f266: ~/.claude 1715 records, ~/.claude-work 205 records,
10503
- * zero overlap. An offset-based append against a 5846662-byte "size" that no
10504
- * single file has could only ever ship nonsense.
11065
+ * zero overlap. `fileSize()` is the sum across the copies, and
11066
+ * `readFromOffset()` reads them joined end to end, primary first.
10505
11067
  */
10506
11068
  spansMultipleSources(prefixedId) {
10507
11069
  return findSessionFiles(this.toRawId(prefixedId)).length > 1;
10508
11070
  }
11071
+ /**
11072
+ * The copies before the last one, each with its size. Claude Code writes to
11073
+ * the home of the profile that runs the session, so the live copy can grow,
11074
+ * appear, vanish or merge back into the primary. Each of those moves bytes
11075
+ * under an offset into the joined copies; this key changes with all of them.
11076
+ */
11077
+ sourceLayout(prefixedId) {
11078
+ const copies = findSessionFiles(this.toRawId(prefixedId));
11079
+ if (copies.length < 2)
11080
+ return "";
11081
+ return copies.slice(0, -1).map((c) => {
11082
+ let size = -1;
11083
+ try {
11084
+ size = statSync9(c.path).size;
11085
+ } catch {
11086
+ }
11087
+ return `${c.path}:${size}`;
11088
+ }).join("|");
11089
+ }
11090
+ /**
11091
+ * Session 8de13d0a had 31 MB in ~/.claude and 2 MB in ~/.claude-work. Over
11092
+ * the 24 MB build ceiling a sync ships the transcript in chunks read here,
11093
+ * and this returned no text for a session in two homes. Each sync shipped
11094
+ * nothing, and the server stayed three hours behind.
11095
+ */
10509
11096
  async readFromOffset(prefixedId, offset, maxBytes) {
10510
- const rawId = this.toRawId(prefixedId);
10511
- const copies = findSessionFiles(rawId);
11097
+ const copies = findSessionFiles(this.toRawId(prefixedId));
10512
11098
  if (copies.length === 0)
10513
11099
  return { text: "", newOffset: offset };
10514
- if (copies.length > 1)
10515
- return { text: "", newOffset: offset };
10516
- return readTailFromOffset(copies[0].path, offset, maxBytes);
11100
+ if (copies.length === 1)
11101
+ return readTailFromOffset(copies[0].path, offset, maxBytes);
11102
+ return readSourcesFromOffset(copies.map((c) => c.path), offset, maxBytes);
10517
11103
  }
10518
11104
  };
10519
11105
  claudeBackend = new ClaudeBackend();
@@ -10528,11 +11114,11 @@ var init_model_pricing = __esm({
10528
11114
  });
10529
11115
 
10530
11116
  // ../engine/dist/parsers/session.js
10531
- import { createReadStream as createReadStream2, existsSync as existsSync12, readdirSync as readdirSync9, readFileSync as readFileSync8, statSync as statSync10, openSync as openSync5, readSync as readSync5, closeSync as closeSync5 } from "fs";
11117
+ import { createReadStream as createReadStream2, existsSync as existsSync13, readdirSync as readdirSync9, readFileSync as readFileSync8, statSync as statSync10, openSync as openSync5, readSync as readSync5, closeSync as closeSync5 } from "fs";
10532
11118
  import { join as join12, basename as basename7 } from "path";
10533
11119
  function hasSubagentsDir(sessionPath) {
10534
11120
  const dir = sessionPath.slice(0, -6);
10535
- return existsSync12(join12(dir, "subagents"));
11121
+ return existsSync13(join12(dir, "subagents"));
10536
11122
  }
10537
11123
  var init_session = __esm({
10538
11124
  "../engine/dist/parsers/session.js"() {
@@ -10631,7 +11217,7 @@ __export(live_session_scan_exports, {
10631
11217
  withSessionReadCache: () => withSessionReadCache,
10632
11218
  withSessionScanScope: () => withSessionScanScope
10633
11219
  });
10634
- import { existsSync as existsSync13, readdirSync as readdirSync10, readFileSync as readFileSync9, statSync as statSync11, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
11220
+ import { existsSync as existsSync14, readdirSync as readdirSync10, readFileSync as readFileSync9, statSync as statSync11, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
10635
11221
  import { join as join13, basename as basename8 } from "path";
10636
11222
  import { createHash as createHash2 } from "crypto";
10637
11223
  function lazyGeminiProjectsJson() {
@@ -10692,7 +11278,7 @@ function sessionFileIndex() {
10692
11278
  return p;
10693
11279
  };
10694
11280
  for (const root of roots) {
10695
- if (seenRoots.has(root) || !existsSync13(root))
11281
+ if (seenRoots.has(root) || !existsSync14(root))
10696
11282
  continue;
10697
11283
  seenRoots.add(root);
10698
11284
  dirs.set(root, dirMtime(root));
@@ -10818,7 +11404,7 @@ function resolveSessionContentPaths(sessionFile) {
10818
11404
  if (!hasSubagentsDir(sessionFile))
10819
11405
  return [sessionFile];
10820
11406
  const subDir = join13(sessionFile.slice(0, -6), "subagents");
10821
- if (!existsSync13(subDir))
11407
+ if (!existsSync14(subDir))
10822
11408
  return [sessionFile];
10823
11409
  const subPaths = readdirSync10(subDir).filter((f) => f.endsWith(".jsonl")).sort().map((f) => join13(subDir, f));
10824
11410
  return [sessionFile, ...subPaths];
@@ -10835,7 +11421,7 @@ function loadGeminiProjectMap() {
10835
11421
  return geminiProjectMapCache;
10836
11422
  const map = /* @__PURE__ */ new Map();
10837
11423
  const projectsPath = lazyGeminiProjectsJson();
10838
- if (existsSync13(projectsPath)) {
11424
+ if (existsSync14(projectsPath)) {
10839
11425
  try {
10840
11426
  const data = JSON.parse(readFileSync9(projectsPath, "utf-8"));
10841
11427
  for (const path of Object.keys(data.projects || {})) {
@@ -10871,7 +11457,7 @@ function geminiIndex() {
10871
11457
  const projMap = loadGeminiProjectMap();
10872
11458
  const isGeminiChat = (f) => f.startsWith("session-") && (f.endsWith(".json") || f.endsWith(".jsonl"));
10873
11459
  for (const tmpRoot of geminiTmpDirs()) {
10874
- if (!existsSync13(tmpRoot))
11460
+ if (!existsSync14(tmpRoot))
10875
11461
  continue;
10876
11462
  let projs;
10877
11463
  try {
@@ -10883,7 +11469,7 @@ function geminiIndex() {
10883
11469
  if (!proj.isDirectory())
10884
11470
  continue;
10885
11471
  const chats = join13(tmpRoot, proj.name, "chats");
10886
- if (!existsSync13(chats))
11472
+ if (!existsSync14(chats))
10887
11473
  continue;
10888
11474
  let files;
10889
11475
  try {
@@ -11004,7 +11590,7 @@ function findCodexSessionFile(sessionId) {
11004
11590
  return null;
11005
11591
  }
11006
11592
  function findCodexSessionFileIn(root, sessionId) {
11007
- if (!existsSync13(root))
11593
+ if (!existsSync14(root))
11008
11594
  return null;
11009
11595
  for (const year of readdirSync10(root, { withFileTypes: true })) {
11010
11596
  if (!year.isDirectory())