chat-recall 0.7.18 → 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;
@@ -5526,6 +5531,13 @@ function grandfatherLegacyHomes() {
5526
5531
  }
5527
5532
  return { seeded };
5528
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
+ }
5529
5541
  function primaryHomes() {
5530
5542
  const home = homedir2();
5531
5543
  const env = process.env;
@@ -5542,7 +5554,8 @@ function primaryHomes() {
5542
5554
  env.CHAT_RECALL_CODEX_HOME || s?.codexHome || join3(home, ".codex"),
5543
5555
  env.CHAT_RECALL_AGY_HOME || s?.agyHome || join3(home, ".gemini", "antigravity-cli"),
5544
5556
  env.CHAT_RECALL_CURSOR_HOME || s?.cursorHome || join3(home, ".cursor"),
5545
- dirOf(opencodeDb)
5557
+ dirOf(opencodeDb),
5558
+ env.CHAT_RECALL_HERMES_HOME || s?.hermesHome || env.HERMES_HOME?.trim() || defaultHermesHome()
5546
5559
  ];
5547
5560
  return candidates.map(normalizeHomePath);
5548
5561
  }
@@ -5621,6 +5634,9 @@ function geminiHomeDir() {
5621
5634
  function opencodeDbPath() {
5622
5635
  return process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath || join4(homedir3(), ".local", "share", "opencode", "opencode.db");
5623
5636
  }
5637
+ function hermesHomeDir() {
5638
+ return process.env.CHAT_RECALL_HERMES_HOME || sources().hermesHome || process.env.HERMES_HOME?.trim() || defaultHermesHome();
5639
+ }
5624
5640
  function codexHomeDir() {
5625
5641
  return process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome || join4(homedir3(), ".codex");
5626
5642
  }
@@ -5726,6 +5742,22 @@ function opencodeDbPaths(opts = {}) {
5726
5742
  dbs.push(primary);
5727
5743
  return applyExclusions(dbs, opts.includeExcluded);
5728
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
+ }
5729
5761
  var sources, _sourceExclusionFilter;
5730
5762
  var init_tool_paths = __esm({
5731
5763
  "../engine/dist/core/tool-paths.js"() {
@@ -7003,6 +7035,71 @@ function readTailFromOffset(path, offset, maxBytes = TAIL_READ_MAX_BYTES) {
7003
7035
  }
7004
7036
  }
7005
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
+ }
7006
7103
  function findNewline(fd, from, size) {
7007
7104
  const block = Buffer.allocUnsafe(SKIP_SCAN_BYTES);
7008
7105
  let pos = from;
@@ -9481,6 +9578,473 @@ var init_cursor = __esm({
9481
9578
  }
9482
9579
  });
9483
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
+
9484
10048
  // ../engine/dist/core/backends/index.js
9485
10049
  function bootstrapBackends() {
9486
10050
  registerBackend(claudeBackend);
@@ -9488,6 +10052,7 @@ function bootstrapBackends() {
9488
10052
  registerBackend(codexBackend);
9489
10053
  registerBackend(agyBackend);
9490
10054
  registerBackend(cursorBackend);
10055
+ registerBackend(hermesBackend);
9491
10056
  }
9492
10057
  var init_backends = __esm({
9493
10058
  "../engine/dist/core/backends/index.js"() {
@@ -9498,6 +10063,7 @@ var init_backends = __esm({
9498
10063
  init_codex2();
9499
10064
  init_agy();
9500
10065
  init_cursor();
10066
+ init_hermes();
9501
10067
  init_tool_backend();
9502
10068
  _setRegistryBootstrapper(bootstrapBackends);
9503
10069
  }
@@ -9710,7 +10276,7 @@ var init_session_outcome = __esm({
9710
10276
  });
9711
10277
 
9712
10278
  // ../engine/dist/core/first-prompt.js
9713
- 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";
9714
10280
  import { StringDecoder } from "string_decoder";
9715
10281
  function stripBanners(s) {
9716
10282
  let out = s;
@@ -9879,7 +10445,7 @@ var init_project_dir_name = __esm({
9879
10445
  });
9880
10446
 
9881
10447
  // ../engine/dist/core/backends/claude.js
9882
- 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";
9883
10449
  import { join as join11, basename as basename6 } from "path";
9884
10450
  function stripReminders(text) {
9885
10451
  return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
@@ -9949,7 +10515,7 @@ var init_claude2 = __esm({
9949
10515
  return join11(this.homeDir(), "history.jsonl");
9950
10516
  }
9951
10517
  isAvailable() {
9952
- return existsSync11(this.projectsDir());
10518
+ return existsSync12(this.projectsDir());
9953
10519
  }
9954
10520
  // ── ID handling ────────────────────────────────────────────────
9955
10521
  /**
@@ -10055,7 +10621,7 @@ var init_claude2 = __esm({
10055
10621
  prior.modified = ref.modified;
10056
10622
  }
10057
10623
  };
10058
- if (!existsSync11(root))
10624
+ if (!existsSync12(root))
10059
10625
  return;
10060
10626
  for (const proj of readdirSync8(root, { withFileTypes: true })) {
10061
10627
  if (!proj.isDirectory())
@@ -10066,12 +10632,12 @@ var init_claude2 = __esm({
10066
10632
  const projectPath = resolveProjectDirName(proj.name);
10067
10633
  const indexPath = join11(projPath, "sessions-index.json");
10068
10634
  const indexed = /* @__PURE__ */ new Set();
10069
- if (existsSync11(indexPath)) {
10635
+ if (existsSync12(indexPath)) {
10070
10636
  try {
10071
10637
  const indexData = JSON.parse(readFileSync7(indexPath, "utf-8"));
10072
10638
  const entries = indexData.entries || [];
10073
10639
  for (const entry of entries) {
10074
- if (!entry.fullPath || !existsSync11(entry.fullPath))
10640
+ if (!entry.fullPath || !existsSync12(entry.fullPath))
10075
10641
  continue;
10076
10642
  indexed.add(entry.sessionId);
10077
10643
  add({
@@ -10396,7 +10962,7 @@ var init_claude2 = __esm({
10396
10962
  collectRecentEdits(opts) {
10397
10963
  const candidates = [];
10398
10964
  for (const root of claudeProjectDirs()) {
10399
- if (!existsSync11(root))
10965
+ if (!existsSync12(root))
10400
10966
  continue;
10401
10967
  for (const projEntry of readdirSync8(root, { withFileTypes: true })) {
10402
10968
  if (!projEntry.isDirectory())
@@ -10418,7 +10984,7 @@ var init_claude2 = __esm({
10418
10984
  const st = statSync9(fp);
10419
10985
  let mtime = st.mtimeMs;
10420
10986
  const subDir = join11(projPath, f.slice(0, -6), "subagents");
10421
- if (existsSync11(subDir)) {
10987
+ if (existsSync12(subDir)) {
10422
10988
  for (const sub of readdirSync8(subDir)) {
10423
10989
  try {
10424
10990
  const subSt = statSync9(join11(subDir, sub));
@@ -10495,27 +11061,45 @@ var init_claude2 = __esm({
10495
11061
  /**
10496
11062
  * True when this session exists in MORE THAN ONE home.
10497
11063
  *
10498
- * Such a session cannot be tail-appended: `fileSize()` is the SUM across
10499
- * copies while a byte offset addresses ONE file, so the two disagree by
10500
- * construction and the "tail" is either empty or the wrong bytes. The caller
10501
- * uses this to force a FULL sync, which ships the unioned container and is
10502
- * correct regardless of how the halves are arranged.
10503
- *
10504
11064
  * Measured on ec05f266: ~/.claude 1715 records, ~/.claude-work 205 records,
10505
- * zero overlap. An offset-based append against a 5846662-byte "size" that no
10506
- * 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.
10507
11067
  */
10508
11068
  spansMultipleSources(prefixedId) {
10509
11069
  return findSessionFiles(this.toRawId(prefixedId)).length > 1;
10510
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
+ */
10511
11096
  async readFromOffset(prefixedId, offset, maxBytes) {
10512
- const rawId = this.toRawId(prefixedId);
10513
- const copies = findSessionFiles(rawId);
11097
+ const copies = findSessionFiles(this.toRawId(prefixedId));
10514
11098
  if (copies.length === 0)
10515
11099
  return { text: "", newOffset: offset };
10516
- if (copies.length > 1)
10517
- return { text: "", newOffset: offset };
10518
- 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);
10519
11103
  }
10520
11104
  };
10521
11105
  claudeBackend = new ClaudeBackend();
@@ -10530,11 +11114,11 @@ var init_model_pricing = __esm({
10530
11114
  });
10531
11115
 
10532
11116
  // ../engine/dist/parsers/session.js
10533
- 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";
10534
11118
  import { join as join12, basename as basename7 } from "path";
10535
11119
  function hasSubagentsDir(sessionPath) {
10536
11120
  const dir = sessionPath.slice(0, -6);
10537
- return existsSync12(join12(dir, "subagents"));
11121
+ return existsSync13(join12(dir, "subagents"));
10538
11122
  }
10539
11123
  var init_session = __esm({
10540
11124
  "../engine/dist/parsers/session.js"() {
@@ -10633,7 +11217,7 @@ __export(live_session_scan_exports, {
10633
11217
  withSessionReadCache: () => withSessionReadCache,
10634
11218
  withSessionScanScope: () => withSessionScanScope
10635
11219
  });
10636
- 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";
10637
11221
  import { join as join13, basename as basename8 } from "path";
10638
11222
  import { createHash as createHash2 } from "crypto";
10639
11223
  function lazyGeminiProjectsJson() {
@@ -10694,7 +11278,7 @@ function sessionFileIndex() {
10694
11278
  return p;
10695
11279
  };
10696
11280
  for (const root of roots) {
10697
- if (seenRoots.has(root) || !existsSync13(root))
11281
+ if (seenRoots.has(root) || !existsSync14(root))
10698
11282
  continue;
10699
11283
  seenRoots.add(root);
10700
11284
  dirs.set(root, dirMtime(root));
@@ -10820,7 +11404,7 @@ function resolveSessionContentPaths(sessionFile) {
10820
11404
  if (!hasSubagentsDir(sessionFile))
10821
11405
  return [sessionFile];
10822
11406
  const subDir = join13(sessionFile.slice(0, -6), "subagents");
10823
- if (!existsSync13(subDir))
11407
+ if (!existsSync14(subDir))
10824
11408
  return [sessionFile];
10825
11409
  const subPaths = readdirSync10(subDir).filter((f) => f.endsWith(".jsonl")).sort().map((f) => join13(subDir, f));
10826
11410
  return [sessionFile, ...subPaths];
@@ -10837,7 +11421,7 @@ function loadGeminiProjectMap() {
10837
11421
  return geminiProjectMapCache;
10838
11422
  const map = /* @__PURE__ */ new Map();
10839
11423
  const projectsPath = lazyGeminiProjectsJson();
10840
- if (existsSync13(projectsPath)) {
11424
+ if (existsSync14(projectsPath)) {
10841
11425
  try {
10842
11426
  const data = JSON.parse(readFileSync9(projectsPath, "utf-8"));
10843
11427
  for (const path of Object.keys(data.projects || {})) {
@@ -10873,7 +11457,7 @@ function geminiIndex() {
10873
11457
  const projMap = loadGeminiProjectMap();
10874
11458
  const isGeminiChat = (f) => f.startsWith("session-") && (f.endsWith(".json") || f.endsWith(".jsonl"));
10875
11459
  for (const tmpRoot of geminiTmpDirs()) {
10876
- if (!existsSync13(tmpRoot))
11460
+ if (!existsSync14(tmpRoot))
10877
11461
  continue;
10878
11462
  let projs;
10879
11463
  try {
@@ -10885,7 +11469,7 @@ function geminiIndex() {
10885
11469
  if (!proj.isDirectory())
10886
11470
  continue;
10887
11471
  const chats = join13(tmpRoot, proj.name, "chats");
10888
- if (!existsSync13(chats))
11472
+ if (!existsSync14(chats))
10889
11473
  continue;
10890
11474
  let files;
10891
11475
  try {
@@ -11006,7 +11590,7 @@ function findCodexSessionFile(sessionId) {
11006
11590
  return null;
11007
11591
  }
11008
11592
  function findCodexSessionFileIn(root, sessionId) {
11009
- if (!existsSync13(root))
11593
+ if (!existsSync14(root))
11010
11594
  return null;
11011
11595
  for (const year of readdirSync10(root, { withFileTypes: true })) {
11012
11596
  if (!year.isDirectory())