@hermespilot/link 0.3.8 → 0.4.0

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.
@@ -4,7 +4,7 @@ import Router from "@koa/router";
4
4
 
5
5
  // src/conversations/conversation-service.ts
6
6
  import { EventEmitter } from "events";
7
- import { randomUUID as randomUUID7 } from "crypto";
7
+ import { randomUUID as randomUUID8 } from "crypto";
8
8
 
9
9
  // src/database/link-database.ts
10
10
  import { mkdir } from "fs/promises";
@@ -3883,7 +3883,7 @@ async function listCronOutputFiles(profileName, jobId) {
3883
3883
  mtimeMs: fileStat.mtimeMs
3884
3884
  });
3885
3885
  }
3886
- return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path25, mtime }) => ({ path: path25, mtime }));
3886
+ return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path26, mtime }) => ({ path: path26, mtime }));
3887
3887
  }
3888
3888
  async function readCronOutput(outputPath) {
3889
3889
  const content = await readFile3(outputPath, "utf8");
@@ -3970,7 +3970,7 @@ import os2 from "os";
3970
3970
  import path5 from "path";
3971
3971
 
3972
3972
  // src/constants.ts
3973
- var LINK_VERSION = "0.3.8";
3973
+ var LINK_VERSION = "0.4.0";
3974
3974
  var LINK_COMMAND = "hermeslink";
3975
3975
  var LINK_DEFAULT_PORT = 52379;
3976
3976
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -4455,6 +4455,9 @@ var DEFAULT_START_TIMEOUT_MS = 12e3;
4455
4455
  var HEALTH_TIMEOUT_MS = 1500;
4456
4456
  var MIN_API_SERVER_VERSION = "0.4.0";
4457
4457
  var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/u;
4458
+ var DASHBOARD_STATUS_URL = "http://127.0.0.1:9119/api/status";
4459
+ var DASHBOARD_STATUS_TIMEOUT_MS = 1500;
4460
+ var MAX_VERSION_LOG_OUTPUT_LENGTH = 1200;
4458
4461
  var gatewayStartInFlightByProfile = /* @__PURE__ */ new Map();
4459
4462
  async function ensureHermesApiServerAvailable(options = {}) {
4460
4463
  const profileName = normalizeProfileName(options.profileName);
@@ -4571,29 +4574,57 @@ async function reloadHermesGateway(options = {}) {
4571
4574
  }
4572
4575
  return ensureHermesApiServerAvailable({ ...options, forceRestart: true });
4573
4576
  }
4574
- async function readHermesVersion() {
4575
- const { stdout } = await execHermesVersion();
4576
- const raw = stdout.trim();
4577
- const version = parseHermesVersion(raw);
4578
- return {
4579
- raw,
4580
- version,
4581
- supportsApiServer: version ? compareSemver(version, MIN_API_SERVER_VERSION) >= 0 : null
4582
- };
4583
- }
4584
- async function execHermesVersion() {
4577
+ async function readHermesVersion(options = {}) {
4585
4578
  try {
4586
- return await execFileAsync2(resolveHermesBin(), ["version"], {
4587
- timeout: 5e3,
4588
- windowsHide: true
4589
- });
4590
- } catch {
4591
- return await execFileAsync2(resolveHermesBin(), ["--version"], {
4592
- timeout: 5e3,
4593
- windowsHide: true
4579
+ const { stdout } = await execHermesVersion(options.logger);
4580
+ const raw = stdout.trim();
4581
+ const version = parseHermesVersion(raw);
4582
+ return buildHermesVersionInfo(raw, version);
4583
+ } catch (cliError) {
4584
+ const dashboardStatusUrl = options.dashboardStatusUrl ?? DASHBOARD_STATUS_URL;
4585
+ void options.logger?.warn("hermes_version_dashboard_fallback_requested", {
4586
+ dashboard_status_url: dashboardStatusUrl,
4587
+ reason: cliError instanceof Error ? cliError.message : String(cliError)
4594
4588
  });
4589
+ try {
4590
+ const fallback = await readHermesDashboardVersion({
4591
+ fetchImpl: options.fetchImpl,
4592
+ statusUrl: dashboardStatusUrl,
4593
+ timeoutMs: options.dashboardTimeoutMs
4594
+ });
4595
+ void options.logger?.info("hermes_version_dashboard_fallback_succeeded", {
4596
+ dashboard_status_url: dashboardStatusUrl,
4597
+ hermes_version: fallback.version
4598
+ });
4599
+ return fallback;
4600
+ } catch (dashboardError) {
4601
+ void options.logger?.warn("hermes_version_dashboard_fallback_failed", {
4602
+ dashboard_status_url: dashboardStatusUrl,
4603
+ error: dashboardError instanceof Error ? dashboardError.message : String(dashboardError)
4604
+ });
4605
+ throw new Error(
4606
+ `Hermes version detection failed. CLI: ${cliError instanceof Error ? cliError.message : String(cliError)}; dashboard fallback: ${dashboardError instanceof Error ? dashboardError.message : String(dashboardError)}`
4607
+ );
4608
+ }
4595
4609
  }
4596
4610
  }
4611
+ async function execHermesVersion(logger) {
4612
+ const hermesBin = resolveHermesBin();
4613
+ const failures = [];
4614
+ for (const args of [["version"], ["--version"]]) {
4615
+ try {
4616
+ return await execFileAsync2(hermesBin, args, {
4617
+ timeout: 5e3,
4618
+ windowsHide: true
4619
+ });
4620
+ } catch (error) {
4621
+ const failure = describeVersionCommandFailure(hermesBin, args, error);
4622
+ failures.push(failure.summary);
4623
+ void logger?.warn("hermes_version_cli_command_failed", failure.fields);
4624
+ }
4625
+ }
4626
+ throw new Error(failures.join("; "));
4627
+ }
4597
4628
  function assertHermesRunsApiSupported(version, status) {
4598
4629
  if (status !== 404) {
4599
4630
  return;
@@ -4617,7 +4648,7 @@ async function startHermesGatewayOnce(paths, profileName, logger) {
4617
4648
  return await gatewayStartInFlightByProfile.get(profileName);
4618
4649
  }
4619
4650
  async function startHermesGateway(paths, profileName, logger) {
4620
- const version = await readHermesVersion().catch((error) => {
4651
+ const version = await readHermesVersion({ logger }).catch((error) => {
4621
4652
  void logger?.error("gateway_hermes_cli_unavailable", {
4622
4653
  error: error instanceof Error ? error.message : String(error)
4623
4654
  });
@@ -4729,7 +4760,7 @@ async function restartHermesGatewayServiceIfAvailable(options) {
4729
4760
  return {
4730
4761
  pid: null,
4731
4762
  logPath,
4732
- version: await readHermesVersion().catch(() => null),
4763
+ version: await readHermesVersion({ logger: options.logger }).catch(() => null),
4733
4764
  ...logHint ? { logHint } : {}
4734
4765
  };
4735
4766
  }
@@ -4972,6 +5003,72 @@ function parseHermesVersion(value) {
4972
5003
  const match = /\bv?(\d+\.\d+\.\d+)\b/u.exec(value);
4973
5004
  return match?.[1] ?? null;
4974
5005
  }
5006
+ function buildHermesVersionInfo(raw, version) {
5007
+ return {
5008
+ raw,
5009
+ version,
5010
+ supportsApiServer: version ? compareSemver(version, MIN_API_SERVER_VERSION) >= 0 : null
5011
+ };
5012
+ }
5013
+ async function readHermesDashboardVersion(options = {}) {
5014
+ const fetcher = options.fetchImpl ?? fetch;
5015
+ const controller = new AbortController();
5016
+ const timer = setTimeout(
5017
+ () => controller.abort(),
5018
+ options.timeoutMs ?? DASHBOARD_STATUS_TIMEOUT_MS
5019
+ );
5020
+ try {
5021
+ const response = await fetcher(options.statusUrl ?? DASHBOARD_STATUS_URL, {
5022
+ method: "GET",
5023
+ headers: { accept: "application/json" },
5024
+ signal: controller.signal
5025
+ });
5026
+ if (!response.ok) {
5027
+ throw new Error(`Hermes dashboard returned HTTP ${response.status}`);
5028
+ }
5029
+ const payload = await response.json().catch(() => null);
5030
+ const record = toRecord2(payload);
5031
+ const versionText = readString4(record, "version");
5032
+ if (!versionText) {
5033
+ throw new Error("Hermes dashboard status did not include a version");
5034
+ }
5035
+ const raw = truncateVersionLogOutput(JSON.stringify(record));
5036
+ const version = parseHermesVersion(versionText) ?? parseHermesVersion(raw);
5037
+ return buildHermesVersionInfo(raw, version);
5038
+ } catch (error) {
5039
+ if (error instanceof Error && error.name === "AbortError") {
5040
+ throw new Error("Hermes dashboard version probe timed out");
5041
+ }
5042
+ throw error;
5043
+ } finally {
5044
+ clearTimeout(timer);
5045
+ }
5046
+ }
5047
+ function describeVersionCommandFailure(hermesBin, args, error) {
5048
+ const message = error instanceof Error ? error.message : String(error);
5049
+ const output = truncateVersionLogOutput(readExecErrorOutput2(error));
5050
+ return {
5051
+ summary: `${hermesBin} ${args.join(" ")} failed: ${message}`,
5052
+ fields: {
5053
+ hermes_bin: hermesBin,
5054
+ command: args.join(" "),
5055
+ error: message,
5056
+ ...output ? { output } : {}
5057
+ }
5058
+ };
5059
+ }
5060
+ function readExecErrorOutput2(error) {
5061
+ if (typeof error !== "object" || error === null) {
5062
+ return "";
5063
+ }
5064
+ const stdout = "stdout" in error && error.stdout != null ? String(error.stdout) : "";
5065
+ const stderr = "stderr" in error && error.stderr != null ? String(error.stderr) : "";
5066
+ return `${stdout}
5067
+ ${stderr}`.trim();
5068
+ }
5069
+ function truncateVersionLogOutput(value) {
5070
+ return value.length > MAX_VERSION_LOG_OUTPUT_LENGTH ? `${value.slice(0, MAX_VERSION_LOG_OUTPUT_LENGTH)}...` : value;
5071
+ }
4975
5072
  function compareSemver(left, right) {
4976
5073
  const leftParts = left.split(".").map((part) => Number.parseInt(part, 10));
4977
5074
  const rightParts = right.split(".").map((part) => Number.parseInt(part, 10));
@@ -6663,6 +6760,70 @@ function safePathSegment(value, fallback) {
6663
6760
  return safe.length > 0 ? safe.slice(0, 120) : fallback;
6664
6761
  }
6665
6762
 
6763
+ // src/conversations/conversation-clear-plans.ts
6764
+ import { randomUUID as randomUUID5 } from "crypto";
6765
+ import { mkdir as mkdir7 } from "fs/promises";
6766
+ import path11 from "path";
6767
+ var PLAN_ID_PATTERN = /^clear_[a-f0-9]{32}$/u;
6768
+ var ConversationClearPlanStore = class {
6769
+ constructor(paths) {
6770
+ this.paths = paths;
6771
+ }
6772
+ paths;
6773
+ async create(conversationIds) {
6774
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6775
+ const plan = {
6776
+ id: `clear_${randomUUID5().replaceAll("-", "")}`,
6777
+ status: "prepared",
6778
+ created_at: now,
6779
+ updated_at: now,
6780
+ total_count: conversationIds.length,
6781
+ deleted_count: 0,
6782
+ failed_count: 0,
6783
+ conversation_ids: conversationIds,
6784
+ conversations: []
6785
+ };
6786
+ await this.write(plan);
6787
+ return plan;
6788
+ }
6789
+ async read(planId) {
6790
+ const normalizedPlanId = normalizePlanId(planId);
6791
+ const plan = await readJsonFile(
6792
+ this.planPath(normalizedPlanId)
6793
+ );
6794
+ if (!plan) {
6795
+ throw new LinkHttpError(
6796
+ 404,
6797
+ "conversation_clear_plan_not_found",
6798
+ "Conversation clear plan was not found"
6799
+ );
6800
+ }
6801
+ return plan;
6802
+ }
6803
+ async write(plan) {
6804
+ const normalizedPlanId = normalizePlanId(plan.id);
6805
+ await mkdir7(this.plansDir(), { recursive: true, mode: 448 });
6806
+ await writeJsonFile(this.planPath(normalizedPlanId), plan);
6807
+ }
6808
+ plansDir() {
6809
+ return path11.join(this.paths.indexesDir, "conversation-clear-plans");
6810
+ }
6811
+ planPath(planId) {
6812
+ return path11.join(this.plansDir(), `${planId}.json`);
6813
+ }
6814
+ };
6815
+ function normalizePlanId(planId) {
6816
+ const normalized = planId.trim();
6817
+ if (!PLAN_ID_PATTERN.test(normalized)) {
6818
+ throw new LinkHttpError(
6819
+ 400,
6820
+ "conversation_clear_plan_id_invalid",
6821
+ "Conversation clear plan id is invalid"
6822
+ );
6823
+ }
6824
+ return normalized;
6825
+ }
6826
+
6666
6827
  // src/conversations/conversation-session-ids.ts
6667
6828
  function normalizeHermesSessionIds(values) {
6668
6829
  const seen = /* @__PURE__ */ new Set();
@@ -6713,8 +6874,134 @@ var MAX_UPLOADED_BLOB_BYTES = 50 * 1024 * 1024;
6713
6874
  var ConversationMaintenanceCoordinator = class {
6714
6875
  constructor(deps) {
6715
6876
  this.deps = deps;
6877
+ this.clearPlans = new ConversationClearPlanStore(deps.paths);
6716
6878
  }
6717
6879
  deps;
6880
+ clearPlans;
6881
+ async prepareClearAllConversationPlan() {
6882
+ const targets = [];
6883
+ for (const conversationId of await this.deps.store.listConversationIds()) {
6884
+ const manifest = await this.deps.store.readManifest(conversationId).catch(() => null);
6885
+ if (manifest?.status !== "active") {
6886
+ continue;
6887
+ }
6888
+ targets.push({
6889
+ id: conversationId,
6890
+ updatedAt: manifest.updated_at
6891
+ });
6892
+ }
6893
+ targets.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
6894
+ return this.clearPlans.create(
6895
+ targets.map((target) => target.id)
6896
+ );
6897
+ }
6898
+ async readClearAllConversationPlan(planId) {
6899
+ return this.clearPlans.read(planId);
6900
+ }
6901
+ async executeClearAllConversationPlan(planId) {
6902
+ let plan = await this.clearPlans.read(planId);
6903
+ if (plan.status === "completed") {
6904
+ return plan;
6905
+ }
6906
+ if (plan.status === "failed") {
6907
+ throw new LinkHttpError(
6908
+ 409,
6909
+ "conversation_clear_plan_already_failed",
6910
+ "Conversation clear plan has already failed"
6911
+ );
6912
+ }
6913
+ const results = [];
6914
+ if (plan.status !== "executing") {
6915
+ plan = await this.writeClearPlan({
6916
+ ...plan,
6917
+ status: "executing",
6918
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
6919
+ conversations: results,
6920
+ deleted_count: 0,
6921
+ failed_count: 0
6922
+ });
6923
+ }
6924
+ for (const conversationId of plan.conversation_ids) {
6925
+ try {
6926
+ const deleted = await this.deleteConversation(conversationId);
6927
+ results.push({ ...deleted, status: "deleted" });
6928
+ } catch (error) {
6929
+ if (error instanceof LinkHttpError && error.code === "conversation_not_found") {
6930
+ results.push({
6931
+ conversation_id: conversationId,
6932
+ status: "deleted",
6933
+ hermes_deleted: false,
6934
+ deleted_at: (/* @__PURE__ */ new Date()).toISOString()
6935
+ });
6936
+ } else {
6937
+ results.push({
6938
+ conversation_id: conversationId,
6939
+ status: "failed",
6940
+ error: {
6941
+ code: error instanceof LinkHttpError ? error.code : "internal_error",
6942
+ message: error instanceof Error ? error.message : "Internal error"
6943
+ }
6944
+ });
6945
+ }
6946
+ }
6947
+ plan = await this.writeClearPlan({
6948
+ ...plan,
6949
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
6950
+ conversations: [...results],
6951
+ deleted_count: results.filter((result) => result.status === "deleted").length,
6952
+ failed_count: results.filter((result) => result.status === "failed").length
6953
+ });
6954
+ }
6955
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
6956
+ return this.writeClearPlan({
6957
+ ...plan,
6958
+ status: plan.failed_count === 0 ? "completed" : "failed",
6959
+ updated_at: completedAt,
6960
+ completed_at: completedAt
6961
+ });
6962
+ }
6963
+ async startClearAllConversationPlan(planId) {
6964
+ const plan = await this.clearPlans.read(planId);
6965
+ if (plan.status === "completed" || plan.status === "executing") {
6966
+ return plan;
6967
+ }
6968
+ if (plan.status === "failed") {
6969
+ throw new LinkHttpError(
6970
+ 409,
6971
+ "conversation_clear_plan_already_failed",
6972
+ "Conversation clear plan has already failed"
6973
+ );
6974
+ }
6975
+ const started = await this.writeClearPlan({
6976
+ ...plan,
6977
+ status: "executing",
6978
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
6979
+ conversations: [],
6980
+ deleted_count: 0,
6981
+ failed_count: 0
6982
+ });
6983
+ void this.executeClearAllConversationPlan(started.id).catch(
6984
+ async (error) => {
6985
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
6986
+ await this.writeClearPlan({
6987
+ ...started,
6988
+ status: "failed",
6989
+ updated_at: failedAt,
6990
+ completed_at: failedAt,
6991
+ failed_count: started.total_count,
6992
+ conversations: started.conversation_ids.map((conversationId) => ({
6993
+ conversation_id: conversationId,
6994
+ status: "failed",
6995
+ error: {
6996
+ code: error instanceof LinkHttpError ? error.code : "internal_error",
6997
+ message: error instanceof Error ? error.message : "Internal error"
6998
+ }
6999
+ }))
7000
+ }).catch(() => void 0);
7001
+ }
7002
+ );
7003
+ return started;
7004
+ }
6718
7005
  async deleteConversation(conversationId) {
6719
7006
  return this.deps.withConversationLock(
6720
7007
  conversationId,
@@ -6920,6 +7207,10 @@ var ConversationMaintenanceCoordinator = class {
6920
7207
  async listConversationBlobIds(conversationId) {
6921
7208
  return listConversationBlobIds(this.deps.paths, conversationId);
6922
7209
  }
7210
+ async writeClearPlan(plan) {
7211
+ await this.clearPlans.write(plan);
7212
+ return plan;
7213
+ }
6923
7214
  };
6924
7215
  function isVoiceAttachmentInput(attachment) {
6925
7216
  return attachment.kind === "voice" || attachment.type === "voice" || attachment.is_voice_note === true || attachment.isVoiceNote === true;
@@ -6999,14 +7290,14 @@ function isUsableLanIpv4(value) {
6999
7290
 
7000
7291
  // src/hermes/session-title.ts
7001
7292
  import { stat as stat7 } from "fs/promises";
7002
- import path11 from "path";
7293
+ import path12 from "path";
7003
7294
  async function readHermesSessionTitle(sessionId, paths, profileName) {
7004
7295
  const trimmedSessionId = sessionId.trim();
7005
7296
  if (!trimmedSessionId) {
7006
7297
  return void 0;
7007
7298
  }
7008
7299
  const resolvedProfileName = isValidProfileName(profileName) ? profileName : "default";
7009
- const dbPath = path11.join(
7300
+ const dbPath = path12.join(
7010
7301
  resolveHermesProfileDir(resolvedProfileName),
7011
7302
  "state.db"
7012
7303
  );
@@ -7027,7 +7318,7 @@ async function readHermesCompressionTip(sessionId, paths, profileName) {
7027
7318
  return void 0;
7028
7319
  }
7029
7320
  const resolvedProfileName = isValidProfileName(profileName) ? profileName : "default";
7030
- const dbPath = path11.join(
7321
+ const dbPath = path12.join(
7031
7322
  resolveHermesProfileDir(resolvedProfileName),
7032
7323
  "state.db"
7033
7324
  );
@@ -7122,11 +7413,19 @@ var ConversationMetadataCoordinator = class {
7122
7413
  const snapshot = options.snapshot ?? await this.deps.store.readSnapshot(manifest.id).catch(
7123
7414
  () => createEmptySnapshot()
7124
7415
  );
7125
- const title = await readHermesSessionTitle(
7416
+ const profileName = latestRuntimeRun(snapshot)?.profile ?? manifest.profile_name_snapshot ?? manifest.profile;
7417
+ const compressionRootSessionId = manifest.hermes_lineage?.kind === "compression" ? manifest.hermes_lineage.root_session_id : void 0;
7418
+ const rootTitle = compressionRootSessionId ? await readHermesSessionTitle(
7419
+ compressionRootSessionId,
7420
+ this.deps.paths,
7421
+ profileName
7422
+ ) : void 0;
7423
+ const tipTitle = await readHermesSessionTitle(
7126
7424
  manifest.hermes_session_id,
7127
7425
  this.deps.paths,
7128
- latestRuntimeRun(snapshot)?.profile
7426
+ profileName
7129
7427
  );
7428
+ const title = rootTitle ?? (compressionRootSessionId ? stripCompressionTitleSuffix(tipTitle) : tipTitle);
7130
7429
  if (!title || title === manifest.title) {
7131
7430
  return manifest;
7132
7431
  }
@@ -7440,9 +7739,18 @@ function canAutoGenerateTitle(manifest) {
7440
7739
  function normalizeConversationTitleSource(title) {
7441
7740
  return isDefaultConversationTitle(title) ? "default" : "hermes";
7442
7741
  }
7742
+ function stripCompressionTitleSuffix(value) {
7743
+ const normalized = value?.replace(/\s+/gu, " ").trim();
7744
+ if (!normalized) {
7745
+ return void 0;
7746
+ }
7747
+ const match = /^(.*?) #\d+$/u.exec(normalized);
7748
+ const stripped = match?.[1]?.trim();
7749
+ return stripped || normalized;
7750
+ }
7443
7751
 
7444
7752
  // src/conversations/conversation-turns.ts
7445
- import { randomUUID as randomUUID5 } from "crypto";
7753
+ import { randomUUID as randomUUID6 } from "crypto";
7446
7754
  var MESSAGE_ORDER_STEP_MS = 10;
7447
7755
  var ASSISTANT_ORDER_OFFSET_MS = 1;
7448
7756
  function createAgentTurnDraft(input) {
@@ -7676,10 +7984,10 @@ function createAssistantMessage(input) {
7676
7984
  };
7677
7985
  }
7678
7986
  function createMessageId() {
7679
- return `msg_${randomUUID5().replaceAll("-", "")}`;
7987
+ return `msg_${randomUUID6().replaceAll("-", "")}`;
7680
7988
  }
7681
7989
  function createRunId() {
7682
- return `run_${randomUUID5().replaceAll("-", "")}`;
7990
+ return `run_${randomUUID6().replaceAll("-", "")}`;
7683
7991
  }
7684
7992
  function hasActiveOrQueuedRuns(snapshot) {
7685
7993
  return snapshot.runs.some(
@@ -8794,20 +9102,20 @@ function hydrateAgentEventBlocks(blocks, agentEvents) {
8794
9102
  // src/conversations/conversation-store.ts
8795
9103
  import {
8796
9104
  appendFile as appendFile2,
8797
- mkdir as mkdir7,
9105
+ mkdir as mkdir8,
8798
9106
  readdir as readdir5,
8799
9107
  readFile as readFile7,
8800
9108
  rm as rm5,
8801
9109
  writeFile as writeFile2
8802
9110
  } from "fs/promises";
8803
- import path12 from "path";
9111
+ import path13 from "path";
8804
9112
  var ConversationStore = class {
8805
9113
  constructor(paths) {
8806
9114
  this.paths = paths;
8807
9115
  }
8808
9116
  paths;
8809
9117
  async ensureConversationsDir() {
8810
- await mkdir7(this.paths.conversationsDir, { recursive: true, mode: 448 });
9118
+ await mkdir8(this.paths.conversationsDir, { recursive: true, mode: 448 });
8811
9119
  }
8812
9120
  async listConversationIds() {
8813
9121
  await this.ensureConversationsDir();
@@ -8822,7 +9130,7 @@ var ConversationStore = class {
8822
9130
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
8823
9131
  }
8824
9132
  async createConversation(manifest, snapshot = createEmptySnapshot2()) {
8825
- await mkdir7(this.conversationDir(manifest.id), {
9133
+ await mkdir8(this.conversationDir(manifest.id), {
8826
9134
  recursive: true,
8827
9135
  mode: 448
8828
9136
  });
@@ -8862,7 +9170,7 @@ var ConversationStore = class {
8862
9170
  conversation_id: conversationId,
8863
9171
  created_at: now
8864
9172
  };
8865
- await mkdir7(this.conversationDir(conversationId), {
9173
+ await mkdir8(this.conversationDir(conversationId), {
8866
9174
  recursive: true,
8867
9175
  mode: 448
8868
9176
  });
@@ -8916,23 +9224,23 @@ var ConversationStore = class {
8916
9224
  return manifest?.status === "active";
8917
9225
  }
8918
9226
  removeConversationAttachments(conversationId) {
8919
- return rm5(path12.join(this.conversationDir(conversationId), "attachments"), {
9227
+ return rm5(path13.join(this.conversationDir(conversationId), "attachments"), {
8920
9228
  recursive: true,
8921
9229
  force: true
8922
9230
  });
8923
9231
  }
8924
9232
  conversationDir(conversationId) {
8925
9233
  assertValidConversationId(conversationId);
8926
- return path12.join(this.paths.conversationsDir, conversationId);
9234
+ return path13.join(this.paths.conversationsDir, conversationId);
8927
9235
  }
8928
9236
  manifestPath(conversationId) {
8929
- return path12.join(this.conversationDir(conversationId), "manifest.json");
9237
+ return path13.join(this.conversationDir(conversationId), "manifest.json");
8930
9238
  }
8931
9239
  snapshotPath(conversationId) {
8932
- return path12.join(this.conversationDir(conversationId), "snapshot.json");
9240
+ return path13.join(this.conversationDir(conversationId), "snapshot.json");
8933
9241
  }
8934
9242
  eventsPath(conversationId) {
8935
- return path12.join(this.conversationDir(conversationId), "events.ndjson");
9243
+ return path13.join(this.conversationDir(conversationId), "events.ndjson");
8936
9244
  }
8937
9245
  };
8938
9246
  function createEmptySnapshot2() {
@@ -8943,14 +9251,14 @@ function isNodeError9(error, code) {
8943
9251
  }
8944
9252
 
8945
9253
  // src/conversations/hermes-session-sync.ts
8946
- import { randomUUID as randomUUID6 } from "crypto";
9254
+ import { randomUUID as randomUUID7 } from "crypto";
8947
9255
  import { readdir as readdir7, readFile as readFile9, stat as stat9 } from "fs/promises";
8948
9256
  import os4 from "os";
8949
- import path14 from "path";
9257
+ import path15 from "path";
8950
9258
 
8951
9259
  // src/conversations/delivery-import.ts
8952
9260
  import { lstat as lstat2, readFile as readFile8, readdir as readdir6, stat as stat8 } from "fs/promises";
8953
- import path13 from "path";
9261
+ import path14 from "path";
8954
9262
  var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
8955
9263
  var MAX_MEDIA_IMPORT_FAILURES = 20;
8956
9264
  var MAX_DELIVERY_FILES = 50;
@@ -9021,16 +9329,16 @@ var SUPPORTED_DELIVERY_EXTENSIONS = /* @__PURE__ */ new Set([
9021
9329
  ".m4a"
9022
9330
  ]);
9023
9331
  function resolveDeliveryStagingTarget(paths, stagingDir) {
9024
- const resolvedDir = path13.resolve(stagingDir);
9025
- const relative = path13.relative(path13.resolve(paths.conversationsDir), resolvedDir);
9026
- if (!relative || relative.startsWith("..") || path13.isAbsolute(relative)) {
9332
+ const resolvedDir = path14.resolve(stagingDir);
9333
+ const relative = path14.relative(path14.resolve(paths.conversationsDir), resolvedDir);
9334
+ if (!relative || relative.startsWith("..") || path14.isAbsolute(relative)) {
9027
9335
  throw new LinkHttpError(
9028
9336
  400,
9029
9337
  "delivery_staging_invalid",
9030
9338
  "delivery staging directory must be inside Hermes Link conversations"
9031
9339
  );
9032
9340
  }
9033
- const segments = relative.split(path13.sep);
9341
+ const segments = relative.split(path14.sep);
9034
9342
  if (segments.length !== 3 || segments[1] !== DELIVERY_STAGING_SEGMENT || !segments[0] || !segments[2]) {
9035
9343
  throw new LinkHttpError(
9036
9344
  400,
@@ -9066,7 +9374,7 @@ async function collectStagedDeliveryReferences(stagingDir) {
9066
9374
  return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isSupportedDeliveryFilename(entry.name)).sort(
9067
9375
  (left, right) => left.name.localeCompare(right.name, "en", { numeric: true })
9068
9376
  ).slice(0, MAX_DELIVERY_FILES).map((entry) => {
9069
- const sourcePath = path13.join(stagingDir, entry.name);
9377
+ const sourcePath = path14.join(stagingDir, entry.name);
9070
9378
  const mime = inferMimeType(sourcePath);
9071
9379
  return {
9072
9380
  path: sourcePath,
@@ -9252,7 +9560,7 @@ async function writeBlobFromFile(deps, conversationId, source) {
9252
9560
  }
9253
9561
  return deps.writeBlob(conversationId, {
9254
9562
  bytes: await readFile8(sourcePath),
9255
- filename: path13.basename(sourcePath),
9563
+ filename: path14.basename(sourcePath),
9256
9564
  mime: source.mime ?? inferMimeType(sourcePath)
9257
9565
  });
9258
9566
  }
@@ -9265,7 +9573,7 @@ function describeMediaImportFailure(reference, sourceKey, error) {
9265
9573
  };
9266
9574
  }
9267
9575
  function isSupportedDeliveryFilename(filename) {
9268
- return SUPPORTED_DELIVERY_EXTENSIONS.has(path13.extname(filename).toLowerCase());
9576
+ return SUPPORTED_DELIVERY_EXTENSIONS.has(path14.extname(filename).toLowerCase());
9269
9577
  }
9270
9578
  function readString8(payload, key) {
9271
9579
  const value = payload[key];
@@ -9322,7 +9630,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
9322
9630
  const candidates = [];
9323
9631
  for (const profileName of profileNames) {
9324
9632
  const profileDir = resolveHermesProfileDir(profileName);
9325
- const dbPath = path14.join(profileDir, "state.db");
9633
+ const dbPath = path15.join(profileDir, "state.db");
9326
9634
  const sessions = await listProfileSessions(dbPath).catch((error) => {
9327
9635
  result.errors.push({
9328
9636
  profile: profileName,
@@ -9352,16 +9660,19 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
9352
9660
  const importableCandidates = candidates.slice(0, maxImports);
9353
9661
  result.skipped_over_limit = Math.max(0, candidates.length - maxImports);
9354
9662
  for (const candidate of importableCandidates) {
9355
- if (knownHermesSessions.ids.has(candidate.session.id)) {
9663
+ const candidateSessionIds = lineageSessionIds(candidate);
9664
+ const knownConversationIds = findKnownConversationIdsForCandidate(
9665
+ knownHermesSessions,
9666
+ candidate
9667
+ );
9668
+ if (knownConversationIds.length > 0) {
9356
9669
  result.skipped_existing += 1;
9357
- const reprojected = await reprojectExistingHermesConversation({
9670
+ const reprojected = await mergeExistingHermesConversation({
9358
9671
  paths,
9359
9672
  store,
9360
9673
  logger,
9361
9674
  candidate,
9362
- conversationIds: knownHermesSessions.conversationIdsBySessionId.get(
9363
- candidate.session.id
9364
- ) ?? []
9675
+ conversationIds: knownConversationIds
9365
9676
  }).catch((error) => {
9366
9677
  result.errors.push({
9367
9678
  profile: candidate.profileName,
@@ -9372,14 +9683,22 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
9372
9683
  if (reprojected) {
9373
9684
  result.reprojected_count += 1;
9374
9685
  }
9686
+ for (const sessionId of candidateSessionIds) {
9687
+ knownHermesSessions.sessionIds.add(sessionId);
9688
+ }
9689
+ continue;
9690
+ }
9691
+ if (candidateSessionIds.some(
9692
+ (sessionId) => knownHermesSessions.sessionIds.has(sessionId)
9693
+ )) {
9694
+ result.skipped_existing += 1;
9375
9695
  continue;
9376
9696
  }
9377
9697
  const imported = await importHermesSession({
9378
9698
  paths,
9379
9699
  store,
9380
9700
  logger,
9381
- candidate,
9382
- existingHermesSessionIds: knownHermesSessions.ids
9701
+ candidate
9383
9702
  }).catch((error) => {
9384
9703
  result.errors.push({
9385
9704
  profile: candidate.profileName,
@@ -9389,6 +9708,9 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
9389
9708
  });
9390
9709
  if (imported) {
9391
9710
  result.imported_count += 1;
9711
+ for (const sessionId of candidateSessionIds) {
9712
+ knownHermesSessions.sessionIds.add(sessionId);
9713
+ }
9392
9714
  }
9393
9715
  }
9394
9716
  if (result.imported_count > 0 || result.reprojected_count > 0 || result.errors.length > 0) {
@@ -9399,13 +9721,14 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
9399
9721
  return result;
9400
9722
  }
9401
9723
  async function importHermesSession(input) {
9402
- const { paths, store, logger, candidate, existingHermesSessionIds } = input;
9724
+ const { paths, store, logger, candidate } = input;
9403
9725
  const profile = await resolveConversationProfileTarget(
9404
9726
  paths,
9405
9727
  candidate.profileName
9406
9728
  );
9407
9729
  const sessionId = candidate.session.id;
9408
- const messages = await readHermesSessionMessages(candidate);
9730
+ const hermesSessionIds = lineageSessionIds(candidate);
9731
+ const messages = await readHermesLineageMessages(candidate);
9409
9732
  const now = (/* @__PURE__ */ new Date()).toISOString();
9410
9733
  const createdAt = isoFromHermesTime(candidate.session.started_at) ?? now;
9411
9734
  const updatedAt = isoFromHermesTime(candidate.session.last_active) ?? isoFromHermesTime(messages.at(-1)?.timestamp) ?? createdAt;
@@ -9422,7 +9745,14 @@ async function importHermesSession(input) {
9422
9745
  }),
9423
9746
  runs: []
9424
9747
  };
9425
- const title = readString9(candidate.session, "title") ?? firstUserText(snapshot);
9748
+ const title = lineageTitle(candidate) ?? readString9(candidate.session, "title") ?? firstUserText(snapshot);
9749
+ const importedStats = buildImportedHermesStats({
9750
+ candidate,
9751
+ snapshot,
9752
+ profileUid: profile.profileUid,
9753
+ profileName: profile.profileName,
9754
+ updatedAt
9755
+ });
9426
9756
  const manifest = {
9427
9757
  id: conversationId,
9428
9758
  schema_version: 1,
@@ -9431,13 +9761,15 @@ async function importHermesSession(input) {
9431
9761
  title_source: title ? "hermes" : "default",
9432
9762
  status: "active",
9433
9763
  hermes_session_id: sessionId,
9434
- hermes_session_ids: [sessionId],
9764
+ hermes_session_ids: hermesSessionIds,
9765
+ ...lineageManifestPatch(candidate),
9435
9766
  profile_uid: profile.profileUid,
9436
9767
  profile_name_snapshot: profile.profileName,
9437
9768
  profile: profile.profileName,
9438
9769
  created_at: createdAt,
9439
9770
  updated_at: updatedAt,
9440
- last_event_seq: 0
9771
+ last_event_seq: 0,
9772
+ stats: importedStats
9441
9773
  };
9442
9774
  await store.createConversation(manifest, snapshot);
9443
9775
  await hydrateImportedConversationMedia({
@@ -9480,59 +9812,288 @@ async function importHermesSession(input) {
9480
9812
  paths,
9481
9813
  toStatsIndexRecord(await store.readManifest(conversationId), stats)
9482
9814
  );
9483
- existingHermesSessionIds.add(sessionId);
9484
9815
  return true;
9485
9816
  }
9486
- async function reprojectExistingHermesConversation(input) {
9817
+ async function mergeExistingHermesConversation(input) {
9818
+ const conversations = await readExistingHermesConversations(
9819
+ input.store,
9820
+ input.conversationIds
9821
+ );
9822
+ const active = conversations.filter((item) => item.manifest.status === "active");
9823
+ const canonical = selectCanonicalHermesConversation(active);
9824
+ if (!canonical) {
9825
+ return false;
9826
+ }
9487
9827
  let changed = false;
9488
- for (const conversationId of input.conversationIds) {
9489
- const manifest = await input.store.readManifest(conversationId).catch(() => null);
9490
- if (!manifest || manifest.status !== "active") {
9491
- continue;
9492
- }
9493
- const snapshot = await input.store.readSnapshot(conversationId).catch(() => null);
9494
- if (!snapshot) {
9828
+ const candidateMessages = await readHermesLineageMessages(input.candidate);
9829
+ const nextCanonical = await mergeHermesCandidateIntoConversation({
9830
+ ...input,
9831
+ existing: canonical,
9832
+ candidateMessages
9833
+ });
9834
+ changed = nextCanonical || changed;
9835
+ for (const duplicate of active) {
9836
+ if (duplicate.conversationId === canonical.conversationId) {
9495
9837
  continue;
9496
9838
  }
9497
- const prefix = collectImportedHermesPrefix(snapshot);
9498
- if (!prefix?.needsUpgrade || prefix.messages.length === 0) {
9839
+ if (!isSafeHermesImportConversation(duplicate) || duplicate.manifest.status !== "active") {
9499
9840
  continue;
9500
9841
  }
9501
- const profile = await resolveConversationProfileTarget(
9502
- input.paths,
9503
- manifest.profile_name_snapshot ?? manifest.profile ?? input.candidate.profileName
9504
- );
9505
- const nextSnapshot = {
9506
- ...snapshot,
9842
+ await softDeleteMergedHermesDuplicate({
9843
+ paths: input.paths,
9844
+ store: input.store,
9845
+ duplicate,
9846
+ candidate: input.candidate
9847
+ });
9848
+ changed = true;
9849
+ }
9850
+ return changed;
9851
+ }
9852
+ async function mergeHermesCandidateIntoConversation(input) {
9853
+ const { candidate, existing } = input;
9854
+ const profile = await resolveConversationProfileTarget(
9855
+ input.paths,
9856
+ existing.manifest.profile_name_snapshot ?? existing.manifest.profile ?? candidate.profileName
9857
+ );
9858
+ let nextSnapshot = existing.snapshot;
9859
+ const pureImport = isSafeHermesImportConversation(existing);
9860
+ const prefix = collectImportedHermesPrefix(existing.snapshot);
9861
+ if (pureImport || prefix?.needsUpgrade && prefix.messages.length > 0) {
9862
+ const messages = pureImport ? input.candidateMessages : prefix.messages;
9863
+ nextSnapshot = {
9864
+ ...existing.snapshot,
9507
9865
  messages: [
9508
9866
  ...toLinkMessages({
9509
- conversationId,
9867
+ conversationId: existing.conversationId,
9510
9868
  profileName: profile.profileName,
9511
9869
  profileUid: profile.profileUid,
9512
9870
  profileDisplayName: profile.profileDisplayName,
9513
- sessionId: input.candidate.session.id,
9514
- messages: prefix.messages
9871
+ sessionId: candidate.session.id,
9872
+ messages
9515
9873
  }),
9516
- ...snapshot.messages.slice(prefix.endIndex)
9874
+ ...pureImport ? [] : existing.snapshot.messages.slice(prefix.endIndex)
9517
9875
  ]
9518
9876
  };
9519
- await input.store.writeSnapshot(conversationId, nextSnapshot);
9877
+ await input.store.writeSnapshot(existing.conversationId, nextSnapshot);
9520
9878
  await hydrateImportedConversationMedia({
9521
9879
  paths: input.paths,
9522
9880
  store: input.store,
9523
9881
  logger: input.logger,
9524
- conversationId
9882
+ conversationId: existing.conversationId
9525
9883
  });
9526
- const hydratedSnapshot = await input.store.readSnapshot(conversationId);
9527
- const stats = buildConversationStats(manifest, hydratedSnapshot);
9528
- await input.store.writeManifest({ ...manifest, stats });
9529
- await upsertConversationStats(
9530
- input.paths,
9531
- toStatsIndexRecord({ ...manifest, stats }, stats)
9532
- );
9533
- changed = true;
9884
+ nextSnapshot = await input.store.readSnapshot(existing.conversationId);
9885
+ }
9886
+ const updatedAt = isoFromHermesTime(candidate.session.last_active) ?? existing.manifest.updated_at;
9887
+ const nextManifest = mergeHermesLineageIntoManifest({
9888
+ manifest: existing.manifest,
9889
+ candidate,
9890
+ snapshot: nextSnapshot,
9891
+ profileUid: profile.profileUid,
9892
+ profileName: profile.profileName,
9893
+ updatedAt
9894
+ });
9895
+ if (manifestEquivalent(existing.manifest, nextManifest)) {
9896
+ return nextSnapshot !== existing.snapshot;
9534
9897
  }
9535
- return changed;
9898
+ await input.store.writeManifest(nextManifest);
9899
+ await upsertConversationStats(
9900
+ input.paths,
9901
+ toStatsIndexRecord(nextManifest, nextManifest.stats)
9902
+ );
9903
+ return true;
9904
+ }
9905
+ function findKnownConversationIdsForCandidate(known, candidate) {
9906
+ const conversationIds = [];
9907
+ for (const sessionId of lineageSessionIds(candidate)) {
9908
+ for (const conversationId of known.conversationIdsBySessionId.get(sessionId) ?? []) {
9909
+ if (!conversationIds.includes(conversationId)) {
9910
+ conversationIds.push(conversationId);
9911
+ }
9912
+ }
9913
+ }
9914
+ return conversationIds;
9915
+ }
9916
+ function lineageSessionIds(candidate) {
9917
+ return normalizeSessionIds([
9918
+ candidate.session._lineage_root_id,
9919
+ ...candidate.session._lineage_session_ids ?? [],
9920
+ candidate.session.id
9921
+ ]);
9922
+ }
9923
+ function lineageTitle(candidate) {
9924
+ return candidate.session._lineage_title ?? stripCompressionTitleSuffix2(readString9(candidate.session, "title") ?? "");
9925
+ }
9926
+ function lineageManifestPatch(candidate) {
9927
+ const sessionIds = lineageSessionIds(candidate);
9928
+ const rootSessionId = candidate.session._lineage_root_id ?? sessionIds[0];
9929
+ const currentSessionId = candidate.session.id;
9930
+ if (!rootSessionId || sessionIds.length <= 1 || rootSessionId === currentSessionId) {
9931
+ return {};
9932
+ }
9933
+ return {
9934
+ hermes_lineage: {
9935
+ kind: "compression",
9936
+ root_session_id: rootSessionId,
9937
+ current_session_id: currentSessionId,
9938
+ session_ids: sessionIds
9939
+ }
9940
+ };
9941
+ }
9942
+ function buildImportedHermesStats(input) {
9943
+ const usage = input.candidate.session._lineage_usage;
9944
+ const base = input.baseStats;
9945
+ const usageInput = usage?.input_tokens ?? 0;
9946
+ const usageOutput = usage?.output_tokens ?? 0;
9947
+ const usageTotal = usage?.total_tokens ?? usageInput + usageOutput;
9948
+ const inputTokens = Math.max(base?.input_tokens ?? 0, usageInput);
9949
+ const outputTokens = Math.max(base?.output_tokens ?? 0, usageOutput);
9950
+ const totalTokens = Math.max(
9951
+ base?.total_tokens ?? 0,
9952
+ usageTotal,
9953
+ inputTokens + outputTokens
9954
+ );
9955
+ const updatedAt = base?.updated_at ? latestTimestamp(base.updated_at, input.updatedAt) : input.updatedAt;
9956
+ return {
9957
+ input_tokens: inputTokens,
9958
+ output_tokens: outputTokens,
9959
+ total_tokens: totalTokens,
9960
+ message_count: Math.max(
9961
+ base?.message_count ?? 0,
9962
+ input.snapshot.messages.length
9963
+ ),
9964
+ run_count: Math.max(base?.run_count ?? 0, usage?.api_call_count ?? 0),
9965
+ profile_uid: base?.profile_uid ?? input.profileUid,
9966
+ profile_name_snapshot: base?.profile_name_snapshot ?? base?.profile ?? input.profileName,
9967
+ profile: base?.profile ?? base?.profile_name_snapshot ?? input.profileName,
9968
+ ...base?.model ?? usage?.model ? { model: base?.model ?? usage?.model } : {},
9969
+ ...base?.provider ?? usage?.provider ? { provider: base?.provider ?? usage?.provider } : {},
9970
+ ...base?.context_window ? { context_window: base.context_window } : {},
9971
+ updated_at: updatedAt
9972
+ };
9973
+ }
9974
+ async function readExistingHermesConversations(store, conversationIds) {
9975
+ const conversations = [];
9976
+ for (const conversationId of [...new Set(conversationIds)]) {
9977
+ const manifest = await store.readManifest(conversationId).catch(() => null);
9978
+ if (!manifest) {
9979
+ continue;
9980
+ }
9981
+ const snapshot = await store.readSnapshot(conversationId).catch(() => ({
9982
+ schema_version: 1,
9983
+ messages: [],
9984
+ runs: []
9985
+ }));
9986
+ conversations.push({ conversationId, manifest, snapshot });
9987
+ }
9988
+ return conversations;
9989
+ }
9990
+ function selectCanonicalHermesConversation(conversations) {
9991
+ const ranked = [...conversations].sort((left, right) => {
9992
+ const rightScore = canonicalConversationScore(right);
9993
+ const leftScore = canonicalConversationScore(left);
9994
+ if (rightScore !== leftScore) {
9995
+ return rightScore - leftScore;
9996
+ }
9997
+ return Date.parse(left.manifest.created_at) - Date.parse(right.manifest.created_at) || left.conversationId.localeCompare(right.conversationId);
9998
+ });
9999
+ return ranked[0] ?? null;
10000
+ }
10001
+ function canonicalConversationScore(item) {
10002
+ let score = 0;
10003
+ if (!isSafeHermesImportConversation(item)) {
10004
+ score += 1e3;
10005
+ }
10006
+ if (item.snapshot.runs.length > 0) {
10007
+ score += 500;
10008
+ }
10009
+ if (item.manifest.title_source === "manual") {
10010
+ score += 200;
10011
+ }
10012
+ if (!hasCompressionTitleSuffix(item.manifest.title)) {
10013
+ score += 100;
10014
+ }
10015
+ if (item.manifest.hermes_lineage?.root_session_id && item.manifest.hermes_session_id === item.manifest.hermes_lineage.root_session_id) {
10016
+ score += 50;
10017
+ }
10018
+ return score;
10019
+ }
10020
+ function isSafeHermesImportConversation(item) {
10021
+ const snapshot = item.snapshot;
10022
+ if (snapshot.runs.length > 0) {
10023
+ return false;
10024
+ }
10025
+ if (item.manifest.title_source === "manual" || item.manifest.title_source === "generated") {
10026
+ return false;
10027
+ }
10028
+ return snapshot.messages.length === 0 || snapshot.messages.every((message) => isHermesImportedMessage(message));
10029
+ }
10030
+ async function softDeleteMergedHermesDuplicate(input) {
10031
+ const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
10032
+ const emptySnapshot3 = {
10033
+ schema_version: 1,
10034
+ messages: [],
10035
+ runs: []
10036
+ };
10037
+ const hermesSessionIds = normalizeSessionIds([
10038
+ input.duplicate.manifest.hermes_session_id,
10039
+ ...input.duplicate.manifest.hermes_session_ids ?? [],
10040
+ ...input.duplicate.manifest.hermes_lineage?.session_ids ?? [],
10041
+ ...lineageSessionIds(input.candidate)
10042
+ ]);
10043
+ const nextManifest = {
10044
+ ...input.duplicate.manifest,
10045
+ status: "deleted_soft",
10046
+ hermes_session_ids: hermesSessionIds,
10047
+ ...lineageManifestPatch(input.candidate),
10048
+ updated_at: deletedAt,
10049
+ deleted_at: deletedAt
10050
+ };
10051
+ const stats = buildConversationStats(
10052
+ { ...nextManifest, stats: input.duplicate.manifest.stats },
10053
+ emptySnapshot3
10054
+ );
10055
+ const tombstone = { ...nextManifest, stats };
10056
+ await input.store.writeManifest(tombstone);
10057
+ await input.store.writeSnapshot(input.duplicate.conversationId, emptySnapshot3);
10058
+ await upsertConversationStats(input.paths, toStatsIndexRecord(tombstone, stats));
10059
+ }
10060
+ function mergeHermesLineageIntoManifest(input) {
10061
+ const hermesSessionIds = normalizeSessionIds([
10062
+ input.manifest.hermes_session_id,
10063
+ ...input.manifest.hermes_session_ids ?? [],
10064
+ ...input.manifest.hermes_lineage?.session_ids ?? [],
10065
+ ...lineageSessionIds(input.candidate)
10066
+ ]);
10067
+ const nextBase = {
10068
+ ...input.manifest,
10069
+ hermes_session_id: input.candidate.session.id,
10070
+ hermes_session_ids: hermesSessionIds,
10071
+ ...lineageManifestPatch(input.candidate),
10072
+ profile_uid: input.manifest.profile_uid ?? input.profileUid,
10073
+ profile_name_snapshot: input.manifest.profile_name_snapshot ?? input.manifest.profile ?? input.profileName,
10074
+ profile: input.manifest.profile ?? input.manifest.profile_name_snapshot ?? input.profileName,
10075
+ updated_at: latestTimestamp(input.manifest.updated_at, input.updatedAt)
10076
+ };
10077
+ const title = lineageTitle(input.candidate);
10078
+ if (title && canSyncHermesTitle(input.manifest)) {
10079
+ nextBase.title = normalizeTitle(title);
10080
+ nextBase.title_source = "hermes";
10081
+ }
10082
+ const baseStats = buildConversationStats(nextBase, input.snapshot);
10083
+ return {
10084
+ ...nextBase,
10085
+ stats: buildImportedHermesStats({
10086
+ candidate: input.candidate,
10087
+ snapshot: input.snapshot,
10088
+ profileUid: input.profileUid,
10089
+ profileName: input.profileName,
10090
+ updatedAt: input.updatedAt,
10091
+ baseStats
10092
+ })
10093
+ };
10094
+ }
10095
+ function manifestEquivalent(left, right) {
10096
+ return stableJson(left) === stableJson(right);
9536
10097
  }
9537
10098
  function collectImportedHermesPrefix(snapshot) {
9538
10099
  const rows = [];
@@ -10006,7 +10567,7 @@ function latestTimestamp(left, right) {
10006
10567
  return leftTime >= rightTime ? left : right;
10007
10568
  }
10008
10569
  async function readKnownHermesSessions(store) {
10009
- const ids = /* @__PURE__ */ new Set();
10570
+ const sessionIds = /* @__PURE__ */ new Set();
10010
10571
  const conversationIdsBySessionId = /* @__PURE__ */ new Map();
10011
10572
  for (const conversationId of await store.listConversationIds()) {
10012
10573
  const manifest = await store.readManifest(conversationId).catch(() => null);
@@ -10014,7 +10575,7 @@ async function readKnownHermesSessions(store) {
10014
10575
  continue;
10015
10576
  }
10016
10577
  if (manifest.hermes_session_id) {
10017
- ids.add(manifest.hermes_session_id);
10578
+ sessionIds.add(manifest.hermes_session_id);
10018
10579
  rememberKnownHermesConversation(
10019
10580
  conversationIdsBySessionId,
10020
10581
  manifest.hermes_session_id,
@@ -10022,7 +10583,22 @@ async function readKnownHermesSessions(store) {
10022
10583
  );
10023
10584
  }
10024
10585
  for (const sessionId of manifest.hermes_session_ids ?? []) {
10025
- ids.add(sessionId);
10586
+ sessionIds.add(sessionId);
10587
+ rememberKnownHermesConversation(
10588
+ conversationIdsBySessionId,
10589
+ sessionId,
10590
+ conversationId
10591
+ );
10592
+ }
10593
+ for (const sessionId of [
10594
+ manifest.hermes_lineage?.root_session_id,
10595
+ manifest.hermes_lineage?.current_session_id,
10596
+ ...manifest.hermes_lineage?.session_ids ?? []
10597
+ ]) {
10598
+ if (!sessionId) {
10599
+ continue;
10600
+ }
10601
+ sessionIds.add(sessionId);
10026
10602
  rememberKnownHermesConversation(
10027
10603
  conversationIdsBySessionId,
10028
10604
  sessionId,
@@ -10030,7 +10606,7 @@ async function readKnownHermesSessions(store) {
10030
10606
  );
10031
10607
  }
10032
10608
  }
10033
- return { ids, conversationIdsBySessionId };
10609
+ return { sessionIds, conversationIdsBySessionId };
10034
10610
  }
10035
10611
  function rememberKnownHermesConversation(map, sessionId, conversationId) {
10036
10612
  const current = map.get(sessionId) ?? [];
@@ -10040,7 +10616,7 @@ function rememberKnownHermesConversation(map, sessionId, conversationId) {
10040
10616
  }
10041
10617
  async function discoverHermesProfileNames() {
10042
10618
  const names = /* @__PURE__ */ new Set([DEFAULT_PROFILE_NAME]);
10043
- const profilesDir = path14.join(os4.homedir(), ".hermes", "profiles");
10619
+ const profilesDir = path15.join(os4.homedir(), ".hermes", "profiles");
10044
10620
  const entries = await readdir7(profilesDir, { withFileTypes: true }).catch(
10045
10621
  (error) => {
10046
10622
  if (isNodeError11(error, "ENOENT")) {
@@ -10148,14 +10724,12 @@ function joinImportedText(left, right) {
10148
10724
  ${right}`;
10149
10725
  }
10150
10726
  function projectCompressionTips(rows) {
10151
- const byId = /* @__PURE__ */ new Map();
10152
10727
  const childrenByParent = /* @__PURE__ */ new Map();
10153
10728
  for (const row of rows) {
10154
10729
  const id = readString9(row, "id");
10155
10730
  if (!id) {
10156
10731
  continue;
10157
10732
  }
10158
- byId.set(id, row);
10159
10733
  const parentId = readString9(row, "parent_session_id");
10160
10734
  if (parentId) {
10161
10735
  const children = childrenByParent.get(parentId) ?? [];
@@ -10171,13 +10745,14 @@ function projectCompressionTips(rows) {
10171
10745
  }
10172
10746
  let tip = row;
10173
10747
  const visited = /* @__PURE__ */ new Set([id]);
10748
+ const chain = [row];
10174
10749
  while (readString9(tip, "end_reason") === "compression") {
10175
10750
  const tipId2 = readString9(tip, "id");
10176
10751
  if (!tipId2) {
10177
10752
  break;
10178
10753
  }
10179
- const next = (childrenByParent.get(tipId2) ?? []).filter((child) => readString9(child, "id")).sort(
10180
- (left, right) => (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
10754
+ const next = (childrenByParent.get(tipId2) ?? []).filter((child) => isCompressionContinuation(tip, child)).sort(
10755
+ (left, right) => (readNumber2(right.started_at) ?? 0) - (readNumber2(left.started_at) ?? 0) || (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
10181
10756
  )[0];
10182
10757
  const nextId = next ? readString9(next, "id") : null;
10183
10758
  if (!next || !nextId || visited.has(nextId)) {
@@ -10185,25 +10760,75 @@ function projectCompressionTips(rows) {
10185
10760
  }
10186
10761
  tip = next;
10187
10762
  visited.add(nextId);
10763
+ chain.push(next);
10188
10764
  }
10189
10765
  const tipId = readString9(tip, "id");
10190
10766
  if (tipId) {
10767
+ const sessionIds = chain.map((item) => readString9(item, "id")).filter((item) => Boolean(item));
10191
10768
  projected.push({
10192
10769
  ...tip,
10193
10770
  id: tipId,
10194
10771
  _lineage_root_id: id,
10772
+ _lineage_session_ids: sessionIds,
10773
+ _lineage_title: readString9(row, "title") ?? stripCompressionTitleSuffix2(readString9(tip, "title") ?? ""),
10774
+ _lineage_usage: aggregateHermesSessionUsage(chain),
10195
10775
  started_at: readNumber2(row.started_at) ?? readNumber2(tip.started_at)
10196
10776
  });
10197
10777
  }
10198
10778
  }
10199
10779
  return projected;
10200
10780
  }
10781
+ function isCompressionContinuation(parent, child) {
10782
+ const childId = readString9(child, "id");
10783
+ const parentEndedAt = readNumber2(parent.ended_at);
10784
+ const childStartedAt = readNumber2(child.started_at);
10785
+ return Boolean(childId) && readString9(parent, "end_reason") === "compression" && parentEndedAt !== null && childStartedAt !== null && childStartedAt >= parentEndedAt;
10786
+ }
10787
+ function aggregateHermesSessionUsage(rows) {
10788
+ let inputTokens = 0;
10789
+ let outputTokens = 0;
10790
+ let apiCallCount = 0;
10791
+ let model;
10792
+ let provider;
10793
+ for (const row of rows) {
10794
+ inputTokens += readNumber2(row.input_tokens) ?? 0;
10795
+ outputTokens += readNumber2(row.output_tokens) ?? 0;
10796
+ apiCallCount += readNumber2(row.api_call_count) ?? 0;
10797
+ model = readString9(row, "model") ?? model;
10798
+ provider = readString9(row, "billing_provider") ?? readString9(row, "provider") ?? provider;
10799
+ }
10800
+ return {
10801
+ input_tokens: inputTokens,
10802
+ output_tokens: outputTokens,
10803
+ total_tokens: inputTokens + outputTokens,
10804
+ api_call_count: apiCallCount,
10805
+ ...model ? { model } : {},
10806
+ ...provider ? { provider } : {}
10807
+ };
10808
+ }
10809
+ async function readHermesLineageMessages(candidate) {
10810
+ const rows = [];
10811
+ for (const sessionId of lineageSessionIds(candidate)) {
10812
+ const messages = await readHermesSessionMessages({
10813
+ ...candidate,
10814
+ session: {
10815
+ ...candidate.session,
10816
+ id: sessionId
10817
+ }
10818
+ });
10819
+ rows.push(...messages);
10820
+ }
10821
+ return rows;
10822
+ }
10201
10823
  async function readHermesSessionMessages(candidate) {
10202
10824
  const [dbMessages, jsonlMessages] = await Promise.all([
10203
10825
  readStateDbMessages(candidate.dbPath, candidate.session.id),
10204
10826
  readJsonlMessages(candidate.profileName, candidate.session.id)
10205
10827
  ]);
10206
- return jsonlMessages.length > dbMessages.length ? jsonlMessages : dbMessages;
10828
+ const selected = jsonlMessages.length > dbMessages.length ? jsonlMessages : dbMessages;
10829
+ return selected.map(
10830
+ (message) => readString9(message, "session_id") ? message : { ...message, session_id: candidate.session.id }
10831
+ );
10207
10832
  }
10208
10833
  async function readStateDbMessages(dbPath, sessionId) {
10209
10834
  if (!await isFile(dbPath)) {
@@ -10241,8 +10866,8 @@ async function readJsonlMessages(profileName, sessionId) {
10241
10866
  return [];
10242
10867
  }
10243
10868
  const profileDir = resolveHermesProfileDir(profileName);
10244
- const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path14.join(profileDir, "sessions"));
10245
- const transcriptPath = path14.join(sessionsDir, `${sessionId}.jsonl`);
10869
+ const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path15.join(profileDir, "sessions"));
10870
+ const transcriptPath = path15.join(sessionsDir, `${sessionId}.jsonl`);
10246
10871
  const raw = await readFile9(transcriptPath, "utf8").catch((error) => {
10247
10872
  if (isNodeError11(error, "ENOENT")) {
10248
10873
  return "";
@@ -10394,9 +11019,10 @@ function formatImportedFilenameList(filenames) {
10394
11019
  function toLinkMessage(input) {
10395
11020
  const role = normalizeMessageRole(input.message.role);
10396
11021
  const text = normalizeContent(input.message.content);
11022
+ const sessionId = readString9(input.message, "session_id") ?? input.sessionId;
10397
11023
  const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
10398
11024
  return {
10399
- id: `msg_${randomUUID6().replaceAll("-", "")}`,
11025
+ id: `msg_${randomUUID7().replaceAll("-", "")}`,
10400
11026
  schema_version: 1,
10401
11027
  conversation_id: input.conversationId,
10402
11028
  role,
@@ -10412,7 +11038,7 @@ function toLinkMessage(input) {
10412
11038
  parts: text ? [{ type: "text", text }] : [],
10413
11039
  attachments: [],
10414
11040
  hermes: {
10415
- session_id: input.sessionId,
11041
+ session_id: sessionId,
10416
11042
  message_id: input.message.id,
10417
11043
  imported_from: "hermes",
10418
11044
  import_projection: HERMES_IMPORT_PROJECTION_VERSION
@@ -10448,6 +11074,47 @@ function normalizeTitle(value) {
10448
11074
  const normalized = value?.replace(/\s+/gu, " ").trim();
10449
11075
  return normalized || DEFAULT_CONVERSATION_TITLE;
10450
11076
  }
11077
+ function canSyncHermesTitle(manifest) {
11078
+ return manifest.title_source !== "manual" && manifest.title_source !== "generated" && manifest.title_source !== "temporary_user_message" && manifest.title_source !== "temporary_fallback";
11079
+ }
11080
+ function stripCompressionTitleSuffix2(value) {
11081
+ const normalized = value.replace(/\s+/gu, " ").trim();
11082
+ if (!normalized) {
11083
+ return void 0;
11084
+ }
11085
+ const match = /^(.*?) #\d+$/u.exec(normalized);
11086
+ const stripped = match?.[1]?.trim();
11087
+ return stripped || normalized;
11088
+ }
11089
+ function hasCompressionTitleSuffix(value) {
11090
+ return / #\d+$/u.test(value.trim());
11091
+ }
11092
+ function normalizeSessionIds(values) {
11093
+ const seen = /* @__PURE__ */ new Set();
11094
+ for (const value of values) {
11095
+ const sessionId = value?.trim();
11096
+ if (!sessionId || seen.has(sessionId)) {
11097
+ continue;
11098
+ }
11099
+ seen.add(sessionId);
11100
+ }
11101
+ return [...seen];
11102
+ }
11103
+ function stableJson(value) {
11104
+ return JSON.stringify(stabilizeJsonValue(value));
11105
+ }
11106
+ function stabilizeJsonValue(value) {
11107
+ if (Array.isArray(value)) {
11108
+ return value.map((item) => stabilizeJsonValue(item));
11109
+ }
11110
+ if (!value || typeof value !== "object") {
11111
+ return value;
11112
+ }
11113
+ const record = value;
11114
+ return Object.fromEntries(
11115
+ Object.keys(record).sort().map((key) => [key, stabilizeJsonValue(record[key])])
11116
+ );
11117
+ }
10451
11118
  function normalizeMessageRole(value) {
10452
11119
  switch (value?.trim().toLowerCase()) {
10453
11120
  case "user":
@@ -10527,7 +11194,7 @@ async function isFile(filePath) {
10527
11194
  });
10528
11195
  }
10529
11196
  function createConversationId() {
10530
- return `conv_${randomUUID6().replaceAll("-", "")}`;
11197
+ return `conv_${randomUUID7().replaceAll("-", "")}`;
10531
11198
  }
10532
11199
  function isoFromHermesTime(value) {
10533
11200
  const numeric = readNumber2(value);
@@ -10671,7 +11338,7 @@ async function createHermesRun(input, options = {}) {
10671
11338
  );
10672
11339
  if (response.status === 404 || response.status === 503) {
10673
11340
  assertHermesRunsApiSupported(
10674
- await readHermesVersion().catch(() => null),
11341
+ await readHermesVersion({ logger: options.logger }).catch(() => null),
10675
11342
  response.status
10676
11343
  );
10677
11344
  throw new LinkHttpError(
@@ -10698,7 +11365,7 @@ async function streamHermesRunEvents(runId, options = {}) {
10698
11365
  options
10699
11366
  );
10700
11367
  assertHermesRunsApiSupported(
10701
- await readHermesVersion().catch(() => null),
11368
+ await readHermesVersion({ logger: options.logger }).catch(() => null),
10702
11369
  response.status
10703
11370
  );
10704
11371
  if (!response.ok || !response.body) {
@@ -10741,7 +11408,7 @@ async function streamHermesResponses(input, options = {}) {
10741
11408
  );
10742
11409
  if (response.status === 404 || response.status === 503) {
10743
11410
  assertHermesRunsApiSupported(
10744
- await readHermesVersion().catch(() => null),
11411
+ await readHermesVersion({ logger: options.logger }).catch(() => null),
10745
11412
  response.status
10746
11413
  );
10747
11414
  throw new LinkHttpError(
@@ -10794,10 +11461,10 @@ async function cancelHermesRun(runId, options = {}) {
10794
11461
  );
10795
11462
  }
10796
11463
  }
10797
- async function callHermesApi(path25, init, options) {
11464
+ async function callHermesApi(path26, init, options) {
10798
11465
  const method = init.method ?? "GET";
10799
11466
  const startedAt = Date.now();
10800
- void options.logger?.debug("hermes_api_request_started", { method, path: path25 });
11467
+ void options.logger?.debug("hermes_api_request_started", { method, path: path26 });
10801
11468
  const availability = await ensureHermesApiServerAvailable({
10802
11469
  fetchImpl: options.fetchImpl,
10803
11470
  logger: options.logger,
@@ -10805,21 +11472,21 @@ async function callHermesApi(path25, init, options) {
10805
11472
  });
10806
11473
  let config = availability.configResult.apiServer;
10807
11474
  const fetcher = options.fetchImpl ?? fetch;
10808
- const request = () => fetchHermesApi(fetcher, config, path25, init, options);
11475
+ const request = () => fetchHermesApi(fetcher, config, path26, init, options);
10809
11476
  let response;
10810
11477
  try {
10811
11478
  response = await request();
10812
11479
  } catch (error) {
10813
- logHermesApiError(options.logger, method, path25, startedAt, error);
11480
+ logHermesApiError(options.logger, method, path26, startedAt, error);
10814
11481
  throw error;
10815
11482
  }
10816
11483
  if (response.status !== 401) {
10817
- logHermesApiResponse(options.logger, method, path25, startedAt, response);
11484
+ logHermesApiResponse(options.logger, method, path26, startedAt, response);
10818
11485
  return response;
10819
11486
  }
10820
11487
  void options.logger?.warn("hermes_api_request_retrying_after_401", {
10821
11488
  method,
10822
- path: path25,
11489
+ path: path26,
10823
11490
  duration_ms: Date.now() - startedAt
10824
11491
  });
10825
11492
  const refreshedAvailability = await ensureHermesApiServerAvailable({
@@ -10832,20 +11499,20 @@ async function callHermesApi(path25, init, options) {
10832
11499
  try {
10833
11500
  response = await request();
10834
11501
  } catch (error) {
10835
- logHermesApiError(options.logger, method, path25, startedAt, error);
11502
+ logHermesApiError(options.logger, method, path26, startedAt, error);
10836
11503
  throw error;
10837
11504
  }
10838
- logHermesApiResponse(options.logger, method, path25, startedAt, response);
11505
+ logHermesApiResponse(options.logger, method, path26, startedAt, response);
10839
11506
  return response;
10840
11507
  }
10841
- async function fetchHermesApi(fetcher, config, path25, init, options) {
11508
+ async function fetchHermesApi(fetcher, config, path26, init, options) {
10842
11509
  const headers = new Headers(init.headers);
10843
11510
  headers.set("accept", headers.get("accept") ?? "application/json");
10844
11511
  if (config.key) {
10845
11512
  headers.set("x-api-key", config.key);
10846
11513
  headers.set("authorization", `Bearer ${config.key}`);
10847
11514
  }
10848
- return await fetcher(`http://127.0.0.1:${config.port}${path25}`, {
11515
+ return await fetcher(`http://127.0.0.1:${config.port}${path26}`, {
10849
11516
  ...init,
10850
11517
  headers
10851
11518
  }).catch((error) => {
@@ -10853,7 +11520,7 @@ async function fetchHermesApi(fetcher, config, path25, init, options) {
10853
11520
  throw error;
10854
11521
  }
10855
11522
  void options.logger?.warn("hermes_api_server_connect_failed", {
10856
- path: path25,
11523
+ path: path26,
10857
11524
  port: config.port ?? null,
10858
11525
  error: error instanceof Error ? error.message : String(error)
10859
11526
  });
@@ -10864,10 +11531,10 @@ async function fetchHermesApi(fetcher, config, path25, init, options) {
10864
11531
  );
10865
11532
  });
10866
11533
  }
10867
- function logHermesApiResponse(logger, method, path25, startedAt, response) {
11534
+ function logHermesApiResponse(logger, method, path26, startedAt, response) {
10868
11535
  const fields = {
10869
11536
  method,
10870
- path: path25,
11537
+ path: path26,
10871
11538
  status: response.status,
10872
11539
  duration_ms: Date.now() - startedAt
10873
11540
  };
@@ -10887,10 +11554,10 @@ async function logHermesApiFailureResponse(logger, fields, response) {
10887
11554
  ...upstreamError ? { upstream_error: upstreamError } : {}
10888
11555
  });
10889
11556
  }
10890
- function logHermesApiError(logger, method, path25, startedAt, error) {
11557
+ function logHermesApiError(logger, method, path26, startedAt, error) {
10891
11558
  void logger?.warn("hermes_api_request_failed", {
10892
11559
  method,
10893
- path: path25,
11560
+ path: path26,
10894
11561
  duration_ms: Date.now() - startedAt,
10895
11562
  ...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
10896
11563
  error: error instanceof Error ? error.message : String(error)
@@ -10953,7 +11620,7 @@ function readString10(payload, key) {
10953
11620
 
10954
11621
  // src/conversations/history-builder.ts
10955
11622
  import { readFile as readFile10, stat as stat10 } from "fs/promises";
10956
- import path15 from "path";
11623
+ import path16 from "path";
10957
11624
  var HISTORY_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
10958
11625
  var HERMES_HISTORY_COLUMNS = [
10959
11626
  "role",
@@ -11014,13 +11681,13 @@ async function readHermesTranscriptHistory(sessionId, profileName) {
11014
11681
  }
11015
11682
  const normalizedProfileName = isValidProfileName2(profileName) ? profileName : "default";
11016
11683
  const profileDir = resolveHermesProfileDir(normalizedProfileName);
11017
- const dbPath = path15.join(profileDir, "state.db");
11684
+ const dbPath = path16.join(profileDir, "state.db");
11018
11685
  const sessionsDirConfig = await readHermesSessionsDir(normalizedProfileName).then((value) => ({
11019
11686
  sessionsDir: value.sessionsDir,
11020
11687
  configured: value.configured,
11021
11688
  configError: false
11022
11689
  })).catch(() => ({
11023
- sessionsDir: path15.join(profileDir, "sessions"),
11690
+ sessionsDir: path16.join(profileDir, "sessions"),
11024
11691
  configured: false,
11025
11692
  configError: true
11026
11693
  }));
@@ -11078,7 +11745,7 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
11078
11745
  if (!isValidSessionFileStem(sessionId)) {
11079
11746
  return empty;
11080
11747
  }
11081
- const transcriptPath = path15.join(sessionsDir, `${sessionId}.jsonl`);
11748
+ const transcriptPath = path16.join(sessionsDir, `${sessionId}.jsonl`);
11082
11749
  const raw = await readFile10(transcriptPath, "utf8").catch((error) => {
11083
11750
  if (isNodeError12(error, "ENOENT")) {
11084
11751
  return "";
@@ -11321,7 +11988,7 @@ function normalizeProfileForCompare(value) {
11321
11988
  // src/hermes/stt.ts
11322
11989
  import { execFile as execFile3 } from "child_process";
11323
11990
  import { access as access2, readFile as readFile11, stat as stat11 } from "fs/promises";
11324
- import path16 from "path";
11991
+ import path17 from "path";
11325
11992
  import { promisify as promisify3 } from "util";
11326
11993
  var execFileAsync3 = promisify3(execFile3);
11327
11994
  var STT_RESULT_PREFIX = "__HERMES_LINK_STT__";
@@ -11416,7 +12083,7 @@ async function buildHermesSttEnv(profileName) {
11416
12083
  };
11417
12084
  const devSource = await findDevHermesAgentSource();
11418
12085
  if (devSource) {
11419
- env.PYTHONPATH = [devSource, env.PYTHONPATH].filter(Boolean).join(path16.delimiter);
12086
+ env.PYTHONPATH = [devSource, env.PYTHONPATH].filter(Boolean).join(path17.delimiter);
11420
12087
  }
11421
12088
  return env;
11422
12089
  }
@@ -11463,14 +12130,14 @@ async function resolveHermesPythonCommand() {
11463
12130
  };
11464
12131
  }
11465
12132
  async function resolveExecutablePath(command) {
11466
- if (path16.isAbsolute(command)) {
12133
+ if (path17.isAbsolute(command)) {
11467
12134
  return await isExecutableFile(command) ? command : null;
11468
12135
  }
11469
12136
  const pathEnv = process.env.PATH ?? "";
11470
12137
  const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
11471
- for (const dir of pathEnv.split(path16.delimiter)) {
12138
+ for (const dir of pathEnv.split(path17.delimiter)) {
11472
12139
  for (const extension of extensions) {
11473
- const candidate = path16.join(dir, `${command}${extension}`);
12140
+ const candidate = path17.join(dir, `${command}${extension}`);
11474
12141
  if (await isExecutableFile(candidate)) {
11475
12142
  return candidate;
11476
12143
  }
@@ -11510,8 +12177,8 @@ function shebangToPythonCommand(shebang) {
11510
12177
  }
11511
12178
  async function findDevHermesAgentSource() {
11512
12179
  const candidates = [
11513
- path16.resolve(process.cwd(), "reference/hermes-agent"),
11514
- path16.resolve(process.cwd(), "../../reference/hermes-agent")
12180
+ path17.resolve(process.cwd(), "reference/hermes-agent"),
12181
+ path17.resolve(process.cwd(), "../../reference/hermes-agent")
11515
12182
  ];
11516
12183
  for (const candidate of candidates) {
11517
12184
  if (await isDirectory(candidate)) {
@@ -13152,7 +13819,7 @@ var ConversationService = class {
13152
13819
  async createConversation(input = {}) {
13153
13820
  await this.store.ensureConversationsDir();
13154
13821
  const now = (/* @__PURE__ */ new Date()).toISOString();
13155
- const id = `conv_${randomUUID7().replaceAll("-", "")}`;
13822
+ const id = `conv_${randomUUID8().replaceAll("-", "")}`;
13156
13823
  const title = input.title?.trim() || DEFAULT_CONVERSATION_TITLE;
13157
13824
  const profile = await resolveConversationProfileTarget(
13158
13825
  this.paths,
@@ -13239,7 +13906,7 @@ var ConversationService = class {
13239
13906
  manifest.profile_name_snapshot ?? manifest.profile ?? input.profileName
13240
13907
  );
13241
13908
  const message = {
13242
- id: `msg_${randomUUID7().replaceAll("-", "")}`,
13909
+ id: `msg_${randomUUID8().replaceAll("-", "")}`,
13243
13910
  schema_version: 1,
13244
13911
  conversation_id: manifest.id,
13245
13912
  role: "assistant",
@@ -13703,6 +14370,18 @@ var ConversationService = class {
13703
14370
  async deleteConversation(conversationId) {
13704
14371
  return this.maintenance.deleteConversation(conversationId);
13705
14372
  }
14373
+ prepareClearAllConversationPlan() {
14374
+ return this.maintenance.prepareClearAllConversationPlan();
14375
+ }
14376
+ readClearAllConversationPlan(planId) {
14377
+ return this.maintenance.readClearAllConversationPlan(planId);
14378
+ }
14379
+ executeClearAllConversationPlan(planId) {
14380
+ return this.maintenance.executeClearAllConversationPlan(planId);
14381
+ }
14382
+ startClearAllConversationPlan(planId) {
14383
+ return this.maintenance.startClearAllConversationPlan(planId);
14384
+ }
13706
14385
  async deleteConversations(conversationIds) {
13707
14386
  return this.maintenance.deleteConversations(conversationIds);
13708
14387
  }
@@ -13831,8 +14510,8 @@ function findApproval(snapshot, approvalId) {
13831
14510
  }
13832
14511
 
13833
14512
  // src/identity/identity.ts
13834
- import { generateKeyPairSync, randomUUID as randomUUID8, sign } from "crypto";
13835
- import { mkdir as mkdir8, chmod as chmod2 } from "fs/promises";
14513
+ import { generateKeyPairSync, randomUUID as randomUUID9, sign } from "crypto";
14514
+ import { mkdir as mkdir9, chmod as chmod2 } from "fs/promises";
13836
14515
  import { z } from "zod";
13837
14516
  var linkIdentitySchema = z.object({
13838
14517
  install_id: z.string().min(1),
@@ -13854,12 +14533,12 @@ async function ensureIdentity(paths = resolveRuntimePaths()) {
13854
14533
  if (existing) {
13855
14534
  return existing;
13856
14535
  }
13857
- await mkdir8(paths.homeDir, { recursive: true, mode: 448 });
14536
+ await mkdir9(paths.homeDir, { recursive: true, mode: 448 });
13858
14537
  await chmod2(paths.homeDir, 448).catch(() => void 0);
13859
14538
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
13860
14539
  const now = (/* @__PURE__ */ new Date()).toISOString();
13861
14540
  const identity = {
13862
- install_id: `install_${randomUUID8().replaceAll("-", "")}`,
14541
+ install_id: `install_${randomUUID9().replaceAll("-", "")}`,
13863
14542
  link_id: null,
13864
14543
  public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
13865
14544
  private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
@@ -13896,7 +14575,7 @@ function getIdentityStatus(identity) {
13896
14575
  }
13897
14576
 
13898
14577
  // src/security/devices.ts
13899
- import { randomBytes as randomBytes2, randomUUID as randomUUID9, timingSafeEqual, createHash as createHash4 } from "crypto";
14578
+ import { randomBytes as randomBytes2, randomUUID as randomUUID10, timingSafeEqual, createHash as createHash4 } from "crypto";
13900
14579
  var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
13901
14580
  var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
13902
14581
  var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
@@ -13914,7 +14593,7 @@ async function createDeviceSession(input, paths = resolveRuntimePaths()) {
13914
14593
  }
13915
14594
  }
13916
14595
  const device = {
13917
- id: `dev_${randomUUID9().replaceAll("-", "")}`,
14596
+ id: `dev_${randomUUID10().replaceAll("-", "")}`,
13918
14597
  label: normalizeDeviceLabel(input.label),
13919
14598
  platform: normalizeDevicePlatform(input.platform),
13920
14599
  model: normalizeDeviceModel(input.model),
@@ -14828,6 +15507,37 @@ function registerConversationRoutes(router, options) {
14828
15507
  await authenticateRequest(ctx, paths);
14829
15508
  ctx.body = { ok: true };
14830
15509
  });
15510
+ router.post("/api/v1/conversations/clear-plans", async (ctx) => {
15511
+ await authenticateRequest(ctx, paths);
15512
+ const plan = await conversations.prepareClearAllConversationPlan();
15513
+ ctx.status = 201;
15514
+ ctx.body = {
15515
+ ok: true,
15516
+ plan
15517
+ };
15518
+ });
15519
+ router.get("/api/v1/conversations/clear-plans/:planId", async (ctx) => {
15520
+ await authenticateRequest(ctx, paths);
15521
+ ctx.set("cache-control", "no-store");
15522
+ ctx.body = {
15523
+ ok: true,
15524
+ plan: await conversations.readClearAllConversationPlan(ctx.params.planId)
15525
+ };
15526
+ });
15527
+ router.post(
15528
+ "/api/v1/conversations/clear-plans/:planId/execute",
15529
+ async (ctx) => {
15530
+ await authenticateRequest(ctx, paths);
15531
+ const plan = await conversations.startClearAllConversationPlan(
15532
+ ctx.params.planId
15533
+ );
15534
+ ctx.status = plan.status === "completed" ? 200 : 202;
15535
+ ctx.body = {
15536
+ ok: true,
15537
+ plan
15538
+ };
15539
+ }
15540
+ );
14831
15541
  router.delete("/api/v1/conversations", async (ctx) => {
14832
15542
  await authenticateRequest(ctx, paths);
14833
15543
  const body = await readJsonBody(ctx.req);
@@ -15051,14 +15761,14 @@ function createHttpErrorMiddleware(logger) {
15051
15761
  // src/hermes/profiles.ts
15052
15762
  import { readdir as readdir9, readFile as readFile12, rename as rename3, rm as rm6, stat as stat12 } from "fs/promises";
15053
15763
  import os5 from "os";
15054
- import path17 from "path";
15764
+ import path18 from "path";
15055
15765
  import YAML2 from "yaml";
15056
15766
  var DEFAULT_PROFILE = "default";
15057
15767
  var PROFILE_NAME_PATTERN4 = /^[a-zA-Z0-9._-]{1,64}$/;
15058
15768
  async function listHermesProfiles(paths = resolveRuntimePaths()) {
15059
15769
  const profiles = /* @__PURE__ */ new Map();
15060
15770
  profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
15061
- const profilesDir = path17.join(os5.homedir(), ".hermes", "profiles");
15771
+ const profilesDir = path18.join(os5.homedir(), ".hermes", "profiles");
15062
15772
  const entries = await readdir9(profilesDir, { withFileTypes: true }).catch(
15063
15773
  (error) => {
15064
15774
  if (isNodeError14(error, "ENOENT")) {
@@ -15153,7 +15863,7 @@ async function readHermesProfileCapabilities(name) {
15153
15863
  return {
15154
15864
  defaultModel: listedModels?.defaultModel ?? null,
15155
15865
  modelCount: listedModels?.models.length ?? 0,
15156
- skillCount: await countSkills(path17.join(profileDir, "skills")).catch(
15866
+ skillCount: await countSkills(path18.join(profileDir, "skills")).catch(
15157
15867
  () => 0
15158
15868
  ),
15159
15869
  toolCount: await countConfiguredTools(name).catch(() => 0)
@@ -15209,7 +15919,7 @@ async function countSkills(root) {
15209
15919
  );
15210
15920
  let count = 0;
15211
15921
  for (const entry of entries) {
15212
- const entryPath = path17.join(root, entry.name);
15922
+ const entryPath = path18.join(root, entry.name);
15213
15923
  if (entry.name === ".git" || entry.name === ".hub") {
15214
15924
  continue;
15215
15925
  }
@@ -15906,12 +16616,12 @@ import { spawn as spawn2 } from "child_process";
15906
16616
  import { EventEmitter as EventEmitter2 } from "events";
15907
16617
  import {
15908
16618
  cp,
15909
- mkdir as mkdir9,
16619
+ mkdir as mkdir10,
15910
16620
  readFile as readFile13,
15911
16621
  rm as rm7,
15912
16622
  stat as stat13
15913
16623
  } from "fs/promises";
15914
- import path18 from "path";
16624
+ import path19 from "path";
15915
16625
  import YAML3 from "yaml";
15916
16626
  var PROFILE_CREATE_LOG_FILE = "profile-create.log";
15917
16627
  var PROFILE_CREATE_LOG_MAX_FILES = 3;
@@ -15959,7 +16669,7 @@ async function startHermesProfileCreation(input, options) {
15959
16669
  signal: null,
15960
16670
  error: null
15961
16671
  };
15962
- await mkdir9(options.paths.runDir, { recursive: true, mode: 448 });
16672
+ await mkdir10(options.paths.runDir, { recursive: true, mode: 448 });
15963
16673
  await writeProfileCreationState(options.paths, started);
15964
16674
  await writer.write(`
15965
16675
  === profile creation started ${startedAt} ===
@@ -16362,7 +17072,7 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
16362
17072
  return keys;
16363
17073
  }
16364
17074
  async function writeEnvValues(profileName, values) {
16365
- const envPath = path18.join(resolveHermesProfileDir(profileName), ".env");
17075
+ const envPath = path19.join(resolveHermesProfileDir(profileName), ".env");
16366
17076
  const existingRaw = await readFile13(envPath, "utf8").catch((error) => {
16367
17077
  if (isNodeError15(error, "ENOENT")) {
16368
17078
  return "";
@@ -16399,8 +17109,8 @@ async function writeEnvValues(profileName, values) {
16399
17109
  await atomicWriteFilePreservingMetadata(envPath, nextRaw);
16400
17110
  }
16401
17111
  async function copySkills(sourceProfile, targetProfile) {
16402
- const sourceSkills = path18.join(resolveHermesProfileDir(sourceProfile), "skills");
16403
- const targetSkills = path18.join(resolveHermesProfileDir(targetProfile), "skills");
17112
+ const sourceSkills = path19.join(resolveHermesProfileDir(sourceProfile), "skills");
17113
+ const targetSkills = path19.join(resolveHermesProfileDir(targetProfile), "skills");
16404
17114
  if (!await pathExists(sourceSkills)) {
16405
17115
  return;
16406
17116
  }
@@ -16495,10 +17205,10 @@ async function readProfileCreationLogLines(paths) {
16495
17205
  );
16496
17206
  }
16497
17207
  function profileCreationStatePath(paths) {
16498
- return path18.join(paths.runDir, "profile-create-state.json");
17208
+ return path19.join(paths.runDir, "profile-create-state.json");
16499
17209
  }
16500
17210
  function profileCreationLogPath(paths) {
16501
- return path18.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
17211
+ return path19.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
16502
17212
  }
16503
17213
  async function clearProfileCreationLogFiles(paths) {
16504
17214
  const primary = profileCreationLogPath(paths);
@@ -16772,7 +17482,7 @@ import {
16772
17482
  readFile as readFile14,
16773
17483
  stat as stat14
16774
17484
  } from "fs/promises";
16775
- import path19 from "path";
17485
+ import path20 from "path";
16776
17486
  import YAML4 from "yaml";
16777
17487
  var ENTRY_DELIMITER = "\n\xA7\n";
16778
17488
  var DEFAULT_MEMORY_LIMIT = 2200;
@@ -16989,7 +17699,7 @@ async function saveProviderSettings(profileName, provider, patch) {
16989
17699
  if (provider === "hindsight") {
16990
17700
  await patchJsonProviderConfig(
16991
17701
  profileName,
16992
- path19.join("hindsight", "config.json"),
17702
+ path20.join("hindsight", "config.json"),
16993
17703
  {
16994
17704
  mode: patch.mode,
16995
17705
  api_url: patch.apiUrl,
@@ -17156,7 +17866,7 @@ async function patchHermesMemoryProvider(profileName, provider) {
17156
17866
  await atomicWriteFilePreservingMetadata(configPath, document.toString());
17157
17867
  }
17158
17868
  function resolveMemoryDir(profileName) {
17159
- return path19.join(resolveHermesProfileDir(profileName), "memories");
17869
+ return path20.join(resolveHermesProfileDir(profileName), "memories");
17160
17870
  }
17161
17871
  async function readMemoryStore(profileName, target, limits) {
17162
17872
  const filePath = memoryFilePath(profileName, target);
@@ -17217,7 +17927,7 @@ async function writeMemoryEntries(profileName, target, entries) {
17217
17927
  );
17218
17928
  }
17219
17929
  function memoryFilePath(profileName, target) {
17220
- return path19.join(
17930
+ return path20.join(
17221
17931
  resolveMemoryDir(profileName),
17222
17932
  target === "user" ? "USER.md" : "MEMORY.md"
17223
17933
  );
@@ -17277,7 +17987,7 @@ async function readCustomProviderSetupSummary(profileName) {
17277
17987
  configurable: true,
17278
17988
  configured: true,
17279
17989
  configurationIssue: null,
17280
- providerConfigPath: path19.join(
17990
+ providerConfigPath: path20.join(
17281
17991
  resolveHermesProfileDir(profileName),
17282
17992
  "<provider>.json"
17283
17993
  ),
@@ -17631,7 +18341,7 @@ async function readProviderSettings(profileName, provider) {
17631
18341
  stringSetting(
17632
18342
  "dbPath",
17633
18343
  "SQLite \u6570\u636E\u5E93\u8DEF\u5F84",
17634
- config.db_path ?? path19.join(resolveHermesProfileDir(profileName), "memory_store.db")
18344
+ config.db_path ?? path20.join(resolveHermesProfileDir(profileName), "memory_store.db")
17635
18345
  ),
17636
18346
  booleanSetting("autoExtract", "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u62BD\u53D6", config.auto_extract ?? false),
17637
18347
  numberSetting("defaultTrust", "\u9ED8\u8BA4\u4FE1\u4EFB\u5206", config.default_trust ?? 0.5),
@@ -17654,7 +18364,7 @@ async function readProviderSettings(profileName, provider) {
17654
18364
  stringSetting(
17655
18365
  "workingDirectory",
17656
18366
  "\u5DE5\u4F5C\u76EE\u5F55",
17657
- path19.join(resolveHermesProfileDir(profileName), "byterover"),
18367
+ path20.join(resolveHermesProfileDir(profileName), "byterover"),
17658
18368
  false
17659
18369
  )
17660
18370
  ];
@@ -17663,16 +18373,16 @@ async function readProviderSettings(profileName, provider) {
17663
18373
  }
17664
18374
  function memoryProviderConfigPath(profileName, provider) {
17665
18375
  if (provider === "honcho") {
17666
- return path19.join(resolveHermesProfileDir(profileName), "honcho.json");
18376
+ return path20.join(resolveHermesProfileDir(profileName), "honcho.json");
17667
18377
  }
17668
18378
  if (provider === "mem0") {
17669
- return path19.join(resolveHermesProfileDir(profileName), "mem0.json");
18379
+ return path20.join(resolveHermesProfileDir(profileName), "mem0.json");
17670
18380
  }
17671
18381
  if (provider === "supermemory") {
17672
- return path19.join(resolveHermesProfileDir(profileName), "supermemory.json");
18382
+ return path20.join(resolveHermesProfileDir(profileName), "supermemory.json");
17673
18383
  }
17674
18384
  if (provider === "hindsight") {
17675
- return path19.join(
18385
+ return path20.join(
17676
18386
  resolveHermesProfileDir(profileName),
17677
18387
  "hindsight",
17678
18388
  "config.json"
@@ -17681,13 +18391,13 @@ function memoryProviderConfigPath(profileName, provider) {
17681
18391
  return null;
17682
18392
  }
17683
18393
  function customProviderConfigPath(profileName, provider) {
17684
- return path19.join(
18394
+ return path20.join(
17685
18395
  resolveHermesProfileDir(profileName),
17686
18396
  `${normalizeCustomProviderId(provider)}.json`
17687
18397
  );
17688
18398
  }
17689
18399
  function customProviderRegistryPath(profileName) {
17690
- return path19.join(
18400
+ return path20.join(
17691
18401
  resolveHermesProfileDir(profileName),
17692
18402
  CUSTOM_PROVIDER_REGISTRY_FILE
17693
18403
  );
@@ -17739,7 +18449,7 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
17739
18449
  );
17740
18450
  }
17741
18451
  async function discoverUserMemoryProviderDescriptors(profileName) {
17742
- const pluginsDir = path19.join(resolveHermesProfileDir(profileName), "plugins");
18452
+ const pluginsDir = path20.join(resolveHermesProfileDir(profileName), "plugins");
17743
18453
  const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
17744
18454
  (error) => {
17745
18455
  if (isNodeError16(error, "ENOENT")) {
@@ -17759,7 +18469,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
17759
18469
  } catch {
17760
18470
  continue;
17761
18471
  }
17762
- const providerDir = path19.join(pluginsDir, entry.name);
18472
+ const providerDir = path20.join(pluginsDir, entry.name);
17763
18473
  if (!await isMemoryProviderPluginDir(providerDir)) {
17764
18474
  continue;
17765
18475
  }
@@ -17773,7 +18483,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
17773
18483
  return descriptors;
17774
18484
  }
17775
18485
  async function isUserMemoryProviderInstalled(profileName, provider) {
17776
- const providerDir = path19.join(
18486
+ const providerDir = path20.join(
17777
18487
  resolveHermesProfileDir(profileName),
17778
18488
  "plugins",
17779
18489
  normalizeCustomProviderId(provider)
@@ -17781,7 +18491,7 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
17781
18491
  return isMemoryProviderPluginDir(providerDir);
17782
18492
  }
17783
18493
  async function isMemoryProviderPluginDir(providerDir) {
17784
- const source = await readFile14(path19.join(providerDir, "__init__.py"), "utf8").catch(
18494
+ const source = await readFile14(path20.join(providerDir, "__init__.py"), "utf8").catch(
17785
18495
  (error) => {
17786
18496
  if (isNodeError16(error, "ENOENT")) {
17787
18497
  return "";
@@ -17793,7 +18503,7 @@ async function isMemoryProviderPluginDir(providerDir) {
17793
18503
  return sample.includes("register_memory_provider") || sample.includes("MemoryProvider");
17794
18504
  }
17795
18505
  async function readPluginMetadata(providerDir) {
17796
- const raw = await readFile14(path19.join(providerDir, "plugin.yaml"), "utf8").catch(
18506
+ const raw = await readFile14(path20.join(providerDir, "plugin.yaml"), "utf8").catch(
17797
18507
  (error) => {
17798
18508
  if (isNodeError16(error, "ENOENT")) {
17799
18509
  return "";
@@ -17805,10 +18515,10 @@ async function readPluginMetadata(providerDir) {
17805
18515
  }
17806
18516
  async function resolveByteRoverCli() {
17807
18517
  const candidates = [
17808
- ...(process.env.PATH ?? "").split(path19.delimiter).filter(Boolean).map((dir) => path19.join(dir, "brv")),
17809
- path19.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
18518
+ ...(process.env.PATH ?? "").split(path20.delimiter).filter(Boolean).map((dir) => path20.join(dir, "brv")),
18519
+ path20.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
17810
18520
  "/usr/local/bin/brv",
17811
- path19.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
18521
+ path20.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
17812
18522
  ].filter(Boolean);
17813
18523
  for (const candidate of candidates) {
17814
18524
  const found = await access3(candidate).then(() => true).catch(() => false);
@@ -17869,7 +18579,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
17869
18579
  if (entries.length === 0) {
17870
18580
  return;
17871
18581
  }
17872
- const envPath = path19.join(resolveHermesProfileDir(profileName), ".env");
18582
+ const envPath = path20.join(resolveHermesProfileDir(profileName), ".env");
17873
18583
  const existingRaw = await readFile14(envPath, "utf8").catch((error) => {
17874
18584
  if (isNodeError16(error, "ENOENT")) {
17875
18585
  return "";
@@ -17943,7 +18653,7 @@ async function readActiveMemoryProvider(profileName) {
17943
18653
  return provider;
17944
18654
  }
17945
18655
  async function patchJsonProviderConfig(profileName, relativePath, patch) {
17946
- const configPath = path19.join(
18656
+ const configPath = path20.join(
17947
18657
  resolveHermesProfileDir(profileName),
17948
18658
  relativePath
17949
18659
  );
@@ -17972,7 +18682,7 @@ async function readJsonObject(filePath) {
17972
18682
  } catch {
17973
18683
  throw new HermesMemoryError(
17974
18684
  "memory_provider_config_invalid",
17975
- `${path19.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
18685
+ `${path20.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
17976
18686
  );
17977
18687
  }
17978
18688
  }
@@ -18518,7 +19228,7 @@ function toMemoryHttpError(error) {
18518
19228
 
18519
19229
  // src/hermes/skills.ts
18520
19230
  import { readFile as readFile15, readdir as readdir11 } from "fs/promises";
18521
- import path20 from "path";
19231
+ import path21 from "path";
18522
19232
  import YAML5 from "yaml";
18523
19233
  var HermesSkillNotFoundError = class extends Error {
18524
19234
  constructor(skillName) {
@@ -18532,7 +19242,7 @@ var EXCLUDED_SKILL_DIRS = /* @__PURE__ */ new Set([".git", ".github", ".hub"]);
18532
19242
  async function listHermesProfileSkills(profileName, paths = resolveRuntimePaths()) {
18533
19243
  const profile = await readExistingProfile(profileName, paths);
18534
19244
  const profileDir = resolveHermesProfileDir(profile.name);
18535
- const skillsRoot = path20.join(profileDir, "skills");
19245
+ const skillsRoot = path21.join(profileDir, "skills");
18536
19246
  const [skillFiles, disabled, provenance] = await Promise.all([
18537
19247
  findSkillFiles(skillsRoot),
18538
19248
  readDisabledSkillNames(resolveHermesConfigPath(profile.name)),
@@ -18623,7 +19333,7 @@ async function collectSkillFiles(directory, results) {
18623
19333
  if (EXCLUDED_SKILL_DIRS.has(entry.name)) {
18624
19334
  continue;
18625
19335
  }
18626
- const entryPath = path20.join(directory, entry.name);
19336
+ const entryPath = path21.join(directory, entry.name);
18627
19337
  if (entry.isDirectory()) {
18628
19338
  await collectSkillFiles(entryPath, results);
18629
19339
  continue;
@@ -18645,10 +19355,10 @@ async function readSkillMetadata(input) {
18645
19355
  if (raw === null) {
18646
19356
  return null;
18647
19357
  }
18648
- const skillDir = path20.dirname(input.skillFile);
19358
+ const skillDir = path21.dirname(input.skillFile);
18649
19359
  const { frontmatter, body } = parseSkillDocument(raw.slice(0, 4e3));
18650
19360
  const name = normalizeSkillName(
18651
- readString16(frontmatter.name) ?? path20.basename(skillDir)
19361
+ readString16(frontmatter.name) ?? path21.basename(skillDir)
18652
19362
  );
18653
19363
  if (!name) {
18654
19364
  return null;
@@ -18667,7 +19377,7 @@ async function readSkillMetadata(input) {
18667
19377
  enabled: !input.disabled.has(name),
18668
19378
  source: provenance.source,
18669
19379
  trust: provenance.trust,
18670
- relativePath: path20.relative(input.skillsRoot, skillDir)
19380
+ relativePath: path21.relative(input.skillsRoot, skillDir)
18671
19381
  };
18672
19382
  }
18673
19383
  function parseSkillDocument(raw) {
@@ -18688,8 +19398,8 @@ function parseSkillDocument(raw) {
18688
19398
  }
18689
19399
  }
18690
19400
  function categoryFromPath(skillsRoot, skillFile) {
18691
- const relative = path20.relative(skillsRoot, skillFile);
18692
- const parts = relative.split(path20.sep).filter(Boolean);
19401
+ const relative = path21.relative(skillsRoot, skillFile);
19402
+ const parts = relative.split(path21.sep).filter(Boolean);
18693
19403
  return parts.length >= 3 ? parts[0] : null;
18694
19404
  }
18695
19405
  function firstBodyDescription(body) {
@@ -18736,7 +19446,7 @@ async function readSkillProvenance(root) {
18736
19446
  return provenance;
18737
19447
  }
18738
19448
  async function readBundledSkillNames(root) {
18739
- const raw = await readFile15(path20.join(root, ".bundled_manifest"), "utf8").catch(
19449
+ const raw = await readFile15(path21.join(root, ".bundled_manifest"), "utf8").catch(
18740
19450
  (error) => {
18741
19451
  if (isNodeError17(error, "ENOENT")) {
18742
19452
  return "";
@@ -18759,7 +19469,7 @@ async function readBundledSkillNames(root) {
18759
19469
  return names;
18760
19470
  }
18761
19471
  async function readHubInstalledSkills(root) {
18762
- const raw = await readFile15(path20.join(root, ".hub", "lock.json"), "utf8").catch(
19472
+ const raw = await readFile15(path21.join(root, ".hub", "lock.json"), "utf8").catch(
18763
19473
  (error) => {
18764
19474
  if (isNodeError17(error, "ENOENT")) {
18765
19475
  return "";
@@ -19362,8 +20072,8 @@ function readModelList(payload) {
19362
20072
  // src/hermes/updates.ts
19363
20073
  import { EventEmitter as EventEmitter3 } from "events";
19364
20074
  import { spawn as spawn3 } from "child_process";
19365
- import { mkdir as mkdir10, readFile as readFile16, rm as rm8 } from "fs/promises";
19366
- import path21 from "path";
20075
+ import { mkdir as mkdir11, readFile as readFile16, rm as rm8 } from "fs/promises";
20076
+ import path22 from "path";
19367
20077
  var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
19368
20078
  var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
19369
20079
  var RELEASE_FETCH_TIMEOUT_MS = 5e3;
@@ -19376,7 +20086,10 @@ var runningUpdate = null;
19376
20086
  async function readHermesUpdateCheck(options) {
19377
20087
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
19378
20088
  const [local, remoteResult] = await Promise.all([
19379
- readHermesVersion().catch((error) => ({
20089
+ readHermesVersion({
20090
+ fetchImpl: options.fetchImpl,
20091
+ logger: options.logger
20092
+ }).catch((error) => ({
19380
20093
  raw: error instanceof Error ? error.message : String(error),
19381
20094
  version: null
19382
20095
  })),
@@ -19423,7 +20136,7 @@ async function startHermesUpdate(options) {
19423
20136
  signal: null,
19424
20137
  error: null
19425
20138
  };
19426
- await mkdir10(options.paths.runDir, { recursive: true, mode: 448 });
20139
+ await mkdir11(options.paths.runDir, { recursive: true, mode: 448 });
19427
20140
  await writer.write(`
19428
20141
  === hermes update started ${startedAt} ===
19429
20142
  `);
@@ -19629,13 +20342,13 @@ async function readUpdateLogLines(paths) {
19629
20342
  );
19630
20343
  }
19631
20344
  function releaseCachePath(paths) {
19632
- return path21.join(paths.indexesDir, "hermes-release-check.json");
20345
+ return path22.join(paths.indexesDir, "hermes-release-check.json");
19633
20346
  }
19634
20347
  function updateStatePath(paths) {
19635
- return path21.join(paths.runDir, "hermes-update-state.json");
20348
+ return path22.join(paths.runDir, "hermes-update-state.json");
19636
20349
  }
19637
20350
  function updateLogPath(paths) {
19638
- return path21.join(paths.logsDir, UPDATE_LOG_FILE);
20351
+ return path22.join(paths.logsDir, UPDATE_LOG_FILE);
19639
20352
  }
19640
20353
  async function clearUpdateLogFiles(paths) {
19641
20354
  const primary = updateLogPath(paths);
@@ -19727,17 +20440,17 @@ function readString17(payload, key) {
19727
20440
  // src/link/updates.ts
19728
20441
  import { spawn as spawn5 } from "child_process";
19729
20442
  import { EventEmitter as EventEmitter4 } from "events";
19730
- import { mkdir as mkdir13, readFile as readFile18, rm as rm11 } from "fs/promises";
19731
- import path23 from "path";
20443
+ import { mkdir as mkdir14, readFile as readFile18, rm as rm11 } from "fs/promises";
20444
+ import path24 from "path";
19732
20445
 
19733
20446
  // src/daemon/process.ts
19734
20447
  import { spawn as spawn4 } from "child_process";
19735
- import { mkdir as mkdir12, readFile as readFile17, rm as rm10 } from "fs/promises";
19736
- import path22 from "path";
20448
+ import { mkdir as mkdir13, readFile as readFile17, rm as rm10 } from "fs/promises";
20449
+ import path23 from "path";
19737
20450
 
19738
20451
  // src/daemon/service.ts
19739
20452
  import { createServer } from "http";
19740
- import { mkdir as mkdir11, rm as rm9, writeFile as writeFile3 } from "fs/promises";
20453
+ import { mkdir as mkdir12, rm as rm9, writeFile as writeFile3 } from "fs/promises";
19741
20454
 
19742
20455
  // src/relay/control-client.ts
19743
20456
  import WebSocket from "ws";
@@ -20616,6 +21329,7 @@ function startHermesSessionSyncScheduler(options) {
20616
21329
  }
20617
21330
 
20618
21331
  // src/daemon/service.ts
21332
+ var DEFAULT_RELAY_READY_TIMEOUT_MS = 2e3;
20619
21333
  async function startLinkService(options = {}) {
20620
21334
  const paths = options.paths ?? resolveRuntimePaths();
20621
21335
  const logger = createFileLogger({ paths });
@@ -20681,6 +21395,10 @@ async function startLinkService(options = {}) {
20681
21395
  });
20682
21396
  let relay = null;
20683
21397
  if (identity?.link_id) {
21398
+ let resolveRelayReady = null;
21399
+ const relayReady = new Promise((resolve) => {
21400
+ resolveRelayReady = resolve;
21401
+ });
20684
21402
  relay = connectRelayControl({
20685
21403
  relayBaseUrl: config.relayBaseUrl,
20686
21404
  linkId: identity.link_id,
@@ -20690,8 +21408,22 @@ async function startLinkService(options = {}) {
20690
21408
  backoffMaxMs: 3e4,
20691
21409
  onStatus: (status) => {
20692
21410
  void logger.info("relay_status", status);
21411
+ if (status.state === "connected") {
21412
+ resolveRelayReady?.(true);
21413
+ resolveRelayReady = null;
21414
+ } else if (status.state === "failed") {
21415
+ resolveRelayReady?.(false);
21416
+ resolveRelayReady = null;
21417
+ }
20693
21418
  }
20694
21419
  });
21420
+ if (options.waitForRelayReady) {
21421
+ await Promise.race([
21422
+ relayReady,
21423
+ waitForRelayReadyTimeout(options.relayReadyTimeoutMs)
21424
+ ]);
21425
+ resolveRelayReady = null;
21426
+ }
20695
21427
  } else {
20696
21428
  void logger.info("relay_skipped", { reason: "link_not_paired" });
20697
21429
  }
@@ -20726,11 +21458,20 @@ async function startLinkService(options = {}) {
20726
21458
  }
20727
21459
  };
20728
21460
  }
21461
+ function waitForRelayReadyTimeout(timeoutMs) {
21462
+ return new Promise((resolve) => {
21463
+ const timer = setTimeout(
21464
+ () => resolve(false),
21465
+ timeoutMs ?? DEFAULT_RELAY_READY_TIMEOUT_MS
21466
+ );
21467
+ timer.unref?.();
21468
+ });
21469
+ }
20729
21470
  function pidFilePath(paths = resolveRuntimePaths()) {
20730
21471
  return `${paths.runDir}/hermeslink.pid`;
20731
21472
  }
20732
21473
  async function writePidFile(paths) {
20733
- await mkdir11(paths.runDir, { recursive: true, mode: 448 });
21474
+ await mkdir12(paths.runDir, { recursive: true, mode: 448 });
20734
21475
  await writeFile3(pidFilePath(paths), `${process.pid}
20735
21476
  `, { mode: 384 });
20736
21477
  }
@@ -20805,8 +21546,8 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
20805
21546
  return status;
20806
21547
  }
20807
21548
  }
20808
- await mkdir12(paths.logsDir, { recursive: true, mode: 448 });
20809
- await mkdir12(paths.runDir, { recursive: true, mode: 448 });
21549
+ await mkdir13(paths.logsDir, { recursive: true, mode: 448 });
21550
+ await mkdir13(paths.runDir, { recursive: true, mode: 448 });
20810
21551
  const scriptPath = currentCliScriptPath();
20811
21552
  const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
20812
21553
  detached: true,
@@ -20824,10 +21565,10 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
20824
21565
  return await getDaemonStatus(paths);
20825
21566
  }
20826
21567
  async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
20827
- await mkdir12(paths.logsDir, { recursive: true, mode: 448 });
21568
+ await mkdir13(paths.logsDir, { recursive: true, mode: 448 });
20828
21569
  const log = createRotatingTextLogWriter({
20829
21570
  paths,
20830
- fileName: path22.basename(daemonLogFile(paths))
21571
+ fileName: path23.basename(daemonLogFile(paths))
20831
21572
  });
20832
21573
  const scriptPath = currentCliScriptPath();
20833
21574
  const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
@@ -21068,7 +21809,7 @@ async function startLinkUpdate(options) {
21068
21809
  error: null,
21069
21810
  manual_command: manualCommand
21070
21811
  };
21071
- await mkdir13(options.paths.runDir, { recursive: true, mode: 448 });
21812
+ await mkdir14(options.paths.runDir, { recursive: true, mode: 448 });
21072
21813
  await writer.write(
21073
21814
  `
21074
21815
  === link update started ${startedAt} target=${targetVersion} ===
@@ -21366,10 +22107,10 @@ async function readUpdateLogLines2(paths) {
21366
22107
  );
21367
22108
  }
21368
22109
  function updateStatePath2(paths) {
21369
- return path23.join(paths.runDir, "link-update-state.json");
22110
+ return path24.join(paths.runDir, "link-update-state.json");
21370
22111
  }
21371
22112
  function updateLogPath2(paths) {
21372
- return path23.join(paths.logsDir, UPDATE_LOG_FILE2);
22113
+ return path24.join(paths.logsDir, UPDATE_LOG_FILE2);
21373
22114
  }
21374
22115
  async function clearUpdateLogFiles2(paths) {
21375
22116
  const primary = updateLogPath2(paths);
@@ -21433,10 +22174,21 @@ function readString18(payload, key) {
21433
22174
  }
21434
22175
 
21435
22176
  // src/pairing/pairing.ts
21436
- import path24 from "path";
22177
+ import path25 from "path";
21437
22178
  import { rm as rm12 } from "fs/promises";
21438
22179
 
21439
22180
  // src/relay/bootstrap.ts
22181
+ var RelayNetworkError = class extends Error {
22182
+ constructor(relayBaseUrl, causeMessage) {
22183
+ super(
22184
+ `Cannot reach Hermes Relay at ${relayBaseUrl}. Please check your network connection, VPN, or proxy settings, then try again.`
22185
+ );
22186
+ this.relayBaseUrl = relayBaseUrl;
22187
+ this.causeMessage = causeMessage;
22188
+ }
22189
+ relayBaseUrl;
22190
+ causeMessage;
22191
+ };
21440
22192
  async function bootstrapRelayLink(options) {
21441
22193
  const fetcher = options.fetchImpl ?? fetch;
21442
22194
  const baseUrl = options.relayBaseUrl.replace(/\/+$/u, "");
@@ -21477,14 +22229,23 @@ async function bootstrapRelayLink(options) {
21477
22229
  };
21478
22230
  }
21479
22231
  async function postJson(fetcher, url, token, body) {
21480
- const response = await fetcher(url, {
21481
- method: "POST",
21482
- headers: {
21483
- authorization: `Bearer ${token}`,
21484
- "content-type": "application/json"
21485
- },
21486
- body: JSON.stringify(body)
21487
- });
22232
+ let response;
22233
+ try {
22234
+ response = await fetcher(url, {
22235
+ method: "POST",
22236
+ headers: {
22237
+ authorization: `Bearer ${token}`,
22238
+ "content-type": "application/json"
22239
+ },
22240
+ body: JSON.stringify(body)
22241
+ });
22242
+ } catch (error) {
22243
+ const baseUrl = new URL(url).origin;
22244
+ throw new RelayNetworkError(
22245
+ baseUrl,
22246
+ error instanceof Error ? error.message : String(error)
22247
+ );
22248
+ }
21488
22249
  const payload = await response.json().catch(() => null);
21489
22250
  if (!response.ok) {
21490
22251
  const message = readErrorMessage4(payload) ?? `Relay request failed with HTTP ${response.status}`;
@@ -21512,14 +22273,22 @@ async function preparePairing(paths = resolveRuntimePaths()) {
21512
22273
  const config = await loadConfig(paths);
21513
22274
  const identity = await ensureIdentity(paths);
21514
22275
  const systemInfo = readLinkSystemInfo();
21515
- const created = await postServerJson(config.serverBaseUrl, "/api/v1/link-pairings", {
21516
- install_id: identity.install_id,
21517
- link_id: identity.link_id ?? void 0,
21518
- display_name: systemInfo.defaultDisplayName,
21519
- platform: systemInfo.platform,
21520
- hostname: systemInfo.hostname ?? void 0,
21521
- public_key_pem: identity.public_key_pem
21522
- });
22276
+ const created = await postServerJson(
22277
+ config.serverBaseUrl,
22278
+ "/api/v1/link-pairings",
22279
+ {
22280
+ install_id: identity.install_id,
22281
+ link_id: identity.link_id ?? void 0,
22282
+ display_name: systemInfo.defaultDisplayName,
22283
+ platform: systemInfo.platform,
22284
+ hostname: systemInfo.hostname ?? void 0,
22285
+ public_key_pem: identity.public_key_pem
22286
+ },
22287
+ {
22288
+ target: "server",
22289
+ action: "create pairing session"
22290
+ }
22291
+ );
21523
22292
  const relayBaseUrl = created.relayBaseUrl || config.relayBaseUrl;
21524
22293
  let assigned;
21525
22294
  let updatedIdentity;
@@ -21541,28 +22310,43 @@ async function preparePairing(paths = resolveRuntimePaths()) {
21541
22310
  installId: updatedIdentity.install_id,
21542
22311
  publicKeyPem: updatedIdentity.public_key_pem
21543
22312
  });
21544
- await patchServerJson(config.serverBaseUrl, `/api/v1/link-pairings/${created.sessionId}/link`, created.pairingToken, {
21545
- install_id: updatedIdentity.install_id,
21546
- link_id: assigned.linkId,
21547
- link_version: LINK_VERSION,
21548
- display_name: systemInfo.defaultDisplayName,
21549
- platform: systemInfo.platform,
21550
- hostname: systemInfo.hostname ?? void 0,
21551
- lan_ips: routes.lanIps,
21552
- public_ipv4s: routes.publicIpv4s,
21553
- public_ipv6s: routes.publicIpv6s,
21554
- preferred_urls: routes.preferredUrls,
21555
- environment: routes.environment
21556
- });
22313
+ await patchServerJson(
22314
+ config.serverBaseUrl,
22315
+ `/api/v1/link-pairings/${created.sessionId}/link`,
22316
+ created.pairingToken,
22317
+ {
22318
+ install_id: updatedIdentity.install_id,
22319
+ link_id: assigned.linkId,
22320
+ link_version: LINK_VERSION,
22321
+ display_name: systemInfo.defaultDisplayName,
22322
+ platform: systemInfo.platform,
22323
+ hostname: systemInfo.hostname ?? void 0,
22324
+ lan_ips: routes.lanIps,
22325
+ public_ipv4s: routes.publicIpv4s,
22326
+ public_ipv6s: routes.publicIpv6s,
22327
+ preferred_urls: routes.preferredUrls,
22328
+ environment: routes.environment
22329
+ },
22330
+ {
22331
+ target: "server",
22332
+ action: "finalize pairing"
22333
+ }
22334
+ );
21557
22335
  } catch (error) {
22336
+ const reportedError = error instanceof RelayNetworkError ? createPairingNetworkError({
22337
+ target: "relay",
22338
+ action: "connect to Relay",
22339
+ baseUrl: error.relayBaseUrl,
22340
+ detail: error.causeMessage
22341
+ }) : error;
21558
22342
  await reportPairingErrorToServer({
21559
22343
  serverBaseUrl: config.serverBaseUrl,
21560
22344
  sessionId: created.sessionId,
21561
22345
  source: "link",
21562
22346
  pairingToken: created.pairingToken,
21563
- error: pairingErrorSnapshot("prepare_pairing", error)
22347
+ error: pairingErrorSnapshot("prepare_pairing", reportedError)
21564
22348
  });
21565
- throw error;
22349
+ throw reportedError;
21566
22350
  }
21567
22351
  const qrPayload = {
21568
22352
  kind: "hermes_link_pairing",
@@ -21662,6 +22446,10 @@ async function claimPairing(input) {
21662
22446
  {
21663
22447
  claim_token: input.claimToken,
21664
22448
  app_instance_id: input.appInstanceId ?? void 0
22449
+ },
22450
+ {
22451
+ target: "server",
22452
+ action: "verify pairing claim"
21665
22453
  }
21666
22454
  );
21667
22455
  } catch (error) {
@@ -21709,15 +22497,25 @@ async function loadRequiredIdentity2(paths) {
21709
22497
  }
21710
22498
  return identity;
21711
22499
  }
21712
- async function postServerJson(serverBaseUrl, path25, body) {
21713
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
21714
- method: "POST",
21715
- headers: {
21716
- accept: "application/json",
21717
- "content-type": "application/json"
21718
- },
21719
- body: JSON.stringify(body)
21720
- });
22500
+ async function postServerJson(serverBaseUrl, path26, body, options) {
22501
+ let response;
22502
+ try {
22503
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
22504
+ method: "POST",
22505
+ headers: {
22506
+ accept: "application/json",
22507
+ "content-type": "application/json"
22508
+ },
22509
+ body: JSON.stringify(body)
22510
+ });
22511
+ } catch (error) {
22512
+ throw createPairingNetworkError({
22513
+ target: options.target,
22514
+ action: options.action,
22515
+ baseUrl: serverBaseUrl,
22516
+ detail: error instanceof Error ? error.message : String(error)
22517
+ });
22518
+ }
21721
22519
  return readJsonResponse2(response);
21722
22520
  }
21723
22521
  async function reportPairingErrorToServer(input) {
@@ -21750,16 +22548,26 @@ function pairingErrorSnapshot(stage, error) {
21750
22548
  occurred_at: (/* @__PURE__ */ new Date()).toISOString()
21751
22549
  };
21752
22550
  }
21753
- async function patchServerJson(serverBaseUrl, path25, token, body) {
21754
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
21755
- method: "PATCH",
21756
- headers: {
21757
- accept: "application/json",
21758
- authorization: `Bearer ${token}`,
21759
- "content-type": "application/json"
21760
- },
21761
- body: JSON.stringify(body)
21762
- });
22551
+ async function patchServerJson(serverBaseUrl, path26, token, body, options) {
22552
+ let response;
22553
+ try {
22554
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
22555
+ method: "PATCH",
22556
+ headers: {
22557
+ accept: "application/json",
22558
+ authorization: `Bearer ${token}`,
22559
+ "content-type": "application/json"
22560
+ },
22561
+ body: JSON.stringify(body)
22562
+ });
22563
+ } catch (error) {
22564
+ throw createPairingNetworkError({
22565
+ target: options.target,
22566
+ action: options.action,
22567
+ baseUrl: serverBaseUrl,
22568
+ detail: error instanceof Error ? error.message : String(error)
22569
+ });
22570
+ }
21763
22571
  return readJsonResponse2(response);
21764
22572
  }
21765
22573
  async function readJsonResponse2(response) {
@@ -21781,11 +22589,20 @@ function readErrorMessage5(payload) {
21781
22589
  const message = error.message;
21782
22590
  return typeof message === "string" ? message : null;
21783
22591
  }
22592
+ function createPairingNetworkError(input) {
22593
+ const baseMessage = input.target === "server" ? `HermesPilot Server is unreachable while trying to ${input.action}.` : `Hermes Relay is unreachable while trying to ${input.action}.`;
22594
+ const hint = "If you are using a VPN, proxy, or corporate network, try turning it off and retrying.";
22595
+ return new LinkHttpError(
22596
+ 503,
22597
+ input.target === "server" ? "pairing_server_unreachable" : "pairing_relay_unreachable",
22598
+ `${baseMessage} Please check whether ${input.baseUrl} is reachable. ${hint} Detail: ${input.detail}`
22599
+ );
22600
+ }
21784
22601
  function pairingClaimPath(sessionId, paths) {
21785
- return path24.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
22602
+ return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
21786
22603
  }
21787
22604
  function pairingSessionPath(sessionId, paths) {
21788
- return path24.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
22605
+ return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
21789
22606
  }
21790
22607
  function qrPreferredUrls(routes) {
21791
22608
  return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
@@ -21839,6 +22656,7 @@ function registerSystemRoutes(router, options) {
21839
22656
  conversation_events: true,
21840
22657
  conversation_delete: true,
21841
22658
  conversation_bulk_delete: true,
22659
+ conversation_clear_plan: true,
21842
22660
  conversation_cancel: true,
21843
22661
  conversation_rename: true,
21844
22662
  blobs: true,
@@ -22432,12 +23250,16 @@ function registerPairingRoutes(router, options) {
22432
23250
  if (!session) {
22433
23251
  throw new LinkHttpError(404, "pairing_session_not_found", "Pairing session was not found");
22434
23252
  }
23253
+ const state = await readPairingState(sessionId, paths);
23254
+ if (!state.claimed && isExpiredAt(session.expires_at)) {
23255
+ throw new LinkHttpError(404, "pairing_session_expired", "Pairing session has expired");
23256
+ }
22435
23257
  ctx.set("cache-control", "no-store");
22436
23258
  ctx.body = {
22437
23259
  ok: true,
22438
23260
  session: {
22439
23261
  ...session,
22440
- claimed: Boolean(await readPairingState(sessionId, paths).then((state) => state.claimed))
23262
+ claimed: state.claimed
22441
23263
  }
22442
23264
  };
22443
23265
  });
@@ -22453,6 +23275,8 @@ async function readPairingState(sessionId, paths) {
22453
23275
  }
22454
23276
  async function renderPairingPage(input) {
22455
23277
  const session = input.session;
23278
+ const expiresAtMs = Date.parse(session.expires_at);
23279
+ const isExpired = !input.state.claimed && Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs;
22456
23280
  const qrPayload = JSON.stringify({
22457
23281
  kind: "hermes_link_pairing",
22458
23282
  version: 1,
@@ -22472,8 +23296,9 @@ async function renderPairingPage(input) {
22472
23296
  const currentUrl = session.local_api_url.replace(/\/+$/u, "");
22473
23297
  const linkIdLabel = escapeHtml(input.linkId ?? session.link_id);
22474
23298
  const expiresLabel = escapeHtml(formatDate(session.expires_at));
22475
- const statusLabel = input.state.claimed ? "\u5DF2\u5B8C\u6210\u914D\u5BF9" : "\u7B49\u5F85 App \u626B\u7801";
22476
- const statusHint = input.state.claimed ? "App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002" : "\u6253\u5F00 App \u626B\u7801\uFF0C\u6216\u8005\u590D\u5236\u914D\u5BF9\u7801\u624B\u52A8\u8F93\u5165\u3002";
23299
+ const statusLabel = input.state.claimed ? "\u5DF2\u5B8C\u6210\u914D\u5BF9" : isExpired ? "\u914D\u5BF9\u5DF2\u8FC7\u671F" : "\u7B49\u5F85 App \u626B\u7801";
23300
+ const statusHint = input.state.claimed ? "App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002" : isExpired ? "\u8FD9\u6B21\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C hermeslink pair\u3002" : "\u6253\u5F00 App \u626B\u7801\uFF0C\u6216\u8005\u590D\u5236\u914D\u5BF9\u7801\u624B\u52A8\u8F93\u5165\u3002";
23301
+ const statusPillLabel = input.state.claimed ? "\u5DF2\u626B\u7801" : isExpired ? "\u5DF2\u8FC7\u671F" : "\u7B49\u5F85\u4E2D";
22477
23302
  return `<!doctype html>
22478
23303
  <html lang="zh-CN">
22479
23304
  <head>
@@ -22778,7 +23603,7 @@ async function renderPairingPage(input) {
22778
23603
  <div class="card">
22779
23604
  <div class="status">
22780
23605
  <h2 class="status-title" id="statusTitle">${escapeHtml(statusLabel)}</h2>
22781
- <span class="pill" id="statusPill">${input.state.claimed ? "\u5DF2\u626B\u7801" : "\u7B49\u5F85\u4E2D"}</span>
23606
+ <span class="pill" id="statusPill">${escapeHtml(statusPillLabel)}</span>
22782
23607
  </div>
22783
23608
  <div class="qr-frame">
22784
23609
  <img src="${qrDataUri}" alt="Hermes Link pairing QR code" />
@@ -22795,22 +23620,62 @@ async function renderPairingPage(input) {
22795
23620
  </main>
22796
23621
  <script>
22797
23622
  const sessionId = ${JSON.stringify(session.session_id)};
23623
+ const expiresAtMs = ${Number.isFinite(expiresAtMs) ? String(expiresAtMs) : "Number.NaN"};
23624
+ const initialClaimed = ${JSON.stringify(input.state.claimed)};
23625
+ const statusTitle = document.querySelector('#statusTitle');
23626
+ const statusPill = document.querySelector('#statusPill');
23627
+ const statusHint = document.querySelector('#statusHint');
23628
+ let refreshTimer = null;
23629
+
23630
+ const stopPolling = () => {
23631
+ if (refreshTimer !== null) {
23632
+ clearInterval(refreshTimer);
23633
+ refreshTimer = null;
23634
+ }
23635
+ };
23636
+
23637
+ const markClaimed = () => {
23638
+ statusTitle.textContent = '\u5DF2\u5B8C\u6210\u914D\u5BF9';
23639
+ statusPill.textContent = '\u5DF2\u626B\u7801';
23640
+ statusHint.textContent = 'App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002';
23641
+ stopPolling();
23642
+ };
23643
+
23644
+ const markExpired = () => {
23645
+ statusTitle.textContent = '\u914D\u5BF9\u5DF2\u8FC7\u671F';
23646
+ statusPill.textContent = '\u5DF2\u8FC7\u671F';
23647
+ statusHint.textContent = '\u8FD9\u6B21\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C hermeslink pair\u3002';
23648
+ stopPolling();
23649
+ };
23650
+
22798
23651
  const refresh = async () => {
23652
+ if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs) {
23653
+ markExpired();
23654
+ return;
23655
+ }
22799
23656
  try {
22800
23657
  const response = await fetch('/api/v1/pairing/session?session_id=' + encodeURIComponent(sessionId), {
22801
23658
  headers: { accept: 'application/json' },
22802
23659
  });
23660
+ if (response.status === 404) {
23661
+ markExpired();
23662
+ return;
23663
+ }
22803
23664
  if (!response.ok) return;
22804
23665
  const payload = await response.json();
22805
23666
  if (payload?.session?.claimed) {
22806
- document.querySelector('#statusPill').textContent = '\u5DF2\u626B\u7801';
22807
- document.querySelector('#statusTitle').textContent = '\u5DF2\u5B8C\u6210\u914D\u5BF9';
22808
- document.querySelector('#statusHint').textContent = 'App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002';
23667
+ markClaimed();
22809
23668
  }
22810
23669
  } catch (_) {}
22811
23670
  };
22812
- setInterval(refresh, 1500);
22813
- refresh();
23671
+ if (initialClaimed) {
23672
+ stopPolling();
23673
+ } else if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs) {
23674
+ markExpired();
23675
+ } else {
23676
+ refreshTimer = window.setInterval(refresh, 1500);
23677
+ refresh();
23678
+ }
22814
23679
  </script>
22815
23680
  </body>
22816
23681
  </html>`;
@@ -22822,6 +23687,10 @@ function formatDate(value) {
22822
23687
  const date = new Date(value);
22823
23688
  return Number.isNaN(date.getTime()) ? value : date.toLocaleString("zh-CN", { hour12: false });
22824
23689
  }
23690
+ function isExpiredAt(value) {
23691
+ const expiresAtMs = Date.parse(value);
23692
+ return Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs;
23693
+ }
22825
23694
 
22826
23695
  // src/http/routes/internal.ts
22827
23696
  function registerInternalRoutes(router, options) {
@@ -22916,6 +23785,7 @@ export {
22916
23785
  createFileLogger,
22917
23786
  getLinkLogFile,
22918
23787
  ensureHermesApiServerAvailable,
23788
+ readHermesVersion,
22919
23789
  loadConfig,
22920
23790
  saveConfig,
22921
23791
  normalizeLanHost,
@@ -22929,6 +23799,7 @@ export {
22929
23799
  readPairingClaim,
22930
23800
  clearPairingClaim,
22931
23801
  createApp,
23802
+ connectRelayControl,
22932
23803
  reportLinkStatusToServer,
22933
23804
  startLinkService,
22934
23805
  startDaemonProcess,