@hermespilot/link 0.3.5 → 0.3.7

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.
@@ -9,8 +9,21 @@ import { randomUUID as randomUUID7 } from "crypto";
9
9
  // src/database/link-database.ts
10
10
  import { mkdir } from "fs/promises";
11
11
  import { randomUUID } from "crypto";
12
- import { createRequire } from "module";
13
12
  import path from "path";
13
+
14
+ // src/database/sqlite.ts
15
+ import Database from "better-sqlite3";
16
+ function openSqliteDatabase(filePath, options = {}) {
17
+ return new Database(filePath, {
18
+ ...options.readonly === void 0 ? {} : {
19
+ readonly: options.readonly,
20
+ fileMustExist: options.readonly
21
+ },
22
+ ...options.timeout === void 0 ? {} : { timeout: options.timeout }
23
+ });
24
+ }
25
+
26
+ // src/database/link-database.ts
14
27
  var MIGRATIONS = [
15
28
  {
16
29
  version: 1,
@@ -136,7 +149,6 @@ var MIGRATIONS = [
136
149
  `
137
150
  }
138
151
  ];
139
- var nodeRequire = createRequire(import.meta.url);
140
152
  async function migrateLinkDatabase(paths) {
141
153
  await mkdir(path.dirname(paths.databaseFile), { recursive: true, mode: 448 });
142
154
  const db = openDatabase(paths);
@@ -210,6 +222,92 @@ async function upsertConversationStats(paths, record) {
210
222
  db.close();
211
223
  }
212
224
  }
225
+ async function listConversationStatsPage(paths, input) {
226
+ await migrateLinkDatabase(paths);
227
+ const rawLimit = Number.isFinite(input.limit) ? Math.trunc(input.limit) : 25;
228
+ const limit = Math.max(1, Math.min(100, rawLimit));
229
+ const db = openDatabase(paths);
230
+ try {
231
+ const conditions = ["status = ?"];
232
+ const params = ["active"];
233
+ if (input.cursor) {
234
+ conditions.push(`(
235
+ updated_at < ?
236
+ OR (updated_at = ? AND conversation_id < ?)
237
+ )`);
238
+ params.push(
239
+ input.cursor.updatedAt,
240
+ input.cursor.updatedAt,
241
+ input.cursor.conversationId
242
+ );
243
+ }
244
+ const rows = db.prepare(`
245
+ SELECT conversation_id, updated_at
246
+ FROM conversation_stats
247
+ WHERE ${conditions.join(" AND ")}
248
+ ORDER BY updated_at DESC, conversation_id DESC
249
+ LIMIT ?
250
+ `).all(...params, limit + 1);
251
+ const records = rows.slice(0, limit).map((row) => ({
252
+ conversationId: readString(row, "conversation_id") ?? "",
253
+ updatedAt: readString(row, "updated_at") ?? ""
254
+ })).filter((row) => row.conversationId && row.updatedAt);
255
+ return {
256
+ records,
257
+ hasMore: rows.length > limit
258
+ };
259
+ } finally {
260
+ db.close();
261
+ }
262
+ }
263
+ async function searchConversationStatsPage(paths, input) {
264
+ await migrateLinkDatabase(paths);
265
+ const rawLimit = Number.isFinite(input.limit) ? Math.trunc(input.limit) : 25;
266
+ const limit = Math.max(1, Math.min(100, rawLimit));
267
+ const query = input.query.trim();
268
+ if (!query) {
269
+ return listConversationStatsPage(paths, {
270
+ limit,
271
+ cursor: input.cursor
272
+ });
273
+ }
274
+ const db = openDatabase(paths);
275
+ try {
276
+ const conditions = ["status = ?", "LOWER(title) LIKE ? ESCAPE '\\'"];
277
+ const params = [
278
+ "active",
279
+ `%${escapeSqlLike(query.toLowerCase())}%`
280
+ ];
281
+ if (input.cursor) {
282
+ conditions.push(`(
283
+ updated_at < ?
284
+ OR (updated_at = ? AND conversation_id < ?)
285
+ )`);
286
+ params.push(
287
+ input.cursor.updatedAt,
288
+ input.cursor.updatedAt,
289
+ input.cursor.conversationId
290
+ );
291
+ }
292
+ const rows = db.prepare(`
293
+ SELECT conversation_id, updated_at
294
+ FROM conversation_stats
295
+ WHERE ${conditions.join(" AND ")}
296
+ ORDER BY updated_at DESC, conversation_id DESC
297
+ LIMIT ?
298
+ `).all(...params, limit + 1);
299
+ const records = rows.slice(0, limit).map((row) => ({
300
+ conversationId: readString(row, "conversation_id") ?? "",
301
+ updatedAt: readString(row, "updated_at") ?? ""
302
+ })).filter((row) => row.conversationId && row.updatedAt);
303
+ return {
304
+ records,
305
+ hasMore: rows.length > limit
306
+ };
307
+ } finally {
308
+ db.close();
309
+ }
310
+ }
213
311
  async function replaceRunUsageFactsForConversation(paths, conversationId, records) {
214
312
  await migrateLinkDatabase(paths);
215
313
  const db = openDatabase(paths);
@@ -618,8 +716,7 @@ async function deleteConversationStatsForProfile(paths, input) {
618
716
  }
619
717
  }
620
718
  function openDatabase(paths) {
621
- const { DatabaseSync } = nodeRequire("node:sqlite");
622
- const db = new DatabaseSync(paths.databaseFile, {
719
+ const db = openSqliteDatabase(paths.databaseFile, {
623
720
  timeout: 5e3
624
721
  });
625
722
  db.exec(`
@@ -826,6 +923,9 @@ function readString(row, key) {
826
923
  const value = row?.[key];
827
924
  return typeof value === "string" && value.trim() ? value : void 0;
828
925
  }
926
+ function escapeSqlLike(value) {
927
+ return value.replace(/[\\%_]/gu, (match) => `\\${match}`);
928
+ }
829
929
  function statisticsWhereClause(filter) {
830
930
  const profileUid = filter.profileUid?.trim();
831
931
  const profileName = filter.profileName?.trim();
@@ -3724,7 +3824,7 @@ import os2 from "os";
3724
3824
  import path5 from "path";
3725
3825
 
3726
3826
  // src/constants.ts
3727
- var LINK_VERSION = "0.3.5";
3827
+ var LINK_VERSION = "0.3.7";
3728
3828
  var LINK_COMMAND = "hermeslink";
3729
3829
  var LINK_DEFAULT_PORT = 52379;
3730
3830
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -4859,6 +4959,9 @@ function toRecord3(value) {
4859
4959
  }
4860
4960
 
4861
4961
  // src/conversations/statistics.ts
4962
+ var ESTIMATED_CONTEXT_CHARS_PER_TOKEN = 4;
4963
+ var ESTIMATED_CONTEXT_BASE_OVERHEAD_TOKENS = 256;
4964
+ var ESTIMATED_CONTEXT_PER_MESSAGE_OVERHEAD_TOKENS = 8;
4862
4965
  function buildConversationStats(manifest, snapshot) {
4863
4966
  if (manifest.stats && manifest.status !== "active" && snapshot.messages.length === 0 && snapshot.runs.length === 0) {
4864
4967
  return manifest.stats;
@@ -4973,12 +5076,21 @@ function readUsage(payload) {
4973
5076
  return void 0;
4974
5077
  }
4975
5078
  const usage = toRecord4(payload.usage);
4976
- const inputTokens = readInteger(usage, "input_tokens") ?? readInteger(usage, "prompt_tokens") ?? readInteger(payload, "input_tokens") ?? readInteger(payload, "prompt_tokens");
4977
- const outputTokens = readInteger(usage, "output_tokens") ?? readInteger(usage, "completion_tokens") ?? readInteger(payload, "output_tokens") ?? readInteger(payload, "completion_tokens");
4978
- const totalTokens = readInteger(usage, "total_tokens") ?? readInteger(payload, "total_tokens") ?? (inputTokens ?? 0) + (outputTokens ?? 0);
4979
- const contextWindow = readInteger(usage, "context_window") ?? readInteger(usage, "context_max") ?? readInteger(payload, "context_window") ?? readInteger(payload, "context_max");
4980
- const explicitContextTokens = readInteger(usage, "context_tokens") ?? readInteger(usage, "context_used") ?? readInteger(usage, "current_context_tokens") ?? readInteger(usage, "last_prompt_tokens") ?? readInteger(payload, "context_tokens") ?? readInteger(payload, "context_used") ?? readInteger(payload, "current_context_tokens") ?? readInteger(payload, "last_prompt_tokens");
4981
- if (!inputTokens && !outputTokens && !totalTokens) {
5079
+ const response = toRecord4(payload.response);
5080
+ const responseUsage = toRecord4(response.usage);
5081
+ const context = firstRecord(
5082
+ payload.context,
5083
+ usage.context,
5084
+ response.context,
5085
+ responseUsage.context
5086
+ );
5087
+ const inputTokens = readInteger(usage, "input_tokens") ?? readInteger(usage, "prompt_tokens") ?? readInteger(responseUsage, "input_tokens") ?? readInteger(responseUsage, "prompt_tokens") ?? readInteger(payload, "input_tokens") ?? readInteger(payload, "prompt_tokens");
5088
+ const outputTokens = readInteger(usage, "output_tokens") ?? readInteger(usage, "completion_tokens") ?? readInteger(responseUsage, "output_tokens") ?? readInteger(responseUsage, "completion_tokens") ?? readInteger(payload, "output_tokens") ?? readInteger(payload, "completion_tokens");
5089
+ const totalTokens = readInteger(usage, "total_tokens") ?? readInteger(responseUsage, "total_tokens") ?? readInteger(payload, "total_tokens") ?? (inputTokens ?? 0) + (outputTokens ?? 0);
5090
+ const contextWindow = readInteger(context, "window_tokens") ?? readInteger(context, "windowTokens") ?? readInteger(context, "context_window") ?? readInteger(context, "context_max") ?? readInteger(context, "context_length") ?? readInteger(usage, "context_window") ?? readInteger(usage, "context_max") ?? readInteger(usage, "context_length") ?? readInteger(responseUsage, "context_window") ?? readInteger(responseUsage, "context_max") ?? readInteger(responseUsage, "context_length") ?? readInteger(response, "context_window") ?? readInteger(response, "context_max") ?? readInteger(response, "context_length") ?? readInteger(payload, "context_window") ?? readInteger(payload, "context_max") ?? readInteger(payload, "context_length");
5091
+ const explicitContextTokens = readInteger(context, "used_tokens") ?? readInteger(context, "usedTokens") ?? readInteger(context, "context_tokens") ?? readInteger(context, "context_used") ?? readInteger(context, "current_context_tokens") ?? readInteger(context, "last_prompt_tokens") ?? readInteger(usage, "context_tokens") ?? readInteger(usage, "context_used") ?? readInteger(usage, "current_context_tokens") ?? readInteger(usage, "last_prompt_tokens") ?? readInteger(responseUsage, "context_tokens") ?? readInteger(responseUsage, "context_used") ?? readInteger(responseUsage, "current_context_tokens") ?? readInteger(responseUsage, "last_prompt_tokens") ?? readInteger(response, "context_tokens") ?? readInteger(response, "context_used") ?? readInteger(response, "current_context_tokens") ?? readInteger(response, "last_prompt_tokens") ?? readInteger(payload, "context_tokens") ?? readInteger(payload, "context_used") ?? readInteger(payload, "current_context_tokens") ?? readInteger(payload, "last_prompt_tokens");
5092
+ const explicitUsagePercent = readInteger(context, "usage_percent") ?? readInteger(context, "context_percent") ?? readInteger(usage, "usage_percent") ?? readInteger(usage, "context_percent") ?? readInteger(responseUsage, "usage_percent") ?? readInteger(responseUsage, "context_percent") ?? readInteger(response, "usage_percent") ?? readInteger(response, "context_percent") ?? readInteger(payload, "usage_percent") ?? readInteger(payload, "context_percent");
5093
+ if (!inputTokens && !outputTokens && !totalTokens && explicitContextTokens === void 0) {
4982
5094
  return void 0;
4983
5095
  }
4984
5096
  return {
@@ -4987,14 +5099,44 @@ function readUsage(payload) {
4987
5099
  total_tokens: totalTokens,
4988
5100
  ...explicitContextTokens !== void 0 ? { context_tokens: explicitContextTokens } : {},
4989
5101
  ...contextWindow !== void 0 ? { context_window: contextWindow } : {},
5102
+ ...explicitContextTokens !== void 0 ? { context_source: "explicit" } : {},
4990
5103
  ...explicitContextTokens !== void 0 && contextWindow ? {
4991
- usage_percent: Math.min(
5104
+ usage_percent: explicitUsagePercent !== void 0 ? Math.min(100, explicitUsagePercent) : Math.min(
4992
5105
  100,
4993
5106
  Math.round(explicitContextTokens / contextWindow * 100)
4994
5107
  )
4995
5108
  } : {}
4996
5109
  };
4997
5110
  }
5111
+ function estimateContextUsage(input) {
5112
+ const currentInput = input.currentInput.trim();
5113
+ const instructions = input.instructions?.trim() ?? "";
5114
+ const messageCount = input.conversationHistory.length + (currentInput ? 1 : 0) + (instructions ? 1 : 0);
5115
+ if (messageCount === 0) {
5116
+ return void 0;
5117
+ }
5118
+ const serializedRequest = JSON.stringify({
5119
+ instructions: instructions || void 0,
5120
+ conversation_history: input.conversationHistory,
5121
+ input: currentInput || void 0
5122
+ });
5123
+ const estimatedTokens = Math.ceil(serializedRequest.length / ESTIMATED_CONTEXT_CHARS_PER_TOKEN) + ESTIMATED_CONTEXT_BASE_OVERHEAD_TOKENS + messageCount * ESTIMATED_CONTEXT_PER_MESSAGE_OVERHEAD_TOKENS;
5124
+ const contextTokens = input.contextWindow ? Math.min(input.contextWindow, estimatedTokens) : estimatedTokens;
5125
+ return {
5126
+ input_tokens: 0,
5127
+ output_tokens: 0,
5128
+ total_tokens: 0,
5129
+ context_tokens: contextTokens,
5130
+ ...input.contextWindow !== void 0 ? { context_window: input.contextWindow } : {},
5131
+ ...input.contextWindow ? {
5132
+ usage_percent: Math.min(
5133
+ 100,
5134
+ Math.round(contextTokens / input.contextWindow * 100)
5135
+ )
5136
+ } : {},
5137
+ context_source: "estimated"
5138
+ };
5139
+ }
4998
5140
  function isAgentRun(run) {
4999
5141
  return run.kind !== "command";
5000
5142
  }
@@ -5015,6 +5157,14 @@ function readInteger(payload, key) {
5015
5157
  function toRecord4(value) {
5016
5158
  return typeof value === "object" && value !== null ? value : {};
5017
5159
  }
5160
+ function firstRecord(...values) {
5161
+ for (const value of values) {
5162
+ if (typeof value === "object" && value !== null) {
5163
+ return value;
5164
+ }
5165
+ }
5166
+ return {};
5167
+ }
5018
5168
 
5019
5169
  // src/conversations/blob-store.ts
5020
5170
  import { randomUUID as randomUUID3 } from "crypto";
@@ -5730,7 +5880,7 @@ async function buildConversationRuntimeMetadata(paths, manifest, snapshot) {
5730
5880
  };
5731
5881
  const contextWindow = current.contextWindow ?? usage.context_window ?? usageRun?.context_window;
5732
5882
  const contextTokens = usage.context_tokens;
5733
- const contextSource = contextTokens === void 0 ? "unknown" : "explicit";
5883
+ const contextSource = contextTokens === void 0 ? "unknown" : usage.context_source ?? "explicit";
5734
5884
  const provider = current.provider ?? usageRun?.provider;
5735
5885
  const reasoningEffort = current.reasoningEffort;
5736
5886
  return {
@@ -6324,12 +6474,12 @@ var ConversationCommandHandlers = class {
6324
6474
  };
6325
6475
  function formatContextUsageLines(runtime) {
6326
6476
  const windowTokens = runtime.context.window_tokens ?? runtime.context.context_window;
6327
- if (runtime.context.source === "explicit") {
6477
+ if (runtime.context.source === "explicit" || runtime.context.source === "estimated") {
6328
6478
  const usedTokens = runtime.context.used_tokens ?? runtime.context.input_tokens;
6329
6479
  const percent = runtime.context.usage_percent ?? (windowTokens && windowTokens > 0 ? Math.min(100, Math.round(usedTokens / windowTokens * 100)) : void 0);
6330
6480
  return [
6331
6481
  `\u4E0A\u4E0B\u6587\uFF1A${usedTokens}${windowTokens ? ` / ${windowTokens}` : ""}${percent === void 0 ? "" : `\uFF08${percent}%\uFF09`}`,
6332
- "\u6765\u6E90\uFF1A\u6A21\u578B\u8FD4\u56DE"
6482
+ runtime.context.source === "estimated" ? "\u6765\u6E90\uFF1A\u672C\u5730\u4F30\u7B97" : "\u6765\u6E90\uFF1A\u6A21\u578B\u8FD4\u56DE"
6333
6483
  ];
6334
6484
  }
6335
6485
  return [
@@ -6703,9 +6853,7 @@ function isUsableLanIpv4(value) {
6703
6853
 
6704
6854
  // src/hermes/session-title.ts
6705
6855
  import { stat as stat6 } from "fs/promises";
6706
- import { createRequire as createRequire2 } from "module";
6707
6856
  import path11 from "path";
6708
- var nodeRequire2 = createRequire2(import.meta.url);
6709
6857
  async function readHermesSessionTitle(sessionId, paths, profileName) {
6710
6858
  const trimmedSessionId = sessionId.trim();
6711
6859
  if (!trimmedSessionId) {
@@ -6751,11 +6899,8 @@ async function readHermesCompressionTip(sessionId, paths, profileName) {
6751
6899
  function readSessionTitleFromStateDb(dbPath, sessionId) {
6752
6900
  let db = null;
6753
6901
  try {
6754
- const { DatabaseSync } = nodeRequire2(
6755
- "node:sqlite"
6756
- );
6757
- db = new DatabaseSync(dbPath, {
6758
- readOnly: true,
6902
+ db = openSqliteDatabase(dbPath, {
6903
+ readonly: true,
6759
6904
  timeout: 1e3
6760
6905
  });
6761
6906
  const row = db.prepare("SELECT title FROM sessions WHERE id = ? LIMIT 1").get(sessionId);
@@ -6770,11 +6915,8 @@ function readSessionTitleFromStateDb(dbPath, sessionId) {
6770
6915
  function readCompressionTipFromStateDb(dbPath, sessionId) {
6771
6916
  let db = null;
6772
6917
  try {
6773
- const { DatabaseSync } = nodeRequire2(
6774
- "node:sqlite"
6775
- );
6776
- db = new DatabaseSync(dbPath, {
6777
- readOnly: true,
6918
+ db = openSqliteDatabase(dbPath, {
6919
+ readonly: true,
6778
6920
  timeout: 1e3
6779
6921
  });
6780
6922
  let current = sessionId;
@@ -8234,12 +8376,63 @@ function toRecord6(value) {
8234
8376
  }
8235
8377
 
8236
8378
  // src/conversations/conversation-queries.ts
8379
+ var DEFAULT_CONVERSATION_LIST_PAGE_SIZE = 25;
8380
+ var MAX_CONVERSATION_LIST_PAGE_SIZE = 100;
8237
8381
  var ConversationQueryCoordinator = class {
8238
8382
  constructor(deps) {
8239
8383
  this.deps = deps;
8240
8384
  }
8241
8385
  deps;
8242
8386
  async listConversations() {
8387
+ return this.listConversationsFromStore();
8388
+ }
8389
+ async listConversationPage(options = {}) {
8390
+ const limit = normalizeConversationListPageLimit(options.limit);
8391
+ const cursor = decodeConversationListCursor(options.cursor);
8392
+ const indexedPage = await listConversationStatsPage(this.deps.paths, {
8393
+ limit,
8394
+ cursor
8395
+ });
8396
+ if (indexedPage.records.length === 0) {
8397
+ return this.listConversationPageFromStore({ limit, cursor });
8398
+ }
8399
+ const summaries = await this.summarizeIndexedConversations(
8400
+ indexedPage.records
8401
+ );
8402
+ return {
8403
+ conversations: summaries,
8404
+ page: {
8405
+ limit,
8406
+ has_more: indexedPage.hasMore,
8407
+ next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
8408
+ }
8409
+ };
8410
+ }
8411
+ async searchConversationPage(options = {}) {
8412
+ const query = normalizeConversationSearchQuery(options.query);
8413
+ if (!query) {
8414
+ return this.listConversationPage(options);
8415
+ }
8416
+ const limit = normalizeConversationListPageLimit(options.limit);
8417
+ const cursor = decodeConversationListCursor(options.cursor);
8418
+ const indexedPage = await searchConversationStatsPage(this.deps.paths, {
8419
+ limit,
8420
+ cursor,
8421
+ query
8422
+ });
8423
+ const summaries = await this.summarizeIndexedConversations(
8424
+ indexedPage.records
8425
+ );
8426
+ return {
8427
+ conversations: summaries,
8428
+ page: {
8429
+ limit,
8430
+ has_more: indexedPage.hasMore,
8431
+ next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
8432
+ }
8433
+ };
8434
+ }
8435
+ async listConversationsFromStore() {
8243
8436
  const summaries = [];
8244
8437
  for (const conversationId of await this.deps.store.listConversationIds()) {
8245
8438
  const manifest = await this.deps.store.readManifest(conversationId).catch(
@@ -8255,9 +8448,41 @@ var ConversationQueryCoordinator = class {
8255
8448
  summaries.push(await this.summarizeConversation(refreshed, snapshot));
8256
8449
  }
8257
8450
  return summaries.sort(
8258
- (left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at)
8451
+ (left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at) || right.id.localeCompare(left.id)
8259
8452
  );
8260
8453
  }
8454
+ async listConversationPageFromStore(input) {
8455
+ const all = await this.listConversationsFromStore();
8456
+ const startIndex = input.cursor ? all.findIndex((summary) => isAfterConversationListCursor(summary, input.cursor)) : 0;
8457
+ const safeStartIndex = startIndex < 0 ? all.length : startIndex;
8458
+ const conversations = all.slice(safeStartIndex, safeStartIndex + input.limit);
8459
+ const hasMore = safeStartIndex + input.limit < all.length;
8460
+ return {
8461
+ conversations,
8462
+ page: {
8463
+ limit: input.limit,
8464
+ has_more: hasMore,
8465
+ next_cursor: hasMore && conversations.length > 0 ? encodeConversationListCursorFromSummary(conversations.at(-1)) : null
8466
+ }
8467
+ };
8468
+ }
8469
+ async summarizeIndexedConversations(records) {
8470
+ const summaries = [];
8471
+ for (const record of records) {
8472
+ const manifest = await this.deps.store.readManifest(record.conversationId).catch(
8473
+ () => null
8474
+ );
8475
+ if (!manifest || manifest.status !== "active") {
8476
+ continue;
8477
+ }
8478
+ const snapshot = await this.deps.store.readSnapshot(record.conversationId).catch(
8479
+ () => emptySnapshot2()
8480
+ );
8481
+ const refreshed = await this.deps.metadata.refreshTitleFromHermes(manifest, { snapshot }).catch(() => manifest);
8482
+ summaries.push(await this.summarizeConversation(refreshed, snapshot));
8483
+ }
8484
+ return summaries;
8485
+ }
8261
8486
  async getMessages(conversationId, options = {}) {
8262
8487
  const manifest = await this.deps.store.readActiveManifest(conversationId);
8263
8488
  const snapshot = await this.deps.store.readSnapshot(conversationId);
@@ -8344,6 +8569,61 @@ var ConversationQueryCoordinator = class {
8344
8569
  });
8345
8570
  }
8346
8571
  };
8572
+ function normalizeConversationListPageLimit(value) {
8573
+ if (value === void 0 || !Number.isFinite(value)) {
8574
+ return DEFAULT_CONVERSATION_LIST_PAGE_SIZE;
8575
+ }
8576
+ return Math.max(
8577
+ 1,
8578
+ Math.min(MAX_CONVERSATION_LIST_PAGE_SIZE, Math.floor(value))
8579
+ );
8580
+ }
8581
+ function encodeConversationListCursor(record) {
8582
+ return Buffer.from(
8583
+ JSON.stringify({
8584
+ updated_at: record.updatedAt,
8585
+ conversation_id: record.conversationId
8586
+ }),
8587
+ "utf8"
8588
+ ).toString("base64url");
8589
+ }
8590
+ function encodeConversationListCursorFromSummary(summary) {
8591
+ return encodeConversationListCursor({
8592
+ conversationId: summary.id,
8593
+ updatedAt: summary.updated_at
8594
+ });
8595
+ }
8596
+ function decodeConversationListCursor(value) {
8597
+ if (!value) {
8598
+ return null;
8599
+ }
8600
+ try {
8601
+ const decoded = JSON.parse(
8602
+ Buffer.from(value, "base64url").toString("utf8")
8603
+ );
8604
+ const updatedAt = readNonEmptyString(decoded.updated_at);
8605
+ const conversationId = readNonEmptyString(decoded.conversation_id);
8606
+ if (!updatedAt || !conversationId) {
8607
+ throw new Error("invalid cursor");
8608
+ }
8609
+ return { updatedAt, conversationId };
8610
+ } catch {
8611
+ throw new LinkHttpError(
8612
+ 400,
8613
+ "conversation_cursor_invalid",
8614
+ "Conversation list cursor is invalid"
8615
+ );
8616
+ }
8617
+ }
8618
+ function readNonEmptyString(value) {
8619
+ return typeof value === "string" && value.trim() ? value.trim() : null;
8620
+ }
8621
+ function normalizeConversationSearchQuery(value) {
8622
+ return typeof value === "string" ? value.trim() : "";
8623
+ }
8624
+ function isAfterConversationListCursor(summary, cursor) {
8625
+ return summary.updated_at < cursor.updatedAt || summary.updated_at === cursor.updatedAt && summary.id < cursor.conversationId;
8626
+ }
8347
8627
  function hydrateAgentEventBlocks(blocks, agentEvents) {
8348
8628
  if (!blocks?.length || agentEvents.length === 0) {
8349
8629
  return blocks;
@@ -8518,61 +8798,393 @@ function isNodeError8(error, code) {
8518
8798
 
8519
8799
  // src/conversations/hermes-session-sync.ts
8520
8800
  import { randomUUID as randomUUID6 } from "crypto";
8521
- import { readdir as readdir5, readFile as readFile8, stat as stat7 } from "fs/promises";
8522
- import { createRequire as createRequire3 } from "module";
8801
+ import { readdir as readdir6, readFile as readFile9, stat as stat8 } from "fs/promises";
8523
8802
  import os4 from "os";
8803
+ import path14 from "path";
8804
+
8805
+ // src/conversations/delivery-import.ts
8806
+ import { lstat, readFile as readFile8, readdir as readdir5, stat as stat7 } from "fs/promises";
8524
8807
  import path13 from "path";
8525
- var nodeRequire3 = createRequire3(import.meta.url);
8526
- var PROFILE_NAME_PATTERN3 = /^[a-zA-Z0-9._-]{1,64}$/u;
8527
- var DEFAULT_PROFILE_NAME = "default";
8528
- var MAX_IMPORTABLE_SESSIONS = 100;
8529
- var HIDDEN_SESSION_SOURCES = /* @__PURE__ */ new Set(["tool"]);
8530
- var HERMES_IMPORT_PROJECTION_VERSION = "turn_blocks_v2";
8531
- var MESSAGE_COLUMNS = [
8532
- "id",
8533
- "session_id",
8534
- "role",
8535
- "content",
8536
- "tool_call_id",
8537
- "tool_calls",
8538
- "tool_name",
8539
- "timestamp",
8540
- "token_count",
8541
- "finish_reason",
8542
- "reasoning",
8543
- "reasoning_content",
8544
- "reasoning_details",
8545
- "codex_reasoning_items"
8546
- ];
8547
- async function syncHermesSessionsIntoConversations(paths, logger, options = {}) {
8548
- const maxImports = options.maxImports ?? MAX_IMPORTABLE_SESSIONS;
8549
- const store = new ConversationStore(paths);
8550
- const knownHermesSessions = await readKnownHermesSessions(store);
8551
- const profileNames = await discoverHermesProfileNames();
8552
- const result = {
8553
- scanned_profiles: profileNames.length,
8554
- scanned_sessions: 0,
8555
- eligible_sessions: 0,
8556
- imported_count: 0,
8557
- reprojected_count: 0,
8558
- skipped_existing: 0,
8559
- skipped_hidden: 0,
8560
- skipped_deleted: 0,
8561
- skipped_over_limit: 0,
8562
- errors: []
8808
+ var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
8809
+ var MAX_MEDIA_IMPORT_FAILURES = 20;
8810
+ var MAX_DELIVERY_FILES = 50;
8811
+ var DELIVERY_STAGING_SEGMENT = "delivery-staging";
8812
+ var SUPPORTED_DELIVERY_EXTENSIONS = /* @__PURE__ */ new Set([
8813
+ ".png",
8814
+ ".jpg",
8815
+ ".jpeg",
8816
+ ".gif",
8817
+ ".webp",
8818
+ ".heic",
8819
+ ".pdf",
8820
+ ".txt",
8821
+ ".log",
8822
+ ".md",
8823
+ ".markdown",
8824
+ ".json",
8825
+ ".jsonl",
8826
+ ".yaml",
8827
+ ".yml",
8828
+ ".toml",
8829
+ ".ini",
8830
+ ".xml",
8831
+ ".html",
8832
+ ".css",
8833
+ ".js",
8834
+ ".ts",
8835
+ ".jsx",
8836
+ ".tsx",
8837
+ ".dart",
8838
+ ".py",
8839
+ ".java",
8840
+ ".kt",
8841
+ ".swift",
8842
+ ".go",
8843
+ ".rs",
8844
+ ".rb",
8845
+ ".php",
8846
+ ".c",
8847
+ ".cc",
8848
+ ".cpp",
8849
+ ".h",
8850
+ ".hpp",
8851
+ ".cs",
8852
+ ".sql",
8853
+ ".csv",
8854
+ ".tsv",
8855
+ ".doc",
8856
+ ".docx",
8857
+ ".xls",
8858
+ ".xlsx",
8859
+ ".ppt",
8860
+ ".pptx",
8861
+ ".zip",
8862
+ ".rar",
8863
+ ".7z",
8864
+ ".tar",
8865
+ ".gz",
8866
+ ".mp4",
8867
+ ".mov",
8868
+ ".avi",
8869
+ ".mkv",
8870
+ ".webm",
8871
+ ".ogg",
8872
+ ".opus",
8873
+ ".mp3",
8874
+ ".wav",
8875
+ ".m4a"
8876
+ ]);
8877
+ function resolveDeliveryStagingTarget(paths, stagingDir) {
8878
+ const resolvedDir = path13.resolve(stagingDir);
8879
+ const relative = path13.relative(path13.resolve(paths.conversationsDir), resolvedDir);
8880
+ if (!relative || relative.startsWith("..") || path13.isAbsolute(relative)) {
8881
+ throw new LinkHttpError(
8882
+ 400,
8883
+ "delivery_staging_invalid",
8884
+ "delivery staging directory must be inside Hermes Link conversations"
8885
+ );
8886
+ }
8887
+ const segments = relative.split(path13.sep);
8888
+ if (segments.length !== 3 || segments[1] !== DELIVERY_STAGING_SEGMENT || !segments[0] || !segments[2]) {
8889
+ throw new LinkHttpError(
8890
+ 400,
8891
+ "delivery_staging_invalid",
8892
+ "delivery staging directory is invalid"
8893
+ );
8894
+ }
8895
+ return {
8896
+ conversationId: segments[0],
8897
+ runId: segments[2],
8898
+ stagingDir: resolvedDir
8563
8899
  };
8564
- const candidates = [];
8565
- for (const profileName of profileNames) {
8566
- const profileDir = resolveHermesProfileDir(profileName);
8567
- const dbPath = path13.join(profileDir, "state.db");
8568
- const sessions = await listProfileSessions(dbPath).catch((error) => {
8569
- result.errors.push({
8570
- profile: profileName,
8571
- message: error instanceof Error ? error.message : String(error)
8572
- });
8573
- return [];
8574
- });
8575
- result.scanned_sessions += sessions.length;
8900
+ }
8901
+ async function collectStagedDeliveryReferences(stagingDir) {
8902
+ const directoryStat = await lstat(stagingDir).catch((error) => {
8903
+ if (isNodeError9(error, "ENOENT")) {
8904
+ throw new LinkHttpError(
8905
+ 404,
8906
+ "delivery_staging_not_found",
8907
+ "delivery staging directory was not found"
8908
+ );
8909
+ }
8910
+ throw error;
8911
+ });
8912
+ if (!directoryStat.isDirectory()) {
8913
+ throw new LinkHttpError(
8914
+ 400,
8915
+ "delivery_staging_not_directory",
8916
+ "delivery staging path is not a directory"
8917
+ );
8918
+ }
8919
+ const entries = await readdir5(stagingDir, { withFileTypes: true });
8920
+ return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isSupportedDeliveryFilename(entry.name)).sort(
8921
+ (left, right) => left.name.localeCompare(right.name, "en", { numeric: true })
8922
+ ).slice(0, MAX_DELIVERY_FILES).map((entry) => {
8923
+ const sourcePath = path13.join(stagingDir, entry.name);
8924
+ const mime = inferMimeType(sourcePath);
8925
+ return {
8926
+ path: sourcePath,
8927
+ kind: mediaKindForMime(mime),
8928
+ mime
8929
+ };
8930
+ });
8931
+ }
8932
+ async function importMediaReferencesForMessage(deps, input) {
8933
+ const references = input.references.slice(0, input.maxReferences ?? MAX_DELIVERY_FILES);
8934
+ if (references.length === 0) {
8935
+ return emptyImportResult(input);
8936
+ }
8937
+ const snapshot = await deps.readSnapshot(input.conversationId);
8938
+ const assistant = snapshot.messages.find(
8939
+ (message) => message.id === input.messageId
8940
+ );
8941
+ if (!assistant) {
8942
+ return emptyImportResult(input);
8943
+ }
8944
+ const importedSourceKeys = readImportedMediaSourceKeys(assistant);
8945
+ const failedSourceKeys = readFailedMediaSourceKeys(assistant);
8946
+ const failureRecordsByKey = new Map(
8947
+ readMediaImportFailures(assistant).map((failure) => [failure.key, failure])
8948
+ );
8949
+ const importedParts = [];
8950
+ const newFailures = [];
8951
+ let skippedCount = 0;
8952
+ for (const reference of references) {
8953
+ let sourceKey;
8954
+ try {
8955
+ sourceKey = mediaSourceKey(reference.path);
8956
+ if (importedSourceKeys.has(sourceKey) || failedSourceKeys.has(sourceKey)) {
8957
+ skippedCount += 1;
8958
+ continue;
8959
+ }
8960
+ const blob = await writeBlobFromFile(deps, input.conversationId, reference);
8961
+ const part = {
8962
+ type: reference.kind ?? mediaKindForMime(blob.mime),
8963
+ blob: blob.id,
8964
+ mime: blob.mime,
8965
+ size: blob.size,
8966
+ filename: blob.filename,
8967
+ url: `/api/v1/conversations/${encodeURIComponent(input.conversationId)}/blobs/${encodeURIComponent(blob.id)}`
8968
+ };
8969
+ assistant.parts.push(part);
8970
+ assistant.attachments.push({
8971
+ blob_id: blob.id,
8972
+ mime: blob.mime,
8973
+ size: blob.size,
8974
+ filename: blob.filename,
8975
+ source: "hermes_output"
8976
+ });
8977
+ importedSourceKeys.add(sourceKey);
8978
+ importedParts.push(part);
8979
+ } catch (error) {
8980
+ if (sourceKey && !failedSourceKeys.has(sourceKey)) {
8981
+ const failure = describeMediaImportFailure(reference, sourceKey, error);
8982
+ failedSourceKeys.add(sourceKey);
8983
+ failureRecordsByKey.set(sourceKey, failure);
8984
+ newFailures.push(failure);
8985
+ }
8986
+ void deps.logger.warn("conversation_media_import_failed", {
8987
+ conversation_id: input.conversationId,
8988
+ run_id: input.runId,
8989
+ message_id: input.messageId,
8990
+ error: error instanceof Error ? error.message : String(error)
8991
+ });
8992
+ }
8993
+ }
8994
+ if (importedParts.length === 0 && newFailures.length === 0) {
8995
+ return {
8996
+ ...emptyImportResult(input),
8997
+ discovered_count: references.length,
8998
+ skipped_count: skippedCount
8999
+ };
9000
+ }
9001
+ assistant.hermes = {
9002
+ ...toRecord7(assistant.hermes),
9003
+ imported_media_source_keys: [...importedSourceKeys],
9004
+ media_import_failed_source_keys: [...failedSourceKeys],
9005
+ media_import_failures: [...failureRecordsByKey.values()].slice(
9006
+ -MAX_MEDIA_IMPORT_FAILURES
9007
+ )
9008
+ };
9009
+ assistant.updated_at = (/* @__PURE__ */ new Date()).toISOString();
9010
+ await deps.writeSnapshot(input.conversationId, snapshot);
9011
+ let lastEventSeq;
9012
+ if (importedParts.length > 0) {
9013
+ const event = await deps.appendEvent(input.conversationId, {
9014
+ type: "message.parts.created",
9015
+ message_id: input.messageId,
9016
+ run_id: input.runId,
9017
+ payload: { parts: importedParts }
9018
+ });
9019
+ lastEventSeq = event.seq;
9020
+ }
9021
+ return {
9022
+ conversation_id: input.conversationId,
9023
+ run_id: input.runId,
9024
+ message_id: input.messageId,
9025
+ discovered_count: references.length,
9026
+ imported_count: importedParts.length,
9027
+ skipped_count: skippedCount,
9028
+ failed_count: newFailures.length,
9029
+ parts: importedParts,
9030
+ ...lastEventSeq ? { last_event_seq: lastEventSeq } : {}
9031
+ };
9032
+ }
9033
+ function readMediaImportFailures(message) {
9034
+ const hermes = toRecord7(message.hermes);
9035
+ const failures = hermes.media_import_failures;
9036
+ if (!Array.isArray(failures)) {
9037
+ return [];
9038
+ }
9039
+ return failures.flatMap((item) => {
9040
+ const record = toRecord7(item);
9041
+ const key = readString8(record, "key");
9042
+ const filename = readString8(record, "filename");
9043
+ const reason = readString8(record, "reason");
9044
+ if (!key || !filename || !reason) {
9045
+ return [];
9046
+ }
9047
+ return [
9048
+ {
9049
+ key,
9050
+ filename,
9051
+ reason,
9052
+ ...readString8(record, "code") ? { code: readString8(record, "code") } : {}
9053
+ }
9054
+ ];
9055
+ });
9056
+ }
9057
+ function readFailedMediaSourceKeys(message) {
9058
+ const hermes = toRecord7(message.hermes);
9059
+ const keys = hermes.media_import_failed_source_keys;
9060
+ if (!Array.isArray(keys)) {
9061
+ return /* @__PURE__ */ new Set();
9062
+ }
9063
+ return new Set(
9064
+ keys.filter(
9065
+ (key) => typeof key === "string" && key.length > 0
9066
+ )
9067
+ );
9068
+ }
9069
+ function emptyImportResult(input) {
9070
+ return {
9071
+ conversation_id: input.conversationId,
9072
+ run_id: input.runId,
9073
+ message_id: input.messageId,
9074
+ discovered_count: 0,
9075
+ imported_count: 0,
9076
+ skipped_count: 0,
9077
+ failed_count: 0,
9078
+ parts: []
9079
+ };
9080
+ }
9081
+ async function writeBlobFromFile(deps, conversationId, source) {
9082
+ const sourcePath = resolveMediaSourcePath(source.path);
9083
+ const fileStat = await stat7(sourcePath).catch((error) => {
9084
+ if (isNodeError9(error, "ENOENT")) {
9085
+ throw new LinkHttpError(
9086
+ 404,
9087
+ "media_source_not_found",
9088
+ "Hermes output file was not found"
9089
+ );
9090
+ }
9091
+ throw error;
9092
+ });
9093
+ if (!fileStat.isFile()) {
9094
+ throw new LinkHttpError(
9095
+ 400,
9096
+ "media_source_not_file",
9097
+ "Hermes output media source is not a file"
9098
+ );
9099
+ }
9100
+ if (fileStat.size > MAX_IMPORTED_BLOB_BYTES) {
9101
+ throw new LinkHttpError(
9102
+ 413,
9103
+ "media_source_too_large",
9104
+ "Hermes output media source is too large"
9105
+ );
9106
+ }
9107
+ return deps.writeBlob(conversationId, {
9108
+ bytes: await readFile8(sourcePath),
9109
+ filename: path13.basename(sourcePath),
9110
+ mime: source.mime ?? inferMimeType(sourcePath)
9111
+ });
9112
+ }
9113
+ function describeMediaImportFailure(reference, sourceKey, error) {
9114
+ return {
9115
+ key: sourceKey,
9116
+ filename: sanitizeFilename(reference.path, "attachment"),
9117
+ reason: error instanceof Error ? error.message : String(error),
9118
+ ...isNodeError9(error) && error.code ? { code: error.code } : {}
9119
+ };
9120
+ }
9121
+ function isSupportedDeliveryFilename(filename) {
9122
+ return SUPPORTED_DELIVERY_EXTENSIONS.has(path13.extname(filename).toLowerCase());
9123
+ }
9124
+ function readString8(payload, key) {
9125
+ const value = payload[key];
9126
+ return typeof value === "string" && value.trim() ? value.trim() : null;
9127
+ }
9128
+ function toRecord7(value) {
9129
+ return typeof value === "object" && value !== null ? value : {};
9130
+ }
9131
+ function isNodeError9(error, code) {
9132
+ return typeof error === "object" && error !== null && "code" in error && (code === void 0 || error.code === code);
9133
+ }
9134
+
9135
+ // src/conversations/hermes-session-sync.ts
9136
+ var PROFILE_NAME_PATTERN3 = /^[a-zA-Z0-9._-]{1,64}$/u;
9137
+ var DEFAULT_PROFILE_NAME = "default";
9138
+ var MAX_IMPORTABLE_SESSIONS = 100;
9139
+ var HIDDEN_SESSION_SOURCES = /* @__PURE__ */ new Set(["tool"]);
9140
+ var HERMES_IMPORT_PROJECTION_VERSION = "turn_blocks_v3";
9141
+ var IMPORTED_MEDIA_PLACEHOLDER_RUN_ID = "imported_from_hermes";
9142
+ var MAX_IMPORTED_HERMES_MEDIA_BYTES = 100 * 1024 * 1024;
9143
+ var MESSAGE_COLUMNS = [
9144
+ "id",
9145
+ "session_id",
9146
+ "role",
9147
+ "content",
9148
+ "tool_call_id",
9149
+ "tool_calls",
9150
+ "tool_name",
9151
+ "timestamp",
9152
+ "token_count",
9153
+ "finish_reason",
9154
+ "reasoning",
9155
+ "reasoning_content",
9156
+ "reasoning_details",
9157
+ "codex_reasoning_items"
9158
+ ];
9159
+ async function syncHermesSessionsIntoConversations(paths, logger, options = {}) {
9160
+ const maxImports = options.maxImports ?? MAX_IMPORTABLE_SESSIONS;
9161
+ const store = new ConversationStore(paths);
9162
+ const knownHermesSessions = await readKnownHermesSessions(store);
9163
+ const profileNames = await discoverHermesProfileNames();
9164
+ const result = {
9165
+ scanned_profiles: profileNames.length,
9166
+ scanned_sessions: 0,
9167
+ eligible_sessions: 0,
9168
+ imported_count: 0,
9169
+ reprojected_count: 0,
9170
+ skipped_existing: 0,
9171
+ skipped_hidden: 0,
9172
+ skipped_deleted: 0,
9173
+ skipped_over_limit: 0,
9174
+ errors: []
9175
+ };
9176
+ const candidates = [];
9177
+ for (const profileName of profileNames) {
9178
+ const profileDir = resolveHermesProfileDir(profileName);
9179
+ const dbPath = path14.join(profileDir, "state.db");
9180
+ const sessions = await listProfileSessions(dbPath).catch((error) => {
9181
+ result.errors.push({
9182
+ profile: profileName,
9183
+ message: error instanceof Error ? error.message : String(error)
9184
+ });
9185
+ return [];
9186
+ });
9187
+ result.scanned_sessions += sessions.length;
8576
9188
  for (const session of sessions) {
8577
9189
  if (isDeletedSession(session)) {
8578
9190
  result.skipped_deleted += 1;
@@ -8599,6 +9211,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
8599
9211
  const reprojected = await reprojectExistingHermesConversation({
8600
9212
  paths,
8601
9213
  store,
9214
+ logger,
8602
9215
  candidate,
8603
9216
  conversationIds: knownHermesSessions.conversationIdsBySessionId.get(
8604
9217
  candidate.session.id
@@ -8618,6 +9231,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
8618
9231
  const imported = await importHermesSession({
8619
9232
  paths,
8620
9233
  store,
9234
+ logger,
8621
9235
  candidate,
8622
9236
  existingHermesSessionIds: knownHermesSessions.ids
8623
9237
  }).catch((error) => {
@@ -8639,7 +9253,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
8639
9253
  return result;
8640
9254
  }
8641
9255
  async function importHermesSession(input) {
8642
- const { paths, store, candidate, existingHermesSessionIds } = input;
9256
+ const { paths, store, logger, candidate, existingHermesSessionIds } = input;
8643
9257
  const profile = await resolveConversationProfileTarget(
8644
9258
  paths,
8645
9259
  candidate.profileName
@@ -8662,7 +9276,7 @@ async function importHermesSession(input) {
8662
9276
  }),
8663
9277
  runs: []
8664
9278
  };
8665
- const title = readString8(candidate.session, "title") ?? firstUserText(snapshot);
9279
+ const title = readString9(candidate.session, "title") ?? firstUserText(snapshot);
8666
9280
  const manifest = {
8667
9281
  id: conversationId,
8668
9282
  schema_version: 1,
@@ -8680,6 +9294,13 @@ async function importHermesSession(input) {
8680
9294
  last_event_seq: 0
8681
9295
  };
8682
9296
  await store.createConversation(manifest, snapshot);
9297
+ await hydrateImportedConversationMedia({
9298
+ paths,
9299
+ store,
9300
+ logger,
9301
+ conversationId
9302
+ });
9303
+ const hydratedSnapshot = await store.readSnapshot(conversationId);
8683
9304
  await store.appendEvent(conversationId, {
8684
9305
  type: "conversation.created",
8685
9306
  payload: {
@@ -8693,7 +9314,7 @@ async function importHermesSession(input) {
8693
9314
  }
8694
9315
  }
8695
9316
  });
8696
- for (const message of snapshot.messages) {
9317
+ for (const message of hydratedSnapshot.messages) {
8697
9318
  await store.appendEvent(conversationId, {
8698
9319
  type: "message.created",
8699
9320
  message_id: message.id,
@@ -8703,7 +9324,7 @@ async function importHermesSession(input) {
8703
9324
  }
8704
9325
  const stats = buildConversationStats(
8705
9326
  await store.readManifest(conversationId),
8706
- snapshot
9327
+ hydratedSnapshot
8707
9328
  );
8708
9329
  await store.writeManifest({
8709
9330
  ...await store.readManifest(conversationId),
@@ -8749,8 +9370,15 @@ async function reprojectExistingHermesConversation(input) {
8749
9370
  ...snapshot.messages.slice(prefix.endIndex)
8750
9371
  ]
8751
9372
  };
8752
- const stats = buildConversationStats(manifest, nextSnapshot);
8753
9373
  await input.store.writeSnapshot(conversationId, nextSnapshot);
9374
+ await hydrateImportedConversationMedia({
9375
+ paths: input.paths,
9376
+ store: input.store,
9377
+ logger: input.logger,
9378
+ conversationId
9379
+ });
9380
+ const hydratedSnapshot = await input.store.readSnapshot(conversationId);
9381
+ const stats = buildConversationStats(manifest, hydratedSnapshot);
8754
9382
  await input.store.writeManifest({ ...manifest, stats });
8755
9383
  await upsertConversationStats(
8756
9384
  input.paths,
@@ -8765,6 +9393,7 @@ function collectImportedHermesPrefix(snapshot) {
8765
9393
  const seen = /* @__PURE__ */ new Set();
8766
9394
  let needsProjectionVersion = false;
8767
9395
  let hasToolMetadata = false;
9396
+ let hasMediaDeliveryMarkup = false;
8768
9397
  let endIndex = 0;
8769
9398
  for (; endIndex < snapshot.messages.length; endIndex += 1) {
8770
9399
  const message = snapshot.messages[endIndex];
@@ -8782,6 +9411,9 @@ function collectImportedHermesPrefix(snapshot) {
8782
9411
  if (hasHermesToolMetadata(row)) {
8783
9412
  hasToolMetadata = true;
8784
9413
  }
9414
+ if (collectMediaTags(normalizeContent(row.content)).length > 0) {
9415
+ hasMediaDeliveryMarkup = true;
9416
+ }
8785
9417
  }
8786
9418
  for (const event of message.agent_events ?? []) {
8787
9419
  for (const row of readHermesRowsFromAgentEvent(event)) {
@@ -8789,6 +9421,9 @@ function collectImportedHermesPrefix(snapshot) {
8789
9421
  if (hasHermesToolMetadata(row)) {
8790
9422
  hasToolMetadata = true;
8791
9423
  }
9424
+ if (collectMediaTags(normalizeContent(row.content)).length > 0) {
9425
+ hasMediaDeliveryMarkup = true;
9426
+ }
8792
9427
  }
8793
9428
  }
8794
9429
  }
@@ -8798,7 +9433,7 @@ function collectImportedHermesPrefix(snapshot) {
8798
9433
  return {
8799
9434
  endIndex,
8800
9435
  messages: rows,
8801
- needsUpgrade: needsProjectionVersion && hasToolMetadata
9436
+ needsUpgrade: needsProjectionVersion && (hasToolMetadata || hasMediaDeliveryMarkup)
8802
9437
  };
8803
9438
  }
8804
9439
  function isHermesImportedMessage(message) {
@@ -8808,13 +9443,13 @@ function readHermesRowsFromAgentEvent(event) {
8808
9443
  if (event.raw?.format !== "hermes-message" && event.raw?.format !== "hermes-message-group") {
8809
9444
  return [];
8810
9445
  }
8811
- const payload = toRecord7(event.raw.payload);
8812
- const message = toRecord7(payload.message);
8813
- if (normalizeMessageRole(readString8(message, "role") ?? void 0) === "tool") {
9446
+ const payload = toRecord8(event.raw.payload);
9447
+ const message = toRecord8(payload.message);
9448
+ if (normalizeMessageRole(readString9(message, "role") ?? void 0) === "tool") {
8814
9449
  return [message];
8815
9450
  }
8816
9451
  return readHermesRawMessageRows(event.raw).filter(
8817
- (row) => Boolean(readString8(row, "role"))
9452
+ (row) => Boolean(readString9(row, "role"))
8818
9453
  );
8819
9454
  }
8820
9455
  function appendHermesRowOnce(rows, seen, row) {
@@ -8832,7 +9467,7 @@ function hermesRowKey(row, fallbackIndex) {
8832
9467
  return `fallback:${fallbackIndex}:${row.role ?? ""}:${row.timestamp ?? ""}:${normalizeContent(row.content)}`;
8833
9468
  }
8834
9469
  function hasHermesToolMetadata(row) {
8835
- return normalizeMessageRole(row.role) === "tool" || readHermesToolCalls(row).length > 0 || Boolean(readString8(row, "tool_call_id")) || Boolean(readString8(row, "tool_name"));
9470
+ return normalizeMessageRole(row.role) === "tool" || readHermesToolCalls(row).length > 0 || Boolean(readString9(row, "tool_call_id")) || Boolean(readString9(row, "tool_name"));
8836
9471
  }
8837
9472
  function toLinkMessages(input) {
8838
9473
  const linkMessages = [];
@@ -8950,8 +9585,8 @@ function toLinkMessages(input) {
8950
9585
  return linkMessages;
8951
9586
  }
8952
9587
  function consumePendingToolCall(input) {
8953
- const toolCallId = readString8(input.toolMessage, "tool_call_id");
8954
- const toolName = readString8(input.toolMessage, "tool_name");
9588
+ const toolCallId = readString9(input.toolMessage, "tool_call_id");
9589
+ const toolName = readString9(input.toolMessage, "tool_name");
8955
9590
  let pending = toolCallId ? input.toolCallsById.get(toolCallId) : void 0;
8956
9591
  if (!pending && toolName) {
8957
9592
  pending = input.pendingToolCalls.find(
@@ -9001,13 +9636,13 @@ function readHermesToolCalls(message) {
9001
9636
  );
9002
9637
  }
9003
9638
  function normalizeHermesToolCall(value) {
9004
- const record = toRecord7(value);
9639
+ const record = toRecord8(value);
9005
9640
  if (Object.keys(record).length === 0) {
9006
9641
  return null;
9007
9642
  }
9008
- const fn = toRecord7(record.function);
9009
- const id = readString8(record, "id") ?? readString8(record, "call_id") ?? readString8(record, "tool_call_id") ?? readString8(fn, "id") ?? void 0;
9010
- const name = readString8(fn, "name") ?? readString8(record, "name") ?? readString8(record, "tool_name") ?? readString8(record, "tool") ?? "tool";
9643
+ const fn = toRecord8(record.function);
9644
+ const id = readString9(record, "id") ?? readString9(record, "call_id") ?? readString9(record, "tool_call_id") ?? readString9(fn, "id") ?? void 0;
9645
+ const name = readString9(fn, "name") ?? readString9(record, "name") ?? readString9(record, "tool_name") ?? readString9(record, "tool") ?? "tool";
9011
9646
  const rawArguments = fn.arguments ?? record.arguments ?? record.args ?? record.input;
9012
9647
  return {
9013
9648
  ...id ? { id } : {},
@@ -9046,8 +9681,8 @@ function projectHermesToolCompletedEvent(input) {
9046
9681
  const createdAt = isoFromHermesTime(input.sourceMessage.timestamp) ?? new Date(Date.now() + input.index).toISOString();
9047
9682
  const output = normalizeContent(input.sourceMessage.content);
9048
9683
  const parsedOutput = parseJsonValue(output);
9049
- const toolCallId = readString8(input.sourceMessage, "tool_call_id") ?? input.pending?.toolCall.id;
9050
- const toolName = readString8(input.sourceMessage, "tool_name") ?? input.pending?.toolCall.name ?? "tool";
9684
+ const toolCallId = readString9(input.sourceMessage, "tool_call_id") ?? input.pending?.toolCall.id;
9685
+ const toolName = readString9(input.sourceMessage, "tool_name") ?? input.pending?.toolCall.name ?? "tool";
9051
9686
  return projectHermesAgentEvent({
9052
9687
  conversationId: input.conversationId,
9053
9688
  messageId: input.messageId,
@@ -9259,10 +9894,10 @@ function rememberKnownHermesConversation(map, sessionId, conversationId) {
9259
9894
  }
9260
9895
  async function discoverHermesProfileNames() {
9261
9896
  const names = /* @__PURE__ */ new Set([DEFAULT_PROFILE_NAME]);
9262
- const profilesDir = path13.join(os4.homedir(), ".hermes", "profiles");
9263
- const entries = await readdir5(profilesDir, { withFileTypes: true }).catch(
9897
+ const profilesDir = path14.join(os4.homedir(), ".hermes", "profiles");
9898
+ const entries = await readdir6(profilesDir, { withFileTypes: true }).catch(
9264
9899
  (error) => {
9265
- if (isNodeError9(error, "ENOENT")) {
9900
+ if (isNodeError10(error, "ENOENT")) {
9266
9901
  return [];
9267
9902
  }
9268
9903
  throw error;
@@ -9289,11 +9924,8 @@ async function listProfileSessions(dbPath) {
9289
9924
  }
9290
9925
  let db = null;
9291
9926
  try {
9292
- const { DatabaseSync } = nodeRequire3(
9293
- "node:sqlite"
9294
- );
9295
- db = new DatabaseSync(dbPath, {
9296
- readOnly: true,
9927
+ db = openSqliteDatabase(dbPath, {
9928
+ readonly: true,
9297
9929
  timeout: 1e3
9298
9930
  });
9299
9931
  const sessionColumns = readTableColumns(db, "sessions");
@@ -9325,683 +9957,458 @@ function appendHermesRawMessage(message, row) {
9325
9957
  format: "hermes-message",
9326
9958
  payload: row
9327
9959
  } : {
9328
- format: "hermes-message-group",
9329
- payload: { messages: [...rows, row] }
9330
- };
9331
- }
9332
- function readHermesRawMessageRows(raw) {
9333
- if (!raw) {
9334
- return [];
9335
- }
9336
- if (raw.format === "hermes-message-group") {
9337
- const payload = toRecord7(raw.payload);
9338
- return Array.isArray(payload.messages) ? payload.messages.filter(
9339
- (item) => typeof item === "object" && item !== null
9340
- ) : [];
9341
- }
9342
- if (raw.format === "hermes-message") {
9343
- return typeof raw.payload === "object" && raw.payload !== null ? [raw.payload] : [];
9344
- }
9345
- return [];
9346
- }
9347
- function rememberHermesMessageId(message, row) {
9348
- if (row.id === void 0 || row.id === null) {
9349
- return;
9350
- }
9351
- const existing = Array.isArray(message.hermes?.message_ids) ? message.hermes.message_ids : message.hermes?.message_id === void 0 ? [] : [message.hermes.message_id];
9352
- const id = row.id;
9353
- message.hermes = {
9354
- ...message.hermes ?? {},
9355
- message_ids: existing.includes(id) ? existing : [...existing, id]
9356
- };
9357
- }
9358
- function joinImportedText(left, right) {
9359
- if (!left) {
9360
- return right;
9361
- }
9362
- if (!right) {
9363
- return left;
9364
- }
9365
- if (/\s$/u.test(left) || /^\s/u.test(right)) {
9366
- return `${left}${right}`;
9367
- }
9368
- return `${left}
9369
-
9370
- ${right}`;
9371
- }
9372
- function projectCompressionTips(rows) {
9373
- const byId = /* @__PURE__ */ new Map();
9374
- const childrenByParent = /* @__PURE__ */ new Map();
9375
- for (const row of rows) {
9376
- const id = readString8(row, "id");
9377
- if (!id) {
9378
- continue;
9379
- }
9380
- byId.set(id, row);
9381
- const parentId = readString8(row, "parent_session_id");
9382
- if (parentId) {
9383
- const children = childrenByParent.get(parentId) ?? [];
9384
- children.push(row);
9385
- childrenByParent.set(parentId, children);
9386
- }
9387
- }
9388
- const projected = [];
9389
- for (const row of rows) {
9390
- const id = readString8(row, "id");
9391
- if (!id || readString8(row, "parent_session_id")) {
9392
- continue;
9393
- }
9394
- let tip = row;
9395
- const visited = /* @__PURE__ */ new Set([id]);
9396
- while (readString8(tip, "end_reason") === "compression") {
9397
- const tipId2 = readString8(tip, "id");
9398
- if (!tipId2) {
9399
- break;
9400
- }
9401
- const next = (childrenByParent.get(tipId2) ?? []).filter((child) => readString8(child, "id")).sort(
9402
- (left, right) => (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
9403
- )[0];
9404
- const nextId = next ? readString8(next, "id") : null;
9405
- if (!next || !nextId || visited.has(nextId)) {
9406
- break;
9407
- }
9408
- tip = next;
9409
- visited.add(nextId);
9410
- }
9411
- const tipId = readString8(tip, "id");
9412
- if (tipId) {
9413
- projected.push({
9414
- ...tip,
9415
- id: tipId,
9416
- _lineage_root_id: id,
9417
- started_at: readNumber2(row.started_at) ?? readNumber2(tip.started_at)
9418
- });
9419
- }
9420
- }
9421
- return projected;
9422
- }
9423
- async function readHermesSessionMessages(candidate) {
9424
- const [dbMessages, jsonlMessages] = await Promise.all([
9425
- readStateDbMessages(candidate.dbPath, candidate.session.id),
9426
- readJsonlMessages(candidate.profileName, candidate.session.id)
9427
- ]);
9428
- return jsonlMessages.length > dbMessages.length ? jsonlMessages : dbMessages;
9429
- }
9430
- async function readStateDbMessages(dbPath, sessionId) {
9431
- if (!await isFile(dbPath)) {
9432
- return [];
9433
- }
9434
- let db = null;
9435
- try {
9436
- const { DatabaseSync } = nodeRequire3(
9437
- "node:sqlite"
9438
- );
9439
- db = new DatabaseSync(dbPath, {
9440
- readOnly: true,
9441
- timeout: 1e3
9442
- });
9443
- const columns = readTableColumns(db, "messages");
9444
- if (!columns.has("session_id") || !columns.has("role")) {
9445
- return [];
9446
- }
9447
- const selectColumns = MESSAGE_COLUMNS.map(
9448
- (column) => columns.has(column) ? quoteIdentifier(column) : `NULL AS ${column}`
9449
- ).join(", ");
9450
- return db.prepare(
9451
- `
9452
- SELECT ${selectColumns}
9453
- FROM messages
9454
- WHERE session_id = ?
9455
- ORDER BY timestamp, id
9456
- `
9457
- ).all(sessionId);
9458
- } catch {
9459
- return [];
9460
- } finally {
9461
- db?.close();
9462
- }
9463
- }
9464
- async function readJsonlMessages(profileName, sessionId) {
9465
- if (!/^[A-Za-z0-9._:-]{1,160}$/u.test(sessionId)) {
9466
- return [];
9467
- }
9468
- const profileDir = resolveHermesProfileDir(profileName);
9469
- const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path13.join(profileDir, "sessions"));
9470
- const transcriptPath = path13.join(sessionsDir, `${sessionId}.jsonl`);
9471
- const raw = await readFile8(transcriptPath, "utf8").catch((error) => {
9472
- if (isNodeError9(error, "ENOENT")) {
9473
- return "";
9474
- }
9475
- throw error;
9476
- });
9477
- if (!raw.trim()) {
9478
- return [];
9479
- }
9480
- const rows = [];
9481
- for (const line of raw.split(/\r?\n/u)) {
9482
- if (!line.trim()) {
9483
- continue;
9484
- }
9485
- try {
9486
- const parsed = JSON.parse(line);
9487
- const normalized = normalizeJsonlMessage(parsed);
9488
- if (normalized) {
9489
- rows.push(normalized);
9490
- }
9491
- } catch {
9492
- continue;
9493
- }
9494
- }
9495
- return rows;
9496
- }
9497
- function normalizeJsonlMessage(row) {
9498
- const role = readString8(row, "role");
9499
- if (!role) {
9500
- return null;
9501
- }
9502
- const content = normalizeContent(row.content);
9503
- const timestamp = readNumber2(row.timestamp) ?? readNumber2(row.created_at) ?? readNumber2(row.createdAt);
9504
- return {
9505
- ...row,
9506
- role,
9507
- content,
9508
- timestamp: timestamp ?? void 0
9509
- };
9510
- }
9511
- function toLinkMessage(input) {
9512
- const role = normalizeMessageRole(input.message.role);
9513
- const text = normalizeContent(input.message.content);
9514
- const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
9515
- return {
9516
- id: `msg_${randomUUID6().replaceAll("-", "")}`,
9517
- schema_version: 1,
9518
- conversation_id: input.conversationId,
9519
- role,
9520
- status: "completed",
9521
- created_at: createdAt,
9522
- updated_at: createdAt,
9523
- sender: senderForRole({
9524
- role,
9525
- profileName: input.profileName,
9526
- profileUid: input.profileUid,
9527
- profileDisplayName: input.profileDisplayName
9528
- }),
9529
- parts: text ? [{ type: "text", text }] : [],
9530
- attachments: [],
9531
- hermes: {
9532
- session_id: input.sessionId,
9533
- message_id: input.message.id,
9534
- imported_from: "hermes",
9535
- import_projection: HERMES_IMPORT_PROJECTION_VERSION
9536
- },
9537
- raw: {
9538
- format: "hermes-message",
9539
- payload: input.message
9540
- }
9541
- };
9542
- }
9543
- function senderForRole(input) {
9544
- switch (input.role) {
9545
- case "user":
9546
- return { id: "hermes_user", type: "human", display_name: "Me" };
9547
- case "assistant":
9548
- return {
9549
- id: `agent_${input.profileName}`,
9550
- type: "agent",
9551
- display_name: input.profileDisplayName,
9552
- profile_uid: input.profileUid,
9553
- profile: input.profileName
9554
- };
9555
- case "tool":
9556
- return { id: "hermes_tool", type: "tool", display_name: "Tool" };
9557
- case "system":
9558
- return { id: "hermes_system", type: "system", display_name: "System" };
9559
- }
9560
- }
9561
- function firstUserText(snapshot) {
9562
- return snapshot.messages.find((message) => message.role === "user")?.parts.find((part) => part.type === "text")?.text?.slice(0, 80);
9563
- }
9564
- function normalizeTitle(value) {
9565
- const normalized = value?.replace(/\s+/gu, " ").trim();
9566
- return normalized || DEFAULT_CONVERSATION_TITLE;
9567
- }
9568
- function normalizeMessageRole(value) {
9569
- switch (value?.trim().toLowerCase()) {
9570
- case "user":
9571
- return "user";
9572
- case "assistant":
9573
- return "assistant";
9574
- case "tool":
9575
- return "tool";
9576
- case "system":
9577
- return "system";
9578
- default:
9579
- return "system";
9580
- }
9581
- }
9582
- function normalizeContent(value) {
9583
- if (typeof value === "string") {
9584
- return value;
9585
- }
9586
- if (Array.isArray(value)) {
9587
- return value.map((item) => {
9588
- if (typeof item === "string") {
9589
- return item;
9590
- }
9591
- if (typeof item === "object" && item !== null) {
9592
- return readString8(item, "text") ?? "";
9593
- }
9594
- return "";
9595
- }).filter(Boolean).join("");
9596
- }
9597
- return "";
9960
+ format: "hermes-message-group",
9961
+ payload: { messages: [...rows, row] }
9962
+ };
9598
9963
  }
9599
- function parseJsonValue(value) {
9600
- if (typeof value !== "string") {
9601
- return void 0;
9964
+ function readHermesRawMessageRows(raw) {
9965
+ if (!raw) {
9966
+ return [];
9602
9967
  }
9603
- const trimmed = value.trim();
9604
- if (!trimmed) {
9605
- return void 0;
9968
+ if (raw.format === "hermes-message-group") {
9969
+ const payload = toRecord8(raw.payload);
9970
+ return Array.isArray(payload.messages) ? payload.messages.filter(
9971
+ (item) => typeof item === "object" && item !== null
9972
+ ) : [];
9606
9973
  }
9607
- try {
9608
- return JSON.parse(trimmed);
9609
- } catch {
9610
- return void 0;
9974
+ if (raw.format === "hermes-message") {
9975
+ return typeof raw.payload === "object" && raw.payload !== null ? [raw.payload] : [];
9611
9976
  }
9977
+ return [];
9612
9978
  }
9613
- function toRecord7(value) {
9614
- return typeof value === "object" && value !== null ? value : {};
9615
- }
9616
- function isDeletedSession(session) {
9617
- return readBoolean(session.deleted) || readBoolean(session.is_deleted) || Boolean(readString8(session, "deleted_at")) || ["deleted", "removed"].includes(readString8(session, "status") ?? "");
9618
- }
9619
- function isHiddenSession(session) {
9620
- const source = readString8(session, "source")?.toLowerCase();
9621
- const status = readString8(session, "status")?.toLowerCase();
9622
- const visibility = readString8(session, "visibility")?.toLowerCase();
9623
- return Boolean(source && HIDDEN_SESSION_SOURCES.has(source)) || readBoolean(session.hidden) || readBoolean(session.archived) || Boolean(readString8(session, "archived_at")) || status === "hidden" || status === "archived" || visibility === "hidden" || visibility === "hide";
9624
- }
9625
- function readTableColumns(db, tableName) {
9626
- try {
9627
- const rows = db.prepare(`PRAGMA table_info(${quoteIdentifier(tableName)})`).all();
9628
- return new Set(
9629
- rows.map((row) => typeof row.name === "string" ? row.name : "").filter(Boolean)
9630
- );
9631
- } catch {
9632
- return /* @__PURE__ */ new Set();
9979
+ function rememberHermesMessageId(message, row) {
9980
+ if (row.id === void 0 || row.id === null) {
9981
+ return;
9633
9982
  }
9983
+ const existing = Array.isArray(message.hermes?.message_ids) ? message.hermes.message_ids : message.hermes?.message_id === void 0 ? [] : [message.hermes.message_id];
9984
+ const id = row.id;
9985
+ message.hermes = {
9986
+ ...message.hermes ?? {},
9987
+ message_ids: existing.includes(id) ? existing : [...existing, id]
9988
+ };
9634
9989
  }
9635
- function quoteIdentifier(value) {
9636
- return `"${value.replaceAll('"', '""')}"`;
9637
- }
9638
- async function isFile(filePath) {
9639
- return stat7(filePath).then((value) => value.isFile()).catch((error) => {
9640
- if (isNodeError9(error, "ENOENT")) {
9641
- return false;
9642
- }
9643
- throw error;
9644
- });
9645
- }
9646
- function createConversationId() {
9647
- return `conv_${randomUUID6().replaceAll("-", "")}`;
9648
- }
9649
- function isoFromHermesTime(value) {
9650
- const numeric = readNumber2(value);
9651
- if (!numeric || numeric <= 0) {
9652
- return void 0;
9990
+ function joinImportedText(left, right) {
9991
+ if (!left) {
9992
+ return right;
9653
9993
  }
9654
- const millis = numeric > 1e10 ? numeric : numeric * 1e3;
9655
- return new Date(millis).toISOString();
9656
- }
9657
- function readString8(payload, key) {
9658
- const value = payload[key];
9659
- return typeof value === "string" && value.trim() ? value.trim() : null;
9660
- }
9661
- function readNumber2(value) {
9662
- return typeof value === "number" && Number.isFinite(value) ? value : null;
9994
+ if (!right) {
9995
+ return left;
9996
+ }
9997
+ if (/\s$/u.test(left) || /^\s/u.test(right)) {
9998
+ return `${left}${right}`;
9999
+ }
10000
+ return `${left}
10001
+
10002
+ ${right}`;
9663
10003
  }
9664
- function readBoolean(value) {
9665
- if (value === true || value === 1) {
9666
- return true;
10004
+ function projectCompressionTips(rows) {
10005
+ const byId = /* @__PURE__ */ new Map();
10006
+ const childrenByParent = /* @__PURE__ */ new Map();
10007
+ for (const row of rows) {
10008
+ const id = readString9(row, "id");
10009
+ if (!id) {
10010
+ continue;
10011
+ }
10012
+ byId.set(id, row);
10013
+ const parentId = readString9(row, "parent_session_id");
10014
+ if (parentId) {
10015
+ const children = childrenByParent.get(parentId) ?? [];
10016
+ children.push(row);
10017
+ childrenByParent.set(parentId, children);
10018
+ }
9667
10019
  }
9668
- if (typeof value === "string") {
9669
- return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
10020
+ const projected = [];
10021
+ for (const row of rows) {
10022
+ const id = readString9(row, "id");
10023
+ if (!id || readString9(row, "parent_session_id")) {
10024
+ continue;
10025
+ }
10026
+ let tip = row;
10027
+ const visited = /* @__PURE__ */ new Set([id]);
10028
+ while (readString9(tip, "end_reason") === "compression") {
10029
+ const tipId2 = readString9(tip, "id");
10030
+ if (!tipId2) {
10031
+ break;
10032
+ }
10033
+ const next = (childrenByParent.get(tipId2) ?? []).filter((child) => readString9(child, "id")).sort(
10034
+ (left, right) => (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
10035
+ )[0];
10036
+ const nextId = next ? readString9(next, "id") : null;
10037
+ if (!next || !nextId || visited.has(nextId)) {
10038
+ break;
10039
+ }
10040
+ tip = next;
10041
+ visited.add(nextId);
10042
+ }
10043
+ const tipId = readString9(tip, "id");
10044
+ if (tipId) {
10045
+ projected.push({
10046
+ ...tip,
10047
+ id: tipId,
10048
+ _lineage_root_id: id,
10049
+ started_at: readNumber2(row.started_at) ?? readNumber2(tip.started_at)
10050
+ });
10051
+ }
9670
10052
  }
9671
- return false;
10053
+ return projected;
9672
10054
  }
9673
- function isNodeError9(error, code) {
9674
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
10055
+ async function readHermesSessionMessages(candidate) {
10056
+ const [dbMessages, jsonlMessages] = await Promise.all([
10057
+ readStateDbMessages(candidate.dbPath, candidate.session.id),
10058
+ readJsonlMessages(candidate.profileName, candidate.session.id)
10059
+ ]);
10060
+ return jsonlMessages.length > dbMessages.length ? jsonlMessages : dbMessages;
9675
10061
  }
9676
-
9677
- // src/conversations/delivery-import.ts
9678
- import { lstat, readFile as readFile9, readdir as readdir6, stat as stat8 } from "fs/promises";
9679
- import path14 from "path";
9680
- var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
9681
- var MAX_MEDIA_IMPORT_FAILURES = 20;
9682
- var MAX_DELIVERY_FILES = 50;
9683
- var DELIVERY_STAGING_SEGMENT = "delivery-staging";
9684
- var SUPPORTED_DELIVERY_EXTENSIONS = /* @__PURE__ */ new Set([
9685
- ".png",
9686
- ".jpg",
9687
- ".jpeg",
9688
- ".gif",
9689
- ".webp",
9690
- ".heic",
9691
- ".pdf",
9692
- ".txt",
9693
- ".log",
9694
- ".md",
9695
- ".markdown",
9696
- ".json",
9697
- ".jsonl",
9698
- ".yaml",
9699
- ".yml",
9700
- ".toml",
9701
- ".ini",
9702
- ".xml",
9703
- ".html",
9704
- ".css",
9705
- ".js",
9706
- ".ts",
9707
- ".jsx",
9708
- ".tsx",
9709
- ".dart",
9710
- ".py",
9711
- ".java",
9712
- ".kt",
9713
- ".swift",
9714
- ".go",
9715
- ".rs",
9716
- ".rb",
9717
- ".php",
9718
- ".c",
9719
- ".cc",
9720
- ".cpp",
9721
- ".h",
9722
- ".hpp",
9723
- ".cs",
9724
- ".sql",
9725
- ".csv",
9726
- ".tsv",
9727
- ".doc",
9728
- ".docx",
9729
- ".xls",
9730
- ".xlsx",
9731
- ".ppt",
9732
- ".pptx",
9733
- ".zip",
9734
- ".rar",
9735
- ".7z",
9736
- ".tar",
9737
- ".gz",
9738
- ".mp4",
9739
- ".mov",
9740
- ".avi",
9741
- ".mkv",
9742
- ".webm",
9743
- ".ogg",
9744
- ".opus",
9745
- ".mp3",
9746
- ".wav",
9747
- ".m4a"
9748
- ]);
9749
- function resolveDeliveryStagingTarget(paths, stagingDir) {
9750
- const resolvedDir = path14.resolve(stagingDir);
9751
- const relative = path14.relative(path14.resolve(paths.conversationsDir), resolvedDir);
9752
- if (!relative || relative.startsWith("..") || path14.isAbsolute(relative)) {
9753
- throw new LinkHttpError(
9754
- 400,
9755
- "delivery_staging_invalid",
9756
- "delivery staging directory must be inside Hermes Link conversations"
9757
- );
10062
+ async function readStateDbMessages(dbPath, sessionId) {
10063
+ if (!await isFile(dbPath)) {
10064
+ return [];
9758
10065
  }
9759
- const segments = relative.split(path14.sep);
9760
- if (segments.length !== 3 || segments[1] !== DELIVERY_STAGING_SEGMENT || !segments[0] || !segments[2]) {
9761
- throw new LinkHttpError(
9762
- 400,
9763
- "delivery_staging_invalid",
9764
- "delivery staging directory is invalid"
9765
- );
10066
+ let db = null;
10067
+ try {
10068
+ db = openSqliteDatabase(dbPath, {
10069
+ readonly: true,
10070
+ timeout: 1e3
10071
+ });
10072
+ const columns = readTableColumns(db, "messages");
10073
+ if (!columns.has("session_id") || !columns.has("role")) {
10074
+ return [];
10075
+ }
10076
+ const selectColumns = MESSAGE_COLUMNS.map(
10077
+ (column) => columns.has(column) ? quoteIdentifier(column) : `NULL AS ${column}`
10078
+ ).join(", ");
10079
+ return db.prepare(
10080
+ `
10081
+ SELECT ${selectColumns}
10082
+ FROM messages
10083
+ WHERE session_id = ?
10084
+ ORDER BY timestamp, id
10085
+ `
10086
+ ).all(sessionId);
10087
+ } catch {
10088
+ return [];
10089
+ } finally {
10090
+ db?.close();
9766
10091
  }
9767
- return {
9768
- conversationId: segments[0],
9769
- runId: segments[2],
9770
- stagingDir: resolvedDir
9771
- };
9772
- }
9773
- async function collectStagedDeliveryReferences(stagingDir) {
9774
- const directoryStat = await lstat(stagingDir).catch((error) => {
10092
+ }
10093
+ async function readJsonlMessages(profileName, sessionId) {
10094
+ if (!/^[A-Za-z0-9._:-]{1,160}$/u.test(sessionId)) {
10095
+ return [];
10096
+ }
10097
+ const profileDir = resolveHermesProfileDir(profileName);
10098
+ const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path14.join(profileDir, "sessions"));
10099
+ const transcriptPath = path14.join(sessionsDir, `${sessionId}.jsonl`);
10100
+ const raw = await readFile9(transcriptPath, "utf8").catch((error) => {
9775
10101
  if (isNodeError10(error, "ENOENT")) {
9776
- throw new LinkHttpError(
9777
- 404,
9778
- "delivery_staging_not_found",
9779
- "delivery staging directory was not found"
9780
- );
10102
+ return "";
9781
10103
  }
9782
10104
  throw error;
9783
10105
  });
9784
- if (!directoryStat.isDirectory()) {
9785
- throw new LinkHttpError(
9786
- 400,
9787
- "delivery_staging_not_directory",
9788
- "delivery staging path is not a directory"
9789
- );
10106
+ if (!raw.trim()) {
10107
+ return [];
9790
10108
  }
9791
- const entries = await readdir6(stagingDir, { withFileTypes: true });
9792
- return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isSupportedDeliveryFilename(entry.name)).sort(
9793
- (left, right) => left.name.localeCompare(right.name, "en", { numeric: true })
9794
- ).slice(0, MAX_DELIVERY_FILES).map((entry) => {
9795
- const sourcePath = path14.join(stagingDir, entry.name);
9796
- const mime = inferMimeType(sourcePath);
9797
- return {
9798
- path: sourcePath,
9799
- kind: mediaKindForMime(mime),
9800
- mime
9801
- };
9802
- });
10109
+ const rows = [];
10110
+ for (const line of raw.split(/\r?\n/u)) {
10111
+ if (!line.trim()) {
10112
+ continue;
10113
+ }
10114
+ try {
10115
+ const parsed = JSON.parse(line);
10116
+ const normalized = normalizeJsonlMessage(parsed);
10117
+ if (normalized) {
10118
+ rows.push(normalized);
10119
+ }
10120
+ } catch {
10121
+ continue;
10122
+ }
10123
+ }
10124
+ return rows;
9803
10125
  }
9804
- async function importMediaReferencesForMessage(deps, input) {
9805
- const references = input.references.slice(0, input.maxReferences ?? MAX_DELIVERY_FILES);
9806
- if (references.length === 0) {
9807
- return emptyImportResult(input);
10126
+ function normalizeJsonlMessage(row) {
10127
+ const role = readString9(row, "role");
10128
+ if (!role) {
10129
+ return null;
9808
10130
  }
9809
- const snapshot = await deps.readSnapshot(input.conversationId);
9810
- const assistant = snapshot.messages.find(
9811
- (message) => message.id === input.messageId
9812
- );
9813
- if (!assistant) {
9814
- return emptyImportResult(input);
10131
+ const content = normalizeContent(row.content);
10132
+ const timestamp = readNumber2(row.timestamp) ?? readNumber2(row.created_at) ?? readNumber2(row.createdAt);
10133
+ return {
10134
+ ...row,
10135
+ role,
10136
+ content,
10137
+ timestamp: timestamp ?? void 0
10138
+ };
10139
+ }
10140
+ async function hydrateImportedConversationMedia(input) {
10141
+ const snapshot = await input.store.readSnapshot(input.conversationId);
10142
+ const imports = snapshot.messages.map((message) => ({
10143
+ messageId: message.id,
10144
+ references: collectMessageMediaReferences(message)
10145
+ })).filter((item) => item.references.length > 0);
10146
+ if (imports.length === 0) {
10147
+ return;
9815
10148
  }
9816
- const importedSourceKeys = readImportedMediaSourceKeys(assistant);
9817
- const failedSourceKeys = readFailedMediaSourceKeys(assistant);
9818
- const failureRecordsByKey = new Map(
9819
- readMediaImportFailures(assistant).map((failure) => [failure.key, failure])
9820
- );
9821
- const importedParts = [];
9822
- const newFailures = [];
9823
- let skippedCount = 0;
9824
- for (const reference of references) {
9825
- let sourceKey;
9826
- try {
9827
- sourceKey = mediaSourceKey(reference.path);
9828
- if (importedSourceKeys.has(sourceKey) || failedSourceKeys.has(sourceKey)) {
9829
- skippedCount += 1;
9830
- continue;
9831
- }
9832
- const blob = await writeBlobFromFile(deps, input.conversationId, reference);
9833
- const part = {
9834
- type: reference.kind ?? mediaKindForMime(blob.mime),
9835
- blob: blob.id,
9836
- mime: blob.mime,
9837
- size: blob.size,
9838
- filename: blob.filename,
9839
- url: `/api/v1/conversations/${encodeURIComponent(input.conversationId)}/blobs/${encodeURIComponent(blob.id)}`
9840
- };
9841
- assistant.parts.push(part);
9842
- assistant.attachments.push({
9843
- blob_id: blob.id,
9844
- mime: blob.mime,
9845
- size: blob.size,
9846
- filename: blob.filename,
9847
- source: "hermes_output"
9848
- });
9849
- importedSourceKeys.add(sourceKey);
9850
- importedParts.push(part);
9851
- } catch (error) {
9852
- if (sourceKey && !failedSourceKeys.has(sourceKey)) {
9853
- const failure = describeMediaImportFailure(reference, sourceKey, error);
9854
- failedSourceKeys.add(sourceKey);
9855
- failureRecordsByKey.set(sourceKey, failure);
9856
- newFailures.push(failure);
10149
+ const outcomes = /* @__PURE__ */ new Map();
10150
+ for (const item of imports) {
10151
+ const result = await importMediaReferencesForMessage(
10152
+ {
10153
+ logger: input.logger,
10154
+ readSnapshot: (conversationId) => input.store.readSnapshot(conversationId),
10155
+ writeSnapshot: (conversationId, nextSnapshot2) => input.store.writeSnapshot(conversationId, nextSnapshot2),
10156
+ appendEvent: async (conversationId, event) => ({
10157
+ seq: 0,
10158
+ conversation_id: conversationId,
10159
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
10160
+ ...event
10161
+ }),
10162
+ writeBlob: (conversationId, blob) => writeConversationBlob(input.paths, conversationId, blob, {
10163
+ maxBytes: MAX_IMPORTED_HERMES_MEDIA_BYTES
10164
+ })
10165
+ },
10166
+ {
10167
+ conversationId: input.conversationId,
10168
+ runId: IMPORTED_MEDIA_PLACEHOLDER_RUN_ID,
10169
+ messageId: item.messageId,
10170
+ references: item.references
9857
10171
  }
9858
- void deps.logger.warn("conversation_media_import_failed", {
9859
- conversation_id: input.conversationId,
9860
- run_id: input.runId,
9861
- message_id: input.messageId,
9862
- error: error instanceof Error ? error.message : String(error)
10172
+ );
10173
+ if (result.imported_count > 0 || result.failed_count > 0) {
10174
+ outcomes.set(item.messageId, {
10175
+ imported: result.imported_count > 0,
10176
+ failed: result.failed_count > 0
9863
10177
  });
9864
10178
  }
9865
10179
  }
9866
- if (importedParts.length === 0 && newFailures.length === 0) {
9867
- return {
9868
- ...emptyImportResult(input),
9869
- discovered_count: references.length,
9870
- skipped_count: skippedCount
9871
- };
10180
+ if (outcomes.size === 0) {
10181
+ return;
9872
10182
  }
9873
- assistant.hermes = {
9874
- ...toRecord8(assistant.hermes),
9875
- imported_media_source_keys: [...importedSourceKeys],
9876
- media_import_failed_source_keys: [...failedSourceKeys],
9877
- media_import_failures: [...failureRecordsByKey.values()].slice(
9878
- -MAX_MEDIA_IMPORT_FAILURES
9879
- )
10183
+ const nextSnapshot = await input.store.readSnapshot(input.conversationId);
10184
+ let changed = false;
10185
+ for (const message of nextSnapshot.messages) {
10186
+ const outcome = outcomes.get(message.id);
10187
+ if (!outcome) {
10188
+ continue;
10189
+ }
10190
+ if (outcome.imported) {
10191
+ cleanMessageTextParts(message);
10192
+ changed = true;
10193
+ }
10194
+ if (outcome.failed) {
10195
+ changed = appendImportedMediaImportFailureNotice(message) || changed;
10196
+ }
10197
+ }
10198
+ if (changed) {
10199
+ await input.store.writeSnapshot(input.conversationId, nextSnapshot);
10200
+ }
10201
+ }
10202
+ function collectMessageMediaReferences(message) {
10203
+ return message.parts.flatMap(
10204
+ (part) => part.type === "text" && part.text ? collectMediaTags(part.text) : []
10205
+ );
10206
+ }
10207
+ function appendImportedMediaImportFailureNotice(message) {
10208
+ const hermes = toRecord8(message.hermes);
10209
+ if (hermes.media_import_failure_notice_appended === true) {
10210
+ return false;
10211
+ }
10212
+ const failures = readMediaImportFailures(message);
10213
+ if (failures.length === 0) {
10214
+ return false;
10215
+ }
10216
+ const notice = formatImportedMediaImportFailureNotice(failures);
10217
+ const textPart = message.parts.find((part) => part.type === "text");
10218
+ if (textPart) {
10219
+ const currentText = textPart.text ?? "";
10220
+ const separator = currentText.trim().length > 0 ? "\n\n" : "";
10221
+ textPart.text = `${currentText.trimEnd()}${separator}${notice}`;
10222
+ } else {
10223
+ message.parts.unshift({ type: "text", text: notice });
10224
+ }
10225
+ message.hermes = {
10226
+ ...hermes,
10227
+ media_import_failure_notice_appended: true
9880
10228
  };
9881
- assistant.updated_at = (/* @__PURE__ */ new Date()).toISOString();
9882
- await deps.writeSnapshot(input.conversationId, snapshot);
9883
- let lastEventSeq;
9884
- if (importedParts.length > 0) {
9885
- const event = await deps.appendEvent(input.conversationId, {
9886
- type: "message.parts.created",
9887
- message_id: input.messageId,
9888
- run_id: input.runId,
9889
- payload: { parts: importedParts }
9890
- });
9891
- lastEventSeq = event.seq;
10229
+ return true;
10230
+ }
10231
+ function formatImportedMediaImportFailureNotice(failures) {
10232
+ const filenames = failures.map((failure) => failure.filename);
10233
+ const target = filenames.length === 1 ? `\u6587\u4EF6\u201C${filenames[0]}\u201D` : `${filenames.length} \u4E2A\u6587\u4EF6\uFF08${formatImportedFilenameList(filenames)}\uFF09`;
10234
+ const permissionDenied = failures.some((failure) => {
10235
+ const code = failure.code?.toUpperCase();
10236
+ return code === "EPERM" || code === "EACCES" || /operation not permitted|permission denied/iu.test(failure.reason);
10237
+ });
10238
+ if (permissionDenied) {
10239
+ return `${target}\u6CA1\u80FD\u4F5C\u4E3A\u9644\u4EF6\u5BFC\u5165\uFF1AHermes Link \u8BFB\u53D6\u6587\u4EF6\u65F6\u88AB macOS \u62D2\u7EDD\u4E86\u3002\u8BF7\u5728\u201C\u7CFB\u7EDF\u8BBE\u7F6E > \u9690\u79C1\u4E0E\u5B89\u5168\u6027 > \u5B8C\u5168\u78C1\u76D8\u8BBF\u95EE\u6743\u9650\u201D\u91CC\u7ED9\u8FD0\u884C Link \u7684\u7EC8\u7AEF\u6216 Node \u6388\u6743\u540E\u91CD\u8BD5\u3002`;
10240
+ }
10241
+ return `${target}\u6CA1\u80FD\u4F5C\u4E3A\u9644\u4EF6\u5BFC\u5165\uFF1AHermes Link \u8BFB\u53D6\u672C\u673A\u6587\u4EF6\u5931\u8D25\u4E86\uFF0C\u8BF7\u786E\u8BA4\u6587\u4EF6\u8FD8\u5728\u539F\u4F4D\u7F6E\u5E76\u7A0D\u540E\u91CD\u8BD5\u3002`;
10242
+ }
10243
+ function formatImportedFilenameList(filenames) {
10244
+ const preview = filenames.slice(0, 3).map((filename) => `\u201C${filename}\u201D`);
10245
+ const remaining = filenames.length - preview.length;
10246
+ return remaining > 0 ? `${preview.join("\u3001")} \u7B49 ${filenames.length} \u4E2A` : preview.join("\u3001");
10247
+ }
10248
+ function toLinkMessage(input) {
10249
+ const role = normalizeMessageRole(input.message.role);
10250
+ const text = normalizeContent(input.message.content);
10251
+ const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
10252
+ return {
10253
+ id: `msg_${randomUUID6().replaceAll("-", "")}`,
10254
+ schema_version: 1,
10255
+ conversation_id: input.conversationId,
10256
+ role,
10257
+ status: "completed",
10258
+ created_at: createdAt,
10259
+ updated_at: createdAt,
10260
+ sender: senderForRole({
10261
+ role,
10262
+ profileName: input.profileName,
10263
+ profileUid: input.profileUid,
10264
+ profileDisplayName: input.profileDisplayName
10265
+ }),
10266
+ parts: text ? [{ type: "text", text }] : [],
10267
+ attachments: [],
10268
+ hermes: {
10269
+ session_id: input.sessionId,
10270
+ message_id: input.message.id,
10271
+ imported_from: "hermes",
10272
+ import_projection: HERMES_IMPORT_PROJECTION_VERSION
10273
+ },
10274
+ raw: {
10275
+ format: "hermes-message",
10276
+ payload: input.message
10277
+ }
10278
+ };
10279
+ }
10280
+ function senderForRole(input) {
10281
+ switch (input.role) {
10282
+ case "user":
10283
+ return { id: "hermes_user", type: "human", display_name: "Me" };
10284
+ case "assistant":
10285
+ return {
10286
+ id: `agent_${input.profileName}`,
10287
+ type: "agent",
10288
+ display_name: input.profileDisplayName,
10289
+ profile_uid: input.profileUid,
10290
+ profile: input.profileName
10291
+ };
10292
+ case "tool":
10293
+ return { id: "hermes_tool", type: "tool", display_name: "Tool" };
10294
+ case "system":
10295
+ return { id: "hermes_system", type: "system", display_name: "System" };
10296
+ }
10297
+ }
10298
+ function firstUserText(snapshot) {
10299
+ return snapshot.messages.find((message) => message.role === "user")?.parts.find((part) => part.type === "text")?.text?.slice(0, 80);
10300
+ }
10301
+ function normalizeTitle(value) {
10302
+ const normalized = value?.replace(/\s+/gu, " ").trim();
10303
+ return normalized || DEFAULT_CONVERSATION_TITLE;
10304
+ }
10305
+ function normalizeMessageRole(value) {
10306
+ switch (value?.trim().toLowerCase()) {
10307
+ case "user":
10308
+ return "user";
10309
+ case "assistant":
10310
+ return "assistant";
10311
+ case "tool":
10312
+ return "tool";
10313
+ case "system":
10314
+ return "system";
10315
+ default:
10316
+ return "system";
9892
10317
  }
9893
- return {
9894
- conversation_id: input.conversationId,
9895
- run_id: input.runId,
9896
- message_id: input.messageId,
9897
- discovered_count: references.length,
9898
- imported_count: importedParts.length,
9899
- skipped_count: skippedCount,
9900
- failed_count: newFailures.length,
9901
- parts: importedParts,
9902
- ...lastEventSeq ? { last_event_seq: lastEventSeq } : {}
9903
- };
9904
10318
  }
9905
- function readMediaImportFailures(message) {
9906
- const hermes = toRecord8(message.hermes);
9907
- const failures = hermes.media_import_failures;
9908
- if (!Array.isArray(failures)) {
9909
- return [];
10319
+ function normalizeContent(value) {
10320
+ if (typeof value === "string") {
10321
+ return value;
9910
10322
  }
9911
- return failures.flatMap((item) => {
9912
- const record = toRecord8(item);
9913
- const key = readString9(record, "key");
9914
- const filename = readString9(record, "filename");
9915
- const reason = readString9(record, "reason");
9916
- if (!key || !filename || !reason) {
9917
- return [];
9918
- }
9919
- return [
9920
- {
9921
- key,
9922
- filename,
9923
- reason,
9924
- ...readString9(record, "code") ? { code: readString9(record, "code") } : {}
10323
+ if (Array.isArray(value)) {
10324
+ return value.map((item) => {
10325
+ if (typeof item === "string") {
10326
+ return item;
9925
10327
  }
9926
- ];
9927
- });
10328
+ if (typeof item === "object" && item !== null) {
10329
+ return readString9(item, "text") ?? "";
10330
+ }
10331
+ return "";
10332
+ }).filter(Boolean).join("");
10333
+ }
10334
+ return "";
9928
10335
  }
9929
- function readFailedMediaSourceKeys(message) {
9930
- const hermes = toRecord8(message.hermes);
9931
- const keys = hermes.media_import_failed_source_keys;
9932
- if (!Array.isArray(keys)) {
10336
+ function parseJsonValue(value) {
10337
+ if (typeof value !== "string") {
10338
+ return void 0;
10339
+ }
10340
+ const trimmed = value.trim();
10341
+ if (!trimmed) {
10342
+ return void 0;
10343
+ }
10344
+ try {
10345
+ return JSON.parse(trimmed);
10346
+ } catch {
10347
+ return void 0;
10348
+ }
10349
+ }
10350
+ function toRecord8(value) {
10351
+ return typeof value === "object" && value !== null ? value : {};
10352
+ }
10353
+ function isDeletedSession(session) {
10354
+ return readBoolean(session.deleted) || readBoolean(session.is_deleted) || Boolean(readString9(session, "deleted_at")) || ["deleted", "removed"].includes(readString9(session, "status") ?? "");
10355
+ }
10356
+ function isHiddenSession(session) {
10357
+ const source = readString9(session, "source")?.toLowerCase();
10358
+ const status = readString9(session, "status")?.toLowerCase();
10359
+ const visibility = readString9(session, "visibility")?.toLowerCase();
10360
+ return Boolean(source && HIDDEN_SESSION_SOURCES.has(source)) || readBoolean(session.hidden) || readBoolean(session.archived) || Boolean(readString9(session, "archived_at")) || status === "hidden" || status === "archived" || visibility === "hidden" || visibility === "hide";
10361
+ }
10362
+ function readTableColumns(db, tableName) {
10363
+ try {
10364
+ const rows = db.prepare(`PRAGMA table_info(${quoteIdentifier(tableName)})`).all();
10365
+ return new Set(
10366
+ rows.map((row) => typeof row.name === "string" ? row.name : "").filter(Boolean)
10367
+ );
10368
+ } catch {
9933
10369
  return /* @__PURE__ */ new Set();
9934
10370
  }
9935
- return new Set(
9936
- keys.filter(
9937
- (key) => typeof key === "string" && key.length > 0
9938
- )
9939
- );
9940
10371
  }
9941
- function emptyImportResult(input) {
9942
- return {
9943
- conversation_id: input.conversationId,
9944
- run_id: input.runId,
9945
- message_id: input.messageId,
9946
- discovered_count: 0,
9947
- imported_count: 0,
9948
- skipped_count: 0,
9949
- failed_count: 0,
9950
- parts: []
9951
- };
10372
+ function quoteIdentifier(value) {
10373
+ return `"${value.replaceAll('"', '""')}"`;
9952
10374
  }
9953
- async function writeBlobFromFile(deps, conversationId, source) {
9954
- const sourcePath = resolveMediaSourcePath(source.path);
9955
- const fileStat = await stat8(sourcePath).catch((error) => {
10375
+ async function isFile(filePath) {
10376
+ return stat8(filePath).then((value) => value.isFile()).catch((error) => {
9956
10377
  if (isNodeError10(error, "ENOENT")) {
9957
- throw new LinkHttpError(
9958
- 404,
9959
- "media_source_not_found",
9960
- "Hermes output file was not found"
9961
- );
10378
+ return false;
9962
10379
  }
9963
10380
  throw error;
9964
10381
  });
9965
- if (!fileStat.isFile()) {
9966
- throw new LinkHttpError(
9967
- 400,
9968
- "media_source_not_file",
9969
- "Hermes output media source is not a file"
9970
- );
9971
- }
9972
- if (fileStat.size > MAX_IMPORTED_BLOB_BYTES) {
9973
- throw new LinkHttpError(
9974
- 413,
9975
- "media_source_too_large",
9976
- "Hermes output media source is too large"
9977
- );
9978
- }
9979
- return deps.writeBlob(conversationId, {
9980
- bytes: await readFile9(sourcePath),
9981
- filename: path14.basename(sourcePath),
9982
- mime: source.mime ?? inferMimeType(sourcePath)
9983
- });
9984
10382
  }
9985
- function describeMediaImportFailure(reference, sourceKey, error) {
9986
- return {
9987
- key: sourceKey,
9988
- filename: sanitizeFilename(reference.path, "attachment"),
9989
- reason: error instanceof Error ? error.message : String(error),
9990
- ...isNodeError10(error) && error.code ? { code: error.code } : {}
9991
- };
10383
+ function createConversationId() {
10384
+ return `conv_${randomUUID6().replaceAll("-", "")}`;
9992
10385
  }
9993
- function isSupportedDeliveryFilename(filename) {
9994
- return SUPPORTED_DELIVERY_EXTENSIONS.has(path14.extname(filename).toLowerCase());
10386
+ function isoFromHermesTime(value) {
10387
+ const numeric = readNumber2(value);
10388
+ if (!numeric || numeric <= 0) {
10389
+ return void 0;
10390
+ }
10391
+ const millis = numeric > 1e10 ? numeric : numeric * 1e3;
10392
+ return new Date(millis).toISOString();
9995
10393
  }
9996
10394
  function readString9(payload, key) {
9997
10395
  const value = payload[key];
9998
10396
  return typeof value === "string" && value.trim() ? value.trim() : null;
9999
10397
  }
10000
- function toRecord8(value) {
10001
- return typeof value === "object" && value !== null ? value : {};
10398
+ function readNumber2(value) {
10399
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
10400
+ }
10401
+ function readBoolean(value) {
10402
+ if (value === true || value === 1) {
10403
+ return true;
10404
+ }
10405
+ if (typeof value === "string") {
10406
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
10407
+ }
10408
+ return false;
10002
10409
  }
10003
10410
  function isNodeError10(error, code) {
10004
- return typeof error === "object" && error !== null && "code" in error && (code === void 0 || error.code === code);
10411
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
10005
10412
  }
10006
10413
 
10007
10414
  // src/conversations/run-lifecycle.ts
@@ -10400,9 +10807,7 @@ function readString10(payload, key) {
10400
10807
 
10401
10808
  // src/conversations/history-builder.ts
10402
10809
  import { readFile as readFile10, stat as stat9 } from "fs/promises";
10403
- import { createRequire as createRequire4 } from "module";
10404
10810
  import path15 from "path";
10405
- var nodeRequire4 = createRequire4(import.meta.url);
10406
10811
  var HISTORY_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
10407
10812
  var HERMES_HISTORY_COLUMNS = [
10408
10813
  "role",
@@ -10564,11 +10969,8 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
10564
10969
  function readHistoryRows(dbPath, sessionId) {
10565
10970
  let db = null;
10566
10971
  try {
10567
- const { DatabaseSync } = nodeRequire4(
10568
- "node:sqlite"
10569
- );
10570
- db = new DatabaseSync(dbPath, {
10571
- readOnly: true,
10972
+ db = openSqliteDatabase(dbPath, {
10973
+ readonly: true,
10572
10974
  timeout: 1e3
10573
10975
  });
10574
10976
  const columns = readTableColumns2(db, "messages");
@@ -11278,8 +11680,9 @@ function readChatCompletionUsage(payload) {
11278
11680
  const input = readInteger2(usage, "prompt_tokens") ?? readInteger2(usage, "input_tokens");
11279
11681
  const output = readInteger2(usage, "completion_tokens") ?? readInteger2(usage, "output_tokens");
11280
11682
  const total = readInteger2(usage, "total_tokens");
11281
- const contextWindow = readInteger2(usage, "context_window") ?? readInteger2(usage, "context_max");
11683
+ const contextWindow = readInteger2(usage, "context_window") ?? readInteger2(usage, "context_max") ?? readInteger2(usage, "context_length");
11282
11684
  const explicitContextTokens = readInteger2(usage, "context_tokens") ?? readInteger2(usage, "context_used") ?? readInteger2(usage, "current_context_tokens") ?? readInteger2(usage, "last_prompt_tokens");
11685
+ const explicitUsagePercent = readInteger2(usage, "usage_percent") ?? readInteger2(usage, "context_percent");
11283
11686
  if (input === void 0 && output === void 0 && total === void 0) {
11284
11687
  return void 0;
11285
11688
  }
@@ -11290,7 +11693,7 @@ function readChatCompletionUsage(payload) {
11290
11693
  ...explicitContextTokens !== void 0 ? { context_tokens: explicitContextTokens } : {},
11291
11694
  ...contextWindow !== void 0 ? { context_window: contextWindow } : {},
11292
11695
  ...explicitContextTokens !== void 0 && contextWindow ? {
11293
- usage_percent: Math.min(
11696
+ usage_percent: explicitUsagePercent !== void 0 ? Math.min(100, explicitUsagePercent) : Math.min(
11294
11697
  100,
11295
11698
  Math.round(explicitContextTokens / contextWindow * 100)
11296
11699
  )
@@ -11437,10 +11840,20 @@ var ConversationRunLifecycle = class {
11437
11840
  });
11438
11841
  return void 0;
11439
11842
  });
11843
+ const instructions = buildRunInstructions(run, deliveryStagingDir);
11844
+ const estimatedUsage = estimateContextUsage({
11845
+ conversationHistory: conversationHistory.messages,
11846
+ currentInput: resolvedInput,
11847
+ instructions,
11848
+ contextWindow: run.context_window
11849
+ });
11850
+ if (estimatedUsage) {
11851
+ await this.updateRun(conversationId, runId, { usage: estimatedUsage });
11852
+ }
11440
11853
  const response = await streamHermesResponses(
11441
11854
  {
11442
11855
  input: resolvedInput,
11443
- instructions: buildRunInstructions(run, deliveryStagingDir),
11856
+ instructions,
11444
11857
  session_id: hermesSessionId,
11445
11858
  model: run.model,
11446
11859
  ...previousResponseId ? { previous_response_id: previousResponseId } : {},
@@ -11943,7 +12356,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
11943
12356
  run.hermes_response_id = responseId;
11944
12357
  }
11945
12358
  if (usage) {
11946
- run.usage = usage;
12359
+ run.usage = mergeRunUsage(run.usage, usage);
11947
12360
  }
11948
12361
  if (assistant) {
11949
12362
  assistant.status = "completed";
@@ -11995,7 +12408,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
11995
12408
  const visibleMessage = formatFailureMessage(message, run.error_detail);
11996
12409
  const usage = readUsage(source?.payload);
11997
12410
  if (usage) {
11998
- run.usage = usage;
12411
+ run.usage = mergeRunUsage(run.usage, usage);
11999
12412
  }
12000
12413
  const assistant = snapshot.messages.find(
12001
12414
  (item) => item.id === run.assistant_message_id
@@ -12014,6 +12427,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
12014
12427
  }
12015
12428
  }
12016
12429
  await this.deps.writeSnapshot(conversationId, snapshot);
12430
+ const contextUsage = contextUsagePayload(run);
12017
12431
  await this.deps.appendEvent(conversationId, {
12018
12432
  type: "run.failed",
12019
12433
  message_id: assistant?.id,
@@ -12022,6 +12436,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
12022
12436
  error: { message },
12023
12437
  ...run.error_detail ? { error_detail: run.error_detail } : {},
12024
12438
  run,
12439
+ ...contextUsage ? { context: contextUsage } : {},
12025
12440
  ...source ? { hermes: source.payload } : {}
12026
12441
  },
12027
12442
  ...source ? { raw: { format: "hermes-run-event", payload: source.rawPayload } } : {}
@@ -12353,6 +12768,7 @@ function contextUsagePayload(run) {
12353
12768
  source: "unknown"
12354
12769
  };
12355
12770
  }
12771
+ const contextSource = usage.context_source ?? "explicit";
12356
12772
  return {
12357
12773
  input_tokens: contextTokens,
12358
12774
  output_tokens: usage?.output_tokens ?? 0,
@@ -12360,7 +12776,7 @@ function contextUsagePayload(run) {
12360
12776
  ...contextWindow ? { context_window: contextWindow } : {},
12361
12777
  used_tokens: contextTokens,
12362
12778
  ...contextWindow ? { window_tokens: contextWindow } : {},
12363
- source: "explicit",
12779
+ source: contextSource,
12364
12780
  ...usage?.usage_percent !== void 0 ? { usage_percent: usage.usage_percent } : contextWindow ? {
12365
12781
  usage_percent: Math.min(
12366
12782
  100,
@@ -12369,6 +12785,41 @@ function contextUsagePayload(run) {
12369
12785
  } : {}
12370
12786
  };
12371
12787
  }
12788
+ function mergeRunUsage(previous, next) {
12789
+ const nextContextWindow = next.context_window ?? previous?.context_window;
12790
+ const nextContextTokens = next.context_tokens ?? refineEstimatedContextTokens(
12791
+ previous,
12792
+ next.input_tokens,
12793
+ nextContextWindow
12794
+ );
12795
+ const nextContextSource = next.context_tokens !== void 0 ? next.context_source ?? "explicit" : nextContextTokens !== void 0 ? previous?.context_source : void 0;
12796
+ return {
12797
+ ...next,
12798
+ ...nextContextTokens !== void 0 ? { context_tokens: nextContextTokens } : {},
12799
+ ...nextContextWindow !== void 0 ? { context_window: nextContextWindow } : {},
12800
+ ...nextContextSource ? { context_source: nextContextSource } : {},
12801
+ ...nextContextTokens !== void 0 && nextContextWindow ? {
12802
+ usage_percent: next.usage_percent ?? previous?.usage_percent ?? Math.min(
12803
+ 100,
12804
+ Math.round(nextContextTokens / nextContextWindow * 100)
12805
+ )
12806
+ } : {}
12807
+ };
12808
+ }
12809
+ function refineEstimatedContextTokens(previous, inputTokens, contextWindow) {
12810
+ if (previous?.context_source !== "estimated") {
12811
+ return void 0;
12812
+ }
12813
+ const currentEstimate = previous.context_tokens;
12814
+ if (currentEstimate === void 0) {
12815
+ return void 0;
12816
+ }
12817
+ const upperBound = inputTokens > 0 ? contextWindow ? Math.min(inputTokens, contextWindow) : inputTokens : contextWindow;
12818
+ if (upperBound === void 0) {
12819
+ return currentEstimate;
12820
+ }
12821
+ return Math.min(currentEstimate, upperBound);
12822
+ }
12372
12823
  function findPreviousHermesResponseId(snapshot, run) {
12373
12824
  const currentProfile = normalizeRunProfileForCompare(run.profile);
12374
12825
  if (!currentProfile) {
@@ -12510,6 +12961,12 @@ var ConversationService = class {
12510
12961
  async listConversations() {
12511
12962
  return this.queries.listConversations();
12512
12963
  }
12964
+ listConversationPage(input = {}) {
12965
+ return this.queries.listConversationPage(input);
12966
+ }
12967
+ searchConversationPage(input = {}) {
12968
+ return this.queries.searchConversationPage(input);
12969
+ }
12513
12970
  async getStatistics(filter = {}) {
12514
12971
  return readLinkStatistics(this.paths, filter);
12515
12972
  }
@@ -13972,13 +14429,84 @@ function isLanHost(hostname) {
13972
14429
  }
13973
14430
 
13974
14431
  // src/http/sse.ts
14432
+ var DEFAULT_SSE_RETRY_MS = 1e3;
14433
+ var DEFAULT_SSE_HEARTBEAT_MS = 15e3;
14434
+ function beginSseStream(request, response, options = {}) {
14435
+ const retryMs = normalizeRetryMs(options.retryMs);
14436
+ const heartbeatMs = Math.max(1e3, options.heartbeatMs ?? DEFAULT_SSE_HEARTBEAT_MS);
14437
+ response.statusCode = 200;
14438
+ response.setHeader("content-type", "text/event-stream; charset=utf-8");
14439
+ response.setHeader("cache-control", "no-store");
14440
+ response.setHeader("connection", "keep-alive");
14441
+ response.flushHeaders();
14442
+ writeSseRetry(response, retryMs);
14443
+ writeSseComment(response, options.initialComment ?? "connected");
14444
+ let closed = false;
14445
+ let heartbeat = null;
14446
+ const cleanup = () => {
14447
+ if (closed) {
14448
+ return;
14449
+ }
14450
+ closed = true;
14451
+ if (heartbeat != null) {
14452
+ clearInterval(heartbeat);
14453
+ heartbeat = null;
14454
+ }
14455
+ request.off("close", cleanup);
14456
+ response.off("close", cleanup);
14457
+ options.onClose?.();
14458
+ if (!response.writableEnded && !response.destroyed) {
14459
+ response.end();
14460
+ }
14461
+ };
14462
+ heartbeat = setInterval(() => {
14463
+ if (response.writableEnded || response.destroyed) {
14464
+ cleanup();
14465
+ return;
14466
+ }
14467
+ writeSseComment(response);
14468
+ }, heartbeatMs);
14469
+ heartbeat.unref();
14470
+ request.once("close", cleanup);
14471
+ response.once("close", cleanup);
14472
+ return cleanup;
14473
+ }
13975
14474
  function writeSseEvent(response, event) {
13976
- response.write(`event: ${event.type}
14475
+ writeJsonSseEvent(response, {
14476
+ event: event.type,
14477
+ data: event,
14478
+ id: event.seq
14479
+ });
14480
+ }
14481
+ function writeJsonSseEvent(response, event) {
14482
+ if (event.retryMs != null) {
14483
+ response.write(`retry: ${normalizeRetryMs(event.retryMs)}
14484
+ `);
14485
+ }
14486
+ if (event.id != null && event.id !== "") {
14487
+ response.write(`id: ${event.id}
14488
+ `);
14489
+ }
14490
+ response.write(`event: ${event.event}
14491
+ `);
14492
+ response.write(`data: ${JSON.stringify(event.data)}
14493
+
13977
14494
  `);
13978
- response.write(`data: ${JSON.stringify(event)}
14495
+ }
14496
+ function writeSseComment(response, comment = "keep-alive") {
14497
+ response.write(`: ${comment}
14498
+
14499
+ `);
14500
+ }
14501
+ function writeSseRetry(response, retryMs) {
14502
+ response.write(`retry: ${normalizeRetryMs(retryMs)}
13979
14503
 
13980
14504
  `);
13981
14505
  }
14506
+ function normalizeRetryMs(retryMs) {
14507
+ const parsed = Number.isFinite(retryMs) ? Math.trunc(retryMs) : DEFAULT_SSE_RETRY_MS;
14508
+ return parsed >= 0 ? parsed : DEFAULT_SSE_RETRY_MS;
14509
+ }
13982
14510
 
13983
14511
  // src/http/routes/conversations.ts
13984
14512
  function registerConversationRoutes(router, options) {
@@ -13986,9 +14514,28 @@ function registerConversationRoutes(router, options) {
13986
14514
  router.get("/api/v1/conversations", async (ctx) => {
13987
14515
  await authenticateRequest(ctx, paths);
13988
14516
  ctx.set("cache-control", "no-store");
14517
+ const result = await conversations.listConversationPage({
14518
+ limit: readLimit(ctx.query.limit),
14519
+ cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after) ?? readQueryString(ctx.query.page_cursor)
14520
+ });
14521
+ ctx.body = {
14522
+ ok: true,
14523
+ conversations: result.conversations,
14524
+ page: result.page
14525
+ };
14526
+ });
14527
+ router.get("/api/v1/conversations/search", async (ctx) => {
14528
+ await authenticateRequest(ctx, paths);
14529
+ ctx.set("cache-control", "no-store");
14530
+ const result = await conversations.searchConversationPage({
14531
+ limit: readLimit(ctx.query.limit),
14532
+ cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after) ?? readQueryString(ctx.query.page_cursor),
14533
+ query: readQueryString(ctx.query.query) ?? readQueryString(ctx.query.q) ?? readQueryString(ctx.query.keyword) ?? ""
14534
+ });
13989
14535
  ctx.body = {
13990
14536
  ok: true,
13991
- conversations: await conversations.listConversations()
14537
+ conversations: result.conversations,
14538
+ page: result.page
13992
14539
  };
13993
14540
  });
13994
14541
  router.post("/api/v1/conversations", async (ctx) => {
@@ -14022,49 +14569,44 @@ function registerConversationRoutes(router, options) {
14022
14569
  const notificationOnly = mode === "notifications";
14023
14570
  ctx.respond = false;
14024
14571
  const response = ctx.res;
14025
- response.statusCode = 200;
14026
- response.setHeader("content-type", "text/event-stream; charset=utf-8");
14027
- response.setHeader("cache-control", "no-store");
14028
- response.setHeader("connection", "keep-alive");
14029
- const unsubscribe = conversations.subscribeAll((event) => {
14572
+ let unsubscribe = () => {
14573
+ };
14574
+ beginSseStream(ctx.req, response, {
14575
+ onClose: () => unsubscribe()
14576
+ });
14577
+ unsubscribe = conversations.subscribeAll((event) => {
14030
14578
  if (notificationOnly && !isConversationNotificationEvent(event)) {
14031
14579
  return;
14032
14580
  }
14033
14581
  writeSseEvent(response, event);
14034
14582
  });
14035
- const cleanup = () => {
14036
- unsubscribe();
14037
- response.end();
14038
- };
14039
- ctx.req.on("close", cleanup);
14040
14583
  });
14041
14584
  router.get("/api/v1/conversations/:conversationId/events", async (ctx) => {
14042
14585
  await authenticateRequest(ctx, paths);
14043
- const after = readInteger3(ctx.query.after) ?? 0;
14586
+ const after = resolveConversationEventCursor({
14587
+ queryAfter: ctx.query.after,
14588
+ lastEventIdHeader: ctx.req.headers["last-event-id"]
14589
+ });
14044
14590
  const history = await conversations.listEvents(
14045
14591
  ctx.params.conversationId,
14046
14592
  after
14047
14593
  );
14048
14594
  ctx.respond = false;
14049
14595
  const response = ctx.res;
14050
- response.statusCode = 200;
14051
- response.setHeader("content-type", "text/event-stream; charset=utf-8");
14052
- response.setHeader("cache-control", "no-store");
14053
- response.setHeader("connection", "keep-alive");
14596
+ let unsubscribe = () => {
14597
+ };
14598
+ beginSseStream(ctx.req, response, {
14599
+ onClose: () => unsubscribe()
14600
+ });
14054
14601
  for (const event of history) {
14055
14602
  writeSseEvent(response, event);
14056
14603
  }
14057
- const unsubscribe = conversations.subscribe(
14604
+ unsubscribe = conversations.subscribe(
14058
14605
  ctx.params.conversationId,
14059
14606
  (event) => {
14060
14607
  writeSseEvent(response, event);
14061
14608
  }
14062
14609
  );
14063
- const cleanup = () => {
14064
- unsubscribe();
14065
- response.end();
14066
- };
14067
- ctx.req.on("close", cleanup);
14068
14610
  });
14069
14611
  router.post("/api/v1/conversations/:conversationId/messages", async (ctx) => {
14070
14612
  await authenticateRequest(ctx, paths);
@@ -14267,6 +14809,19 @@ function registerConversationRoutes(router, options) {
14267
14809
  }
14268
14810
  );
14269
14811
  }
14812
+ function resolveConversationEventCursor(input) {
14813
+ const queryAfter = readInteger3(input.queryAfter) ?? 0;
14814
+ const headerAfter = readNonNegativeIntegerHeader(input.lastEventIdHeader) ?? 0;
14815
+ return Math.max(queryAfter, headerAfter);
14816
+ }
14817
+ function readNonNegativeIntegerHeader(value) {
14818
+ const raw = Array.isArray(value) ? value[0] : value;
14819
+ if (!raw) {
14820
+ return null;
14821
+ }
14822
+ const parsed = Number.parseInt(raw, 10);
14823
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
14824
+ }
14270
14825
  function contentDispositionInline(filename) {
14271
14826
  const fallback = asciiFilenameFallback(filename);
14272
14827
  return `inline; filename="${fallback}"; filename*=UTF-8''${encodeRfc5987Value(filename)}`;
@@ -18260,22 +18815,18 @@ function registerProfileRoutes(router, options) {
18260
18815
  await authenticateRequest(ctx, paths);
18261
18816
  ctx.respond = false;
18262
18817
  const response = ctx.res;
18263
- response.statusCode = 200;
18264
- response.setHeader("content-type", "text/event-stream; charset=utf-8");
18265
- response.setHeader("cache-control", "no-store");
18266
- response.setHeader("connection", "keep-alive");
18818
+ let unsubscribe = () => {
18819
+ };
18820
+ beginSseStream(ctx.req, response, {
18821
+ onClose: () => unsubscribe()
18822
+ });
18267
18823
  writeProfileCreationSseEvent(
18268
18824
  response,
18269
18825
  await readHermesProfileCreationStatus(paths)
18270
18826
  );
18271
- const unsubscribe = subscribeHermesProfileCreationStatus((status) => {
18827
+ unsubscribe = subscribeHermesProfileCreationStatus((status) => {
18272
18828
  writeProfileCreationSseEvent(response, status);
18273
18829
  });
18274
- const cleanup = () => {
18275
- unsubscribe();
18276
- response.end();
18277
- };
18278
- ctx.req.on("close", cleanup);
18279
18830
  });
18280
18831
  router.get("/api/v1/profiles/:name/status", async (ctx) => {
18281
18832
  await authenticateRequest(ctx, paths);
@@ -18491,10 +19042,10 @@ function isProfileAvatarUrl(value) {
18491
19042
  return isHttpUrl(value) || /^data:image\/[a-z0-9.+-]+;base64,/iu.test(value);
18492
19043
  }
18493
19044
  function writeProfileCreationSseEvent(response, status) {
18494
- response.write("event: profile.creation.status\n");
18495
- response.write(`data: ${JSON.stringify(status)}
18496
-
18497
- `);
19045
+ writeJsonSseEvent(response, {
19046
+ event: "profile.creation.status",
19047
+ data: status
19048
+ });
18498
19049
  }
18499
19050
 
18500
19051
  // src/http/routes/runs.ts
@@ -19784,6 +20335,7 @@ var DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS = 15 * 6e4;
19784
20335
  function startLanIpMonitor(options) {
19785
20336
  let running = false;
19786
20337
  let closed = false;
20338
+ let current = Promise.resolve();
19787
20339
  const check = async (context = {}) => {
19788
20340
  if (running || closed) {
19789
20341
  return;
@@ -19799,15 +20351,16 @@ function startLanIpMonitor(options) {
19799
20351
  running = false;
19800
20352
  }
19801
20353
  };
19802
- void check({ forceReport: true, publishToRelay: true });
20354
+ current = check({ forceReport: true, publishToRelay: true });
19803
20355
  const timer = setInterval(() => {
19804
- void check();
20356
+ current = check();
19805
20357
  }, options.intervalMs ?? DEFAULT_INTERVAL_MS);
19806
20358
  timer.unref?.();
19807
20359
  return {
19808
- close() {
20360
+ async close() {
19809
20361
  closed = true;
19810
20362
  clearInterval(timer);
20363
+ await current.catch(() => void 0);
19811
20364
  }
19812
20365
  };
19813
20366
  }
@@ -19877,6 +20430,7 @@ async function checkLanIpChange(options, context = {}) {
19877
20430
  // src/daemon/scheduler.ts
19878
20431
  function startCronDeliveryScheduler(options) {
19879
20432
  let running = false;
20433
+ let current = Promise.resolve();
19880
20434
  const syncCronDeliveries = async () => {
19881
20435
  if (running) {
19882
20436
  return;
@@ -19897,17 +20451,19 @@ function startCronDeliveryScheduler(options) {
19897
20451
  }
19898
20452
  };
19899
20453
  const timer = setInterval(() => {
19900
- void syncCronDeliveries();
20454
+ current = syncCronDeliveries();
19901
20455
  }, options.intervalMs ?? 3e4);
19902
20456
  timer.unref?.();
19903
20457
  return {
19904
- close() {
20458
+ async close() {
19905
20459
  clearInterval(timer);
20460
+ await current.catch(() => void 0);
19906
20461
  }
19907
20462
  };
19908
20463
  }
19909
20464
  function startHermesSessionSyncScheduler(options) {
19910
20465
  let running = false;
20466
+ let current = Promise.resolve();
19911
20467
  const syncSessions = async () => {
19912
20468
  if (running) {
19913
20469
  return;
@@ -19924,12 +20480,13 @@ function startHermesSessionSyncScheduler(options) {
19924
20480
  }
19925
20481
  };
19926
20482
  const timer = setInterval(() => {
19927
- void syncSessions();
20483
+ current = syncSessions();
19928
20484
  }, options.intervalMs ?? 10 * 60 * 1e3);
19929
20485
  timer.unref?.();
19930
20486
  return {
19931
- close() {
20487
+ async close() {
19932
20488
  clearInterval(timer);
20489
+ await current.catch(() => void 0);
19933
20490
  }
19934
20491
  };
19935
20492
  }
@@ -19953,8 +20510,9 @@ async function startLinkService(options = {}) {
19953
20510
  }
19954
20511
  const conversations = new ConversationService(paths, logger);
19955
20512
  await conversations.rebuildStatisticsIndex();
20513
+ let hermesSessionSync = Promise.resolve();
19956
20514
  const triggerHermesSessionSync = () => {
19957
- void conversations.syncHermesSessions().catch((error) => {
20515
+ hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
19958
20516
  void logger.warn("hermes_session_sync_failed", {
19959
20517
  error: error instanceof Error ? error.message : String(error)
19960
20518
  });
@@ -20028,11 +20586,14 @@ async function startLinkService(options = {}) {
20028
20586
  }
20029
20587
  return {
20030
20588
  async close() {
20031
- scheduler.close();
20032
- hermesSessionSyncScheduler.close();
20033
- lanIpMonitor.close();
20034
20589
  relay?.close();
20035
20590
  await closeServer(server);
20591
+ await Promise.all([
20592
+ scheduler.close(),
20593
+ hermesSessionSyncScheduler.close(),
20594
+ lanIpMonitor.close(),
20595
+ hermesSessionSync.catch(() => void 0)
20596
+ ]);
20036
20597
  await logger.info("service_stopped");
20037
20598
  await logger.flush();
20038
20599
  if (options.writePidFile) {
@@ -21646,26 +22207,22 @@ function registerHermesUpdateRoutes(router, options) {
21646
22207
  await authenticateRequest(ctx, paths);
21647
22208
  ctx.respond = false;
21648
22209
  const response = ctx.res;
21649
- response.statusCode = 200;
21650
- response.setHeader("content-type", "text/event-stream; charset=utf-8");
21651
- response.setHeader("cache-control", "no-store");
21652
- response.setHeader("connection", "keep-alive");
22210
+ let unsubscribe = () => {
22211
+ };
22212
+ beginSseStream(ctx.req, response, {
22213
+ onClose: () => unsubscribe()
22214
+ });
21653
22215
  writeUpdateSseEvent(response, await readHermesUpdateStatus(paths));
21654
- const unsubscribe = subscribeHermesUpdateStatus((status) => {
22216
+ unsubscribe = subscribeHermesUpdateStatus((status) => {
21655
22217
  writeUpdateSseEvent(response, status);
21656
22218
  });
21657
- const cleanup = () => {
21658
- unsubscribe();
21659
- response.end();
21660
- };
21661
- ctx.req.on("close", cleanup);
21662
22219
  });
21663
22220
  }
21664
22221
  function writeUpdateSseEvent(response, status) {
21665
- response.write("event: hermes.update.status\n");
21666
- response.write(`data: ${JSON.stringify(status)}
21667
-
21668
- `);
22222
+ writeJsonSseEvent(response, {
22223
+ event: "hermes.update.status",
22224
+ data: status
22225
+ });
21669
22226
  }
21670
22227
 
21671
22228
  // src/http/routes/link-updates.ts
@@ -21695,26 +22252,22 @@ function registerLinkUpdateRoutes(router, options) {
21695
22252
  await authenticateRequest(ctx, paths);
21696
22253
  ctx.respond = false;
21697
22254
  const response = ctx.res;
21698
- response.statusCode = 200;
21699
- response.setHeader("content-type", "text/event-stream; charset=utf-8");
21700
- response.setHeader("cache-control", "no-store");
21701
- response.setHeader("connection", "keep-alive");
22255
+ let unsubscribe = () => {
22256
+ };
22257
+ beginSseStream(ctx.req, response, {
22258
+ onClose: () => unsubscribe()
22259
+ });
21702
22260
  writeUpdateSseEvent2(response, await readLinkUpdateStatus(paths));
21703
- const unsubscribe = subscribeLinkUpdateStatus((status) => {
22261
+ unsubscribe = subscribeLinkUpdateStatus((status) => {
21704
22262
  writeUpdateSseEvent2(response, status);
21705
22263
  });
21706
- const cleanup = () => {
21707
- unsubscribe();
21708
- response.end();
21709
- };
21710
- ctx.req.on("close", cleanup);
21711
22264
  });
21712
22265
  }
21713
22266
  function writeUpdateSseEvent2(response, status) {
21714
- response.write("event: link.update.status\n");
21715
- response.write(`data: ${JSON.stringify(status)}
21716
-
21717
- `);
22267
+ writeJsonSseEvent(response, {
22268
+ event: "link.update.status",
22269
+ data: status
22270
+ });
21718
22271
  }
21719
22272
 
21720
22273
  // src/http/routes/pairing.ts