@hermespilot/link 0.3.5 → 0.3.6

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.
@@ -3724,7 +3724,7 @@ import os2 from "os";
3724
3724
  import path5 from "path";
3725
3725
 
3726
3726
  // src/constants.ts
3727
- var LINK_VERSION = "0.3.5";
3727
+ var LINK_VERSION = "0.3.6";
3728
3728
  var LINK_COMMAND = "hermeslink";
3729
3729
  var LINK_DEFAULT_PORT = 52379;
3730
3730
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -4859,6 +4859,9 @@ function toRecord3(value) {
4859
4859
  }
4860
4860
 
4861
4861
  // src/conversations/statistics.ts
4862
+ var ESTIMATED_CONTEXT_CHARS_PER_TOKEN = 4;
4863
+ var ESTIMATED_CONTEXT_BASE_OVERHEAD_TOKENS = 256;
4864
+ var ESTIMATED_CONTEXT_PER_MESSAGE_OVERHEAD_TOKENS = 8;
4862
4865
  function buildConversationStats(manifest, snapshot) {
4863
4866
  if (manifest.stats && manifest.status !== "active" && snapshot.messages.length === 0 && snapshot.runs.length === 0) {
4864
4867
  return manifest.stats;
@@ -4973,12 +4976,21 @@ function readUsage(payload) {
4973
4976
  return void 0;
4974
4977
  }
4975
4978
  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) {
4979
+ const response = toRecord4(payload.response);
4980
+ const responseUsage = toRecord4(response.usage);
4981
+ const context = firstRecord(
4982
+ payload.context,
4983
+ usage.context,
4984
+ response.context,
4985
+ responseUsage.context
4986
+ );
4987
+ 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");
4988
+ 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");
4989
+ const totalTokens = readInteger(usage, "total_tokens") ?? readInteger(responseUsage, "total_tokens") ?? readInteger(payload, "total_tokens") ?? (inputTokens ?? 0) + (outputTokens ?? 0);
4990
+ 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");
4991
+ 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");
4992
+ 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");
4993
+ if (!inputTokens && !outputTokens && !totalTokens && explicitContextTokens === void 0) {
4982
4994
  return void 0;
4983
4995
  }
4984
4996
  return {
@@ -4987,14 +4999,44 @@ function readUsage(payload) {
4987
4999
  total_tokens: totalTokens,
4988
5000
  ...explicitContextTokens !== void 0 ? { context_tokens: explicitContextTokens } : {},
4989
5001
  ...contextWindow !== void 0 ? { context_window: contextWindow } : {},
5002
+ ...explicitContextTokens !== void 0 ? { context_source: "explicit" } : {},
4990
5003
  ...explicitContextTokens !== void 0 && contextWindow ? {
4991
- usage_percent: Math.min(
5004
+ usage_percent: explicitUsagePercent !== void 0 ? Math.min(100, explicitUsagePercent) : Math.min(
4992
5005
  100,
4993
5006
  Math.round(explicitContextTokens / contextWindow * 100)
4994
5007
  )
4995
5008
  } : {}
4996
5009
  };
4997
5010
  }
5011
+ function estimateContextUsage(input) {
5012
+ const currentInput = input.currentInput.trim();
5013
+ const instructions = input.instructions?.trim() ?? "";
5014
+ const messageCount = input.conversationHistory.length + (currentInput ? 1 : 0) + (instructions ? 1 : 0);
5015
+ if (messageCount === 0) {
5016
+ return void 0;
5017
+ }
5018
+ const serializedRequest = JSON.stringify({
5019
+ instructions: instructions || void 0,
5020
+ conversation_history: input.conversationHistory,
5021
+ input: currentInput || void 0
5022
+ });
5023
+ const estimatedTokens = Math.ceil(serializedRequest.length / ESTIMATED_CONTEXT_CHARS_PER_TOKEN) + ESTIMATED_CONTEXT_BASE_OVERHEAD_TOKENS + messageCount * ESTIMATED_CONTEXT_PER_MESSAGE_OVERHEAD_TOKENS;
5024
+ const contextTokens = input.contextWindow ? Math.min(input.contextWindow, estimatedTokens) : estimatedTokens;
5025
+ return {
5026
+ input_tokens: 0,
5027
+ output_tokens: 0,
5028
+ total_tokens: 0,
5029
+ context_tokens: contextTokens,
5030
+ ...input.contextWindow !== void 0 ? { context_window: input.contextWindow } : {},
5031
+ ...input.contextWindow ? {
5032
+ usage_percent: Math.min(
5033
+ 100,
5034
+ Math.round(contextTokens / input.contextWindow * 100)
5035
+ )
5036
+ } : {},
5037
+ context_source: "estimated"
5038
+ };
5039
+ }
4998
5040
  function isAgentRun(run) {
4999
5041
  return run.kind !== "command";
5000
5042
  }
@@ -5015,6 +5057,14 @@ function readInteger(payload, key) {
5015
5057
  function toRecord4(value) {
5016
5058
  return typeof value === "object" && value !== null ? value : {};
5017
5059
  }
5060
+ function firstRecord(...values) {
5061
+ for (const value of values) {
5062
+ if (typeof value === "object" && value !== null) {
5063
+ return value;
5064
+ }
5065
+ }
5066
+ return {};
5067
+ }
5018
5068
 
5019
5069
  // src/conversations/blob-store.ts
5020
5070
  import { randomUUID as randomUUID3 } from "crypto";
@@ -5730,7 +5780,7 @@ async function buildConversationRuntimeMetadata(paths, manifest, snapshot) {
5730
5780
  };
5731
5781
  const contextWindow = current.contextWindow ?? usage.context_window ?? usageRun?.context_window;
5732
5782
  const contextTokens = usage.context_tokens;
5733
- const contextSource = contextTokens === void 0 ? "unknown" : "explicit";
5783
+ const contextSource = contextTokens === void 0 ? "unknown" : usage.context_source ?? "explicit";
5734
5784
  const provider = current.provider ?? usageRun?.provider;
5735
5785
  const reasoningEffort = current.reasoningEffort;
5736
5786
  return {
@@ -6324,12 +6374,12 @@ var ConversationCommandHandlers = class {
6324
6374
  };
6325
6375
  function formatContextUsageLines(runtime) {
6326
6376
  const windowTokens = runtime.context.window_tokens ?? runtime.context.context_window;
6327
- if (runtime.context.source === "explicit") {
6377
+ if (runtime.context.source === "explicit" || runtime.context.source === "estimated") {
6328
6378
  const usedTokens = runtime.context.used_tokens ?? runtime.context.input_tokens;
6329
6379
  const percent = runtime.context.usage_percent ?? (windowTokens && windowTokens > 0 ? Math.min(100, Math.round(usedTokens / windowTokens * 100)) : void 0);
6330
6380
  return [
6331
6381
  `\u4E0A\u4E0B\u6587\uFF1A${usedTokens}${windowTokens ? ` / ${windowTokens}` : ""}${percent === void 0 ? "" : `\uFF08${percent}%\uFF09`}`,
6332
- "\u6765\u6E90\uFF1A\u6A21\u578B\u8FD4\u56DE"
6382
+ runtime.context.source === "estimated" ? "\u6765\u6E90\uFF1A\u672C\u5730\u4F30\u7B97" : "\u6765\u6E90\uFF1A\u6A21\u578B\u8FD4\u56DE"
6333
6383
  ];
6334
6384
  }
6335
6385
  return [
@@ -8518,307 +8568,664 @@ function isNodeError8(error, code) {
8518
8568
 
8519
8569
  // src/conversations/hermes-session-sync.ts
8520
8570
  import { randomUUID as randomUUID6 } from "crypto";
8521
- import { readdir as readdir5, readFile as readFile8, stat as stat7 } from "fs/promises";
8571
+ import { readdir as readdir6, readFile as readFile9, stat as stat8 } from "fs/promises";
8522
8572
  import { createRequire as createRequire3 } from "module";
8523
8573
  import os4 from "os";
8574
+ import path14 from "path";
8575
+
8576
+ // src/conversations/delivery-import.ts
8577
+ import { lstat, readFile as readFile8, readdir as readdir5, stat as stat7 } from "fs/promises";
8524
8578
  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: []
8579
+ var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
8580
+ var MAX_MEDIA_IMPORT_FAILURES = 20;
8581
+ var MAX_DELIVERY_FILES = 50;
8582
+ var DELIVERY_STAGING_SEGMENT = "delivery-staging";
8583
+ var SUPPORTED_DELIVERY_EXTENSIONS = /* @__PURE__ */ new Set([
8584
+ ".png",
8585
+ ".jpg",
8586
+ ".jpeg",
8587
+ ".gif",
8588
+ ".webp",
8589
+ ".heic",
8590
+ ".pdf",
8591
+ ".txt",
8592
+ ".log",
8593
+ ".md",
8594
+ ".markdown",
8595
+ ".json",
8596
+ ".jsonl",
8597
+ ".yaml",
8598
+ ".yml",
8599
+ ".toml",
8600
+ ".ini",
8601
+ ".xml",
8602
+ ".html",
8603
+ ".css",
8604
+ ".js",
8605
+ ".ts",
8606
+ ".jsx",
8607
+ ".tsx",
8608
+ ".dart",
8609
+ ".py",
8610
+ ".java",
8611
+ ".kt",
8612
+ ".swift",
8613
+ ".go",
8614
+ ".rs",
8615
+ ".rb",
8616
+ ".php",
8617
+ ".c",
8618
+ ".cc",
8619
+ ".cpp",
8620
+ ".h",
8621
+ ".hpp",
8622
+ ".cs",
8623
+ ".sql",
8624
+ ".csv",
8625
+ ".tsv",
8626
+ ".doc",
8627
+ ".docx",
8628
+ ".xls",
8629
+ ".xlsx",
8630
+ ".ppt",
8631
+ ".pptx",
8632
+ ".zip",
8633
+ ".rar",
8634
+ ".7z",
8635
+ ".tar",
8636
+ ".gz",
8637
+ ".mp4",
8638
+ ".mov",
8639
+ ".avi",
8640
+ ".mkv",
8641
+ ".webm",
8642
+ ".ogg",
8643
+ ".opus",
8644
+ ".mp3",
8645
+ ".wav",
8646
+ ".m4a"
8647
+ ]);
8648
+ function resolveDeliveryStagingTarget(paths, stagingDir) {
8649
+ const resolvedDir = path13.resolve(stagingDir);
8650
+ const relative = path13.relative(path13.resolve(paths.conversationsDir), resolvedDir);
8651
+ if (!relative || relative.startsWith("..") || path13.isAbsolute(relative)) {
8652
+ throw new LinkHttpError(
8653
+ 400,
8654
+ "delivery_staging_invalid",
8655
+ "delivery staging directory must be inside Hermes Link conversations"
8656
+ );
8657
+ }
8658
+ const segments = relative.split(path13.sep);
8659
+ if (segments.length !== 3 || segments[1] !== DELIVERY_STAGING_SEGMENT || !segments[0] || !segments[2]) {
8660
+ throw new LinkHttpError(
8661
+ 400,
8662
+ "delivery_staging_invalid",
8663
+ "delivery staging directory is invalid"
8664
+ );
8665
+ }
8666
+ return {
8667
+ conversationId: segments[0],
8668
+ runId: segments[2],
8669
+ stagingDir: resolvedDir
8563
8670
  };
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;
8576
- for (const session of sessions) {
8577
- if (isDeletedSession(session)) {
8578
- result.skipped_deleted += 1;
8579
- continue;
8580
- }
8581
- if (isHiddenSession(session)) {
8582
- result.skipped_hidden += 1;
8583
- continue;
8584
- }
8585
- result.eligible_sessions += 1;
8586
- candidates.push({ profileName, profileDir, dbPath, session });
8671
+ }
8672
+ async function collectStagedDeliveryReferences(stagingDir) {
8673
+ const directoryStat = await lstat(stagingDir).catch((error) => {
8674
+ if (isNodeError9(error, "ENOENT")) {
8675
+ throw new LinkHttpError(
8676
+ 404,
8677
+ "delivery_staging_not_found",
8678
+ "delivery staging directory was not found"
8679
+ );
8587
8680
  }
8588
- }
8589
- candidates.sort((left, right) => {
8590
- const rightTime = readNumber2(right.session.last_active) ?? 0;
8591
- const leftTime = readNumber2(left.session.last_active) ?? 0;
8592
- return rightTime - leftTime;
8681
+ throw error;
8593
8682
  });
8594
- const importableCandidates = candidates.slice(0, maxImports);
8595
- result.skipped_over_limit = Math.max(0, candidates.length - maxImports);
8596
- for (const candidate of importableCandidates) {
8597
- if (knownHermesSessions.ids.has(candidate.session.id)) {
8598
- result.skipped_existing += 1;
8599
- const reprojected = await reprojectExistingHermesConversation({
8600
- paths,
8601
- store,
8602
- candidate,
8603
- conversationIds: knownHermesSessions.conversationIdsBySessionId.get(
8604
- candidate.session.id
8605
- ) ?? []
8606
- }).catch((error) => {
8607
- result.errors.push({
8608
- profile: candidate.profileName,
8609
- message: error instanceof Error ? error.message : String(error)
8610
- });
8611
- return false;
8612
- });
8613
- if (reprojected) {
8614
- result.reprojected_count += 1;
8615
- }
8616
- continue;
8617
- }
8618
- const imported = await importHermesSession({
8619
- paths,
8620
- store,
8621
- candidate,
8622
- existingHermesSessionIds: knownHermesSessions.ids
8623
- }).catch((error) => {
8624
- result.errors.push({
8625
- profile: candidate.profileName,
8626
- message: error instanceof Error ? error.message : String(error)
8627
- });
8628
- return false;
8629
- });
8630
- if (imported) {
8631
- result.imported_count += 1;
8632
- }
8633
- }
8634
- if (result.imported_count > 0 || result.reprojected_count > 0 || result.errors.length > 0) {
8635
- void logger.info("hermes_session_sync_completed", { ...result });
8636
- } else {
8637
- void logger.debug("hermes_session_sync_completed", { ...result });
8683
+ if (!directoryStat.isDirectory()) {
8684
+ throw new LinkHttpError(
8685
+ 400,
8686
+ "delivery_staging_not_directory",
8687
+ "delivery staging path is not a directory"
8688
+ );
8638
8689
  }
8639
- return result;
8690
+ const entries = await readdir5(stagingDir, { withFileTypes: true });
8691
+ return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isSupportedDeliveryFilename(entry.name)).sort(
8692
+ (left, right) => left.name.localeCompare(right.name, "en", { numeric: true })
8693
+ ).slice(0, MAX_DELIVERY_FILES).map((entry) => {
8694
+ const sourcePath = path13.join(stagingDir, entry.name);
8695
+ const mime = inferMimeType(sourcePath);
8696
+ return {
8697
+ path: sourcePath,
8698
+ kind: mediaKindForMime(mime),
8699
+ mime
8700
+ };
8701
+ });
8640
8702
  }
8641
- async function importHermesSession(input) {
8642
- const { paths, store, candidate, existingHermesSessionIds } = input;
8643
- const profile = await resolveConversationProfileTarget(
8644
- paths,
8645
- candidate.profileName
8703
+ async function importMediaReferencesForMessage(deps, input) {
8704
+ const references = input.references.slice(0, input.maxReferences ?? MAX_DELIVERY_FILES);
8705
+ if (references.length === 0) {
8706
+ return emptyImportResult(input);
8707
+ }
8708
+ const snapshot = await deps.readSnapshot(input.conversationId);
8709
+ const assistant = snapshot.messages.find(
8710
+ (message) => message.id === input.messageId
8646
8711
  );
8647
- const sessionId = candidate.session.id;
8648
- const messages = await readHermesSessionMessages(candidate);
8649
- const now = (/* @__PURE__ */ new Date()).toISOString();
8650
- const createdAt = isoFromHermesTime(candidate.session.started_at) ?? now;
8651
- const updatedAt = isoFromHermesTime(candidate.session.last_active) ?? isoFromHermesTime(messages.at(-1)?.timestamp) ?? createdAt;
8652
- const conversationId = createConversationId();
8653
- const snapshot = {
8654
- schema_version: 1,
8655
- messages: toLinkMessages({
8656
- conversationId,
8657
- profileName: profile.profileName,
8658
- profileUid: profile.profileUid,
8659
- profileDisplayName: profile.profileDisplayName,
8660
- sessionId,
8661
- messages
8662
- }),
8663
- runs: []
8664
- };
8665
- const title = readString8(candidate.session, "title") ?? firstUserText(snapshot);
8666
- const manifest = {
8667
- id: conversationId,
8668
- schema_version: 1,
8669
- kind: "direct",
8670
- title: normalizeTitle(title),
8671
- title_source: title ? "hermes" : "default",
8672
- status: "active",
8673
- hermes_session_id: sessionId,
8674
- hermes_session_ids: [sessionId],
8675
- profile_uid: profile.profileUid,
8676
- profile_name_snapshot: profile.profileName,
8677
- profile: profile.profileName,
8678
- created_at: createdAt,
8679
- updated_at: updatedAt,
8680
- last_event_seq: 0
8681
- };
8682
- await store.createConversation(manifest, snapshot);
8683
- await store.appendEvent(conversationId, {
8684
- type: "conversation.created",
8685
- payload: {
8686
- imported_from: "hermes",
8687
- hermes_session_id: sessionId,
8688
- profile: {
8689
- uid: profile.profileUid,
8690
- name: profile.profileName,
8691
- display_name: profile.profileDisplayName,
8692
- avatar_url: profile.profileAvatarUrl
8693
- }
8694
- }
8695
- });
8696
- for (const message of snapshot.messages) {
8697
- await store.appendEvent(conversationId, {
8698
- type: "message.created",
8699
- message_id: message.id,
8700
- payload: { message, imported_from: "hermes" },
8701
- raw: message.raw
8702
- });
8712
+ if (!assistant) {
8713
+ return emptyImportResult(input);
8703
8714
  }
8704
- const stats = buildConversationStats(
8705
- await store.readManifest(conversationId),
8706
- snapshot
8707
- );
8708
- await store.writeManifest({
8709
- ...await store.readManifest(conversationId),
8710
- stats
8711
- });
8712
- await upsertConversationStats(
8713
- paths,
8714
- toStatsIndexRecord(await store.readManifest(conversationId), stats)
8715
+ const importedSourceKeys = readImportedMediaSourceKeys(assistant);
8716
+ const failedSourceKeys = readFailedMediaSourceKeys(assistant);
8717
+ const failureRecordsByKey = new Map(
8718
+ readMediaImportFailures(assistant).map((failure) => [failure.key, failure])
8715
8719
  );
8716
- existingHermesSessionIds.add(sessionId);
8717
- return true;
8718
- }
8719
- async function reprojectExistingHermesConversation(input) {
8720
- let changed = false;
8721
- for (const conversationId of input.conversationIds) {
8722
- const manifest = await input.store.readManifest(conversationId).catch(() => null);
8723
- if (!manifest || manifest.status !== "active") {
8724
- continue;
8725
- }
8726
- const snapshot = await input.store.readSnapshot(conversationId).catch(() => null);
8727
- if (!snapshot) {
8728
- continue;
8729
- }
8730
- const prefix = collectImportedHermesPrefix(snapshot);
8731
- if (!prefix?.needsUpgrade || prefix.messages.length === 0) {
8732
- continue;
8733
- }
8734
- const profile = await resolveConversationProfileTarget(
8735
- input.paths,
8736
- manifest.profile_name_snapshot ?? manifest.profile ?? input.candidate.profileName
8737
- );
8738
- const nextSnapshot = {
8739
- ...snapshot,
8740
- messages: [
8741
- ...toLinkMessages({
8742
- conversationId,
8743
- profileName: profile.profileName,
8744
- profileUid: profile.profileUid,
8745
- profileDisplayName: profile.profileDisplayName,
8746
- sessionId: input.candidate.session.id,
8747
- messages: prefix.messages
8748
- }),
8749
- ...snapshot.messages.slice(prefix.endIndex)
8750
- ]
8751
- };
8752
- const stats = buildConversationStats(manifest, nextSnapshot);
8753
- await input.store.writeSnapshot(conversationId, nextSnapshot);
8754
- await input.store.writeManifest({ ...manifest, stats });
8755
- await upsertConversationStats(
8756
- input.paths,
8757
- toStatsIndexRecord({ ...manifest, stats }, stats)
8758
- );
8759
- changed = true;
8760
- }
8761
- return changed;
8762
- }
8763
- function collectImportedHermesPrefix(snapshot) {
8764
- const rows = [];
8765
- const seen = /* @__PURE__ */ new Set();
8766
- let needsProjectionVersion = false;
8767
- let hasToolMetadata = false;
8768
- let endIndex = 0;
8769
- for (; endIndex < snapshot.messages.length; endIndex += 1) {
8770
- const message = snapshot.messages[endIndex];
8771
- if (!isHermesImportedMessage(message)) {
8772
- break;
8773
- }
8774
- if (message.hermes?.import_projection !== HERMES_IMPORT_PROJECTION_VERSION) {
8775
- needsProjectionVersion = true;
8776
- }
8777
- if (message.role === "tool") {
8778
- hasToolMetadata = true;
8779
- }
8780
- for (const row of readHermesRawMessageRows(message.raw)) {
8781
- appendHermesRowOnce(rows, seen, row);
8782
- if (hasHermesToolMetadata(row)) {
8783
- hasToolMetadata = true;
8720
+ const importedParts = [];
8721
+ const newFailures = [];
8722
+ let skippedCount = 0;
8723
+ for (const reference of references) {
8724
+ let sourceKey;
8725
+ try {
8726
+ sourceKey = mediaSourceKey(reference.path);
8727
+ if (importedSourceKeys.has(sourceKey) || failedSourceKeys.has(sourceKey)) {
8728
+ skippedCount += 1;
8729
+ continue;
8784
8730
  }
8785
- }
8786
- for (const event of message.agent_events ?? []) {
8787
- for (const row of readHermesRowsFromAgentEvent(event)) {
8788
- appendHermesRowOnce(rows, seen, row);
8789
- if (hasHermesToolMetadata(row)) {
8790
- hasToolMetadata = true;
8791
- }
8731
+ const blob = await writeBlobFromFile(deps, input.conversationId, reference);
8732
+ const part = {
8733
+ type: reference.kind ?? mediaKindForMime(blob.mime),
8734
+ blob: blob.id,
8735
+ mime: blob.mime,
8736
+ size: blob.size,
8737
+ filename: blob.filename,
8738
+ url: `/api/v1/conversations/${encodeURIComponent(input.conversationId)}/blobs/${encodeURIComponent(blob.id)}`
8739
+ };
8740
+ assistant.parts.push(part);
8741
+ assistant.attachments.push({
8742
+ blob_id: blob.id,
8743
+ mime: blob.mime,
8744
+ size: blob.size,
8745
+ filename: blob.filename,
8746
+ source: "hermes_output"
8747
+ });
8748
+ importedSourceKeys.add(sourceKey);
8749
+ importedParts.push(part);
8750
+ } catch (error) {
8751
+ if (sourceKey && !failedSourceKeys.has(sourceKey)) {
8752
+ const failure = describeMediaImportFailure(reference, sourceKey, error);
8753
+ failedSourceKeys.add(sourceKey);
8754
+ failureRecordsByKey.set(sourceKey, failure);
8755
+ newFailures.push(failure);
8792
8756
  }
8757
+ void deps.logger.warn("conversation_media_import_failed", {
8758
+ conversation_id: input.conversationId,
8759
+ run_id: input.runId,
8760
+ message_id: input.messageId,
8761
+ error: error instanceof Error ? error.message : String(error)
8762
+ });
8793
8763
  }
8794
8764
  }
8795
- if (rows.length === 0 || endIndex === 0) {
8796
- return null;
8765
+ if (importedParts.length === 0 && newFailures.length === 0) {
8766
+ return {
8767
+ ...emptyImportResult(input),
8768
+ discovered_count: references.length,
8769
+ skipped_count: skippedCount
8770
+ };
8771
+ }
8772
+ assistant.hermes = {
8773
+ ...toRecord7(assistant.hermes),
8774
+ imported_media_source_keys: [...importedSourceKeys],
8775
+ media_import_failed_source_keys: [...failedSourceKeys],
8776
+ media_import_failures: [...failureRecordsByKey.values()].slice(
8777
+ -MAX_MEDIA_IMPORT_FAILURES
8778
+ )
8779
+ };
8780
+ assistant.updated_at = (/* @__PURE__ */ new Date()).toISOString();
8781
+ await deps.writeSnapshot(input.conversationId, snapshot);
8782
+ let lastEventSeq;
8783
+ if (importedParts.length > 0) {
8784
+ const event = await deps.appendEvent(input.conversationId, {
8785
+ type: "message.parts.created",
8786
+ message_id: input.messageId,
8787
+ run_id: input.runId,
8788
+ payload: { parts: importedParts }
8789
+ });
8790
+ lastEventSeq = event.seq;
8797
8791
  }
8798
8792
  return {
8799
- endIndex,
8800
- messages: rows,
8801
- needsUpgrade: needsProjectionVersion && hasToolMetadata
8793
+ conversation_id: input.conversationId,
8794
+ run_id: input.runId,
8795
+ message_id: input.messageId,
8796
+ discovered_count: references.length,
8797
+ imported_count: importedParts.length,
8798
+ skipped_count: skippedCount,
8799
+ failed_count: newFailures.length,
8800
+ parts: importedParts,
8801
+ ...lastEventSeq ? { last_event_seq: lastEventSeq } : {}
8802
8802
  };
8803
8803
  }
8804
- function isHermesImportedMessage(message) {
8805
- return message.hermes?.imported_from === "hermes" || message.raw?.format === "hermes-message" || message.raw?.format === "hermes-message-group";
8806
- }
8807
- function readHermesRowsFromAgentEvent(event) {
8808
- if (event.raw?.format !== "hermes-message" && event.raw?.format !== "hermes-message-group") {
8804
+ function readMediaImportFailures(message) {
8805
+ const hermes = toRecord7(message.hermes);
8806
+ const failures = hermes.media_import_failures;
8807
+ if (!Array.isArray(failures)) {
8809
8808
  return [];
8810
8809
  }
8811
- const payload = toRecord7(event.raw.payload);
8812
- const message = toRecord7(payload.message);
8813
- if (normalizeMessageRole(readString8(message, "role") ?? void 0) === "tool") {
8814
- return [message];
8810
+ return failures.flatMap((item) => {
8811
+ const record = toRecord7(item);
8812
+ const key = readString8(record, "key");
8813
+ const filename = readString8(record, "filename");
8814
+ const reason = readString8(record, "reason");
8815
+ if (!key || !filename || !reason) {
8816
+ return [];
8817
+ }
8818
+ return [
8819
+ {
8820
+ key,
8821
+ filename,
8822
+ reason,
8823
+ ...readString8(record, "code") ? { code: readString8(record, "code") } : {}
8824
+ }
8825
+ ];
8826
+ });
8827
+ }
8828
+ function readFailedMediaSourceKeys(message) {
8829
+ const hermes = toRecord7(message.hermes);
8830
+ const keys = hermes.media_import_failed_source_keys;
8831
+ if (!Array.isArray(keys)) {
8832
+ return /* @__PURE__ */ new Set();
8815
8833
  }
8816
- return readHermesRawMessageRows(event.raw).filter(
8817
- (row) => Boolean(readString8(row, "role"))
8834
+ return new Set(
8835
+ keys.filter(
8836
+ (key) => typeof key === "string" && key.length > 0
8837
+ )
8818
8838
  );
8819
8839
  }
8820
- function appendHermesRowOnce(rows, seen, row) {
8821
- const key = hermesRowKey(row, rows.length);
8840
+ function emptyImportResult(input) {
8841
+ return {
8842
+ conversation_id: input.conversationId,
8843
+ run_id: input.runId,
8844
+ message_id: input.messageId,
8845
+ discovered_count: 0,
8846
+ imported_count: 0,
8847
+ skipped_count: 0,
8848
+ failed_count: 0,
8849
+ parts: []
8850
+ };
8851
+ }
8852
+ async function writeBlobFromFile(deps, conversationId, source) {
8853
+ const sourcePath = resolveMediaSourcePath(source.path);
8854
+ const fileStat = await stat7(sourcePath).catch((error) => {
8855
+ if (isNodeError9(error, "ENOENT")) {
8856
+ throw new LinkHttpError(
8857
+ 404,
8858
+ "media_source_not_found",
8859
+ "Hermes output file was not found"
8860
+ );
8861
+ }
8862
+ throw error;
8863
+ });
8864
+ if (!fileStat.isFile()) {
8865
+ throw new LinkHttpError(
8866
+ 400,
8867
+ "media_source_not_file",
8868
+ "Hermes output media source is not a file"
8869
+ );
8870
+ }
8871
+ if (fileStat.size > MAX_IMPORTED_BLOB_BYTES) {
8872
+ throw new LinkHttpError(
8873
+ 413,
8874
+ "media_source_too_large",
8875
+ "Hermes output media source is too large"
8876
+ );
8877
+ }
8878
+ return deps.writeBlob(conversationId, {
8879
+ bytes: await readFile8(sourcePath),
8880
+ filename: path13.basename(sourcePath),
8881
+ mime: source.mime ?? inferMimeType(sourcePath)
8882
+ });
8883
+ }
8884
+ function describeMediaImportFailure(reference, sourceKey, error) {
8885
+ return {
8886
+ key: sourceKey,
8887
+ filename: sanitizeFilename(reference.path, "attachment"),
8888
+ reason: error instanceof Error ? error.message : String(error),
8889
+ ...isNodeError9(error) && error.code ? { code: error.code } : {}
8890
+ };
8891
+ }
8892
+ function isSupportedDeliveryFilename(filename) {
8893
+ return SUPPORTED_DELIVERY_EXTENSIONS.has(path13.extname(filename).toLowerCase());
8894
+ }
8895
+ function readString8(payload, key) {
8896
+ const value = payload[key];
8897
+ return typeof value === "string" && value.trim() ? value.trim() : null;
8898
+ }
8899
+ function toRecord7(value) {
8900
+ return typeof value === "object" && value !== null ? value : {};
8901
+ }
8902
+ function isNodeError9(error, code) {
8903
+ return typeof error === "object" && error !== null && "code" in error && (code === void 0 || error.code === code);
8904
+ }
8905
+
8906
+ // src/conversations/hermes-session-sync.ts
8907
+ var nodeRequire3 = createRequire3(import.meta.url);
8908
+ var PROFILE_NAME_PATTERN3 = /^[a-zA-Z0-9._-]{1,64}$/u;
8909
+ var DEFAULT_PROFILE_NAME = "default";
8910
+ var MAX_IMPORTABLE_SESSIONS = 100;
8911
+ var HIDDEN_SESSION_SOURCES = /* @__PURE__ */ new Set(["tool"]);
8912
+ var HERMES_IMPORT_PROJECTION_VERSION = "turn_blocks_v3";
8913
+ var IMPORTED_MEDIA_PLACEHOLDER_RUN_ID = "imported_from_hermes";
8914
+ var MAX_IMPORTED_HERMES_MEDIA_BYTES = 100 * 1024 * 1024;
8915
+ var MESSAGE_COLUMNS = [
8916
+ "id",
8917
+ "session_id",
8918
+ "role",
8919
+ "content",
8920
+ "tool_call_id",
8921
+ "tool_calls",
8922
+ "tool_name",
8923
+ "timestamp",
8924
+ "token_count",
8925
+ "finish_reason",
8926
+ "reasoning",
8927
+ "reasoning_content",
8928
+ "reasoning_details",
8929
+ "codex_reasoning_items"
8930
+ ];
8931
+ async function syncHermesSessionsIntoConversations(paths, logger, options = {}) {
8932
+ const maxImports = options.maxImports ?? MAX_IMPORTABLE_SESSIONS;
8933
+ const store = new ConversationStore(paths);
8934
+ const knownHermesSessions = await readKnownHermesSessions(store);
8935
+ const profileNames = await discoverHermesProfileNames();
8936
+ const result = {
8937
+ scanned_profiles: profileNames.length,
8938
+ scanned_sessions: 0,
8939
+ eligible_sessions: 0,
8940
+ imported_count: 0,
8941
+ reprojected_count: 0,
8942
+ skipped_existing: 0,
8943
+ skipped_hidden: 0,
8944
+ skipped_deleted: 0,
8945
+ skipped_over_limit: 0,
8946
+ errors: []
8947
+ };
8948
+ const candidates = [];
8949
+ for (const profileName of profileNames) {
8950
+ const profileDir = resolveHermesProfileDir(profileName);
8951
+ const dbPath = path14.join(profileDir, "state.db");
8952
+ const sessions = await listProfileSessions(dbPath).catch((error) => {
8953
+ result.errors.push({
8954
+ profile: profileName,
8955
+ message: error instanceof Error ? error.message : String(error)
8956
+ });
8957
+ return [];
8958
+ });
8959
+ result.scanned_sessions += sessions.length;
8960
+ for (const session of sessions) {
8961
+ if (isDeletedSession(session)) {
8962
+ result.skipped_deleted += 1;
8963
+ continue;
8964
+ }
8965
+ if (isHiddenSession(session)) {
8966
+ result.skipped_hidden += 1;
8967
+ continue;
8968
+ }
8969
+ result.eligible_sessions += 1;
8970
+ candidates.push({ profileName, profileDir, dbPath, session });
8971
+ }
8972
+ }
8973
+ candidates.sort((left, right) => {
8974
+ const rightTime = readNumber2(right.session.last_active) ?? 0;
8975
+ const leftTime = readNumber2(left.session.last_active) ?? 0;
8976
+ return rightTime - leftTime;
8977
+ });
8978
+ const importableCandidates = candidates.slice(0, maxImports);
8979
+ result.skipped_over_limit = Math.max(0, candidates.length - maxImports);
8980
+ for (const candidate of importableCandidates) {
8981
+ if (knownHermesSessions.ids.has(candidate.session.id)) {
8982
+ result.skipped_existing += 1;
8983
+ const reprojected = await reprojectExistingHermesConversation({
8984
+ paths,
8985
+ store,
8986
+ logger,
8987
+ candidate,
8988
+ conversationIds: knownHermesSessions.conversationIdsBySessionId.get(
8989
+ candidate.session.id
8990
+ ) ?? []
8991
+ }).catch((error) => {
8992
+ result.errors.push({
8993
+ profile: candidate.profileName,
8994
+ message: error instanceof Error ? error.message : String(error)
8995
+ });
8996
+ return false;
8997
+ });
8998
+ if (reprojected) {
8999
+ result.reprojected_count += 1;
9000
+ }
9001
+ continue;
9002
+ }
9003
+ const imported = await importHermesSession({
9004
+ paths,
9005
+ store,
9006
+ logger,
9007
+ candidate,
9008
+ existingHermesSessionIds: knownHermesSessions.ids
9009
+ }).catch((error) => {
9010
+ result.errors.push({
9011
+ profile: candidate.profileName,
9012
+ message: error instanceof Error ? error.message : String(error)
9013
+ });
9014
+ return false;
9015
+ });
9016
+ if (imported) {
9017
+ result.imported_count += 1;
9018
+ }
9019
+ }
9020
+ if (result.imported_count > 0 || result.reprojected_count > 0 || result.errors.length > 0) {
9021
+ void logger.info("hermes_session_sync_completed", { ...result });
9022
+ } else {
9023
+ void logger.debug("hermes_session_sync_completed", { ...result });
9024
+ }
9025
+ return result;
9026
+ }
9027
+ async function importHermesSession(input) {
9028
+ const { paths, store, logger, candidate, existingHermesSessionIds } = input;
9029
+ const profile = await resolveConversationProfileTarget(
9030
+ paths,
9031
+ candidate.profileName
9032
+ );
9033
+ const sessionId = candidate.session.id;
9034
+ const messages = await readHermesSessionMessages(candidate);
9035
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9036
+ const createdAt = isoFromHermesTime(candidate.session.started_at) ?? now;
9037
+ const updatedAt = isoFromHermesTime(candidate.session.last_active) ?? isoFromHermesTime(messages.at(-1)?.timestamp) ?? createdAt;
9038
+ const conversationId = createConversationId();
9039
+ const snapshot = {
9040
+ schema_version: 1,
9041
+ messages: toLinkMessages({
9042
+ conversationId,
9043
+ profileName: profile.profileName,
9044
+ profileUid: profile.profileUid,
9045
+ profileDisplayName: profile.profileDisplayName,
9046
+ sessionId,
9047
+ messages
9048
+ }),
9049
+ runs: []
9050
+ };
9051
+ const title = readString9(candidate.session, "title") ?? firstUserText(snapshot);
9052
+ const manifest = {
9053
+ id: conversationId,
9054
+ schema_version: 1,
9055
+ kind: "direct",
9056
+ title: normalizeTitle(title),
9057
+ title_source: title ? "hermes" : "default",
9058
+ status: "active",
9059
+ hermes_session_id: sessionId,
9060
+ hermes_session_ids: [sessionId],
9061
+ profile_uid: profile.profileUid,
9062
+ profile_name_snapshot: profile.profileName,
9063
+ profile: profile.profileName,
9064
+ created_at: createdAt,
9065
+ updated_at: updatedAt,
9066
+ last_event_seq: 0
9067
+ };
9068
+ await store.createConversation(manifest, snapshot);
9069
+ await hydrateImportedConversationMedia({
9070
+ paths,
9071
+ store,
9072
+ logger,
9073
+ conversationId
9074
+ });
9075
+ const hydratedSnapshot = await store.readSnapshot(conversationId);
9076
+ await store.appendEvent(conversationId, {
9077
+ type: "conversation.created",
9078
+ payload: {
9079
+ imported_from: "hermes",
9080
+ hermes_session_id: sessionId,
9081
+ profile: {
9082
+ uid: profile.profileUid,
9083
+ name: profile.profileName,
9084
+ display_name: profile.profileDisplayName,
9085
+ avatar_url: profile.profileAvatarUrl
9086
+ }
9087
+ }
9088
+ });
9089
+ for (const message of hydratedSnapshot.messages) {
9090
+ await store.appendEvent(conversationId, {
9091
+ type: "message.created",
9092
+ message_id: message.id,
9093
+ payload: { message, imported_from: "hermes" },
9094
+ raw: message.raw
9095
+ });
9096
+ }
9097
+ const stats = buildConversationStats(
9098
+ await store.readManifest(conversationId),
9099
+ hydratedSnapshot
9100
+ );
9101
+ await store.writeManifest({
9102
+ ...await store.readManifest(conversationId),
9103
+ stats
9104
+ });
9105
+ await upsertConversationStats(
9106
+ paths,
9107
+ toStatsIndexRecord(await store.readManifest(conversationId), stats)
9108
+ );
9109
+ existingHermesSessionIds.add(sessionId);
9110
+ return true;
9111
+ }
9112
+ async function reprojectExistingHermesConversation(input) {
9113
+ let changed = false;
9114
+ for (const conversationId of input.conversationIds) {
9115
+ const manifest = await input.store.readManifest(conversationId).catch(() => null);
9116
+ if (!manifest || manifest.status !== "active") {
9117
+ continue;
9118
+ }
9119
+ const snapshot = await input.store.readSnapshot(conversationId).catch(() => null);
9120
+ if (!snapshot) {
9121
+ continue;
9122
+ }
9123
+ const prefix = collectImportedHermesPrefix(snapshot);
9124
+ if (!prefix?.needsUpgrade || prefix.messages.length === 0) {
9125
+ continue;
9126
+ }
9127
+ const profile = await resolveConversationProfileTarget(
9128
+ input.paths,
9129
+ manifest.profile_name_snapshot ?? manifest.profile ?? input.candidate.profileName
9130
+ );
9131
+ const nextSnapshot = {
9132
+ ...snapshot,
9133
+ messages: [
9134
+ ...toLinkMessages({
9135
+ conversationId,
9136
+ profileName: profile.profileName,
9137
+ profileUid: profile.profileUid,
9138
+ profileDisplayName: profile.profileDisplayName,
9139
+ sessionId: input.candidate.session.id,
9140
+ messages: prefix.messages
9141
+ }),
9142
+ ...snapshot.messages.slice(prefix.endIndex)
9143
+ ]
9144
+ };
9145
+ await input.store.writeSnapshot(conversationId, nextSnapshot);
9146
+ await hydrateImportedConversationMedia({
9147
+ paths: input.paths,
9148
+ store: input.store,
9149
+ logger: input.logger,
9150
+ conversationId
9151
+ });
9152
+ const hydratedSnapshot = await input.store.readSnapshot(conversationId);
9153
+ const stats = buildConversationStats(manifest, hydratedSnapshot);
9154
+ await input.store.writeManifest({ ...manifest, stats });
9155
+ await upsertConversationStats(
9156
+ input.paths,
9157
+ toStatsIndexRecord({ ...manifest, stats }, stats)
9158
+ );
9159
+ changed = true;
9160
+ }
9161
+ return changed;
9162
+ }
9163
+ function collectImportedHermesPrefix(snapshot) {
9164
+ const rows = [];
9165
+ const seen = /* @__PURE__ */ new Set();
9166
+ let needsProjectionVersion = false;
9167
+ let hasToolMetadata = false;
9168
+ let hasMediaDeliveryMarkup = false;
9169
+ let endIndex = 0;
9170
+ for (; endIndex < snapshot.messages.length; endIndex += 1) {
9171
+ const message = snapshot.messages[endIndex];
9172
+ if (!isHermesImportedMessage(message)) {
9173
+ break;
9174
+ }
9175
+ if (message.hermes?.import_projection !== HERMES_IMPORT_PROJECTION_VERSION) {
9176
+ needsProjectionVersion = true;
9177
+ }
9178
+ if (message.role === "tool") {
9179
+ hasToolMetadata = true;
9180
+ }
9181
+ for (const row of readHermesRawMessageRows(message.raw)) {
9182
+ appendHermesRowOnce(rows, seen, row);
9183
+ if (hasHermesToolMetadata(row)) {
9184
+ hasToolMetadata = true;
9185
+ }
9186
+ if (collectMediaTags(normalizeContent(row.content)).length > 0) {
9187
+ hasMediaDeliveryMarkup = true;
9188
+ }
9189
+ }
9190
+ for (const event of message.agent_events ?? []) {
9191
+ for (const row of readHermesRowsFromAgentEvent(event)) {
9192
+ appendHermesRowOnce(rows, seen, row);
9193
+ if (hasHermesToolMetadata(row)) {
9194
+ hasToolMetadata = true;
9195
+ }
9196
+ if (collectMediaTags(normalizeContent(row.content)).length > 0) {
9197
+ hasMediaDeliveryMarkup = true;
9198
+ }
9199
+ }
9200
+ }
9201
+ }
9202
+ if (rows.length === 0 || endIndex === 0) {
9203
+ return null;
9204
+ }
9205
+ return {
9206
+ endIndex,
9207
+ messages: rows,
9208
+ needsUpgrade: needsProjectionVersion && (hasToolMetadata || hasMediaDeliveryMarkup)
9209
+ };
9210
+ }
9211
+ function isHermesImportedMessage(message) {
9212
+ return message.hermes?.imported_from === "hermes" || message.raw?.format === "hermes-message" || message.raw?.format === "hermes-message-group";
9213
+ }
9214
+ function readHermesRowsFromAgentEvent(event) {
9215
+ if (event.raw?.format !== "hermes-message" && event.raw?.format !== "hermes-message-group") {
9216
+ return [];
9217
+ }
9218
+ const payload = toRecord8(event.raw.payload);
9219
+ const message = toRecord8(payload.message);
9220
+ if (normalizeMessageRole(readString9(message, "role") ?? void 0) === "tool") {
9221
+ return [message];
9222
+ }
9223
+ return readHermesRawMessageRows(event.raw).filter(
9224
+ (row) => Boolean(readString9(row, "role"))
9225
+ );
9226
+ }
9227
+ function appendHermesRowOnce(rows, seen, row) {
9228
+ const key = hermesRowKey(row, rows.length);
8822
9229
  if (seen.has(key)) {
8823
9230
  return;
8824
9231
  }
@@ -8832,7 +9239,7 @@ function hermesRowKey(row, fallbackIndex) {
8832
9239
  return `fallback:${fallbackIndex}:${row.role ?? ""}:${row.timestamp ?? ""}:${normalizeContent(row.content)}`;
8833
9240
  }
8834
9241
  function hasHermesToolMetadata(row) {
8835
- return normalizeMessageRole(row.role) === "tool" || readHermesToolCalls(row).length > 0 || Boolean(readString8(row, "tool_call_id")) || Boolean(readString8(row, "tool_name"));
9242
+ return normalizeMessageRole(row.role) === "tool" || readHermesToolCalls(row).length > 0 || Boolean(readString9(row, "tool_call_id")) || Boolean(readString9(row, "tool_name"));
8836
9243
  }
8837
9244
  function toLinkMessages(input) {
8838
9245
  const linkMessages = [];
@@ -8950,8 +9357,8 @@ function toLinkMessages(input) {
8950
9357
  return linkMessages;
8951
9358
  }
8952
9359
  function consumePendingToolCall(input) {
8953
- const toolCallId = readString8(input.toolMessage, "tool_call_id");
8954
- const toolName = readString8(input.toolMessage, "tool_name");
9360
+ const toolCallId = readString9(input.toolMessage, "tool_call_id");
9361
+ const toolName = readString9(input.toolMessage, "tool_name");
8955
9362
  let pending = toolCallId ? input.toolCallsById.get(toolCallId) : void 0;
8956
9363
  if (!pending && toolName) {
8957
9364
  pending = input.pendingToolCalls.find(
@@ -9001,13 +9408,13 @@ function readHermesToolCalls(message) {
9001
9408
  );
9002
9409
  }
9003
9410
  function normalizeHermesToolCall(value) {
9004
- const record = toRecord7(value);
9411
+ const record = toRecord8(value);
9005
9412
  if (Object.keys(record).length === 0) {
9006
9413
  return null;
9007
9414
  }
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";
9415
+ const fn = toRecord8(record.function);
9416
+ const id = readString9(record, "id") ?? readString9(record, "call_id") ?? readString9(record, "tool_call_id") ?? readString9(fn, "id") ?? void 0;
9417
+ const name = readString9(fn, "name") ?? readString9(record, "name") ?? readString9(record, "tool_name") ?? readString9(record, "tool") ?? "tool";
9011
9418
  const rawArguments = fn.arguments ?? record.arguments ?? record.args ?? record.input;
9012
9419
  return {
9013
9420
  ...id ? { id } : {},
@@ -9046,8 +9453,8 @@ function projectHermesToolCompletedEvent(input) {
9046
9453
  const createdAt = isoFromHermesTime(input.sourceMessage.timestamp) ?? new Date(Date.now() + input.index).toISOString();
9047
9454
  const output = normalizeContent(input.sourceMessage.content);
9048
9455
  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";
9456
+ const toolCallId = readString9(input.sourceMessage, "tool_call_id") ?? input.pending?.toolCall.id;
9457
+ const toolName = readString9(input.sourceMessage, "tool_name") ?? input.pending?.toolCall.name ?? "tool";
9051
9458
  return projectHermesAgentEvent({
9052
9459
  conversationId: input.conversationId,
9053
9460
  messageId: input.messageId,
@@ -9215,219 +9622,75 @@ function appendAgentEventBlock(message, event, updatedAt) {
9215
9622
  }
9216
9623
  function latestTimestamp(left, right) {
9217
9624
  const leftTime = Date.parse(left);
9218
- const rightTime = Date.parse(right);
9219
- if (Number.isNaN(leftTime)) {
9220
- return right;
9221
- }
9222
- if (Number.isNaN(rightTime)) {
9223
- return left;
9224
- }
9225
- return leftTime >= rightTime ? left : right;
9226
- }
9227
- async function readKnownHermesSessions(store) {
9228
- const ids = /* @__PURE__ */ new Set();
9229
- const conversationIdsBySessionId = /* @__PURE__ */ new Map();
9230
- for (const conversationId of await store.listConversationIds()) {
9231
- const manifest = await store.readManifest(conversationId).catch(() => null);
9232
- if (!manifest) {
9233
- continue;
9234
- }
9235
- if (manifest.hermes_session_id) {
9236
- ids.add(manifest.hermes_session_id);
9237
- rememberKnownHermesConversation(
9238
- conversationIdsBySessionId,
9239
- manifest.hermes_session_id,
9240
- conversationId
9241
- );
9242
- }
9243
- for (const sessionId of manifest.hermes_session_ids ?? []) {
9244
- ids.add(sessionId);
9245
- rememberKnownHermesConversation(
9246
- conversationIdsBySessionId,
9247
- sessionId,
9248
- conversationId
9249
- );
9250
- }
9251
- }
9252
- return { ids, conversationIdsBySessionId };
9253
- }
9254
- function rememberKnownHermesConversation(map, sessionId, conversationId) {
9255
- const current = map.get(sessionId) ?? [];
9256
- if (!current.includes(conversationId)) {
9257
- map.set(sessionId, [...current, conversationId]);
9258
- }
9259
- }
9260
- async function discoverHermesProfileNames() {
9261
- 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(
9264
- (error) => {
9265
- if (isNodeError9(error, "ENOENT")) {
9266
- return [];
9267
- }
9268
- throw error;
9269
- }
9270
- );
9271
- for (const entry of entries) {
9272
- if (entry.isDirectory() && PROFILE_NAME_PATTERN3.test(entry.name)) {
9273
- names.add(entry.name);
9274
- }
9275
- }
9276
- return [...names].sort((left, right) => {
9277
- if (left === DEFAULT_PROFILE_NAME) {
9278
- return -1;
9279
- }
9280
- if (right === DEFAULT_PROFILE_NAME) {
9281
- return 1;
9282
- }
9283
- return left.localeCompare(right);
9284
- });
9285
- }
9286
- async function listProfileSessions(dbPath) {
9287
- if (!await isFile(dbPath)) {
9288
- return [];
9289
- }
9290
- let db = null;
9291
- try {
9292
- const { DatabaseSync } = nodeRequire3(
9293
- "node:sqlite"
9294
- );
9295
- db = new DatabaseSync(dbPath, {
9296
- readOnly: true,
9297
- timeout: 1e3
9298
- });
9299
- const sessionColumns = readTableColumns(db, "sessions");
9300
- if (!sessionColumns.has("id")) {
9301
- return [];
9302
- }
9303
- const messageColumns = readTableColumns(db, "messages");
9304
- const selectColumns = [...sessionColumns].map((column) => `s.${quoteIdentifier(column)}`).join(", ");
9305
- const lastActiveSql = messageColumns.has("timestamp") ? `COALESCE(
9306
- (SELECT MAX(m.timestamp) FROM messages m WHERE m.session_id = s.id),
9307
- s.started_at,
9308
- 0
9309
- ) AS last_active` : "COALESCE(s.started_at, 0) AS last_active";
9310
- const rows = db.prepare(
9311
- `
9312
- SELECT ${selectColumns}, ${lastActiveSql}
9313
- FROM sessions s
9314
- ORDER BY last_active DESC
9315
- `
9316
- ).all();
9317
- return projectCompressionTips(rows);
9318
- } finally {
9319
- db?.close();
9320
- }
9321
- }
9322
- function appendHermesRawMessage(message, row) {
9323
- const rows = readHermesRawMessageRows(message.raw);
9324
- message.raw = rows.length === 0 ? {
9325
- format: "hermes-message",
9326
- payload: row
9327
- } : {
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) {
9625
+ const rightTime = Date.parse(right);
9626
+ if (Number.isNaN(leftTime)) {
9360
9627
  return right;
9361
9628
  }
9362
- if (!right) {
9629
+ if (Number.isNaN(rightTime)) {
9363
9630
  return left;
9364
9631
  }
9365
- if (/\s$/u.test(left) || /^\s/u.test(right)) {
9366
- return `${left}${right}`;
9367
- }
9368
- return `${left}
9369
-
9370
- ${right}`;
9632
+ return leftTime >= rightTime ? left : right;
9371
9633
  }
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) {
9634
+ async function readKnownHermesSessions(store) {
9635
+ const ids = /* @__PURE__ */ new Set();
9636
+ const conversationIdsBySessionId = /* @__PURE__ */ new Map();
9637
+ for (const conversationId of await store.listConversationIds()) {
9638
+ const manifest = await store.readManifest(conversationId).catch(() => null);
9639
+ if (!manifest) {
9378
9640
  continue;
9379
9641
  }
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);
9642
+ if (manifest.hermes_session_id) {
9643
+ ids.add(manifest.hermes_session_id);
9644
+ rememberKnownHermesConversation(
9645
+ conversationIdsBySessionId,
9646
+ manifest.hermes_session_id,
9647
+ conversationId
9648
+ );
9386
9649
  }
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;
9650
+ for (const sessionId of manifest.hermes_session_ids ?? []) {
9651
+ ids.add(sessionId);
9652
+ rememberKnownHermesConversation(
9653
+ conversationIdsBySessionId,
9654
+ sessionId,
9655
+ conversationId
9656
+ );
9393
9657
  }
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;
9658
+ }
9659
+ return { ids, conversationIdsBySessionId };
9660
+ }
9661
+ function rememberKnownHermesConversation(map, sessionId, conversationId) {
9662
+ const current = map.get(sessionId) ?? [];
9663
+ if (!current.includes(conversationId)) {
9664
+ map.set(sessionId, [...current, conversationId]);
9665
+ }
9666
+ }
9667
+ async function discoverHermesProfileNames() {
9668
+ const names = /* @__PURE__ */ new Set([DEFAULT_PROFILE_NAME]);
9669
+ const profilesDir = path14.join(os4.homedir(), ".hermes", "profiles");
9670
+ const entries = await readdir6(profilesDir, { withFileTypes: true }).catch(
9671
+ (error) => {
9672
+ if (isNodeError10(error, "ENOENT")) {
9673
+ return [];
9407
9674
  }
9408
- tip = next;
9409
- visited.add(nextId);
9675
+ throw error;
9410
9676
  }
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
- });
9677
+ );
9678
+ for (const entry of entries) {
9679
+ if (entry.isDirectory() && PROFILE_NAME_PATTERN3.test(entry.name)) {
9680
+ names.add(entry.name);
9419
9681
  }
9420
9682
  }
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;
9683
+ return [...names].sort((left, right) => {
9684
+ if (left === DEFAULT_PROFILE_NAME) {
9685
+ return -1;
9686
+ }
9687
+ if (right === DEFAULT_PROFILE_NAME) {
9688
+ return 1;
9689
+ }
9690
+ return left.localeCompare(right);
9691
+ });
9429
9692
  }
9430
- async function readStateDbMessages(dbPath, sessionId) {
9693
+ async function listProfileSessions(dbPath) {
9431
9694
  if (!await isFile(dbPath)) {
9432
9695
  return [];
9433
9696
  }
@@ -9440,568 +9703,490 @@ async function readStateDbMessages(dbPath, sessionId) {
9440
9703
  readOnly: true,
9441
9704
  timeout: 1e3
9442
9705
  });
9443
- const columns = readTableColumns(db, "messages");
9444
- if (!columns.has("session_id") || !columns.has("role")) {
9706
+ const sessionColumns = readTableColumns(db, "sessions");
9707
+ if (!sessionColumns.has("id")) {
9445
9708
  return [];
9446
9709
  }
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
9710
+ const messageColumns = readTableColumns(db, "messages");
9711
+ const selectColumns = [...sessionColumns].map((column) => `s.${quoteIdentifier(column)}`).join(", ");
9712
+ const lastActiveSql = messageColumns.has("timestamp") ? `COALESCE(
9713
+ (SELECT MAX(m.timestamp) FROM messages m WHERE m.session_id = s.id),
9714
+ s.started_at,
9715
+ 0
9716
+ ) AS last_active` : "COALESCE(s.started_at, 0) AS last_active";
9717
+ const rows = db.prepare(
9456
9718
  `
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("");
9719
+ SELECT ${selectColumns}, ${lastActiveSql}
9720
+ FROM sessions s
9721
+ ORDER BY last_active DESC
9722
+ `
9723
+ ).all();
9724
+ return projectCompressionTips(rows);
9725
+ } finally {
9726
+ db?.close();
9596
9727
  }
9597
- return "";
9598
9728
  }
9599
- function parseJsonValue(value) {
9600
- if (typeof value !== "string") {
9601
- return void 0;
9729
+ function appendHermesRawMessage(message, row) {
9730
+ const rows = readHermesRawMessageRows(message.raw);
9731
+ message.raw = rows.length === 0 ? {
9732
+ format: "hermes-message",
9733
+ payload: row
9734
+ } : {
9735
+ format: "hermes-message-group",
9736
+ payload: { messages: [...rows, row] }
9737
+ };
9738
+ }
9739
+ function readHermesRawMessageRows(raw) {
9740
+ if (!raw) {
9741
+ return [];
9602
9742
  }
9603
- const trimmed = value.trim();
9604
- if (!trimmed) {
9605
- return void 0;
9743
+ if (raw.format === "hermes-message-group") {
9744
+ const payload = toRecord8(raw.payload);
9745
+ return Array.isArray(payload.messages) ? payload.messages.filter(
9746
+ (item) => typeof item === "object" && item !== null
9747
+ ) : [];
9606
9748
  }
9607
- try {
9608
- return JSON.parse(trimmed);
9609
- } catch {
9610
- return void 0;
9749
+ if (raw.format === "hermes-message") {
9750
+ return typeof raw.payload === "object" && raw.payload !== null ? [raw.payload] : [];
9611
9751
  }
9752
+ return [];
9612
9753
  }
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();
9754
+ function rememberHermesMessageId(message, row) {
9755
+ if (row.id === void 0 || row.id === null) {
9756
+ return;
9633
9757
  }
9758
+ const existing = Array.isArray(message.hermes?.message_ids) ? message.hermes.message_ids : message.hermes?.message_id === void 0 ? [] : [message.hermes.message_id];
9759
+ const id = row.id;
9760
+ message.hermes = {
9761
+ ...message.hermes ?? {},
9762
+ message_ids: existing.includes(id) ? existing : [...existing, id]
9763
+ };
9634
9764
  }
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;
9765
+ function joinImportedText(left, right) {
9766
+ if (!left) {
9767
+ return right;
9653
9768
  }
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;
9769
+ if (!right) {
9770
+ return left;
9771
+ }
9772
+ if (/\s$/u.test(left) || /^\s/u.test(right)) {
9773
+ return `${left}${right}`;
9774
+ }
9775
+ return `${left}
9776
+
9777
+ ${right}`;
9663
9778
  }
9664
- function readBoolean(value) {
9665
- if (value === true || value === 1) {
9666
- return true;
9779
+ function projectCompressionTips(rows) {
9780
+ const byId = /* @__PURE__ */ new Map();
9781
+ const childrenByParent = /* @__PURE__ */ new Map();
9782
+ for (const row of rows) {
9783
+ const id = readString9(row, "id");
9784
+ if (!id) {
9785
+ continue;
9786
+ }
9787
+ byId.set(id, row);
9788
+ const parentId = readString9(row, "parent_session_id");
9789
+ if (parentId) {
9790
+ const children = childrenByParent.get(parentId) ?? [];
9791
+ children.push(row);
9792
+ childrenByParent.set(parentId, children);
9793
+ }
9667
9794
  }
9668
- if (typeof value === "string") {
9669
- return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
9795
+ const projected = [];
9796
+ for (const row of rows) {
9797
+ const id = readString9(row, "id");
9798
+ if (!id || readString9(row, "parent_session_id")) {
9799
+ continue;
9800
+ }
9801
+ let tip = row;
9802
+ const visited = /* @__PURE__ */ new Set([id]);
9803
+ while (readString9(tip, "end_reason") === "compression") {
9804
+ const tipId2 = readString9(tip, "id");
9805
+ if (!tipId2) {
9806
+ break;
9807
+ }
9808
+ const next = (childrenByParent.get(tipId2) ?? []).filter((child) => readString9(child, "id")).sort(
9809
+ (left, right) => (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
9810
+ )[0];
9811
+ const nextId = next ? readString9(next, "id") : null;
9812
+ if (!next || !nextId || visited.has(nextId)) {
9813
+ break;
9814
+ }
9815
+ tip = next;
9816
+ visited.add(nextId);
9817
+ }
9818
+ const tipId = readString9(tip, "id");
9819
+ if (tipId) {
9820
+ projected.push({
9821
+ ...tip,
9822
+ id: tipId,
9823
+ _lineage_root_id: id,
9824
+ started_at: readNumber2(row.started_at) ?? readNumber2(tip.started_at)
9825
+ });
9826
+ }
9670
9827
  }
9671
- return false;
9828
+ return projected;
9672
9829
  }
9673
- function isNodeError9(error, code) {
9674
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
9830
+ async function readHermesSessionMessages(candidate) {
9831
+ const [dbMessages, jsonlMessages] = await Promise.all([
9832
+ readStateDbMessages(candidate.dbPath, candidate.session.id),
9833
+ readJsonlMessages(candidate.profileName, candidate.session.id)
9834
+ ]);
9835
+ return jsonlMessages.length > dbMessages.length ? jsonlMessages : dbMessages;
9675
9836
  }
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
- );
9837
+ async function readStateDbMessages(dbPath, sessionId) {
9838
+ if (!await isFile(dbPath)) {
9839
+ return [];
9758
9840
  }
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"
9841
+ let db = null;
9842
+ try {
9843
+ const { DatabaseSync } = nodeRequire3(
9844
+ "node:sqlite"
9765
9845
  );
9846
+ db = new DatabaseSync(dbPath, {
9847
+ readOnly: true,
9848
+ timeout: 1e3
9849
+ });
9850
+ const columns = readTableColumns(db, "messages");
9851
+ if (!columns.has("session_id") || !columns.has("role")) {
9852
+ return [];
9853
+ }
9854
+ const selectColumns = MESSAGE_COLUMNS.map(
9855
+ (column) => columns.has(column) ? quoteIdentifier(column) : `NULL AS ${column}`
9856
+ ).join(", ");
9857
+ return db.prepare(
9858
+ `
9859
+ SELECT ${selectColumns}
9860
+ FROM messages
9861
+ WHERE session_id = ?
9862
+ ORDER BY timestamp, id
9863
+ `
9864
+ ).all(sessionId);
9865
+ } catch {
9866
+ return [];
9867
+ } finally {
9868
+ db?.close();
9766
9869
  }
9767
- return {
9768
- conversationId: segments[0],
9769
- runId: segments[2],
9770
- stagingDir: resolvedDir
9771
- };
9772
9870
  }
9773
- async function collectStagedDeliveryReferences(stagingDir) {
9774
- const directoryStat = await lstat(stagingDir).catch((error) => {
9871
+ async function readJsonlMessages(profileName, sessionId) {
9872
+ if (!/^[A-Za-z0-9._:-]{1,160}$/u.test(sessionId)) {
9873
+ return [];
9874
+ }
9875
+ const profileDir = resolveHermesProfileDir(profileName);
9876
+ const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path14.join(profileDir, "sessions"));
9877
+ const transcriptPath = path14.join(sessionsDir, `${sessionId}.jsonl`);
9878
+ const raw = await readFile9(transcriptPath, "utf8").catch((error) => {
9775
9879
  if (isNodeError10(error, "ENOENT")) {
9776
- throw new LinkHttpError(
9777
- 404,
9778
- "delivery_staging_not_found",
9779
- "delivery staging directory was not found"
9780
- );
9880
+ return "";
9781
9881
  }
9782
9882
  throw error;
9783
9883
  });
9784
- if (!directoryStat.isDirectory()) {
9785
- throw new LinkHttpError(
9786
- 400,
9787
- "delivery_staging_not_directory",
9788
- "delivery staging path is not a directory"
9789
- );
9884
+ if (!raw.trim()) {
9885
+ return [];
9790
9886
  }
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
- });
9887
+ const rows = [];
9888
+ for (const line of raw.split(/\r?\n/u)) {
9889
+ if (!line.trim()) {
9890
+ continue;
9891
+ }
9892
+ try {
9893
+ const parsed = JSON.parse(line);
9894
+ const normalized = normalizeJsonlMessage(parsed);
9895
+ if (normalized) {
9896
+ rows.push(normalized);
9897
+ }
9898
+ } catch {
9899
+ continue;
9900
+ }
9901
+ }
9902
+ return rows;
9803
9903
  }
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);
9904
+ function normalizeJsonlMessage(row) {
9905
+ const role = readString9(row, "role");
9906
+ if (!role) {
9907
+ return null;
9808
9908
  }
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);
9909
+ const content = normalizeContent(row.content);
9910
+ const timestamp = readNumber2(row.timestamp) ?? readNumber2(row.created_at) ?? readNumber2(row.createdAt);
9911
+ return {
9912
+ ...row,
9913
+ role,
9914
+ content,
9915
+ timestamp: timestamp ?? void 0
9916
+ };
9917
+ }
9918
+ async function hydrateImportedConversationMedia(input) {
9919
+ const snapshot = await input.store.readSnapshot(input.conversationId);
9920
+ const imports = snapshot.messages.map((message) => ({
9921
+ messageId: message.id,
9922
+ references: collectMessageMediaReferences(message)
9923
+ })).filter((item) => item.references.length > 0);
9924
+ if (imports.length === 0) {
9925
+ return;
9815
9926
  }
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);
9927
+ const outcomes = /* @__PURE__ */ new Map();
9928
+ for (const item of imports) {
9929
+ const result = await importMediaReferencesForMessage(
9930
+ {
9931
+ logger: input.logger,
9932
+ readSnapshot: (conversationId) => input.store.readSnapshot(conversationId),
9933
+ writeSnapshot: (conversationId, nextSnapshot2) => input.store.writeSnapshot(conversationId, nextSnapshot2),
9934
+ appendEvent: async (conversationId, event) => ({
9935
+ seq: 0,
9936
+ conversation_id: conversationId,
9937
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
9938
+ ...event
9939
+ }),
9940
+ writeBlob: (conversationId, blob) => writeConversationBlob(input.paths, conversationId, blob, {
9941
+ maxBytes: MAX_IMPORTED_HERMES_MEDIA_BYTES
9942
+ })
9943
+ },
9944
+ {
9945
+ conversationId: input.conversationId,
9946
+ runId: IMPORTED_MEDIA_PLACEHOLDER_RUN_ID,
9947
+ messageId: item.messageId,
9948
+ references: item.references
9857
9949
  }
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)
9950
+ );
9951
+ if (result.imported_count > 0 || result.failed_count > 0) {
9952
+ outcomes.set(item.messageId, {
9953
+ imported: result.imported_count > 0,
9954
+ failed: result.failed_count > 0
9863
9955
  });
9864
9956
  }
9865
9957
  }
9866
- if (importedParts.length === 0 && newFailures.length === 0) {
9867
- return {
9868
- ...emptyImportResult(input),
9869
- discovered_count: references.length,
9870
- skipped_count: skippedCount
9871
- };
9958
+ if (outcomes.size === 0) {
9959
+ return;
9960
+ }
9961
+ const nextSnapshot = await input.store.readSnapshot(input.conversationId);
9962
+ let changed = false;
9963
+ for (const message of nextSnapshot.messages) {
9964
+ const outcome = outcomes.get(message.id);
9965
+ if (!outcome) {
9966
+ continue;
9967
+ }
9968
+ if (outcome.imported) {
9969
+ cleanMessageTextParts(message);
9970
+ changed = true;
9971
+ }
9972
+ if (outcome.failed) {
9973
+ changed = appendImportedMediaImportFailureNotice(message) || changed;
9974
+ }
9975
+ }
9976
+ if (changed) {
9977
+ await input.store.writeSnapshot(input.conversationId, nextSnapshot);
9978
+ }
9979
+ }
9980
+ function collectMessageMediaReferences(message) {
9981
+ return message.parts.flatMap(
9982
+ (part) => part.type === "text" && part.text ? collectMediaTags(part.text) : []
9983
+ );
9984
+ }
9985
+ function appendImportedMediaImportFailureNotice(message) {
9986
+ const hermes = toRecord8(message.hermes);
9987
+ if (hermes.media_import_failure_notice_appended === true) {
9988
+ return false;
9989
+ }
9990
+ const failures = readMediaImportFailures(message);
9991
+ if (failures.length === 0) {
9992
+ return false;
9993
+ }
9994
+ const notice = formatImportedMediaImportFailureNotice(failures);
9995
+ const textPart = message.parts.find((part) => part.type === "text");
9996
+ if (textPart) {
9997
+ const currentText = textPart.text ?? "";
9998
+ const separator = currentText.trim().length > 0 ? "\n\n" : "";
9999
+ textPart.text = `${currentText.trimEnd()}${separator}${notice}`;
10000
+ } else {
10001
+ message.parts.unshift({ type: "text", text: notice });
10002
+ }
10003
+ message.hermes = {
10004
+ ...hermes,
10005
+ media_import_failure_notice_appended: true
10006
+ };
10007
+ return true;
10008
+ }
10009
+ function formatImportedMediaImportFailureNotice(failures) {
10010
+ const filenames = failures.map((failure) => failure.filename);
10011
+ const target = filenames.length === 1 ? `\u6587\u4EF6\u201C${filenames[0]}\u201D` : `${filenames.length} \u4E2A\u6587\u4EF6\uFF08${formatImportedFilenameList(filenames)}\uFF09`;
10012
+ const permissionDenied = failures.some((failure) => {
10013
+ const code = failure.code?.toUpperCase();
10014
+ return code === "EPERM" || code === "EACCES" || /operation not permitted|permission denied/iu.test(failure.reason);
10015
+ });
10016
+ if (permissionDenied) {
10017
+ 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`;
10018
+ }
10019
+ 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`;
10020
+ }
10021
+ function formatImportedFilenameList(filenames) {
10022
+ const preview = filenames.slice(0, 3).map((filename) => `\u201C${filename}\u201D`);
10023
+ const remaining = filenames.length - preview.length;
10024
+ return remaining > 0 ? `${preview.join("\u3001")} \u7B49 ${filenames.length} \u4E2A` : preview.join("\u3001");
10025
+ }
10026
+ function toLinkMessage(input) {
10027
+ const role = normalizeMessageRole(input.message.role);
10028
+ const text = normalizeContent(input.message.content);
10029
+ const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
10030
+ return {
10031
+ id: `msg_${randomUUID6().replaceAll("-", "")}`,
10032
+ schema_version: 1,
10033
+ conversation_id: input.conversationId,
10034
+ role,
10035
+ status: "completed",
10036
+ created_at: createdAt,
10037
+ updated_at: createdAt,
10038
+ sender: senderForRole({
10039
+ role,
10040
+ profileName: input.profileName,
10041
+ profileUid: input.profileUid,
10042
+ profileDisplayName: input.profileDisplayName
10043
+ }),
10044
+ parts: text ? [{ type: "text", text }] : [],
10045
+ attachments: [],
10046
+ hermes: {
10047
+ session_id: input.sessionId,
10048
+ message_id: input.message.id,
10049
+ imported_from: "hermes",
10050
+ import_projection: HERMES_IMPORT_PROJECTION_VERSION
10051
+ },
10052
+ raw: {
10053
+ format: "hermes-message",
10054
+ payload: input.message
10055
+ }
10056
+ };
10057
+ }
10058
+ function senderForRole(input) {
10059
+ switch (input.role) {
10060
+ case "user":
10061
+ return { id: "hermes_user", type: "human", display_name: "Me" };
10062
+ case "assistant":
10063
+ return {
10064
+ id: `agent_${input.profileName}`,
10065
+ type: "agent",
10066
+ display_name: input.profileDisplayName,
10067
+ profile_uid: input.profileUid,
10068
+ profile: input.profileName
10069
+ };
10070
+ case "tool":
10071
+ return { id: "hermes_tool", type: "tool", display_name: "Tool" };
10072
+ case "system":
10073
+ return { id: "hermes_system", type: "system", display_name: "System" };
10074
+ }
10075
+ }
10076
+ function firstUserText(snapshot) {
10077
+ return snapshot.messages.find((message) => message.role === "user")?.parts.find((part) => part.type === "text")?.text?.slice(0, 80);
10078
+ }
10079
+ function normalizeTitle(value) {
10080
+ const normalized = value?.replace(/\s+/gu, " ").trim();
10081
+ return normalized || DEFAULT_CONVERSATION_TITLE;
10082
+ }
10083
+ function normalizeMessageRole(value) {
10084
+ switch (value?.trim().toLowerCase()) {
10085
+ case "user":
10086
+ return "user";
10087
+ case "assistant":
10088
+ return "assistant";
10089
+ case "tool":
10090
+ return "tool";
10091
+ case "system":
10092
+ return "system";
10093
+ default:
10094
+ return "system";
10095
+ }
10096
+ }
10097
+ function normalizeContent(value) {
10098
+ if (typeof value === "string") {
10099
+ return value;
9872
10100
  }
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
- )
9880
- };
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;
10101
+ if (Array.isArray(value)) {
10102
+ return value.map((item) => {
10103
+ if (typeof item === "string") {
10104
+ return item;
10105
+ }
10106
+ if (typeof item === "object" && item !== null) {
10107
+ return readString9(item, "text") ?? "";
10108
+ }
10109
+ return "";
10110
+ }).filter(Boolean).join("");
9892
10111
  }
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
- };
10112
+ return "";
9904
10113
  }
9905
- function readMediaImportFailures(message) {
9906
- const hermes = toRecord8(message.hermes);
9907
- const failures = hermes.media_import_failures;
9908
- if (!Array.isArray(failures)) {
9909
- return [];
10114
+ function parseJsonValue(value) {
10115
+ if (typeof value !== "string") {
10116
+ return void 0;
10117
+ }
10118
+ const trimmed = value.trim();
10119
+ if (!trimmed) {
10120
+ return void 0;
10121
+ }
10122
+ try {
10123
+ return JSON.parse(trimmed);
10124
+ } catch {
10125
+ return void 0;
9910
10126
  }
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") } : {}
9925
- }
9926
- ];
9927
- });
9928
10127
  }
9929
- function readFailedMediaSourceKeys(message) {
9930
- const hermes = toRecord8(message.hermes);
9931
- const keys = hermes.media_import_failed_source_keys;
9932
- if (!Array.isArray(keys)) {
10128
+ function toRecord8(value) {
10129
+ return typeof value === "object" && value !== null ? value : {};
10130
+ }
10131
+ function isDeletedSession(session) {
10132
+ return readBoolean(session.deleted) || readBoolean(session.is_deleted) || Boolean(readString9(session, "deleted_at")) || ["deleted", "removed"].includes(readString9(session, "status") ?? "");
10133
+ }
10134
+ function isHiddenSession(session) {
10135
+ const source = readString9(session, "source")?.toLowerCase();
10136
+ const status = readString9(session, "status")?.toLowerCase();
10137
+ const visibility = readString9(session, "visibility")?.toLowerCase();
10138
+ 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";
10139
+ }
10140
+ function readTableColumns(db, tableName) {
10141
+ try {
10142
+ const rows = db.prepare(`PRAGMA table_info(${quoteIdentifier(tableName)})`).all();
10143
+ return new Set(
10144
+ rows.map((row) => typeof row.name === "string" ? row.name : "").filter(Boolean)
10145
+ );
10146
+ } catch {
9933
10147
  return /* @__PURE__ */ new Set();
9934
10148
  }
9935
- return new Set(
9936
- keys.filter(
9937
- (key) => typeof key === "string" && key.length > 0
9938
- )
9939
- );
9940
10149
  }
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
- };
10150
+ function quoteIdentifier(value) {
10151
+ return `"${value.replaceAll('"', '""')}"`;
9952
10152
  }
9953
- async function writeBlobFromFile(deps, conversationId, source) {
9954
- const sourcePath = resolveMediaSourcePath(source.path);
9955
- const fileStat = await stat8(sourcePath).catch((error) => {
10153
+ async function isFile(filePath) {
10154
+ return stat8(filePath).then((value) => value.isFile()).catch((error) => {
9956
10155
  if (isNodeError10(error, "ENOENT")) {
9957
- throw new LinkHttpError(
9958
- 404,
9959
- "media_source_not_found",
9960
- "Hermes output file was not found"
9961
- );
10156
+ return false;
9962
10157
  }
9963
10158
  throw error;
9964
10159
  });
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
10160
  }
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
- };
10161
+ function createConversationId() {
10162
+ return `conv_${randomUUID6().replaceAll("-", "")}`;
9992
10163
  }
9993
- function isSupportedDeliveryFilename(filename) {
9994
- return SUPPORTED_DELIVERY_EXTENSIONS.has(path14.extname(filename).toLowerCase());
10164
+ function isoFromHermesTime(value) {
10165
+ const numeric = readNumber2(value);
10166
+ if (!numeric || numeric <= 0) {
10167
+ return void 0;
10168
+ }
10169
+ const millis = numeric > 1e10 ? numeric : numeric * 1e3;
10170
+ return new Date(millis).toISOString();
9995
10171
  }
9996
10172
  function readString9(payload, key) {
9997
10173
  const value = payload[key];
9998
10174
  return typeof value === "string" && value.trim() ? value.trim() : null;
9999
10175
  }
10000
- function toRecord8(value) {
10001
- return typeof value === "object" && value !== null ? value : {};
10176
+ function readNumber2(value) {
10177
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
10178
+ }
10179
+ function readBoolean(value) {
10180
+ if (value === true || value === 1) {
10181
+ return true;
10182
+ }
10183
+ if (typeof value === "string") {
10184
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
10185
+ }
10186
+ return false;
10002
10187
  }
10003
10188
  function isNodeError10(error, code) {
10004
- return typeof error === "object" && error !== null && "code" in error && (code === void 0 || error.code === code);
10189
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
10005
10190
  }
10006
10191
 
10007
10192
  // src/conversations/run-lifecycle.ts
@@ -11278,8 +11463,9 @@ function readChatCompletionUsage(payload) {
11278
11463
  const input = readInteger2(usage, "prompt_tokens") ?? readInteger2(usage, "input_tokens");
11279
11464
  const output = readInteger2(usage, "completion_tokens") ?? readInteger2(usage, "output_tokens");
11280
11465
  const total = readInteger2(usage, "total_tokens");
11281
- const contextWindow = readInteger2(usage, "context_window") ?? readInteger2(usage, "context_max");
11466
+ const contextWindow = readInteger2(usage, "context_window") ?? readInteger2(usage, "context_max") ?? readInteger2(usage, "context_length");
11282
11467
  const explicitContextTokens = readInteger2(usage, "context_tokens") ?? readInteger2(usage, "context_used") ?? readInteger2(usage, "current_context_tokens") ?? readInteger2(usage, "last_prompt_tokens");
11468
+ const explicitUsagePercent = readInteger2(usage, "usage_percent") ?? readInteger2(usage, "context_percent");
11283
11469
  if (input === void 0 && output === void 0 && total === void 0) {
11284
11470
  return void 0;
11285
11471
  }
@@ -11290,7 +11476,7 @@ function readChatCompletionUsage(payload) {
11290
11476
  ...explicitContextTokens !== void 0 ? { context_tokens: explicitContextTokens } : {},
11291
11477
  ...contextWindow !== void 0 ? { context_window: contextWindow } : {},
11292
11478
  ...explicitContextTokens !== void 0 && contextWindow ? {
11293
- usage_percent: Math.min(
11479
+ usage_percent: explicitUsagePercent !== void 0 ? Math.min(100, explicitUsagePercent) : Math.min(
11294
11480
  100,
11295
11481
  Math.round(explicitContextTokens / contextWindow * 100)
11296
11482
  )
@@ -11437,10 +11623,20 @@ var ConversationRunLifecycle = class {
11437
11623
  });
11438
11624
  return void 0;
11439
11625
  });
11626
+ const instructions = buildRunInstructions(run, deliveryStagingDir);
11627
+ const estimatedUsage = estimateContextUsage({
11628
+ conversationHistory: conversationHistory.messages,
11629
+ currentInput: resolvedInput,
11630
+ instructions,
11631
+ contextWindow: run.context_window
11632
+ });
11633
+ if (estimatedUsage) {
11634
+ await this.updateRun(conversationId, runId, { usage: estimatedUsage });
11635
+ }
11440
11636
  const response = await streamHermesResponses(
11441
11637
  {
11442
11638
  input: resolvedInput,
11443
- instructions: buildRunInstructions(run, deliveryStagingDir),
11639
+ instructions,
11444
11640
  session_id: hermesSessionId,
11445
11641
  model: run.model,
11446
11642
  ...previousResponseId ? { previous_response_id: previousResponseId } : {},
@@ -11943,7 +12139,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
11943
12139
  run.hermes_response_id = responseId;
11944
12140
  }
11945
12141
  if (usage) {
11946
- run.usage = usage;
12142
+ run.usage = mergeRunUsage(run.usage, usage);
11947
12143
  }
11948
12144
  if (assistant) {
11949
12145
  assistant.status = "completed";
@@ -11995,7 +12191,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
11995
12191
  const visibleMessage = formatFailureMessage(message, run.error_detail);
11996
12192
  const usage = readUsage(source?.payload);
11997
12193
  if (usage) {
11998
- run.usage = usage;
12194
+ run.usage = mergeRunUsage(run.usage, usage);
11999
12195
  }
12000
12196
  const assistant = snapshot.messages.find(
12001
12197
  (item) => item.id === run.assistant_message_id
@@ -12014,6 +12210,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
12014
12210
  }
12015
12211
  }
12016
12212
  await this.deps.writeSnapshot(conversationId, snapshot);
12213
+ const contextUsage = contextUsagePayload(run);
12017
12214
  await this.deps.appendEvent(conversationId, {
12018
12215
  type: "run.failed",
12019
12216
  message_id: assistant?.id,
@@ -12022,6 +12219,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
12022
12219
  error: { message },
12023
12220
  ...run.error_detail ? { error_detail: run.error_detail } : {},
12024
12221
  run,
12222
+ ...contextUsage ? { context: contextUsage } : {},
12025
12223
  ...source ? { hermes: source.payload } : {}
12026
12224
  },
12027
12225
  ...source ? { raw: { format: "hermes-run-event", payload: source.rawPayload } } : {}
@@ -12353,6 +12551,7 @@ function contextUsagePayload(run) {
12353
12551
  source: "unknown"
12354
12552
  };
12355
12553
  }
12554
+ const contextSource = usage.context_source ?? "explicit";
12356
12555
  return {
12357
12556
  input_tokens: contextTokens,
12358
12557
  output_tokens: usage?.output_tokens ?? 0,
@@ -12360,7 +12559,7 @@ function contextUsagePayload(run) {
12360
12559
  ...contextWindow ? { context_window: contextWindow } : {},
12361
12560
  used_tokens: contextTokens,
12362
12561
  ...contextWindow ? { window_tokens: contextWindow } : {},
12363
- source: "explicit",
12562
+ source: contextSource,
12364
12563
  ...usage?.usage_percent !== void 0 ? { usage_percent: usage.usage_percent } : contextWindow ? {
12365
12564
  usage_percent: Math.min(
12366
12565
  100,
@@ -12369,6 +12568,41 @@ function contextUsagePayload(run) {
12369
12568
  } : {}
12370
12569
  };
12371
12570
  }
12571
+ function mergeRunUsage(previous, next) {
12572
+ const nextContextWindow = next.context_window ?? previous?.context_window;
12573
+ const nextContextTokens = next.context_tokens ?? refineEstimatedContextTokens(
12574
+ previous,
12575
+ next.input_tokens,
12576
+ nextContextWindow
12577
+ );
12578
+ const nextContextSource = next.context_tokens !== void 0 ? next.context_source ?? "explicit" : nextContextTokens !== void 0 ? previous?.context_source : void 0;
12579
+ return {
12580
+ ...next,
12581
+ ...nextContextTokens !== void 0 ? { context_tokens: nextContextTokens } : {},
12582
+ ...nextContextWindow !== void 0 ? { context_window: nextContextWindow } : {},
12583
+ ...nextContextSource ? { context_source: nextContextSource } : {},
12584
+ ...nextContextTokens !== void 0 && nextContextWindow ? {
12585
+ usage_percent: next.usage_percent ?? previous?.usage_percent ?? Math.min(
12586
+ 100,
12587
+ Math.round(nextContextTokens / nextContextWindow * 100)
12588
+ )
12589
+ } : {}
12590
+ };
12591
+ }
12592
+ function refineEstimatedContextTokens(previous, inputTokens, contextWindow) {
12593
+ if (previous?.context_source !== "estimated") {
12594
+ return void 0;
12595
+ }
12596
+ const currentEstimate = previous.context_tokens;
12597
+ if (currentEstimate === void 0) {
12598
+ return void 0;
12599
+ }
12600
+ const upperBound = inputTokens > 0 ? contextWindow ? Math.min(inputTokens, contextWindow) : inputTokens : contextWindow;
12601
+ if (upperBound === void 0) {
12602
+ return currentEstimate;
12603
+ }
12604
+ return Math.min(currentEstimate, upperBound);
12605
+ }
12372
12606
  function findPreviousHermesResponseId(snapshot, run) {
12373
12607
  const currentProfile = normalizeRunProfileForCompare(run.profile);
12374
12608
  if (!currentProfile) {
@@ -13972,13 +14206,84 @@ function isLanHost(hostname) {
13972
14206
  }
13973
14207
 
13974
14208
  // src/http/sse.ts
14209
+ var DEFAULT_SSE_RETRY_MS = 1e3;
14210
+ var DEFAULT_SSE_HEARTBEAT_MS = 15e3;
14211
+ function beginSseStream(request, response, options = {}) {
14212
+ const retryMs = normalizeRetryMs(options.retryMs);
14213
+ const heartbeatMs = Math.max(1e3, options.heartbeatMs ?? DEFAULT_SSE_HEARTBEAT_MS);
14214
+ response.statusCode = 200;
14215
+ response.setHeader("content-type", "text/event-stream; charset=utf-8");
14216
+ response.setHeader("cache-control", "no-store");
14217
+ response.setHeader("connection", "keep-alive");
14218
+ response.flushHeaders();
14219
+ writeSseRetry(response, retryMs);
14220
+ writeSseComment(response, options.initialComment ?? "connected");
14221
+ let closed = false;
14222
+ let heartbeat = null;
14223
+ const cleanup = () => {
14224
+ if (closed) {
14225
+ return;
14226
+ }
14227
+ closed = true;
14228
+ if (heartbeat != null) {
14229
+ clearInterval(heartbeat);
14230
+ heartbeat = null;
14231
+ }
14232
+ request.off("close", cleanup);
14233
+ response.off("close", cleanup);
14234
+ options.onClose?.();
14235
+ if (!response.writableEnded && !response.destroyed) {
14236
+ response.end();
14237
+ }
14238
+ };
14239
+ heartbeat = setInterval(() => {
14240
+ if (response.writableEnded || response.destroyed) {
14241
+ cleanup();
14242
+ return;
14243
+ }
14244
+ writeSseComment(response);
14245
+ }, heartbeatMs);
14246
+ heartbeat.unref();
14247
+ request.once("close", cleanup);
14248
+ response.once("close", cleanup);
14249
+ return cleanup;
14250
+ }
13975
14251
  function writeSseEvent(response, event) {
13976
- response.write(`event: ${event.type}
14252
+ writeJsonSseEvent(response, {
14253
+ event: event.type,
14254
+ data: event,
14255
+ id: event.seq
14256
+ });
14257
+ }
14258
+ function writeJsonSseEvent(response, event) {
14259
+ if (event.retryMs != null) {
14260
+ response.write(`retry: ${normalizeRetryMs(event.retryMs)}
14261
+ `);
14262
+ }
14263
+ if (event.id != null && event.id !== "") {
14264
+ response.write(`id: ${event.id}
14265
+ `);
14266
+ }
14267
+ response.write(`event: ${event.event}
14268
+ `);
14269
+ response.write(`data: ${JSON.stringify(event.data)}
14270
+
14271
+ `);
14272
+ }
14273
+ function writeSseComment(response, comment = "keep-alive") {
14274
+ response.write(`: ${comment}
14275
+
13977
14276
  `);
13978
- response.write(`data: ${JSON.stringify(event)}
14277
+ }
14278
+ function writeSseRetry(response, retryMs) {
14279
+ response.write(`retry: ${normalizeRetryMs(retryMs)}
13979
14280
 
13980
14281
  `);
13981
14282
  }
14283
+ function normalizeRetryMs(retryMs) {
14284
+ const parsed = Number.isFinite(retryMs) ? Math.trunc(retryMs) : DEFAULT_SSE_RETRY_MS;
14285
+ return parsed >= 0 ? parsed : DEFAULT_SSE_RETRY_MS;
14286
+ }
13982
14287
 
13983
14288
  // src/http/routes/conversations.ts
13984
14289
  function registerConversationRoutes(router, options) {
@@ -14022,49 +14327,44 @@ function registerConversationRoutes(router, options) {
14022
14327
  const notificationOnly = mode === "notifications";
14023
14328
  ctx.respond = false;
14024
14329
  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) => {
14330
+ let unsubscribe = () => {
14331
+ };
14332
+ beginSseStream(ctx.req, response, {
14333
+ onClose: () => unsubscribe()
14334
+ });
14335
+ unsubscribe = conversations.subscribeAll((event) => {
14030
14336
  if (notificationOnly && !isConversationNotificationEvent(event)) {
14031
14337
  return;
14032
14338
  }
14033
14339
  writeSseEvent(response, event);
14034
14340
  });
14035
- const cleanup = () => {
14036
- unsubscribe();
14037
- response.end();
14038
- };
14039
- ctx.req.on("close", cleanup);
14040
14341
  });
14041
14342
  router.get("/api/v1/conversations/:conversationId/events", async (ctx) => {
14042
14343
  await authenticateRequest(ctx, paths);
14043
- const after = readInteger3(ctx.query.after) ?? 0;
14344
+ const after = resolveConversationEventCursor({
14345
+ queryAfter: ctx.query.after,
14346
+ lastEventIdHeader: ctx.req.headers["last-event-id"]
14347
+ });
14044
14348
  const history = await conversations.listEvents(
14045
14349
  ctx.params.conversationId,
14046
14350
  after
14047
14351
  );
14048
14352
  ctx.respond = false;
14049
14353
  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");
14354
+ let unsubscribe = () => {
14355
+ };
14356
+ beginSseStream(ctx.req, response, {
14357
+ onClose: () => unsubscribe()
14358
+ });
14054
14359
  for (const event of history) {
14055
14360
  writeSseEvent(response, event);
14056
14361
  }
14057
- const unsubscribe = conversations.subscribe(
14362
+ unsubscribe = conversations.subscribe(
14058
14363
  ctx.params.conversationId,
14059
14364
  (event) => {
14060
14365
  writeSseEvent(response, event);
14061
14366
  }
14062
14367
  );
14063
- const cleanup = () => {
14064
- unsubscribe();
14065
- response.end();
14066
- };
14067
- ctx.req.on("close", cleanup);
14068
14368
  });
14069
14369
  router.post("/api/v1/conversations/:conversationId/messages", async (ctx) => {
14070
14370
  await authenticateRequest(ctx, paths);
@@ -14267,6 +14567,19 @@ function registerConversationRoutes(router, options) {
14267
14567
  }
14268
14568
  );
14269
14569
  }
14570
+ function resolveConversationEventCursor(input) {
14571
+ const queryAfter = readInteger3(input.queryAfter) ?? 0;
14572
+ const headerAfter = readNonNegativeIntegerHeader(input.lastEventIdHeader) ?? 0;
14573
+ return Math.max(queryAfter, headerAfter);
14574
+ }
14575
+ function readNonNegativeIntegerHeader(value) {
14576
+ const raw = Array.isArray(value) ? value[0] : value;
14577
+ if (!raw) {
14578
+ return null;
14579
+ }
14580
+ const parsed = Number.parseInt(raw, 10);
14581
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
14582
+ }
14270
14583
  function contentDispositionInline(filename) {
14271
14584
  const fallback = asciiFilenameFallback(filename);
14272
14585
  return `inline; filename="${fallback}"; filename*=UTF-8''${encodeRfc5987Value(filename)}`;
@@ -18260,22 +18573,18 @@ function registerProfileRoutes(router, options) {
18260
18573
  await authenticateRequest(ctx, paths);
18261
18574
  ctx.respond = false;
18262
18575
  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");
18576
+ let unsubscribe = () => {
18577
+ };
18578
+ beginSseStream(ctx.req, response, {
18579
+ onClose: () => unsubscribe()
18580
+ });
18267
18581
  writeProfileCreationSseEvent(
18268
18582
  response,
18269
18583
  await readHermesProfileCreationStatus(paths)
18270
18584
  );
18271
- const unsubscribe = subscribeHermesProfileCreationStatus((status) => {
18585
+ unsubscribe = subscribeHermesProfileCreationStatus((status) => {
18272
18586
  writeProfileCreationSseEvent(response, status);
18273
18587
  });
18274
- const cleanup = () => {
18275
- unsubscribe();
18276
- response.end();
18277
- };
18278
- ctx.req.on("close", cleanup);
18279
18588
  });
18280
18589
  router.get("/api/v1/profiles/:name/status", async (ctx) => {
18281
18590
  await authenticateRequest(ctx, paths);
@@ -18491,10 +18800,10 @@ function isProfileAvatarUrl(value) {
18491
18800
  return isHttpUrl(value) || /^data:image\/[a-z0-9.+-]+;base64,/iu.test(value);
18492
18801
  }
18493
18802
  function writeProfileCreationSseEvent(response, status) {
18494
- response.write("event: profile.creation.status\n");
18495
- response.write(`data: ${JSON.stringify(status)}
18496
-
18497
- `);
18803
+ writeJsonSseEvent(response, {
18804
+ event: "profile.creation.status",
18805
+ data: status
18806
+ });
18498
18807
  }
18499
18808
 
18500
18809
  // src/http/routes/runs.ts
@@ -21646,26 +21955,22 @@ function registerHermesUpdateRoutes(router, options) {
21646
21955
  await authenticateRequest(ctx, paths);
21647
21956
  ctx.respond = false;
21648
21957
  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");
21958
+ let unsubscribe = () => {
21959
+ };
21960
+ beginSseStream(ctx.req, response, {
21961
+ onClose: () => unsubscribe()
21962
+ });
21653
21963
  writeUpdateSseEvent(response, await readHermesUpdateStatus(paths));
21654
- const unsubscribe = subscribeHermesUpdateStatus((status) => {
21964
+ unsubscribe = subscribeHermesUpdateStatus((status) => {
21655
21965
  writeUpdateSseEvent(response, status);
21656
21966
  });
21657
- const cleanup = () => {
21658
- unsubscribe();
21659
- response.end();
21660
- };
21661
- ctx.req.on("close", cleanup);
21662
21967
  });
21663
21968
  }
21664
21969
  function writeUpdateSseEvent(response, status) {
21665
- response.write("event: hermes.update.status\n");
21666
- response.write(`data: ${JSON.stringify(status)}
21667
-
21668
- `);
21970
+ writeJsonSseEvent(response, {
21971
+ event: "hermes.update.status",
21972
+ data: status
21973
+ });
21669
21974
  }
21670
21975
 
21671
21976
  // src/http/routes/link-updates.ts
@@ -21695,26 +22000,22 @@ function registerLinkUpdateRoutes(router, options) {
21695
22000
  await authenticateRequest(ctx, paths);
21696
22001
  ctx.respond = false;
21697
22002
  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");
22003
+ let unsubscribe = () => {
22004
+ };
22005
+ beginSseStream(ctx.req, response, {
22006
+ onClose: () => unsubscribe()
22007
+ });
21702
22008
  writeUpdateSseEvent2(response, await readLinkUpdateStatus(paths));
21703
- const unsubscribe = subscribeLinkUpdateStatus((status) => {
22009
+ unsubscribe = subscribeLinkUpdateStatus((status) => {
21704
22010
  writeUpdateSseEvent2(response, status);
21705
22011
  });
21706
- const cleanup = () => {
21707
- unsubscribe();
21708
- response.end();
21709
- };
21710
- ctx.req.on("close", cleanup);
21711
22012
  });
21712
22013
  }
21713
22014
  function writeUpdateSseEvent2(response, status) {
21714
- response.write("event: link.update.status\n");
21715
- response.write(`data: ${JSON.stringify(status)}
21716
-
21717
- `);
22015
+ writeJsonSseEvent(response, {
22016
+ event: "link.update.status",
22017
+ data: status
22018
+ });
21718
22019
  }
21719
22020
 
21720
22021
  // src/http/routes/pairing.ts