@hermespilot/link 0.3.9 → 0.4.1

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";
@@ -1500,6 +1500,13 @@ function resolveHermesProfileDir(profileName = "default") {
1500
1500
  }
1501
1501
  return path3.join(os.homedir(), ".hermes", "profiles", profileName);
1502
1502
  }
1503
+ function resolveHermesProfilesDir() {
1504
+ const hermesHome = process.env.HERMES_HOME?.trim();
1505
+ if (hermesHome) {
1506
+ return path3.join(resolveDefaultHermesRoot(path3.resolve(hermesHome)), "profiles");
1507
+ }
1508
+ return path3.join(os.homedir(), ".hermes", "profiles");
1509
+ }
1503
1510
  function resolveHermesConfigPath(profileName = "default") {
1504
1511
  return path3.join(resolveHermesProfileDir(profileName), "config.yaml");
1505
1512
  }
@@ -3883,7 +3890,7 @@ async function listCronOutputFiles(profileName, jobId) {
3883
3890
  mtimeMs: fileStat.mtimeMs
3884
3891
  });
3885
3892
  }
3886
- return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path25, mtime }) => ({ path: path25, mtime }));
3893
+ return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path26, mtime }) => ({ path: path26, mtime }));
3887
3894
  }
3888
3895
  async function readCronOutput(outputPath) {
3889
3896
  const content = await readFile3(outputPath, "utf8");
@@ -3970,7 +3977,7 @@ import os2 from "os";
3970
3977
  import path5 from "path";
3971
3978
 
3972
3979
  // src/constants.ts
3973
- var LINK_VERSION = "0.3.9";
3980
+ var LINK_VERSION = "0.4.1";
3974
3981
  var LINK_COMMAND = "hermeslink";
3975
3982
  var LINK_DEFAULT_PORT = 52379;
3976
3983
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -6760,6 +6767,70 @@ function safePathSegment(value, fallback) {
6760
6767
  return safe.length > 0 ? safe.slice(0, 120) : fallback;
6761
6768
  }
6762
6769
 
6770
+ // src/conversations/conversation-clear-plans.ts
6771
+ import { randomUUID as randomUUID5 } from "crypto";
6772
+ import { mkdir as mkdir7 } from "fs/promises";
6773
+ import path11 from "path";
6774
+ var PLAN_ID_PATTERN = /^clear_[a-f0-9]{32}$/u;
6775
+ var ConversationClearPlanStore = class {
6776
+ constructor(paths) {
6777
+ this.paths = paths;
6778
+ }
6779
+ paths;
6780
+ async create(conversationIds) {
6781
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6782
+ const plan = {
6783
+ id: `clear_${randomUUID5().replaceAll("-", "")}`,
6784
+ status: "prepared",
6785
+ created_at: now,
6786
+ updated_at: now,
6787
+ total_count: conversationIds.length,
6788
+ deleted_count: 0,
6789
+ failed_count: 0,
6790
+ conversation_ids: conversationIds,
6791
+ conversations: []
6792
+ };
6793
+ await this.write(plan);
6794
+ return plan;
6795
+ }
6796
+ async read(planId) {
6797
+ const normalizedPlanId = normalizePlanId(planId);
6798
+ const plan = await readJsonFile(
6799
+ this.planPath(normalizedPlanId)
6800
+ );
6801
+ if (!plan) {
6802
+ throw new LinkHttpError(
6803
+ 404,
6804
+ "conversation_clear_plan_not_found",
6805
+ "Conversation clear plan was not found"
6806
+ );
6807
+ }
6808
+ return plan;
6809
+ }
6810
+ async write(plan) {
6811
+ const normalizedPlanId = normalizePlanId(plan.id);
6812
+ await mkdir7(this.plansDir(), { recursive: true, mode: 448 });
6813
+ await writeJsonFile(this.planPath(normalizedPlanId), plan);
6814
+ }
6815
+ plansDir() {
6816
+ return path11.join(this.paths.indexesDir, "conversation-clear-plans");
6817
+ }
6818
+ planPath(planId) {
6819
+ return path11.join(this.plansDir(), `${planId}.json`);
6820
+ }
6821
+ };
6822
+ function normalizePlanId(planId) {
6823
+ const normalized = planId.trim();
6824
+ if (!PLAN_ID_PATTERN.test(normalized)) {
6825
+ throw new LinkHttpError(
6826
+ 400,
6827
+ "conversation_clear_plan_id_invalid",
6828
+ "Conversation clear plan id is invalid"
6829
+ );
6830
+ }
6831
+ return normalized;
6832
+ }
6833
+
6763
6834
  // src/conversations/conversation-session-ids.ts
6764
6835
  function normalizeHermesSessionIds(values) {
6765
6836
  const seen = /* @__PURE__ */ new Set();
@@ -6810,8 +6881,134 @@ var MAX_UPLOADED_BLOB_BYTES = 50 * 1024 * 1024;
6810
6881
  var ConversationMaintenanceCoordinator = class {
6811
6882
  constructor(deps) {
6812
6883
  this.deps = deps;
6884
+ this.clearPlans = new ConversationClearPlanStore(deps.paths);
6813
6885
  }
6814
6886
  deps;
6887
+ clearPlans;
6888
+ async prepareClearAllConversationPlan() {
6889
+ const targets = [];
6890
+ for (const conversationId of await this.deps.store.listConversationIds()) {
6891
+ const manifest = await this.deps.store.readManifest(conversationId).catch(() => null);
6892
+ if (manifest?.status !== "active") {
6893
+ continue;
6894
+ }
6895
+ targets.push({
6896
+ id: conversationId,
6897
+ updatedAt: manifest.updated_at
6898
+ });
6899
+ }
6900
+ targets.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
6901
+ return this.clearPlans.create(
6902
+ targets.map((target) => target.id)
6903
+ );
6904
+ }
6905
+ async readClearAllConversationPlan(planId) {
6906
+ return this.clearPlans.read(planId);
6907
+ }
6908
+ async executeClearAllConversationPlan(planId) {
6909
+ let plan = await this.clearPlans.read(planId);
6910
+ if (plan.status === "completed") {
6911
+ return plan;
6912
+ }
6913
+ if (plan.status === "failed") {
6914
+ throw new LinkHttpError(
6915
+ 409,
6916
+ "conversation_clear_plan_already_failed",
6917
+ "Conversation clear plan has already failed"
6918
+ );
6919
+ }
6920
+ const results = [];
6921
+ if (plan.status !== "executing") {
6922
+ plan = await this.writeClearPlan({
6923
+ ...plan,
6924
+ status: "executing",
6925
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
6926
+ conversations: results,
6927
+ deleted_count: 0,
6928
+ failed_count: 0
6929
+ });
6930
+ }
6931
+ for (const conversationId of plan.conversation_ids) {
6932
+ try {
6933
+ const deleted = await this.deleteConversation(conversationId);
6934
+ results.push({ ...deleted, status: "deleted" });
6935
+ } catch (error) {
6936
+ if (error instanceof LinkHttpError && error.code === "conversation_not_found") {
6937
+ results.push({
6938
+ conversation_id: conversationId,
6939
+ status: "deleted",
6940
+ hermes_deleted: false,
6941
+ deleted_at: (/* @__PURE__ */ new Date()).toISOString()
6942
+ });
6943
+ } else {
6944
+ results.push({
6945
+ conversation_id: conversationId,
6946
+ status: "failed",
6947
+ error: {
6948
+ code: error instanceof LinkHttpError ? error.code : "internal_error",
6949
+ message: error instanceof Error ? error.message : "Internal error"
6950
+ }
6951
+ });
6952
+ }
6953
+ }
6954
+ plan = await this.writeClearPlan({
6955
+ ...plan,
6956
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
6957
+ conversations: [...results],
6958
+ deleted_count: results.filter((result) => result.status === "deleted").length,
6959
+ failed_count: results.filter((result) => result.status === "failed").length
6960
+ });
6961
+ }
6962
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
6963
+ return this.writeClearPlan({
6964
+ ...plan,
6965
+ status: plan.failed_count === 0 ? "completed" : "failed",
6966
+ updated_at: completedAt,
6967
+ completed_at: completedAt
6968
+ });
6969
+ }
6970
+ async startClearAllConversationPlan(planId) {
6971
+ const plan = await this.clearPlans.read(planId);
6972
+ if (plan.status === "completed" || plan.status === "executing") {
6973
+ return plan;
6974
+ }
6975
+ if (plan.status === "failed") {
6976
+ throw new LinkHttpError(
6977
+ 409,
6978
+ "conversation_clear_plan_already_failed",
6979
+ "Conversation clear plan has already failed"
6980
+ );
6981
+ }
6982
+ const started = await this.writeClearPlan({
6983
+ ...plan,
6984
+ status: "executing",
6985
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
6986
+ conversations: [],
6987
+ deleted_count: 0,
6988
+ failed_count: 0
6989
+ });
6990
+ void this.executeClearAllConversationPlan(started.id).catch(
6991
+ async (error) => {
6992
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
6993
+ await this.writeClearPlan({
6994
+ ...started,
6995
+ status: "failed",
6996
+ updated_at: failedAt,
6997
+ completed_at: failedAt,
6998
+ failed_count: started.total_count,
6999
+ conversations: started.conversation_ids.map((conversationId) => ({
7000
+ conversation_id: conversationId,
7001
+ status: "failed",
7002
+ error: {
7003
+ code: error instanceof LinkHttpError ? error.code : "internal_error",
7004
+ message: error instanceof Error ? error.message : "Internal error"
7005
+ }
7006
+ }))
7007
+ }).catch(() => void 0);
7008
+ }
7009
+ );
7010
+ return started;
7011
+ }
6815
7012
  async deleteConversation(conversationId) {
6816
7013
  return this.deps.withConversationLock(
6817
7014
  conversationId,
@@ -7017,6 +7214,10 @@ var ConversationMaintenanceCoordinator = class {
7017
7214
  async listConversationBlobIds(conversationId) {
7018
7215
  return listConversationBlobIds(this.deps.paths, conversationId);
7019
7216
  }
7217
+ async writeClearPlan(plan) {
7218
+ await this.clearPlans.write(plan);
7219
+ return plan;
7220
+ }
7020
7221
  };
7021
7222
  function isVoiceAttachmentInput(attachment) {
7022
7223
  return attachment.kind === "voice" || attachment.type === "voice" || attachment.is_voice_note === true || attachment.isVoiceNote === true;
@@ -7096,14 +7297,14 @@ function isUsableLanIpv4(value) {
7096
7297
 
7097
7298
  // src/hermes/session-title.ts
7098
7299
  import { stat as stat7 } from "fs/promises";
7099
- import path11 from "path";
7300
+ import path12 from "path";
7100
7301
  async function readHermesSessionTitle(sessionId, paths, profileName) {
7101
7302
  const trimmedSessionId = sessionId.trim();
7102
7303
  if (!trimmedSessionId) {
7103
7304
  return void 0;
7104
7305
  }
7105
7306
  const resolvedProfileName = isValidProfileName(profileName) ? profileName : "default";
7106
- const dbPath = path11.join(
7307
+ const dbPath = path12.join(
7107
7308
  resolveHermesProfileDir(resolvedProfileName),
7108
7309
  "state.db"
7109
7310
  );
@@ -7124,7 +7325,7 @@ async function readHermesCompressionTip(sessionId, paths, profileName) {
7124
7325
  return void 0;
7125
7326
  }
7126
7327
  const resolvedProfileName = isValidProfileName(profileName) ? profileName : "default";
7127
- const dbPath = path11.join(
7328
+ const dbPath = path12.join(
7128
7329
  resolveHermesProfileDir(resolvedProfileName),
7129
7330
  "state.db"
7130
7331
  );
@@ -7219,11 +7420,19 @@ var ConversationMetadataCoordinator = class {
7219
7420
  const snapshot = options.snapshot ?? await this.deps.store.readSnapshot(manifest.id).catch(
7220
7421
  () => createEmptySnapshot()
7221
7422
  );
7222
- const title = await readHermesSessionTitle(
7423
+ const profileName = latestRuntimeRun(snapshot)?.profile ?? manifest.profile_name_snapshot ?? manifest.profile;
7424
+ const compressionRootSessionId = manifest.hermes_lineage?.kind === "compression" ? manifest.hermes_lineage.root_session_id : void 0;
7425
+ const rootTitle = compressionRootSessionId ? await readHermesSessionTitle(
7426
+ compressionRootSessionId,
7427
+ this.deps.paths,
7428
+ profileName
7429
+ ) : void 0;
7430
+ const tipTitle = await readHermesSessionTitle(
7223
7431
  manifest.hermes_session_id,
7224
7432
  this.deps.paths,
7225
- latestRuntimeRun(snapshot)?.profile
7433
+ profileName
7226
7434
  );
7435
+ const title = rootTitle ?? (compressionRootSessionId ? stripCompressionTitleSuffix(tipTitle) : tipTitle);
7227
7436
  if (!title || title === manifest.title) {
7228
7437
  return manifest;
7229
7438
  }
@@ -7537,9 +7746,18 @@ function canAutoGenerateTitle(manifest) {
7537
7746
  function normalizeConversationTitleSource(title) {
7538
7747
  return isDefaultConversationTitle(title) ? "default" : "hermes";
7539
7748
  }
7749
+ function stripCompressionTitleSuffix(value) {
7750
+ const normalized = value?.replace(/\s+/gu, " ").trim();
7751
+ if (!normalized) {
7752
+ return void 0;
7753
+ }
7754
+ const match = /^(.*?) #\d+$/u.exec(normalized);
7755
+ const stripped = match?.[1]?.trim();
7756
+ return stripped || normalized;
7757
+ }
7540
7758
 
7541
7759
  // src/conversations/conversation-turns.ts
7542
- import { randomUUID as randomUUID5 } from "crypto";
7760
+ import { randomUUID as randomUUID6 } from "crypto";
7543
7761
  var MESSAGE_ORDER_STEP_MS = 10;
7544
7762
  var ASSISTANT_ORDER_OFFSET_MS = 1;
7545
7763
  function createAgentTurnDraft(input) {
@@ -7773,10 +7991,10 @@ function createAssistantMessage(input) {
7773
7991
  };
7774
7992
  }
7775
7993
  function createMessageId() {
7776
- return `msg_${randomUUID5().replaceAll("-", "")}`;
7994
+ return `msg_${randomUUID6().replaceAll("-", "")}`;
7777
7995
  }
7778
7996
  function createRunId() {
7779
- return `run_${randomUUID5().replaceAll("-", "")}`;
7997
+ return `run_${randomUUID6().replaceAll("-", "")}`;
7780
7998
  }
7781
7999
  function hasActiveOrQueuedRuns(snapshot) {
7782
8000
  return snapshot.runs.some(
@@ -8891,20 +9109,20 @@ function hydrateAgentEventBlocks(blocks, agentEvents) {
8891
9109
  // src/conversations/conversation-store.ts
8892
9110
  import {
8893
9111
  appendFile as appendFile2,
8894
- mkdir as mkdir7,
9112
+ mkdir as mkdir8,
8895
9113
  readdir as readdir5,
8896
9114
  readFile as readFile7,
8897
9115
  rm as rm5,
8898
9116
  writeFile as writeFile2
8899
9117
  } from "fs/promises";
8900
- import path12 from "path";
9118
+ import path13 from "path";
8901
9119
  var ConversationStore = class {
8902
9120
  constructor(paths) {
8903
9121
  this.paths = paths;
8904
9122
  }
8905
9123
  paths;
8906
9124
  async ensureConversationsDir() {
8907
- await mkdir7(this.paths.conversationsDir, { recursive: true, mode: 448 });
9125
+ await mkdir8(this.paths.conversationsDir, { recursive: true, mode: 448 });
8908
9126
  }
8909
9127
  async listConversationIds() {
8910
9128
  await this.ensureConversationsDir();
@@ -8919,7 +9137,7 @@ var ConversationStore = class {
8919
9137
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
8920
9138
  }
8921
9139
  async createConversation(manifest, snapshot = createEmptySnapshot2()) {
8922
- await mkdir7(this.conversationDir(manifest.id), {
9140
+ await mkdir8(this.conversationDir(manifest.id), {
8923
9141
  recursive: true,
8924
9142
  mode: 448
8925
9143
  });
@@ -8959,7 +9177,7 @@ var ConversationStore = class {
8959
9177
  conversation_id: conversationId,
8960
9178
  created_at: now
8961
9179
  };
8962
- await mkdir7(this.conversationDir(conversationId), {
9180
+ await mkdir8(this.conversationDir(conversationId), {
8963
9181
  recursive: true,
8964
9182
  mode: 448
8965
9183
  });
@@ -9013,23 +9231,23 @@ var ConversationStore = class {
9013
9231
  return manifest?.status === "active";
9014
9232
  }
9015
9233
  removeConversationAttachments(conversationId) {
9016
- return rm5(path12.join(this.conversationDir(conversationId), "attachments"), {
9234
+ return rm5(path13.join(this.conversationDir(conversationId), "attachments"), {
9017
9235
  recursive: true,
9018
9236
  force: true
9019
9237
  });
9020
9238
  }
9021
9239
  conversationDir(conversationId) {
9022
9240
  assertValidConversationId(conversationId);
9023
- return path12.join(this.paths.conversationsDir, conversationId);
9241
+ return path13.join(this.paths.conversationsDir, conversationId);
9024
9242
  }
9025
9243
  manifestPath(conversationId) {
9026
- return path12.join(this.conversationDir(conversationId), "manifest.json");
9244
+ return path13.join(this.conversationDir(conversationId), "manifest.json");
9027
9245
  }
9028
9246
  snapshotPath(conversationId) {
9029
- return path12.join(this.conversationDir(conversationId), "snapshot.json");
9247
+ return path13.join(this.conversationDir(conversationId), "snapshot.json");
9030
9248
  }
9031
9249
  eventsPath(conversationId) {
9032
- return path12.join(this.conversationDir(conversationId), "events.ndjson");
9250
+ return path13.join(this.conversationDir(conversationId), "events.ndjson");
9033
9251
  }
9034
9252
  };
9035
9253
  function createEmptySnapshot2() {
@@ -9040,14 +9258,13 @@ function isNodeError9(error, code) {
9040
9258
  }
9041
9259
 
9042
9260
  // src/conversations/hermes-session-sync.ts
9043
- import { randomUUID as randomUUID6 } from "crypto";
9261
+ import { randomUUID as randomUUID7 } from "crypto";
9044
9262
  import { readdir as readdir7, readFile as readFile9, stat as stat9 } from "fs/promises";
9045
- import os4 from "os";
9046
- import path14 from "path";
9263
+ import path15 from "path";
9047
9264
 
9048
9265
  // src/conversations/delivery-import.ts
9049
9266
  import { lstat as lstat2, readFile as readFile8, readdir as readdir6, stat as stat8 } from "fs/promises";
9050
- import path13 from "path";
9267
+ import path14 from "path";
9051
9268
  var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
9052
9269
  var MAX_MEDIA_IMPORT_FAILURES = 20;
9053
9270
  var MAX_DELIVERY_FILES = 50;
@@ -9118,16 +9335,16 @@ var SUPPORTED_DELIVERY_EXTENSIONS = /* @__PURE__ */ new Set([
9118
9335
  ".m4a"
9119
9336
  ]);
9120
9337
  function resolveDeliveryStagingTarget(paths, stagingDir) {
9121
- const resolvedDir = path13.resolve(stagingDir);
9122
- const relative = path13.relative(path13.resolve(paths.conversationsDir), resolvedDir);
9123
- if (!relative || relative.startsWith("..") || path13.isAbsolute(relative)) {
9338
+ const resolvedDir = path14.resolve(stagingDir);
9339
+ const relative = path14.relative(path14.resolve(paths.conversationsDir), resolvedDir);
9340
+ if (!relative || relative.startsWith("..") || path14.isAbsolute(relative)) {
9124
9341
  throw new LinkHttpError(
9125
9342
  400,
9126
9343
  "delivery_staging_invalid",
9127
9344
  "delivery staging directory must be inside Hermes Link conversations"
9128
9345
  );
9129
9346
  }
9130
- const segments = relative.split(path13.sep);
9347
+ const segments = relative.split(path14.sep);
9131
9348
  if (segments.length !== 3 || segments[1] !== DELIVERY_STAGING_SEGMENT || !segments[0] || !segments[2]) {
9132
9349
  throw new LinkHttpError(
9133
9350
  400,
@@ -9163,7 +9380,7 @@ async function collectStagedDeliveryReferences(stagingDir) {
9163
9380
  return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isSupportedDeliveryFilename(entry.name)).sort(
9164
9381
  (left, right) => left.name.localeCompare(right.name, "en", { numeric: true })
9165
9382
  ).slice(0, MAX_DELIVERY_FILES).map((entry) => {
9166
- const sourcePath = path13.join(stagingDir, entry.name);
9383
+ const sourcePath = path14.join(stagingDir, entry.name);
9167
9384
  const mime = inferMimeType(sourcePath);
9168
9385
  return {
9169
9386
  path: sourcePath,
@@ -9349,7 +9566,7 @@ async function writeBlobFromFile(deps, conversationId, source) {
9349
9566
  }
9350
9567
  return deps.writeBlob(conversationId, {
9351
9568
  bytes: await readFile8(sourcePath),
9352
- filename: path13.basename(sourcePath),
9569
+ filename: path14.basename(sourcePath),
9353
9570
  mime: source.mime ?? inferMimeType(sourcePath)
9354
9571
  });
9355
9572
  }
@@ -9362,7 +9579,7 @@ function describeMediaImportFailure(reference, sourceKey, error) {
9362
9579
  };
9363
9580
  }
9364
9581
  function isSupportedDeliveryFilename(filename) {
9365
- return SUPPORTED_DELIVERY_EXTENSIONS.has(path13.extname(filename).toLowerCase());
9582
+ return SUPPORTED_DELIVERY_EXTENSIONS.has(path14.extname(filename).toLowerCase());
9366
9583
  }
9367
9584
  function readString8(payload, key) {
9368
9585
  const value = payload[key];
@@ -9419,7 +9636,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
9419
9636
  const candidates = [];
9420
9637
  for (const profileName of profileNames) {
9421
9638
  const profileDir = resolveHermesProfileDir(profileName);
9422
- const dbPath = path14.join(profileDir, "state.db");
9639
+ const dbPath = path15.join(profileDir, "state.db");
9423
9640
  const sessions = await listProfileSessions(dbPath).catch((error) => {
9424
9641
  result.errors.push({
9425
9642
  profile: profileName,
@@ -9449,16 +9666,19 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
9449
9666
  const importableCandidates = candidates.slice(0, maxImports);
9450
9667
  result.skipped_over_limit = Math.max(0, candidates.length - maxImports);
9451
9668
  for (const candidate of importableCandidates) {
9452
- if (knownHermesSessions.ids.has(candidate.session.id)) {
9669
+ const candidateSessionIds = lineageSessionIds(candidate);
9670
+ const knownConversationIds = findKnownConversationIdsForCandidate(
9671
+ knownHermesSessions,
9672
+ candidate
9673
+ );
9674
+ if (knownConversationIds.length > 0) {
9453
9675
  result.skipped_existing += 1;
9454
- const reprojected = await reprojectExistingHermesConversation({
9676
+ const reprojected = await mergeExistingHermesConversation({
9455
9677
  paths,
9456
9678
  store,
9457
9679
  logger,
9458
9680
  candidate,
9459
- conversationIds: knownHermesSessions.conversationIdsBySessionId.get(
9460
- candidate.session.id
9461
- ) ?? []
9681
+ conversationIds: knownConversationIds
9462
9682
  }).catch((error) => {
9463
9683
  result.errors.push({
9464
9684
  profile: candidate.profileName,
@@ -9469,14 +9689,22 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
9469
9689
  if (reprojected) {
9470
9690
  result.reprojected_count += 1;
9471
9691
  }
9692
+ for (const sessionId of candidateSessionIds) {
9693
+ knownHermesSessions.sessionIds.add(sessionId);
9694
+ }
9695
+ continue;
9696
+ }
9697
+ if (candidateSessionIds.some(
9698
+ (sessionId) => knownHermesSessions.sessionIds.has(sessionId)
9699
+ )) {
9700
+ result.skipped_existing += 1;
9472
9701
  continue;
9473
9702
  }
9474
9703
  const imported = await importHermesSession({
9475
9704
  paths,
9476
9705
  store,
9477
9706
  logger,
9478
- candidate,
9479
- existingHermesSessionIds: knownHermesSessions.ids
9707
+ candidate
9480
9708
  }).catch((error) => {
9481
9709
  result.errors.push({
9482
9710
  profile: candidate.profileName,
@@ -9486,6 +9714,9 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
9486
9714
  });
9487
9715
  if (imported) {
9488
9716
  result.imported_count += 1;
9717
+ for (const sessionId of candidateSessionIds) {
9718
+ knownHermesSessions.sessionIds.add(sessionId);
9719
+ }
9489
9720
  }
9490
9721
  }
9491
9722
  if (result.imported_count > 0 || result.reprojected_count > 0 || result.errors.length > 0) {
@@ -9496,13 +9727,14 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
9496
9727
  return result;
9497
9728
  }
9498
9729
  async function importHermesSession(input) {
9499
- const { paths, store, logger, candidate, existingHermesSessionIds } = input;
9730
+ const { paths, store, logger, candidate } = input;
9500
9731
  const profile = await resolveConversationProfileTarget(
9501
9732
  paths,
9502
9733
  candidate.profileName
9503
9734
  );
9504
9735
  const sessionId = candidate.session.id;
9505
- const messages = await readHermesSessionMessages(candidate);
9736
+ const hermesSessionIds = lineageSessionIds(candidate);
9737
+ const messages = await readHermesLineageMessages(candidate);
9506
9738
  const now = (/* @__PURE__ */ new Date()).toISOString();
9507
9739
  const createdAt = isoFromHermesTime(candidate.session.started_at) ?? now;
9508
9740
  const updatedAt = isoFromHermesTime(candidate.session.last_active) ?? isoFromHermesTime(messages.at(-1)?.timestamp) ?? createdAt;
@@ -9519,7 +9751,14 @@ async function importHermesSession(input) {
9519
9751
  }),
9520
9752
  runs: []
9521
9753
  };
9522
- const title = readString9(candidate.session, "title") ?? firstUserText(snapshot);
9754
+ const title = lineageTitle(candidate) ?? readString9(candidate.session, "title") ?? firstUserText(snapshot);
9755
+ const importedStats = buildImportedHermesStats({
9756
+ candidate,
9757
+ snapshot,
9758
+ profileUid: profile.profileUid,
9759
+ profileName: profile.profileName,
9760
+ updatedAt
9761
+ });
9523
9762
  const manifest = {
9524
9763
  id: conversationId,
9525
9764
  schema_version: 1,
@@ -9528,13 +9767,15 @@ async function importHermesSession(input) {
9528
9767
  title_source: title ? "hermes" : "default",
9529
9768
  status: "active",
9530
9769
  hermes_session_id: sessionId,
9531
- hermes_session_ids: [sessionId],
9770
+ hermes_session_ids: hermesSessionIds,
9771
+ ...lineageManifestPatch(candidate),
9532
9772
  profile_uid: profile.profileUid,
9533
9773
  profile_name_snapshot: profile.profileName,
9534
9774
  profile: profile.profileName,
9535
9775
  created_at: createdAt,
9536
9776
  updated_at: updatedAt,
9537
- last_event_seq: 0
9777
+ last_event_seq: 0,
9778
+ stats: importedStats
9538
9779
  };
9539
9780
  await store.createConversation(manifest, snapshot);
9540
9781
  await hydrateImportedConversationMedia({
@@ -9577,59 +9818,288 @@ async function importHermesSession(input) {
9577
9818
  paths,
9578
9819
  toStatsIndexRecord(await store.readManifest(conversationId), stats)
9579
9820
  );
9580
- existingHermesSessionIds.add(sessionId);
9581
9821
  return true;
9582
9822
  }
9583
- async function reprojectExistingHermesConversation(input) {
9823
+ async function mergeExistingHermesConversation(input) {
9824
+ const conversations = await readExistingHermesConversations(
9825
+ input.store,
9826
+ input.conversationIds
9827
+ );
9828
+ const active = conversations.filter((item) => item.manifest.status === "active");
9829
+ const canonical = selectCanonicalHermesConversation(active);
9830
+ if (!canonical) {
9831
+ return false;
9832
+ }
9584
9833
  let changed = false;
9585
- for (const conversationId of input.conversationIds) {
9586
- const manifest = await input.store.readManifest(conversationId).catch(() => null);
9587
- if (!manifest || manifest.status !== "active") {
9588
- continue;
9589
- }
9590
- const snapshot = await input.store.readSnapshot(conversationId).catch(() => null);
9591
- if (!snapshot) {
9834
+ const candidateMessages = await readHermesLineageMessages(input.candidate);
9835
+ const nextCanonical = await mergeHermesCandidateIntoConversation({
9836
+ ...input,
9837
+ existing: canonical,
9838
+ candidateMessages
9839
+ });
9840
+ changed = nextCanonical || changed;
9841
+ for (const duplicate of active) {
9842
+ if (duplicate.conversationId === canonical.conversationId) {
9592
9843
  continue;
9593
9844
  }
9594
- const prefix = collectImportedHermesPrefix(snapshot);
9595
- if (!prefix?.needsUpgrade || prefix.messages.length === 0) {
9845
+ if (!isSafeHermesImportConversation(duplicate) || duplicate.manifest.status !== "active") {
9596
9846
  continue;
9597
9847
  }
9598
- const profile = await resolveConversationProfileTarget(
9599
- input.paths,
9600
- manifest.profile_name_snapshot ?? manifest.profile ?? input.candidate.profileName
9601
- );
9602
- const nextSnapshot = {
9603
- ...snapshot,
9848
+ await softDeleteMergedHermesDuplicate({
9849
+ paths: input.paths,
9850
+ store: input.store,
9851
+ duplicate,
9852
+ candidate: input.candidate
9853
+ });
9854
+ changed = true;
9855
+ }
9856
+ return changed;
9857
+ }
9858
+ async function mergeHermesCandidateIntoConversation(input) {
9859
+ const { candidate, existing } = input;
9860
+ const profile = await resolveConversationProfileTarget(
9861
+ input.paths,
9862
+ existing.manifest.profile_name_snapshot ?? existing.manifest.profile ?? candidate.profileName
9863
+ );
9864
+ let nextSnapshot = existing.snapshot;
9865
+ const pureImport = isSafeHermesImportConversation(existing);
9866
+ const prefix = collectImportedHermesPrefix(existing.snapshot);
9867
+ if (pureImport || prefix?.needsUpgrade && prefix.messages.length > 0) {
9868
+ const messages = pureImport ? input.candidateMessages : prefix.messages;
9869
+ nextSnapshot = {
9870
+ ...existing.snapshot,
9604
9871
  messages: [
9605
9872
  ...toLinkMessages({
9606
- conversationId,
9873
+ conversationId: existing.conversationId,
9607
9874
  profileName: profile.profileName,
9608
9875
  profileUid: profile.profileUid,
9609
9876
  profileDisplayName: profile.profileDisplayName,
9610
- sessionId: input.candidate.session.id,
9611
- messages: prefix.messages
9877
+ sessionId: candidate.session.id,
9878
+ messages
9612
9879
  }),
9613
- ...snapshot.messages.slice(prefix.endIndex)
9880
+ ...pureImport ? [] : existing.snapshot.messages.slice(prefix.endIndex)
9614
9881
  ]
9615
9882
  };
9616
- await input.store.writeSnapshot(conversationId, nextSnapshot);
9883
+ await input.store.writeSnapshot(existing.conversationId, nextSnapshot);
9617
9884
  await hydrateImportedConversationMedia({
9618
9885
  paths: input.paths,
9619
9886
  store: input.store,
9620
9887
  logger: input.logger,
9621
- conversationId
9888
+ conversationId: existing.conversationId
9622
9889
  });
9623
- const hydratedSnapshot = await input.store.readSnapshot(conversationId);
9624
- const stats = buildConversationStats(manifest, hydratedSnapshot);
9625
- await input.store.writeManifest({ ...manifest, stats });
9626
- await upsertConversationStats(
9627
- input.paths,
9628
- toStatsIndexRecord({ ...manifest, stats }, stats)
9629
- );
9630
- changed = true;
9890
+ nextSnapshot = await input.store.readSnapshot(existing.conversationId);
9891
+ }
9892
+ const updatedAt = isoFromHermesTime(candidate.session.last_active) ?? existing.manifest.updated_at;
9893
+ const nextManifest = mergeHermesLineageIntoManifest({
9894
+ manifest: existing.manifest,
9895
+ candidate,
9896
+ snapshot: nextSnapshot,
9897
+ profileUid: profile.profileUid,
9898
+ profileName: profile.profileName,
9899
+ updatedAt
9900
+ });
9901
+ if (manifestEquivalent(existing.manifest, nextManifest)) {
9902
+ return nextSnapshot !== existing.snapshot;
9631
9903
  }
9632
- return changed;
9904
+ await input.store.writeManifest(nextManifest);
9905
+ await upsertConversationStats(
9906
+ input.paths,
9907
+ toStatsIndexRecord(nextManifest, nextManifest.stats)
9908
+ );
9909
+ return true;
9910
+ }
9911
+ function findKnownConversationIdsForCandidate(known, candidate) {
9912
+ const conversationIds = [];
9913
+ for (const sessionId of lineageSessionIds(candidate)) {
9914
+ for (const conversationId of known.conversationIdsBySessionId.get(sessionId) ?? []) {
9915
+ if (!conversationIds.includes(conversationId)) {
9916
+ conversationIds.push(conversationId);
9917
+ }
9918
+ }
9919
+ }
9920
+ return conversationIds;
9921
+ }
9922
+ function lineageSessionIds(candidate) {
9923
+ return normalizeSessionIds([
9924
+ candidate.session._lineage_root_id,
9925
+ ...candidate.session._lineage_session_ids ?? [],
9926
+ candidate.session.id
9927
+ ]);
9928
+ }
9929
+ function lineageTitle(candidate) {
9930
+ return candidate.session._lineage_title ?? stripCompressionTitleSuffix2(readString9(candidate.session, "title") ?? "");
9931
+ }
9932
+ function lineageManifestPatch(candidate) {
9933
+ const sessionIds = lineageSessionIds(candidate);
9934
+ const rootSessionId = candidate.session._lineage_root_id ?? sessionIds[0];
9935
+ const currentSessionId = candidate.session.id;
9936
+ if (!rootSessionId || sessionIds.length <= 1 || rootSessionId === currentSessionId) {
9937
+ return {};
9938
+ }
9939
+ return {
9940
+ hermes_lineage: {
9941
+ kind: "compression",
9942
+ root_session_id: rootSessionId,
9943
+ current_session_id: currentSessionId,
9944
+ session_ids: sessionIds
9945
+ }
9946
+ };
9947
+ }
9948
+ function buildImportedHermesStats(input) {
9949
+ const usage = input.candidate.session._lineage_usage;
9950
+ const base = input.baseStats;
9951
+ const usageInput = usage?.input_tokens ?? 0;
9952
+ const usageOutput = usage?.output_tokens ?? 0;
9953
+ const usageTotal = usage?.total_tokens ?? usageInput + usageOutput;
9954
+ const inputTokens = Math.max(base?.input_tokens ?? 0, usageInput);
9955
+ const outputTokens = Math.max(base?.output_tokens ?? 0, usageOutput);
9956
+ const totalTokens = Math.max(
9957
+ base?.total_tokens ?? 0,
9958
+ usageTotal,
9959
+ inputTokens + outputTokens
9960
+ );
9961
+ const updatedAt = base?.updated_at ? latestTimestamp(base.updated_at, input.updatedAt) : input.updatedAt;
9962
+ return {
9963
+ input_tokens: inputTokens,
9964
+ output_tokens: outputTokens,
9965
+ total_tokens: totalTokens,
9966
+ message_count: Math.max(
9967
+ base?.message_count ?? 0,
9968
+ input.snapshot.messages.length
9969
+ ),
9970
+ run_count: Math.max(base?.run_count ?? 0, usage?.api_call_count ?? 0),
9971
+ profile_uid: base?.profile_uid ?? input.profileUid,
9972
+ profile_name_snapshot: base?.profile_name_snapshot ?? base?.profile ?? input.profileName,
9973
+ profile: base?.profile ?? base?.profile_name_snapshot ?? input.profileName,
9974
+ ...base?.model ?? usage?.model ? { model: base?.model ?? usage?.model } : {},
9975
+ ...base?.provider ?? usage?.provider ? { provider: base?.provider ?? usage?.provider } : {},
9976
+ ...base?.context_window ? { context_window: base.context_window } : {},
9977
+ updated_at: updatedAt
9978
+ };
9979
+ }
9980
+ async function readExistingHermesConversations(store, conversationIds) {
9981
+ const conversations = [];
9982
+ for (const conversationId of [...new Set(conversationIds)]) {
9983
+ const manifest = await store.readManifest(conversationId).catch(() => null);
9984
+ if (!manifest) {
9985
+ continue;
9986
+ }
9987
+ const snapshot = await store.readSnapshot(conversationId).catch(() => ({
9988
+ schema_version: 1,
9989
+ messages: [],
9990
+ runs: []
9991
+ }));
9992
+ conversations.push({ conversationId, manifest, snapshot });
9993
+ }
9994
+ return conversations;
9995
+ }
9996
+ function selectCanonicalHermesConversation(conversations) {
9997
+ const ranked = [...conversations].sort((left, right) => {
9998
+ const rightScore = canonicalConversationScore(right);
9999
+ const leftScore = canonicalConversationScore(left);
10000
+ if (rightScore !== leftScore) {
10001
+ return rightScore - leftScore;
10002
+ }
10003
+ return Date.parse(left.manifest.created_at) - Date.parse(right.manifest.created_at) || left.conversationId.localeCompare(right.conversationId);
10004
+ });
10005
+ return ranked[0] ?? null;
10006
+ }
10007
+ function canonicalConversationScore(item) {
10008
+ let score = 0;
10009
+ if (!isSafeHermesImportConversation(item)) {
10010
+ score += 1e3;
10011
+ }
10012
+ if (item.snapshot.runs.length > 0) {
10013
+ score += 500;
10014
+ }
10015
+ if (item.manifest.title_source === "manual") {
10016
+ score += 200;
10017
+ }
10018
+ if (!hasCompressionTitleSuffix(item.manifest.title)) {
10019
+ score += 100;
10020
+ }
10021
+ if (item.manifest.hermes_lineage?.root_session_id && item.manifest.hermes_session_id === item.manifest.hermes_lineage.root_session_id) {
10022
+ score += 50;
10023
+ }
10024
+ return score;
10025
+ }
10026
+ function isSafeHermesImportConversation(item) {
10027
+ const snapshot = item.snapshot;
10028
+ if (snapshot.runs.length > 0) {
10029
+ return false;
10030
+ }
10031
+ if (item.manifest.title_source === "manual" || item.manifest.title_source === "generated") {
10032
+ return false;
10033
+ }
10034
+ return snapshot.messages.length === 0 || snapshot.messages.every((message) => isHermesImportedMessage(message));
10035
+ }
10036
+ async function softDeleteMergedHermesDuplicate(input) {
10037
+ const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
10038
+ const emptySnapshot3 = {
10039
+ schema_version: 1,
10040
+ messages: [],
10041
+ runs: []
10042
+ };
10043
+ const hermesSessionIds = normalizeSessionIds([
10044
+ input.duplicate.manifest.hermes_session_id,
10045
+ ...input.duplicate.manifest.hermes_session_ids ?? [],
10046
+ ...input.duplicate.manifest.hermes_lineage?.session_ids ?? [],
10047
+ ...lineageSessionIds(input.candidate)
10048
+ ]);
10049
+ const nextManifest = {
10050
+ ...input.duplicate.manifest,
10051
+ status: "deleted_soft",
10052
+ hermes_session_ids: hermesSessionIds,
10053
+ ...lineageManifestPatch(input.candidate),
10054
+ updated_at: deletedAt,
10055
+ deleted_at: deletedAt
10056
+ };
10057
+ const stats = buildConversationStats(
10058
+ { ...nextManifest, stats: input.duplicate.manifest.stats },
10059
+ emptySnapshot3
10060
+ );
10061
+ const tombstone = { ...nextManifest, stats };
10062
+ await input.store.writeManifest(tombstone);
10063
+ await input.store.writeSnapshot(input.duplicate.conversationId, emptySnapshot3);
10064
+ await upsertConversationStats(input.paths, toStatsIndexRecord(tombstone, stats));
10065
+ }
10066
+ function mergeHermesLineageIntoManifest(input) {
10067
+ const hermesSessionIds = normalizeSessionIds([
10068
+ input.manifest.hermes_session_id,
10069
+ ...input.manifest.hermes_session_ids ?? [],
10070
+ ...input.manifest.hermes_lineage?.session_ids ?? [],
10071
+ ...lineageSessionIds(input.candidate)
10072
+ ]);
10073
+ const nextBase = {
10074
+ ...input.manifest,
10075
+ hermes_session_id: input.candidate.session.id,
10076
+ hermes_session_ids: hermesSessionIds,
10077
+ ...lineageManifestPatch(input.candidate),
10078
+ profile_uid: input.manifest.profile_uid ?? input.profileUid,
10079
+ profile_name_snapshot: input.manifest.profile_name_snapshot ?? input.manifest.profile ?? input.profileName,
10080
+ profile: input.manifest.profile ?? input.manifest.profile_name_snapshot ?? input.profileName,
10081
+ updated_at: latestTimestamp(input.manifest.updated_at, input.updatedAt)
10082
+ };
10083
+ const title = lineageTitle(input.candidate);
10084
+ if (title && canSyncHermesTitle(input.manifest)) {
10085
+ nextBase.title = normalizeTitle(title);
10086
+ nextBase.title_source = "hermes";
10087
+ }
10088
+ const baseStats = buildConversationStats(nextBase, input.snapshot);
10089
+ return {
10090
+ ...nextBase,
10091
+ stats: buildImportedHermesStats({
10092
+ candidate: input.candidate,
10093
+ snapshot: input.snapshot,
10094
+ profileUid: input.profileUid,
10095
+ profileName: input.profileName,
10096
+ updatedAt: input.updatedAt,
10097
+ baseStats
10098
+ })
10099
+ };
10100
+ }
10101
+ function manifestEquivalent(left, right) {
10102
+ return stableJson(left) === stableJson(right);
9633
10103
  }
9634
10104
  function collectImportedHermesPrefix(snapshot) {
9635
10105
  const rows = [];
@@ -10103,7 +10573,7 @@ function latestTimestamp(left, right) {
10103
10573
  return leftTime >= rightTime ? left : right;
10104
10574
  }
10105
10575
  async function readKnownHermesSessions(store) {
10106
- const ids = /* @__PURE__ */ new Set();
10576
+ const sessionIds = /* @__PURE__ */ new Set();
10107
10577
  const conversationIdsBySessionId = /* @__PURE__ */ new Map();
10108
10578
  for (const conversationId of await store.listConversationIds()) {
10109
10579
  const manifest = await store.readManifest(conversationId).catch(() => null);
@@ -10111,7 +10581,7 @@ async function readKnownHermesSessions(store) {
10111
10581
  continue;
10112
10582
  }
10113
10583
  if (manifest.hermes_session_id) {
10114
- ids.add(manifest.hermes_session_id);
10584
+ sessionIds.add(manifest.hermes_session_id);
10115
10585
  rememberKnownHermesConversation(
10116
10586
  conversationIdsBySessionId,
10117
10587
  manifest.hermes_session_id,
@@ -10119,7 +10589,22 @@ async function readKnownHermesSessions(store) {
10119
10589
  );
10120
10590
  }
10121
10591
  for (const sessionId of manifest.hermes_session_ids ?? []) {
10122
- ids.add(sessionId);
10592
+ sessionIds.add(sessionId);
10593
+ rememberKnownHermesConversation(
10594
+ conversationIdsBySessionId,
10595
+ sessionId,
10596
+ conversationId
10597
+ );
10598
+ }
10599
+ for (const sessionId of [
10600
+ manifest.hermes_lineage?.root_session_id,
10601
+ manifest.hermes_lineage?.current_session_id,
10602
+ ...manifest.hermes_lineage?.session_ids ?? []
10603
+ ]) {
10604
+ if (!sessionId) {
10605
+ continue;
10606
+ }
10607
+ sessionIds.add(sessionId);
10123
10608
  rememberKnownHermesConversation(
10124
10609
  conversationIdsBySessionId,
10125
10610
  sessionId,
@@ -10127,7 +10612,7 @@ async function readKnownHermesSessions(store) {
10127
10612
  );
10128
10613
  }
10129
10614
  }
10130
- return { ids, conversationIdsBySessionId };
10615
+ return { sessionIds, conversationIdsBySessionId };
10131
10616
  }
10132
10617
  function rememberKnownHermesConversation(map, sessionId, conversationId) {
10133
10618
  const current = map.get(sessionId) ?? [];
@@ -10137,7 +10622,7 @@ function rememberKnownHermesConversation(map, sessionId, conversationId) {
10137
10622
  }
10138
10623
  async function discoverHermesProfileNames() {
10139
10624
  const names = /* @__PURE__ */ new Set([DEFAULT_PROFILE_NAME]);
10140
- const profilesDir = path14.join(os4.homedir(), ".hermes", "profiles");
10625
+ const profilesDir = resolveHermesProfilesDir();
10141
10626
  const entries = await readdir7(profilesDir, { withFileTypes: true }).catch(
10142
10627
  (error) => {
10143
10628
  if (isNodeError11(error, "ENOENT")) {
@@ -10245,14 +10730,12 @@ function joinImportedText(left, right) {
10245
10730
  ${right}`;
10246
10731
  }
10247
10732
  function projectCompressionTips(rows) {
10248
- const byId = /* @__PURE__ */ new Map();
10249
10733
  const childrenByParent = /* @__PURE__ */ new Map();
10250
10734
  for (const row of rows) {
10251
10735
  const id = readString9(row, "id");
10252
10736
  if (!id) {
10253
10737
  continue;
10254
10738
  }
10255
- byId.set(id, row);
10256
10739
  const parentId = readString9(row, "parent_session_id");
10257
10740
  if (parentId) {
10258
10741
  const children = childrenByParent.get(parentId) ?? [];
@@ -10268,13 +10751,14 @@ function projectCompressionTips(rows) {
10268
10751
  }
10269
10752
  let tip = row;
10270
10753
  const visited = /* @__PURE__ */ new Set([id]);
10754
+ const chain = [row];
10271
10755
  while (readString9(tip, "end_reason") === "compression") {
10272
10756
  const tipId2 = readString9(tip, "id");
10273
10757
  if (!tipId2) {
10274
10758
  break;
10275
10759
  }
10276
- const next = (childrenByParent.get(tipId2) ?? []).filter((child) => readString9(child, "id")).sort(
10277
- (left, right) => (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
10760
+ const next = (childrenByParent.get(tipId2) ?? []).filter((child) => isCompressionContinuation(tip, child)).sort(
10761
+ (left, right) => (readNumber2(right.started_at) ?? 0) - (readNumber2(left.started_at) ?? 0) || (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
10278
10762
  )[0];
10279
10763
  const nextId = next ? readString9(next, "id") : null;
10280
10764
  if (!next || !nextId || visited.has(nextId)) {
@@ -10282,25 +10766,75 @@ function projectCompressionTips(rows) {
10282
10766
  }
10283
10767
  tip = next;
10284
10768
  visited.add(nextId);
10769
+ chain.push(next);
10285
10770
  }
10286
10771
  const tipId = readString9(tip, "id");
10287
10772
  if (tipId) {
10773
+ const sessionIds = chain.map((item) => readString9(item, "id")).filter((item) => Boolean(item));
10288
10774
  projected.push({
10289
10775
  ...tip,
10290
10776
  id: tipId,
10291
10777
  _lineage_root_id: id,
10778
+ _lineage_session_ids: sessionIds,
10779
+ _lineage_title: readString9(row, "title") ?? stripCompressionTitleSuffix2(readString9(tip, "title") ?? ""),
10780
+ _lineage_usage: aggregateHermesSessionUsage(chain),
10292
10781
  started_at: readNumber2(row.started_at) ?? readNumber2(tip.started_at)
10293
10782
  });
10294
10783
  }
10295
10784
  }
10296
10785
  return projected;
10297
10786
  }
10787
+ function isCompressionContinuation(parent, child) {
10788
+ const childId = readString9(child, "id");
10789
+ const parentEndedAt = readNumber2(parent.ended_at);
10790
+ const childStartedAt = readNumber2(child.started_at);
10791
+ return Boolean(childId) && readString9(parent, "end_reason") === "compression" && parentEndedAt !== null && childStartedAt !== null && childStartedAt >= parentEndedAt;
10792
+ }
10793
+ function aggregateHermesSessionUsage(rows) {
10794
+ let inputTokens = 0;
10795
+ let outputTokens = 0;
10796
+ let apiCallCount = 0;
10797
+ let model;
10798
+ let provider;
10799
+ for (const row of rows) {
10800
+ inputTokens += readNumber2(row.input_tokens) ?? 0;
10801
+ outputTokens += readNumber2(row.output_tokens) ?? 0;
10802
+ apiCallCount += readNumber2(row.api_call_count) ?? 0;
10803
+ model = readString9(row, "model") ?? model;
10804
+ provider = readString9(row, "billing_provider") ?? readString9(row, "provider") ?? provider;
10805
+ }
10806
+ return {
10807
+ input_tokens: inputTokens,
10808
+ output_tokens: outputTokens,
10809
+ total_tokens: inputTokens + outputTokens,
10810
+ api_call_count: apiCallCount,
10811
+ ...model ? { model } : {},
10812
+ ...provider ? { provider } : {}
10813
+ };
10814
+ }
10815
+ async function readHermesLineageMessages(candidate) {
10816
+ const rows = [];
10817
+ for (const sessionId of lineageSessionIds(candidate)) {
10818
+ const messages = await readHermesSessionMessages({
10819
+ ...candidate,
10820
+ session: {
10821
+ ...candidate.session,
10822
+ id: sessionId
10823
+ }
10824
+ });
10825
+ rows.push(...messages);
10826
+ }
10827
+ return rows;
10828
+ }
10298
10829
  async function readHermesSessionMessages(candidate) {
10299
10830
  const [dbMessages, jsonlMessages] = await Promise.all([
10300
10831
  readStateDbMessages(candidate.dbPath, candidate.session.id),
10301
10832
  readJsonlMessages(candidate.profileName, candidate.session.id)
10302
10833
  ]);
10303
- return jsonlMessages.length > dbMessages.length ? jsonlMessages : dbMessages;
10834
+ const selected = jsonlMessages.length > dbMessages.length ? jsonlMessages : dbMessages;
10835
+ return selected.map(
10836
+ (message) => readString9(message, "session_id") ? message : { ...message, session_id: candidate.session.id }
10837
+ );
10304
10838
  }
10305
10839
  async function readStateDbMessages(dbPath, sessionId) {
10306
10840
  if (!await isFile(dbPath)) {
@@ -10338,8 +10872,8 @@ async function readJsonlMessages(profileName, sessionId) {
10338
10872
  return [];
10339
10873
  }
10340
10874
  const profileDir = resolveHermesProfileDir(profileName);
10341
- const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path14.join(profileDir, "sessions"));
10342
- const transcriptPath = path14.join(sessionsDir, `${sessionId}.jsonl`);
10875
+ const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path15.join(profileDir, "sessions"));
10876
+ const transcriptPath = path15.join(sessionsDir, `${sessionId}.jsonl`);
10343
10877
  const raw = await readFile9(transcriptPath, "utf8").catch((error) => {
10344
10878
  if (isNodeError11(error, "ENOENT")) {
10345
10879
  return "";
@@ -10491,9 +11025,10 @@ function formatImportedFilenameList(filenames) {
10491
11025
  function toLinkMessage(input) {
10492
11026
  const role = normalizeMessageRole(input.message.role);
10493
11027
  const text = normalizeContent(input.message.content);
11028
+ const sessionId = readString9(input.message, "session_id") ?? input.sessionId;
10494
11029
  const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
10495
11030
  return {
10496
- id: `msg_${randomUUID6().replaceAll("-", "")}`,
11031
+ id: `msg_${randomUUID7().replaceAll("-", "")}`,
10497
11032
  schema_version: 1,
10498
11033
  conversation_id: input.conversationId,
10499
11034
  role,
@@ -10509,7 +11044,7 @@ function toLinkMessage(input) {
10509
11044
  parts: text ? [{ type: "text", text }] : [],
10510
11045
  attachments: [],
10511
11046
  hermes: {
10512
- session_id: input.sessionId,
11047
+ session_id: sessionId,
10513
11048
  message_id: input.message.id,
10514
11049
  imported_from: "hermes",
10515
11050
  import_projection: HERMES_IMPORT_PROJECTION_VERSION
@@ -10545,6 +11080,47 @@ function normalizeTitle(value) {
10545
11080
  const normalized = value?.replace(/\s+/gu, " ").trim();
10546
11081
  return normalized || DEFAULT_CONVERSATION_TITLE;
10547
11082
  }
11083
+ function canSyncHermesTitle(manifest) {
11084
+ return manifest.title_source !== "manual" && manifest.title_source !== "generated" && manifest.title_source !== "temporary_user_message" && manifest.title_source !== "temporary_fallback";
11085
+ }
11086
+ function stripCompressionTitleSuffix2(value) {
11087
+ const normalized = value.replace(/\s+/gu, " ").trim();
11088
+ if (!normalized) {
11089
+ return void 0;
11090
+ }
11091
+ const match = /^(.*?) #\d+$/u.exec(normalized);
11092
+ const stripped = match?.[1]?.trim();
11093
+ return stripped || normalized;
11094
+ }
11095
+ function hasCompressionTitleSuffix(value) {
11096
+ return / #\d+$/u.test(value.trim());
11097
+ }
11098
+ function normalizeSessionIds(values) {
11099
+ const seen = /* @__PURE__ */ new Set();
11100
+ for (const value of values) {
11101
+ const sessionId = value?.trim();
11102
+ if (!sessionId || seen.has(sessionId)) {
11103
+ continue;
11104
+ }
11105
+ seen.add(sessionId);
11106
+ }
11107
+ return [...seen];
11108
+ }
11109
+ function stableJson(value) {
11110
+ return JSON.stringify(stabilizeJsonValue(value));
11111
+ }
11112
+ function stabilizeJsonValue(value) {
11113
+ if (Array.isArray(value)) {
11114
+ return value.map((item) => stabilizeJsonValue(item));
11115
+ }
11116
+ if (!value || typeof value !== "object") {
11117
+ return value;
11118
+ }
11119
+ const record = value;
11120
+ return Object.fromEntries(
11121
+ Object.keys(record).sort().map((key) => [key, stabilizeJsonValue(record[key])])
11122
+ );
11123
+ }
10548
11124
  function normalizeMessageRole(value) {
10549
11125
  switch (value?.trim().toLowerCase()) {
10550
11126
  case "user":
@@ -10624,7 +11200,7 @@ async function isFile(filePath) {
10624
11200
  });
10625
11201
  }
10626
11202
  function createConversationId() {
10627
- return `conv_${randomUUID6().replaceAll("-", "")}`;
11203
+ return `conv_${randomUUID7().replaceAll("-", "")}`;
10628
11204
  }
10629
11205
  function isoFromHermesTime(value) {
10630
11206
  const numeric = readNumber2(value);
@@ -10891,10 +11467,10 @@ async function cancelHermesRun(runId, options = {}) {
10891
11467
  );
10892
11468
  }
10893
11469
  }
10894
- async function callHermesApi(path25, init, options) {
11470
+ async function callHermesApi(path26, init, options) {
10895
11471
  const method = init.method ?? "GET";
10896
11472
  const startedAt = Date.now();
10897
- void options.logger?.debug("hermes_api_request_started", { method, path: path25 });
11473
+ void options.logger?.debug("hermes_api_request_started", { method, path: path26 });
10898
11474
  const availability = await ensureHermesApiServerAvailable({
10899
11475
  fetchImpl: options.fetchImpl,
10900
11476
  logger: options.logger,
@@ -10902,21 +11478,21 @@ async function callHermesApi(path25, init, options) {
10902
11478
  });
10903
11479
  let config = availability.configResult.apiServer;
10904
11480
  const fetcher = options.fetchImpl ?? fetch;
10905
- const request = () => fetchHermesApi(fetcher, config, path25, init, options);
11481
+ const request = () => fetchHermesApi(fetcher, config, path26, init, options);
10906
11482
  let response;
10907
11483
  try {
10908
11484
  response = await request();
10909
11485
  } catch (error) {
10910
- logHermesApiError(options.logger, method, path25, startedAt, error);
11486
+ logHermesApiError(options.logger, method, path26, startedAt, error);
10911
11487
  throw error;
10912
11488
  }
10913
11489
  if (response.status !== 401) {
10914
- logHermesApiResponse(options.logger, method, path25, startedAt, response);
11490
+ logHermesApiResponse(options.logger, method, path26, startedAt, response);
10915
11491
  return response;
10916
11492
  }
10917
11493
  void options.logger?.warn("hermes_api_request_retrying_after_401", {
10918
11494
  method,
10919
- path: path25,
11495
+ path: path26,
10920
11496
  duration_ms: Date.now() - startedAt
10921
11497
  });
10922
11498
  const refreshedAvailability = await ensureHermesApiServerAvailable({
@@ -10929,20 +11505,20 @@ async function callHermesApi(path25, init, options) {
10929
11505
  try {
10930
11506
  response = await request();
10931
11507
  } catch (error) {
10932
- logHermesApiError(options.logger, method, path25, startedAt, error);
11508
+ logHermesApiError(options.logger, method, path26, startedAt, error);
10933
11509
  throw error;
10934
11510
  }
10935
- logHermesApiResponse(options.logger, method, path25, startedAt, response);
11511
+ logHermesApiResponse(options.logger, method, path26, startedAt, response);
10936
11512
  return response;
10937
11513
  }
10938
- async function fetchHermesApi(fetcher, config, path25, init, options) {
11514
+ async function fetchHermesApi(fetcher, config, path26, init, options) {
10939
11515
  const headers = new Headers(init.headers);
10940
11516
  headers.set("accept", headers.get("accept") ?? "application/json");
10941
11517
  if (config.key) {
10942
11518
  headers.set("x-api-key", config.key);
10943
11519
  headers.set("authorization", `Bearer ${config.key}`);
10944
11520
  }
10945
- return await fetcher(`http://127.0.0.1:${config.port}${path25}`, {
11521
+ return await fetcher(`http://127.0.0.1:${config.port}${path26}`, {
10946
11522
  ...init,
10947
11523
  headers
10948
11524
  }).catch((error) => {
@@ -10950,7 +11526,7 @@ async function fetchHermesApi(fetcher, config, path25, init, options) {
10950
11526
  throw error;
10951
11527
  }
10952
11528
  void options.logger?.warn("hermes_api_server_connect_failed", {
10953
- path: path25,
11529
+ path: path26,
10954
11530
  port: config.port ?? null,
10955
11531
  error: error instanceof Error ? error.message : String(error)
10956
11532
  });
@@ -10961,10 +11537,10 @@ async function fetchHermesApi(fetcher, config, path25, init, options) {
10961
11537
  );
10962
11538
  });
10963
11539
  }
10964
- function logHermesApiResponse(logger, method, path25, startedAt, response) {
11540
+ function logHermesApiResponse(logger, method, path26, startedAt, response) {
10965
11541
  const fields = {
10966
11542
  method,
10967
- path: path25,
11543
+ path: path26,
10968
11544
  status: response.status,
10969
11545
  duration_ms: Date.now() - startedAt
10970
11546
  };
@@ -10984,10 +11560,10 @@ async function logHermesApiFailureResponse(logger, fields, response) {
10984
11560
  ...upstreamError ? { upstream_error: upstreamError } : {}
10985
11561
  });
10986
11562
  }
10987
- function logHermesApiError(logger, method, path25, startedAt, error) {
11563
+ function logHermesApiError(logger, method, path26, startedAt, error) {
10988
11564
  void logger?.warn("hermes_api_request_failed", {
10989
11565
  method,
10990
- path: path25,
11566
+ path: path26,
10991
11567
  duration_ms: Date.now() - startedAt,
10992
11568
  ...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
10993
11569
  error: error instanceof Error ? error.message : String(error)
@@ -11050,7 +11626,7 @@ function readString10(payload, key) {
11050
11626
 
11051
11627
  // src/conversations/history-builder.ts
11052
11628
  import { readFile as readFile10, stat as stat10 } from "fs/promises";
11053
- import path15 from "path";
11629
+ import path16 from "path";
11054
11630
  var HISTORY_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
11055
11631
  var HERMES_HISTORY_COLUMNS = [
11056
11632
  "role",
@@ -11111,13 +11687,13 @@ async function readHermesTranscriptHistory(sessionId, profileName) {
11111
11687
  }
11112
11688
  const normalizedProfileName = isValidProfileName2(profileName) ? profileName : "default";
11113
11689
  const profileDir = resolveHermesProfileDir(normalizedProfileName);
11114
- const dbPath = path15.join(profileDir, "state.db");
11690
+ const dbPath = path16.join(profileDir, "state.db");
11115
11691
  const sessionsDirConfig = await readHermesSessionsDir(normalizedProfileName).then((value) => ({
11116
11692
  sessionsDir: value.sessionsDir,
11117
11693
  configured: value.configured,
11118
11694
  configError: false
11119
11695
  })).catch(() => ({
11120
- sessionsDir: path15.join(profileDir, "sessions"),
11696
+ sessionsDir: path16.join(profileDir, "sessions"),
11121
11697
  configured: false,
11122
11698
  configError: true
11123
11699
  }));
@@ -11175,7 +11751,7 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
11175
11751
  if (!isValidSessionFileStem(sessionId)) {
11176
11752
  return empty;
11177
11753
  }
11178
- const transcriptPath = path15.join(sessionsDir, `${sessionId}.jsonl`);
11754
+ const transcriptPath = path16.join(sessionsDir, `${sessionId}.jsonl`);
11179
11755
  const raw = await readFile10(transcriptPath, "utf8").catch((error) => {
11180
11756
  if (isNodeError12(error, "ENOENT")) {
11181
11757
  return "";
@@ -11418,7 +11994,7 @@ function normalizeProfileForCompare(value) {
11418
11994
  // src/hermes/stt.ts
11419
11995
  import { execFile as execFile3 } from "child_process";
11420
11996
  import { access as access2, readFile as readFile11, stat as stat11 } from "fs/promises";
11421
- import path16 from "path";
11997
+ import path17 from "path";
11422
11998
  import { promisify as promisify3 } from "util";
11423
11999
  var execFileAsync3 = promisify3(execFile3);
11424
12000
  var STT_RESULT_PREFIX = "__HERMES_LINK_STT__";
@@ -11513,7 +12089,7 @@ async function buildHermesSttEnv(profileName) {
11513
12089
  };
11514
12090
  const devSource = await findDevHermesAgentSource();
11515
12091
  if (devSource) {
11516
- env.PYTHONPATH = [devSource, env.PYTHONPATH].filter(Boolean).join(path16.delimiter);
12092
+ env.PYTHONPATH = [devSource, env.PYTHONPATH].filter(Boolean).join(path17.delimiter);
11517
12093
  }
11518
12094
  return env;
11519
12095
  }
@@ -11560,14 +12136,14 @@ async function resolveHermesPythonCommand() {
11560
12136
  };
11561
12137
  }
11562
12138
  async function resolveExecutablePath(command) {
11563
- if (path16.isAbsolute(command)) {
12139
+ if (path17.isAbsolute(command)) {
11564
12140
  return await isExecutableFile(command) ? command : null;
11565
12141
  }
11566
12142
  const pathEnv = process.env.PATH ?? "";
11567
12143
  const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
11568
- for (const dir of pathEnv.split(path16.delimiter)) {
12144
+ for (const dir of pathEnv.split(path17.delimiter)) {
11569
12145
  for (const extension of extensions) {
11570
- const candidate = path16.join(dir, `${command}${extension}`);
12146
+ const candidate = path17.join(dir, `${command}${extension}`);
11571
12147
  if (await isExecutableFile(candidate)) {
11572
12148
  return candidate;
11573
12149
  }
@@ -11607,8 +12183,8 @@ function shebangToPythonCommand(shebang) {
11607
12183
  }
11608
12184
  async function findDevHermesAgentSource() {
11609
12185
  const candidates = [
11610
- path16.resolve(process.cwd(), "reference/hermes-agent"),
11611
- path16.resolve(process.cwd(), "../../reference/hermes-agent")
12186
+ path17.resolve(process.cwd(), "reference/hermes-agent"),
12187
+ path17.resolve(process.cwd(), "../../reference/hermes-agent")
11612
12188
  ];
11613
12189
  for (const candidate of candidates) {
11614
12190
  if (await isDirectory(candidate)) {
@@ -13249,7 +13825,7 @@ var ConversationService = class {
13249
13825
  async createConversation(input = {}) {
13250
13826
  await this.store.ensureConversationsDir();
13251
13827
  const now = (/* @__PURE__ */ new Date()).toISOString();
13252
- const id = `conv_${randomUUID7().replaceAll("-", "")}`;
13828
+ const id = `conv_${randomUUID8().replaceAll("-", "")}`;
13253
13829
  const title = input.title?.trim() || DEFAULT_CONVERSATION_TITLE;
13254
13830
  const profile = await resolveConversationProfileTarget(
13255
13831
  this.paths,
@@ -13336,7 +13912,7 @@ var ConversationService = class {
13336
13912
  manifest.profile_name_snapshot ?? manifest.profile ?? input.profileName
13337
13913
  );
13338
13914
  const message = {
13339
- id: `msg_${randomUUID7().replaceAll("-", "")}`,
13915
+ id: `msg_${randomUUID8().replaceAll("-", "")}`,
13340
13916
  schema_version: 1,
13341
13917
  conversation_id: manifest.id,
13342
13918
  role: "assistant",
@@ -13800,6 +14376,18 @@ var ConversationService = class {
13800
14376
  async deleteConversation(conversationId) {
13801
14377
  return this.maintenance.deleteConversation(conversationId);
13802
14378
  }
14379
+ prepareClearAllConversationPlan() {
14380
+ return this.maintenance.prepareClearAllConversationPlan();
14381
+ }
14382
+ readClearAllConversationPlan(planId) {
14383
+ return this.maintenance.readClearAllConversationPlan(planId);
14384
+ }
14385
+ executeClearAllConversationPlan(planId) {
14386
+ return this.maintenance.executeClearAllConversationPlan(planId);
14387
+ }
14388
+ startClearAllConversationPlan(planId) {
14389
+ return this.maintenance.startClearAllConversationPlan(planId);
14390
+ }
13803
14391
  async deleteConversations(conversationIds) {
13804
14392
  return this.maintenance.deleteConversations(conversationIds);
13805
14393
  }
@@ -13928,8 +14516,8 @@ function findApproval(snapshot, approvalId) {
13928
14516
  }
13929
14517
 
13930
14518
  // src/identity/identity.ts
13931
- import { generateKeyPairSync, randomUUID as randomUUID8, sign } from "crypto";
13932
- import { mkdir as mkdir8, chmod as chmod2 } from "fs/promises";
14519
+ import { generateKeyPairSync, randomUUID as randomUUID9, sign } from "crypto";
14520
+ import { mkdir as mkdir9, chmod as chmod2 } from "fs/promises";
13933
14521
  import { z } from "zod";
13934
14522
  var linkIdentitySchema = z.object({
13935
14523
  install_id: z.string().min(1),
@@ -13951,12 +14539,12 @@ async function ensureIdentity(paths = resolveRuntimePaths()) {
13951
14539
  if (existing) {
13952
14540
  return existing;
13953
14541
  }
13954
- await mkdir8(paths.homeDir, { recursive: true, mode: 448 });
14542
+ await mkdir9(paths.homeDir, { recursive: true, mode: 448 });
13955
14543
  await chmod2(paths.homeDir, 448).catch(() => void 0);
13956
14544
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
13957
14545
  const now = (/* @__PURE__ */ new Date()).toISOString();
13958
14546
  const identity = {
13959
- install_id: `install_${randomUUID8().replaceAll("-", "")}`,
14547
+ install_id: `install_${randomUUID9().replaceAll("-", "")}`,
13960
14548
  link_id: null,
13961
14549
  public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
13962
14550
  private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
@@ -13993,7 +14581,7 @@ function getIdentityStatus(identity) {
13993
14581
  }
13994
14582
 
13995
14583
  // src/security/devices.ts
13996
- import { randomBytes as randomBytes2, randomUUID as randomUUID9, timingSafeEqual, createHash as createHash4 } from "crypto";
14584
+ import { randomBytes as randomBytes2, randomUUID as randomUUID10, timingSafeEqual, createHash as createHash4 } from "crypto";
13997
14585
  var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
13998
14586
  var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
13999
14587
  var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
@@ -14011,7 +14599,7 @@ async function createDeviceSession(input, paths = resolveRuntimePaths()) {
14011
14599
  }
14012
14600
  }
14013
14601
  const device = {
14014
- id: `dev_${randomUUID9().replaceAll("-", "")}`,
14602
+ id: `dev_${randomUUID10().replaceAll("-", "")}`,
14015
14603
  label: normalizeDeviceLabel(input.label),
14016
14604
  platform: normalizeDevicePlatform(input.platform),
14017
14605
  model: normalizeDeviceModel(input.model),
@@ -14925,6 +15513,37 @@ function registerConversationRoutes(router, options) {
14925
15513
  await authenticateRequest(ctx, paths);
14926
15514
  ctx.body = { ok: true };
14927
15515
  });
15516
+ router.post("/api/v1/conversations/clear-plans", async (ctx) => {
15517
+ await authenticateRequest(ctx, paths);
15518
+ const plan = await conversations.prepareClearAllConversationPlan();
15519
+ ctx.status = 201;
15520
+ ctx.body = {
15521
+ ok: true,
15522
+ plan
15523
+ };
15524
+ });
15525
+ router.get("/api/v1/conversations/clear-plans/:planId", async (ctx) => {
15526
+ await authenticateRequest(ctx, paths);
15527
+ ctx.set("cache-control", "no-store");
15528
+ ctx.body = {
15529
+ ok: true,
15530
+ plan: await conversations.readClearAllConversationPlan(ctx.params.planId)
15531
+ };
15532
+ });
15533
+ router.post(
15534
+ "/api/v1/conversations/clear-plans/:planId/execute",
15535
+ async (ctx) => {
15536
+ await authenticateRequest(ctx, paths);
15537
+ const plan = await conversations.startClearAllConversationPlan(
15538
+ ctx.params.planId
15539
+ );
15540
+ ctx.status = plan.status === "completed" ? 200 : 202;
15541
+ ctx.body = {
15542
+ ok: true,
15543
+ plan
15544
+ };
15545
+ }
15546
+ );
14928
15547
  router.delete("/api/v1/conversations", async (ctx) => {
14929
15548
  await authenticateRequest(ctx, paths);
14930
15549
  const body = await readJsonBody(ctx.req);
@@ -15147,15 +15766,14 @@ function createHttpErrorMiddleware(logger) {
15147
15766
 
15148
15767
  // src/hermes/profiles.ts
15149
15768
  import { readdir as readdir9, readFile as readFile12, rename as rename3, rm as rm6, stat as stat12 } from "fs/promises";
15150
- import os5 from "os";
15151
- import path17 from "path";
15769
+ import path18 from "path";
15152
15770
  import YAML2 from "yaml";
15153
15771
  var DEFAULT_PROFILE = "default";
15154
15772
  var PROFILE_NAME_PATTERN4 = /^[a-zA-Z0-9._-]{1,64}$/;
15155
15773
  async function listHermesProfiles(paths = resolveRuntimePaths()) {
15156
15774
  const profiles = /* @__PURE__ */ new Map();
15157
15775
  profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
15158
- const profilesDir = path17.join(os5.homedir(), ".hermes", "profiles");
15776
+ const profilesDir = resolveHermesProfilesDir();
15159
15777
  const entries = await readdir9(profilesDir, { withFileTypes: true }).catch(
15160
15778
  (error) => {
15161
15779
  if (isNodeError14(error, "ENOENT")) {
@@ -15250,7 +15868,7 @@ async function readHermesProfileCapabilities(name) {
15250
15868
  return {
15251
15869
  defaultModel: listedModels?.defaultModel ?? null,
15252
15870
  modelCount: listedModels?.models.length ?? 0,
15253
- skillCount: await countSkills(path17.join(profileDir, "skills")).catch(
15871
+ skillCount: await countSkills(path18.join(profileDir, "skills")).catch(
15254
15872
  () => 0
15255
15873
  ),
15256
15874
  toolCount: await countConfiguredTools(name).catch(() => 0)
@@ -15306,7 +15924,7 @@ async function countSkills(root) {
15306
15924
  );
15307
15925
  let count = 0;
15308
15926
  for (const entry of entries) {
15309
- const entryPath = path17.join(root, entry.name);
15927
+ const entryPath = path18.join(root, entry.name);
15310
15928
  if (entry.name === ".git" || entry.name === ".hub") {
15311
15929
  continue;
15312
15930
  }
@@ -16003,12 +16621,12 @@ import { spawn as spawn2 } from "child_process";
16003
16621
  import { EventEmitter as EventEmitter2 } from "events";
16004
16622
  import {
16005
16623
  cp,
16006
- mkdir as mkdir9,
16624
+ mkdir as mkdir10,
16007
16625
  readFile as readFile13,
16008
16626
  rm as rm7,
16009
16627
  stat as stat13
16010
16628
  } from "fs/promises";
16011
- import path18 from "path";
16629
+ import path19 from "path";
16012
16630
  import YAML3 from "yaml";
16013
16631
  var PROFILE_CREATE_LOG_FILE = "profile-create.log";
16014
16632
  var PROFILE_CREATE_LOG_MAX_FILES = 3;
@@ -16056,7 +16674,7 @@ async function startHermesProfileCreation(input, options) {
16056
16674
  signal: null,
16057
16675
  error: null
16058
16676
  };
16059
- await mkdir9(options.paths.runDir, { recursive: true, mode: 448 });
16677
+ await mkdir10(options.paths.runDir, { recursive: true, mode: 448 });
16060
16678
  await writeProfileCreationState(options.paths, started);
16061
16679
  await writer.write(`
16062
16680
  === profile creation started ${startedAt} ===
@@ -16459,7 +17077,7 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
16459
17077
  return keys;
16460
17078
  }
16461
17079
  async function writeEnvValues(profileName, values) {
16462
- const envPath = path18.join(resolveHermesProfileDir(profileName), ".env");
17080
+ const envPath = path19.join(resolveHermesProfileDir(profileName), ".env");
16463
17081
  const existingRaw = await readFile13(envPath, "utf8").catch((error) => {
16464
17082
  if (isNodeError15(error, "ENOENT")) {
16465
17083
  return "";
@@ -16496,8 +17114,8 @@ async function writeEnvValues(profileName, values) {
16496
17114
  await atomicWriteFilePreservingMetadata(envPath, nextRaw);
16497
17115
  }
16498
17116
  async function copySkills(sourceProfile, targetProfile) {
16499
- const sourceSkills = path18.join(resolveHermesProfileDir(sourceProfile), "skills");
16500
- const targetSkills = path18.join(resolveHermesProfileDir(targetProfile), "skills");
17117
+ const sourceSkills = path19.join(resolveHermesProfileDir(sourceProfile), "skills");
17118
+ const targetSkills = path19.join(resolveHermesProfileDir(targetProfile), "skills");
16501
17119
  if (!await pathExists(sourceSkills)) {
16502
17120
  return;
16503
17121
  }
@@ -16592,10 +17210,10 @@ async function readProfileCreationLogLines(paths) {
16592
17210
  );
16593
17211
  }
16594
17212
  function profileCreationStatePath(paths) {
16595
- return path18.join(paths.runDir, "profile-create-state.json");
17213
+ return path19.join(paths.runDir, "profile-create-state.json");
16596
17214
  }
16597
17215
  function profileCreationLogPath(paths) {
16598
- return path18.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
17216
+ return path19.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
16599
17217
  }
16600
17218
  async function clearProfileCreationLogFiles(paths) {
16601
17219
  const primary = profileCreationLogPath(paths);
@@ -16869,7 +17487,7 @@ import {
16869
17487
  readFile as readFile14,
16870
17488
  stat as stat14
16871
17489
  } from "fs/promises";
16872
- import path19 from "path";
17490
+ import path20 from "path";
16873
17491
  import YAML4 from "yaml";
16874
17492
  var ENTRY_DELIMITER = "\n\xA7\n";
16875
17493
  var DEFAULT_MEMORY_LIMIT = 2200;
@@ -17086,7 +17704,7 @@ async function saveProviderSettings(profileName, provider, patch) {
17086
17704
  if (provider === "hindsight") {
17087
17705
  await patchJsonProviderConfig(
17088
17706
  profileName,
17089
- path19.join("hindsight", "config.json"),
17707
+ path20.join("hindsight", "config.json"),
17090
17708
  {
17091
17709
  mode: patch.mode,
17092
17710
  api_url: patch.apiUrl,
@@ -17253,7 +17871,7 @@ async function patchHermesMemoryProvider(profileName, provider) {
17253
17871
  await atomicWriteFilePreservingMetadata(configPath, document.toString());
17254
17872
  }
17255
17873
  function resolveMemoryDir(profileName) {
17256
- return path19.join(resolveHermesProfileDir(profileName), "memories");
17874
+ return path20.join(resolveHermesProfileDir(profileName), "memories");
17257
17875
  }
17258
17876
  async function readMemoryStore(profileName, target, limits) {
17259
17877
  const filePath = memoryFilePath(profileName, target);
@@ -17314,7 +17932,7 @@ async function writeMemoryEntries(profileName, target, entries) {
17314
17932
  );
17315
17933
  }
17316
17934
  function memoryFilePath(profileName, target) {
17317
- return path19.join(
17935
+ return path20.join(
17318
17936
  resolveMemoryDir(profileName),
17319
17937
  target === "user" ? "USER.md" : "MEMORY.md"
17320
17938
  );
@@ -17374,7 +17992,7 @@ async function readCustomProviderSetupSummary(profileName) {
17374
17992
  configurable: true,
17375
17993
  configured: true,
17376
17994
  configurationIssue: null,
17377
- providerConfigPath: path19.join(
17995
+ providerConfigPath: path20.join(
17378
17996
  resolveHermesProfileDir(profileName),
17379
17997
  "<provider>.json"
17380
17998
  ),
@@ -17728,7 +18346,7 @@ async function readProviderSettings(profileName, provider) {
17728
18346
  stringSetting(
17729
18347
  "dbPath",
17730
18348
  "SQLite \u6570\u636E\u5E93\u8DEF\u5F84",
17731
- config.db_path ?? path19.join(resolveHermesProfileDir(profileName), "memory_store.db")
18349
+ config.db_path ?? path20.join(resolveHermesProfileDir(profileName), "memory_store.db")
17732
18350
  ),
17733
18351
  booleanSetting("autoExtract", "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u62BD\u53D6", config.auto_extract ?? false),
17734
18352
  numberSetting("defaultTrust", "\u9ED8\u8BA4\u4FE1\u4EFB\u5206", config.default_trust ?? 0.5),
@@ -17751,7 +18369,7 @@ async function readProviderSettings(profileName, provider) {
17751
18369
  stringSetting(
17752
18370
  "workingDirectory",
17753
18371
  "\u5DE5\u4F5C\u76EE\u5F55",
17754
- path19.join(resolveHermesProfileDir(profileName), "byterover"),
18372
+ path20.join(resolveHermesProfileDir(profileName), "byterover"),
17755
18373
  false
17756
18374
  )
17757
18375
  ];
@@ -17760,16 +18378,16 @@ async function readProviderSettings(profileName, provider) {
17760
18378
  }
17761
18379
  function memoryProviderConfigPath(profileName, provider) {
17762
18380
  if (provider === "honcho") {
17763
- return path19.join(resolveHermesProfileDir(profileName), "honcho.json");
18381
+ return path20.join(resolveHermesProfileDir(profileName), "honcho.json");
17764
18382
  }
17765
18383
  if (provider === "mem0") {
17766
- return path19.join(resolveHermesProfileDir(profileName), "mem0.json");
18384
+ return path20.join(resolveHermesProfileDir(profileName), "mem0.json");
17767
18385
  }
17768
18386
  if (provider === "supermemory") {
17769
- return path19.join(resolveHermesProfileDir(profileName), "supermemory.json");
18387
+ return path20.join(resolveHermesProfileDir(profileName), "supermemory.json");
17770
18388
  }
17771
18389
  if (provider === "hindsight") {
17772
- return path19.join(
18390
+ return path20.join(
17773
18391
  resolveHermesProfileDir(profileName),
17774
18392
  "hindsight",
17775
18393
  "config.json"
@@ -17778,13 +18396,13 @@ function memoryProviderConfigPath(profileName, provider) {
17778
18396
  return null;
17779
18397
  }
17780
18398
  function customProviderConfigPath(profileName, provider) {
17781
- return path19.join(
18399
+ return path20.join(
17782
18400
  resolveHermesProfileDir(profileName),
17783
18401
  `${normalizeCustomProviderId(provider)}.json`
17784
18402
  );
17785
18403
  }
17786
18404
  function customProviderRegistryPath(profileName) {
17787
- return path19.join(
18405
+ return path20.join(
17788
18406
  resolveHermesProfileDir(profileName),
17789
18407
  CUSTOM_PROVIDER_REGISTRY_FILE
17790
18408
  );
@@ -17836,7 +18454,7 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
17836
18454
  );
17837
18455
  }
17838
18456
  async function discoverUserMemoryProviderDescriptors(profileName) {
17839
- const pluginsDir = path19.join(resolveHermesProfileDir(profileName), "plugins");
18457
+ const pluginsDir = path20.join(resolveHermesProfileDir(profileName), "plugins");
17840
18458
  const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
17841
18459
  (error) => {
17842
18460
  if (isNodeError16(error, "ENOENT")) {
@@ -17856,7 +18474,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
17856
18474
  } catch {
17857
18475
  continue;
17858
18476
  }
17859
- const providerDir = path19.join(pluginsDir, entry.name);
18477
+ const providerDir = path20.join(pluginsDir, entry.name);
17860
18478
  if (!await isMemoryProviderPluginDir(providerDir)) {
17861
18479
  continue;
17862
18480
  }
@@ -17870,7 +18488,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
17870
18488
  return descriptors;
17871
18489
  }
17872
18490
  async function isUserMemoryProviderInstalled(profileName, provider) {
17873
- const providerDir = path19.join(
18491
+ const providerDir = path20.join(
17874
18492
  resolveHermesProfileDir(profileName),
17875
18493
  "plugins",
17876
18494
  normalizeCustomProviderId(provider)
@@ -17878,7 +18496,7 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
17878
18496
  return isMemoryProviderPluginDir(providerDir);
17879
18497
  }
17880
18498
  async function isMemoryProviderPluginDir(providerDir) {
17881
- const source = await readFile14(path19.join(providerDir, "__init__.py"), "utf8").catch(
18499
+ const source = await readFile14(path20.join(providerDir, "__init__.py"), "utf8").catch(
17882
18500
  (error) => {
17883
18501
  if (isNodeError16(error, "ENOENT")) {
17884
18502
  return "";
@@ -17890,7 +18508,7 @@ async function isMemoryProviderPluginDir(providerDir) {
17890
18508
  return sample.includes("register_memory_provider") || sample.includes("MemoryProvider");
17891
18509
  }
17892
18510
  async function readPluginMetadata(providerDir) {
17893
- const raw = await readFile14(path19.join(providerDir, "plugin.yaml"), "utf8").catch(
18511
+ const raw = await readFile14(path20.join(providerDir, "plugin.yaml"), "utf8").catch(
17894
18512
  (error) => {
17895
18513
  if (isNodeError16(error, "ENOENT")) {
17896
18514
  return "";
@@ -17902,10 +18520,10 @@ async function readPluginMetadata(providerDir) {
17902
18520
  }
17903
18521
  async function resolveByteRoverCli() {
17904
18522
  const candidates = [
17905
- ...(process.env.PATH ?? "").split(path19.delimiter).filter(Boolean).map((dir) => path19.join(dir, "brv")),
17906
- path19.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
18523
+ ...(process.env.PATH ?? "").split(path20.delimiter).filter(Boolean).map((dir) => path20.join(dir, "brv")),
18524
+ path20.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
17907
18525
  "/usr/local/bin/brv",
17908
- path19.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
18526
+ path20.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
17909
18527
  ].filter(Boolean);
17910
18528
  for (const candidate of candidates) {
17911
18529
  const found = await access3(candidate).then(() => true).catch(() => false);
@@ -17966,7 +18584,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
17966
18584
  if (entries.length === 0) {
17967
18585
  return;
17968
18586
  }
17969
- const envPath = path19.join(resolveHermesProfileDir(profileName), ".env");
18587
+ const envPath = path20.join(resolveHermesProfileDir(profileName), ".env");
17970
18588
  const existingRaw = await readFile14(envPath, "utf8").catch((error) => {
17971
18589
  if (isNodeError16(error, "ENOENT")) {
17972
18590
  return "";
@@ -18040,7 +18658,7 @@ async function readActiveMemoryProvider(profileName) {
18040
18658
  return provider;
18041
18659
  }
18042
18660
  async function patchJsonProviderConfig(profileName, relativePath, patch) {
18043
- const configPath = path19.join(
18661
+ const configPath = path20.join(
18044
18662
  resolveHermesProfileDir(profileName),
18045
18663
  relativePath
18046
18664
  );
@@ -18069,7 +18687,7 @@ async function readJsonObject(filePath) {
18069
18687
  } catch {
18070
18688
  throw new HermesMemoryError(
18071
18689
  "memory_provider_config_invalid",
18072
- `${path19.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
18690
+ `${path20.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
18073
18691
  );
18074
18692
  }
18075
18693
  }
@@ -18615,7 +19233,7 @@ function toMemoryHttpError(error) {
18615
19233
 
18616
19234
  // src/hermes/skills.ts
18617
19235
  import { readFile as readFile15, readdir as readdir11 } from "fs/promises";
18618
- import path20 from "path";
19236
+ import path21 from "path";
18619
19237
  import YAML5 from "yaml";
18620
19238
  var HermesSkillNotFoundError = class extends Error {
18621
19239
  constructor(skillName) {
@@ -18629,7 +19247,7 @@ var EXCLUDED_SKILL_DIRS = /* @__PURE__ */ new Set([".git", ".github", ".hub"]);
18629
19247
  async function listHermesProfileSkills(profileName, paths = resolveRuntimePaths()) {
18630
19248
  const profile = await readExistingProfile(profileName, paths);
18631
19249
  const profileDir = resolveHermesProfileDir(profile.name);
18632
- const skillsRoot = path20.join(profileDir, "skills");
19250
+ const skillsRoot = path21.join(profileDir, "skills");
18633
19251
  const [skillFiles, disabled, provenance] = await Promise.all([
18634
19252
  findSkillFiles(skillsRoot),
18635
19253
  readDisabledSkillNames(resolveHermesConfigPath(profile.name)),
@@ -18720,7 +19338,7 @@ async function collectSkillFiles(directory, results) {
18720
19338
  if (EXCLUDED_SKILL_DIRS.has(entry.name)) {
18721
19339
  continue;
18722
19340
  }
18723
- const entryPath = path20.join(directory, entry.name);
19341
+ const entryPath = path21.join(directory, entry.name);
18724
19342
  if (entry.isDirectory()) {
18725
19343
  await collectSkillFiles(entryPath, results);
18726
19344
  continue;
@@ -18742,10 +19360,10 @@ async function readSkillMetadata(input) {
18742
19360
  if (raw === null) {
18743
19361
  return null;
18744
19362
  }
18745
- const skillDir = path20.dirname(input.skillFile);
19363
+ const skillDir = path21.dirname(input.skillFile);
18746
19364
  const { frontmatter, body } = parseSkillDocument(raw.slice(0, 4e3));
18747
19365
  const name = normalizeSkillName(
18748
- readString16(frontmatter.name) ?? path20.basename(skillDir)
19366
+ readString16(frontmatter.name) ?? path21.basename(skillDir)
18749
19367
  );
18750
19368
  if (!name) {
18751
19369
  return null;
@@ -18764,7 +19382,7 @@ async function readSkillMetadata(input) {
18764
19382
  enabled: !input.disabled.has(name),
18765
19383
  source: provenance.source,
18766
19384
  trust: provenance.trust,
18767
- relativePath: path20.relative(input.skillsRoot, skillDir)
19385
+ relativePath: path21.relative(input.skillsRoot, skillDir)
18768
19386
  };
18769
19387
  }
18770
19388
  function parseSkillDocument(raw) {
@@ -18785,8 +19403,8 @@ function parseSkillDocument(raw) {
18785
19403
  }
18786
19404
  }
18787
19405
  function categoryFromPath(skillsRoot, skillFile) {
18788
- const relative = path20.relative(skillsRoot, skillFile);
18789
- const parts = relative.split(path20.sep).filter(Boolean);
19406
+ const relative = path21.relative(skillsRoot, skillFile);
19407
+ const parts = relative.split(path21.sep).filter(Boolean);
18790
19408
  return parts.length >= 3 ? parts[0] : null;
18791
19409
  }
18792
19410
  function firstBodyDescription(body) {
@@ -18833,7 +19451,7 @@ async function readSkillProvenance(root) {
18833
19451
  return provenance;
18834
19452
  }
18835
19453
  async function readBundledSkillNames(root) {
18836
- const raw = await readFile15(path20.join(root, ".bundled_manifest"), "utf8").catch(
19454
+ const raw = await readFile15(path21.join(root, ".bundled_manifest"), "utf8").catch(
18837
19455
  (error) => {
18838
19456
  if (isNodeError17(error, "ENOENT")) {
18839
19457
  return "";
@@ -18856,7 +19474,7 @@ async function readBundledSkillNames(root) {
18856
19474
  return names;
18857
19475
  }
18858
19476
  async function readHubInstalledSkills(root) {
18859
- const raw = await readFile15(path20.join(root, ".hub", "lock.json"), "utf8").catch(
19477
+ const raw = await readFile15(path21.join(root, ".hub", "lock.json"), "utf8").catch(
18860
19478
  (error) => {
18861
19479
  if (isNodeError17(error, "ENOENT")) {
18862
19480
  return "";
@@ -19459,8 +20077,8 @@ function readModelList(payload) {
19459
20077
  // src/hermes/updates.ts
19460
20078
  import { EventEmitter as EventEmitter3 } from "events";
19461
20079
  import { spawn as spawn3 } from "child_process";
19462
- import { mkdir as mkdir10, readFile as readFile16, rm as rm8 } from "fs/promises";
19463
- import path21 from "path";
20080
+ import { mkdir as mkdir11, readFile as readFile16, rm as rm8 } from "fs/promises";
20081
+ import path22 from "path";
19464
20082
  var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
19465
20083
  var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
19466
20084
  var RELEASE_FETCH_TIMEOUT_MS = 5e3;
@@ -19523,7 +20141,7 @@ async function startHermesUpdate(options) {
19523
20141
  signal: null,
19524
20142
  error: null
19525
20143
  };
19526
- await mkdir10(options.paths.runDir, { recursive: true, mode: 448 });
20144
+ await mkdir11(options.paths.runDir, { recursive: true, mode: 448 });
19527
20145
  await writer.write(`
19528
20146
  === hermes update started ${startedAt} ===
19529
20147
  `);
@@ -19729,13 +20347,13 @@ async function readUpdateLogLines(paths) {
19729
20347
  );
19730
20348
  }
19731
20349
  function releaseCachePath(paths) {
19732
- return path21.join(paths.indexesDir, "hermes-release-check.json");
20350
+ return path22.join(paths.indexesDir, "hermes-release-check.json");
19733
20351
  }
19734
20352
  function updateStatePath(paths) {
19735
- return path21.join(paths.runDir, "hermes-update-state.json");
20353
+ return path22.join(paths.runDir, "hermes-update-state.json");
19736
20354
  }
19737
20355
  function updateLogPath(paths) {
19738
- return path21.join(paths.logsDir, UPDATE_LOG_FILE);
20356
+ return path22.join(paths.logsDir, UPDATE_LOG_FILE);
19739
20357
  }
19740
20358
  async function clearUpdateLogFiles(paths) {
19741
20359
  const primary = updateLogPath(paths);
@@ -19827,17 +20445,17 @@ function readString17(payload, key) {
19827
20445
  // src/link/updates.ts
19828
20446
  import { spawn as spawn5 } from "child_process";
19829
20447
  import { EventEmitter as EventEmitter4 } from "events";
19830
- import { mkdir as mkdir13, readFile as readFile18, rm as rm11 } from "fs/promises";
19831
- import path23 from "path";
20448
+ import { mkdir as mkdir14, readFile as readFile18, rm as rm11 } from "fs/promises";
20449
+ import path24 from "path";
19832
20450
 
19833
20451
  // src/daemon/process.ts
19834
20452
  import { spawn as spawn4 } from "child_process";
19835
- import { mkdir as mkdir12, readFile as readFile17, rm as rm10 } from "fs/promises";
19836
- import path22 from "path";
20453
+ import { mkdir as mkdir13, readFile as readFile17, rm as rm10 } from "fs/promises";
20454
+ import path23 from "path";
19837
20455
 
19838
20456
  // src/daemon/service.ts
19839
20457
  import { createServer } from "http";
19840
- import { mkdir as mkdir11, rm as rm9, writeFile as writeFile3 } from "fs/promises";
20458
+ import { mkdir as mkdir12, rm as rm9, writeFile as writeFile3 } from "fs/promises";
19841
20459
 
19842
20460
  // src/relay/control-client.ts
19843
20461
  import WebSocket from "ws";
@@ -20011,7 +20629,7 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
20011
20629
  // src/runtime/system-info.ts
20012
20630
  import { execFileSync } from "child_process";
20013
20631
  import { readFileSync } from "fs";
20014
- import os6 from "os";
20632
+ import os4 from "os";
20015
20633
  function readLinkSystemInfo() {
20016
20634
  const platform = process.platform;
20017
20635
  const hostname = readHostname(platform);
@@ -20050,7 +20668,7 @@ function readHostname(platform) {
20050
20668
  return computerName;
20051
20669
  }
20052
20670
  }
20053
- return normalizeText(os6.hostname());
20671
+ return normalizeText(os4.hostname());
20054
20672
  }
20055
20673
  function readOsLabel(platform) {
20056
20674
  if (platform === "darwin") {
@@ -20058,12 +20676,12 @@ function readOsLabel(platform) {
20058
20676
  return version ? `macOS ${version}` : "macOS";
20059
20677
  }
20060
20678
  if (platform === "linux") {
20061
- return readLinuxOsRelease() ?? `Linux ${os6.release()}`;
20679
+ return readLinuxOsRelease() ?? `Linux ${os4.release()}`;
20062
20680
  }
20063
20681
  if (platform === "win32") {
20064
- return `Windows ${os6.release()}`;
20682
+ return `Windows ${os4.release()}`;
20065
20683
  }
20066
- return `${os6.type()} ${os6.release()}`.trim();
20684
+ return `${os4.type()} ${os4.release()}`.trim();
20067
20685
  }
20068
20686
  function readLinuxOsRelease() {
20069
20687
  for (const file of ["/etc/os-release", "/usr/lib/os-release"]) {
@@ -20110,11 +20728,11 @@ function truncateText(value, maxLength) {
20110
20728
  }
20111
20729
 
20112
20730
  // src/topology/network.ts
20113
- import os8 from "os";
20731
+ import os6 from "os";
20114
20732
 
20115
20733
  // src/topology/environment.ts
20116
20734
  import { existsSync, readFileSync as readFileSync2 } from "fs";
20117
- import os7 from "os";
20735
+ import os5 from "os";
20118
20736
  function detectRuntimeEnvironment(env = process.env) {
20119
20737
  if (isWsl(env)) {
20120
20738
  return {
@@ -20143,7 +20761,7 @@ function isWsl(env) {
20143
20761
  if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
20144
20762
  return true;
20145
20763
  }
20146
- const release = os7.release().toLowerCase();
20764
+ const release = os5.release().toLowerCase();
20147
20765
  return release.includes("microsoft") || release.includes("wsl");
20148
20766
  }
20149
20767
  function isContainer(env) {
@@ -20188,7 +20806,7 @@ async function discoverRouteCandidates(options) {
20188
20806
  };
20189
20807
  }
20190
20808
  function discoverLanIps() {
20191
- return discoverLanIpsFromInterfaces(os8.networkInterfaces());
20809
+ return discoverLanIpsFromInterfaces(os6.networkInterfaces());
20192
20810
  }
20193
20811
  function discoverLanIpsFromInterfaces(interfaces) {
20194
20812
  const result = /* @__PURE__ */ new Set();
@@ -20716,6 +21334,7 @@ function startHermesSessionSyncScheduler(options) {
20716
21334
  }
20717
21335
 
20718
21336
  // src/daemon/service.ts
21337
+ var DEFAULT_RELAY_READY_TIMEOUT_MS = 2e3;
20719
21338
  async function startLinkService(options = {}) {
20720
21339
  const paths = options.paths ?? resolveRuntimePaths();
20721
21340
  const logger = createFileLogger({ paths });
@@ -20781,6 +21400,10 @@ async function startLinkService(options = {}) {
20781
21400
  });
20782
21401
  let relay = null;
20783
21402
  if (identity?.link_id) {
21403
+ let resolveRelayReady = null;
21404
+ const relayReady = new Promise((resolve) => {
21405
+ resolveRelayReady = resolve;
21406
+ });
20784
21407
  relay = connectRelayControl({
20785
21408
  relayBaseUrl: config.relayBaseUrl,
20786
21409
  linkId: identity.link_id,
@@ -20790,8 +21413,22 @@ async function startLinkService(options = {}) {
20790
21413
  backoffMaxMs: 3e4,
20791
21414
  onStatus: (status) => {
20792
21415
  void logger.info("relay_status", status);
21416
+ if (status.state === "connected") {
21417
+ resolveRelayReady?.(true);
21418
+ resolveRelayReady = null;
21419
+ } else if (status.state === "failed") {
21420
+ resolveRelayReady?.(false);
21421
+ resolveRelayReady = null;
21422
+ }
20793
21423
  }
20794
21424
  });
21425
+ if (options.waitForRelayReady) {
21426
+ await Promise.race([
21427
+ relayReady,
21428
+ waitForRelayReadyTimeout(options.relayReadyTimeoutMs)
21429
+ ]);
21430
+ resolveRelayReady = null;
21431
+ }
20795
21432
  } else {
20796
21433
  void logger.info("relay_skipped", { reason: "link_not_paired" });
20797
21434
  }
@@ -20826,11 +21463,20 @@ async function startLinkService(options = {}) {
20826
21463
  }
20827
21464
  };
20828
21465
  }
21466
+ function waitForRelayReadyTimeout(timeoutMs) {
21467
+ return new Promise((resolve) => {
21468
+ const timer = setTimeout(
21469
+ () => resolve(false),
21470
+ timeoutMs ?? DEFAULT_RELAY_READY_TIMEOUT_MS
21471
+ );
21472
+ timer.unref?.();
21473
+ });
21474
+ }
20829
21475
  function pidFilePath(paths = resolveRuntimePaths()) {
20830
21476
  return `${paths.runDir}/hermeslink.pid`;
20831
21477
  }
20832
21478
  async function writePidFile(paths) {
20833
- await mkdir11(paths.runDir, { recursive: true, mode: 448 });
21479
+ await mkdir12(paths.runDir, { recursive: true, mode: 448 });
20834
21480
  await writeFile3(pidFilePath(paths), `${process.pid}
20835
21481
  `, { mode: 384 });
20836
21482
  }
@@ -20905,8 +21551,8 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
20905
21551
  return status;
20906
21552
  }
20907
21553
  }
20908
- await mkdir12(paths.logsDir, { recursive: true, mode: 448 });
20909
- await mkdir12(paths.runDir, { recursive: true, mode: 448 });
21554
+ await mkdir13(paths.logsDir, { recursive: true, mode: 448 });
21555
+ await mkdir13(paths.runDir, { recursive: true, mode: 448 });
20910
21556
  const scriptPath = currentCliScriptPath();
20911
21557
  const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
20912
21558
  detached: true,
@@ -20924,10 +21570,10 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
20924
21570
  return await getDaemonStatus(paths);
20925
21571
  }
20926
21572
  async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
20927
- await mkdir12(paths.logsDir, { recursive: true, mode: 448 });
21573
+ await mkdir13(paths.logsDir, { recursive: true, mode: 448 });
20928
21574
  const log = createRotatingTextLogWriter({
20929
21575
  paths,
20930
- fileName: path22.basename(daemonLogFile(paths))
21576
+ fileName: path23.basename(daemonLogFile(paths))
20931
21577
  });
20932
21578
  const scriptPath = currentCliScriptPath();
20933
21579
  const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
@@ -21168,7 +21814,7 @@ async function startLinkUpdate(options) {
21168
21814
  error: null,
21169
21815
  manual_command: manualCommand
21170
21816
  };
21171
- await mkdir13(options.paths.runDir, { recursive: true, mode: 448 });
21817
+ await mkdir14(options.paths.runDir, { recursive: true, mode: 448 });
21172
21818
  await writer.write(
21173
21819
  `
21174
21820
  === link update started ${startedAt} target=${targetVersion} ===
@@ -21466,10 +22112,10 @@ async function readUpdateLogLines2(paths) {
21466
22112
  );
21467
22113
  }
21468
22114
  function updateStatePath2(paths) {
21469
- return path23.join(paths.runDir, "link-update-state.json");
22115
+ return path24.join(paths.runDir, "link-update-state.json");
21470
22116
  }
21471
22117
  function updateLogPath2(paths) {
21472
- return path23.join(paths.logsDir, UPDATE_LOG_FILE2);
22118
+ return path24.join(paths.logsDir, UPDATE_LOG_FILE2);
21473
22119
  }
21474
22120
  async function clearUpdateLogFiles2(paths) {
21475
22121
  const primary = updateLogPath2(paths);
@@ -21533,7 +22179,7 @@ function readString18(payload, key) {
21533
22179
  }
21534
22180
 
21535
22181
  // src/pairing/pairing.ts
21536
- import path24 from "path";
22182
+ import path25 from "path";
21537
22183
  import { rm as rm12 } from "fs/promises";
21538
22184
 
21539
22185
  // src/relay/bootstrap.ts
@@ -21767,6 +22413,10 @@ async function readPairingSession(sessionId, paths = resolveRuntimePaths()) {
21767
22413
  expires_at: record.expires_at
21768
22414
  };
21769
22415
  }
22416
+ function isPairingSessionExpired(session) {
22417
+ const expiresAtMs = Date.parse(session.expires_at);
22418
+ return !Number.isFinite(expiresAtMs) || Date.now() >= expiresAtMs;
22419
+ }
21770
22420
  async function recordPairingClaim(input, paths = resolveRuntimePaths()) {
21771
22421
  const record = {
21772
22422
  session_id: input.sessionId,
@@ -21796,7 +22446,20 @@ async function clearPairingClaim(sessionId, paths = resolveRuntimePaths()) {
21796
22446
  }
21797
22447
  async function claimPairing(input) {
21798
22448
  const paths = input.paths ?? resolveRuntimePaths();
21799
- const [identity, config] = await Promise.all([loadRequiredIdentity2(paths), loadConfig(paths)]);
22449
+ const [identity, config, localSession] = await Promise.all([
22450
+ loadRequiredIdentity2(paths),
22451
+ loadConfig(paths),
22452
+ readPairingSession(input.sessionId, paths)
22453
+ ]);
22454
+ if (!localSession) {
22455
+ throw new LinkHttpError(404, "pairing_session_not_found", "Pairing session was not found");
22456
+ }
22457
+ if (isPairingSessionExpired(localSession)) {
22458
+ throw new LinkHttpError(404, "pairing_session_expired", "Pairing session has expired");
22459
+ }
22460
+ if (localSession.link_id !== identity.link_id) {
22461
+ throw new LinkHttpError(409, "pairing_claim_mismatch", "Pairing claim does not match this Link");
22462
+ }
21800
22463
  let verified;
21801
22464
  try {
21802
22465
  verified = await postServerJson(
@@ -21856,10 +22519,10 @@ async function loadRequiredIdentity2(paths) {
21856
22519
  }
21857
22520
  return identity;
21858
22521
  }
21859
- async function postServerJson(serverBaseUrl, path25, body, options) {
22522
+ async function postServerJson(serverBaseUrl, path26, body, options) {
21860
22523
  let response;
21861
22524
  try {
21862
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
22525
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
21863
22526
  method: "POST",
21864
22527
  headers: {
21865
22528
  accept: "application/json",
@@ -21907,10 +22570,10 @@ function pairingErrorSnapshot(stage, error) {
21907
22570
  occurred_at: (/* @__PURE__ */ new Date()).toISOString()
21908
22571
  };
21909
22572
  }
21910
- async function patchServerJson(serverBaseUrl, path25, token, body, options) {
22573
+ async function patchServerJson(serverBaseUrl, path26, token, body, options) {
21911
22574
  let response;
21912
22575
  try {
21913
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
22576
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
21914
22577
  method: "PATCH",
21915
22578
  headers: {
21916
22579
  accept: "application/json",
@@ -21958,10 +22621,10 @@ function createPairingNetworkError(input) {
21958
22621
  );
21959
22622
  }
21960
22623
  function pairingClaimPath(sessionId, paths) {
21961
- return path24.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
22624
+ return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
21962
22625
  }
21963
22626
  function pairingSessionPath(sessionId, paths) {
21964
- return path24.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
22627
+ return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
21965
22628
  }
21966
22629
  function qrPreferredUrls(routes) {
21967
22630
  return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
@@ -22015,6 +22678,7 @@ function registerSystemRoutes(router, options) {
22015
22678
  conversation_events: true,
22016
22679
  conversation_delete: true,
22017
22680
  conversation_bulk_delete: true,
22681
+ conversation_clear_plan: true,
22018
22682
  conversation_cancel: true,
22019
22683
  conversation_rename: true,
22020
22684
  blobs: true,
@@ -22608,12 +23272,16 @@ function registerPairingRoutes(router, options) {
22608
23272
  if (!session) {
22609
23273
  throw new LinkHttpError(404, "pairing_session_not_found", "Pairing session was not found");
22610
23274
  }
23275
+ const state = await readPairingState(sessionId, paths);
23276
+ if (!state.claimed && isPairingSessionExpired(session)) {
23277
+ throw new LinkHttpError(404, "pairing_session_expired", "Pairing session has expired");
23278
+ }
22611
23279
  ctx.set("cache-control", "no-store");
22612
23280
  ctx.body = {
22613
23281
  ok: true,
22614
23282
  session: {
22615
23283
  ...session,
22616
- claimed: Boolean(await readPairingState(sessionId, paths).then((state) => state.claimed))
23284
+ claimed: state.claimed
22617
23285
  }
22618
23286
  };
22619
23287
  });
@@ -22629,6 +23297,8 @@ async function readPairingState(sessionId, paths) {
22629
23297
  }
22630
23298
  async function renderPairingPage(input) {
22631
23299
  const session = input.session;
23300
+ const expiresAtMs = Date.parse(session.expires_at);
23301
+ const isExpired = !input.state.claimed && isPairingSessionExpired(session);
22632
23302
  const qrPayload = JSON.stringify({
22633
23303
  kind: "hermes_link_pairing",
22634
23304
  version: 1,
@@ -22648,8 +23318,9 @@ async function renderPairingPage(input) {
22648
23318
  const currentUrl = session.local_api_url.replace(/\/+$/u, "");
22649
23319
  const linkIdLabel = escapeHtml(input.linkId ?? session.link_id);
22650
23320
  const expiresLabel = escapeHtml(formatDate(session.expires_at));
22651
- const statusLabel = input.state.claimed ? "\u5DF2\u5B8C\u6210\u914D\u5BF9" : "\u7B49\u5F85 App \u626B\u7801";
22652
- 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";
23321
+ const statusLabel = input.state.claimed ? "\u5DF2\u5B8C\u6210\u914D\u5BF9" : isExpired ? "\u914D\u5BF9\u5DF2\u8FC7\u671F" : "\u7B49\u5F85 App \u626B\u7801";
23322
+ 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";
23323
+ const statusPillLabel = input.state.claimed ? "\u5DF2\u626B\u7801" : isExpired ? "\u5DF2\u8FC7\u671F" : "\u7B49\u5F85\u4E2D";
22653
23324
  return `<!doctype html>
22654
23325
  <html lang="zh-CN">
22655
23326
  <head>
@@ -22954,7 +23625,7 @@ async function renderPairingPage(input) {
22954
23625
  <div class="card">
22955
23626
  <div class="status">
22956
23627
  <h2 class="status-title" id="statusTitle">${escapeHtml(statusLabel)}</h2>
22957
- <span class="pill" id="statusPill">${input.state.claimed ? "\u5DF2\u626B\u7801" : "\u7B49\u5F85\u4E2D"}</span>
23628
+ <span class="pill" id="statusPill">${escapeHtml(statusPillLabel)}</span>
22958
23629
  </div>
22959
23630
  <div class="qr-frame">
22960
23631
  <img src="${qrDataUri}" alt="Hermes Link pairing QR code" />
@@ -22971,22 +23642,62 @@ async function renderPairingPage(input) {
22971
23642
  </main>
22972
23643
  <script>
22973
23644
  const sessionId = ${JSON.stringify(session.session_id)};
23645
+ const expiresAtMs = ${Number.isFinite(expiresAtMs) ? String(expiresAtMs) : "Number.NaN"};
23646
+ const initialClaimed = ${JSON.stringify(input.state.claimed)};
23647
+ const statusTitle = document.querySelector('#statusTitle');
23648
+ const statusPill = document.querySelector('#statusPill');
23649
+ const statusHint = document.querySelector('#statusHint');
23650
+ let refreshTimer = null;
23651
+
23652
+ const stopPolling = () => {
23653
+ if (refreshTimer !== null) {
23654
+ clearInterval(refreshTimer);
23655
+ refreshTimer = null;
23656
+ }
23657
+ };
23658
+
23659
+ const markClaimed = () => {
23660
+ statusTitle.textContent = '\u5DF2\u5B8C\u6210\u914D\u5BF9';
23661
+ statusPill.textContent = '\u5DF2\u626B\u7801';
23662
+ statusHint.textContent = 'App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002';
23663
+ stopPolling();
23664
+ };
23665
+
23666
+ const markExpired = () => {
23667
+ statusTitle.textContent = '\u914D\u5BF9\u5DF2\u8FC7\u671F';
23668
+ statusPill.textContent = '\u5DF2\u8FC7\u671F';
23669
+ statusHint.textContent = '\u8FD9\u6B21\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C hermeslink pair\u3002';
23670
+ stopPolling();
23671
+ };
23672
+
22974
23673
  const refresh = async () => {
23674
+ if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs) {
23675
+ markExpired();
23676
+ return;
23677
+ }
22975
23678
  try {
22976
23679
  const response = await fetch('/api/v1/pairing/session?session_id=' + encodeURIComponent(sessionId), {
22977
23680
  headers: { accept: 'application/json' },
22978
23681
  });
23682
+ if (response.status === 404) {
23683
+ markExpired();
23684
+ return;
23685
+ }
22979
23686
  if (!response.ok) return;
22980
23687
  const payload = await response.json();
22981
23688
  if (payload?.session?.claimed) {
22982
- document.querySelector('#statusPill').textContent = '\u5DF2\u626B\u7801';
22983
- document.querySelector('#statusTitle').textContent = '\u5DF2\u5B8C\u6210\u914D\u5BF9';
22984
- document.querySelector('#statusHint').textContent = 'App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002';
23689
+ markClaimed();
22985
23690
  }
22986
23691
  } catch (_) {}
22987
23692
  };
22988
- setInterval(refresh, 1500);
22989
- refresh();
23693
+ if (initialClaimed) {
23694
+ stopPolling();
23695
+ } else if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs) {
23696
+ markExpired();
23697
+ } else {
23698
+ refreshTimer = window.setInterval(refresh, 1500);
23699
+ refresh();
23700
+ }
22990
23701
  </script>
22991
23702
  </body>
22992
23703
  </html>`;
@@ -23106,6 +23817,7 @@ export {
23106
23817
  readPairingClaim,
23107
23818
  clearPairingClaim,
23108
23819
  createApp,
23820
+ connectRelayControl,
23109
23821
  reportLinkStatusToServer,
23110
23822
  startLinkService,
23111
23823
  startDaemonProcess,