@hermespilot/link 0.4.9 → 0.5.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 randomUUID8 } from "crypto";
7
+ import { randomUUID as randomUUID10 } from "crypto";
8
8
 
9
9
  // src/database/link-database.ts
10
10
  import { mkdir } from "fs/promises";
@@ -150,7 +150,10 @@ var MIGRATIONS = [
150
150
  }
151
151
  ];
152
152
  async function migrateLinkDatabase(paths) {
153
- await mkdir(path.dirname(paths.databaseFile), { recursive: true, mode: 448 });
153
+ await mkdir(path.dirname(paths.databaseFile), {
154
+ recursive: true,
155
+ mode: 448
156
+ });
154
157
  const db = openDatabase(paths);
155
158
  const appliedVersions = [];
156
159
  try {
@@ -169,11 +172,9 @@ async function migrateLinkDatabase(paths) {
169
172
  db.exec("BEGIN IMMEDIATE");
170
173
  try {
171
174
  db.exec(migration.sql);
172
- db.prepare("INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)").run(
173
- migration.version,
174
- migration.name,
175
- (/* @__PURE__ */ new Date()).toISOString()
176
- );
175
+ db.prepare(
176
+ "INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)"
177
+ ).run(migration.version, migration.name, (/* @__PURE__ */ new Date()).toISOString());
177
178
  db.exec("COMMIT");
178
179
  applied.add(migration.version);
179
180
  appliedVersions.push(migration.version);
@@ -217,7 +218,9 @@ async function upsertConversationStats(paths, record) {
217
218
  await migrateLinkDatabase(paths);
218
219
  const db = openDatabase(paths);
219
220
  try {
220
- db.prepare(conversationStatsUpsertSql()).run(...conversationStatsParams(record));
221
+ db.prepare(conversationStatsUpsertSql()).run(
222
+ ...conversationStatsParams(record)
223
+ );
221
224
  } finally {
222
225
  db.close();
223
226
  }
@@ -226,10 +229,11 @@ async function listConversationStatsPage(paths, input) {
226
229
  await migrateLinkDatabase(paths);
227
230
  const rawLimit = Number.isFinite(input.limit) ? Math.trunc(input.limit) : 25;
228
231
  const limit = Math.max(1, Math.min(100, rawLimit));
232
+ const status = input.status ?? "active";
229
233
  const db = openDatabase(paths);
230
234
  try {
231
235
  const conditions = ["status = ?"];
232
- const params = ["active"];
236
+ const params = [status];
233
237
  if (input.cursor) {
234
238
  conditions.push(`(
235
239
  updated_at < ?
@@ -241,13 +245,15 @@ async function listConversationStatsPage(paths, input) {
241
245
  input.cursor.conversationId
242
246
  );
243
247
  }
244
- const rows = db.prepare(`
248
+ const rows = db.prepare(
249
+ `
245
250
  SELECT conversation_id, updated_at
246
251
  FROM conversation_stats
247
252
  WHERE ${conditions.join(" AND ")}
248
253
  ORDER BY updated_at DESC, conversation_id DESC
249
254
  LIMIT ?
250
- `).all(...params, limit + 1);
255
+ `
256
+ ).all(...params, limit + 1);
251
257
  const records = rows.slice(0, limit).map((row) => ({
252
258
  conversationId: readString(row, "conversation_id") ?? "",
253
259
  updatedAt: readString(row, "updated_at") ?? ""
@@ -264,18 +270,20 @@ async function searchConversationStatsPage(paths, input) {
264
270
  await migrateLinkDatabase(paths);
265
271
  const rawLimit = Number.isFinite(input.limit) ? Math.trunc(input.limit) : 25;
266
272
  const limit = Math.max(1, Math.min(100, rawLimit));
273
+ const status = input.status ?? "active";
267
274
  const query = input.query.trim();
268
275
  if (!query) {
269
276
  return listConversationStatsPage(paths, {
270
277
  limit,
271
- cursor: input.cursor
278
+ cursor: input.cursor,
279
+ status
272
280
  });
273
281
  }
274
282
  const db = openDatabase(paths);
275
283
  try {
276
284
  const conditions = ["status = ?", "LOWER(title) LIKE ? ESCAPE '\\'"];
277
285
  const params = [
278
- "active",
286
+ status,
279
287
  `%${escapeSqlLike(query.toLowerCase())}%`
280
288
  ];
281
289
  if (input.cursor) {
@@ -289,13 +297,15 @@ async function searchConversationStatsPage(paths, input) {
289
297
  input.cursor.conversationId
290
298
  );
291
299
  }
292
- const rows = db.prepare(`
300
+ const rows = db.prepare(
301
+ `
293
302
  SELECT conversation_id, updated_at
294
303
  FROM conversation_stats
295
304
  WHERE ${conditions.join(" AND ")}
296
305
  ORDER BY updated_at DESC, conversation_id DESC
297
306
  LIMIT ?
298
- `).all(...params, limit + 1);
307
+ `
308
+ ).all(...params, limit + 1);
299
309
  const records = rows.slice(0, limit).map((row) => ({
300
310
  conversationId: readString(row, "conversation_id") ?? "",
301
311
  updatedAt: readString(row, "updated_at") ?? ""
@@ -336,11 +346,13 @@ async function readLinkStatistics(paths, filter = {}) {
336
346
  try {
337
347
  const conversationWhere = statisticsWhereClause(filter);
338
348
  const usageWhere = runUsageWhereClause(filter);
339
- const conversationRow = db.prepare(`
349
+ const conversationRow = db.prepare(
350
+ `
340
351
  SELECT
341
352
  COUNT(*) AS total_conversations,
342
353
  SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active_conversations,
343
- SUM(CASE WHEN status <> 'active' THEN 1 ELSE 0 END) AS deleted_conversations,
354
+ SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END) AS archived_conversations,
355
+ SUM(CASE WHEN status = 'deleted_soft' THEN 1 ELSE 0 END) AS deleted_conversations,
344
356
  COUNT(DISTINCT CASE
345
357
  WHEN profile_uid IS NOT NULL AND profile_uid <> '' THEN profile_uid
346
358
  WHEN profile_name_snapshot IS NOT NULL AND profile_name_snapshot <> '' THEN profile_name_snapshot
@@ -349,8 +361,10 @@ async function readLinkStatistics(paths, filter = {}) {
349
361
  MAX(stats_updated_at) AS updated_at
350
362
  FROM conversation_stats
351
363
  ${conversationWhere.sql}
352
- `).get(...conversationWhere.params);
353
- const usageRow = db.prepare(`
364
+ `
365
+ ).get(...conversationWhere.params);
366
+ const usageRow = db.prepare(
367
+ `
354
368
  SELECT
355
369
  COALESCE(SUM(input_tokens), 0) AS input_tokens,
356
370
  COALESCE(SUM(output_tokens), 0) AS output_tokens,
@@ -366,12 +380,14 @@ async function readLinkStatistics(paths, filter = {}) {
366
380
  MAX(updated_at) AS updated_at
367
381
  FROM run_usage_facts
368
382
  ${usageWhere.sql}
369
- `).get(...usageWhere.params);
383
+ `
384
+ ).get(...usageWhere.params);
370
385
  const updatedAt = readString(usageRow, "updated_at") ?? readString(conversationRow, "updated_at");
371
386
  return {
372
387
  conversations: {
373
388
  total: readNumber(conversationRow, "total_conversations"),
374
389
  active: readNumber(conversationRow, "active_conversations"),
390
+ archived: readNumber(conversationRow, "archived_conversations"),
375
391
  deleted: readNumber(conversationRow, "deleted_conversations")
376
392
  },
377
393
  tokens: {
@@ -413,7 +429,8 @@ async function readLinkUsageStatistics(paths, filter = {}) {
413
429
  from: range.fromInclusive,
414
430
  to: range.toExclusive
415
431
  });
416
- const totalsRow = db.prepare(`
432
+ const totalsRow = db.prepare(
433
+ `
417
434
  SELECT
418
435
  COALESCE(SUM(input_tokens), 0) AS input_tokens,
419
436
  COALESCE(SUM(output_tokens), 0) AS output_tokens,
@@ -425,8 +442,10 @@ async function readLinkUsageStatistics(paths, filter = {}) {
425
442
  MAX(updated_at) AS updated_at
426
443
  FROM run_usage_facts
427
444
  ${where.sql}
428
- `).get(...where.params);
429
- const dailyRows = db.prepare(`
445
+ `
446
+ ).get(...where.params);
447
+ const dailyRows = db.prepare(
448
+ `
430
449
  SELECT
431
450
  substr(completed_at, 1, 10) AS date,
432
451
  COALESCE(SUM(input_tokens), 0) AS input_tokens,
@@ -438,8 +457,10 @@ async function readLinkUsageStatistics(paths, filter = {}) {
438
457
  ${where.sql}
439
458
  GROUP BY substr(completed_at, 1, 10)
440
459
  ORDER BY date ASC
441
- `).all(...where.params);
442
- const modelRows = db.prepare(`
460
+ `
461
+ ).all(...where.params);
462
+ const modelRows = db.prepare(
463
+ `
443
464
  SELECT
444
465
  COALESCE(NULLIF(model, ''), 'unknown') AS model,
445
466
  provider,
@@ -454,8 +475,10 @@ async function readLinkUsageStatistics(paths, filter = {}) {
454
475
  HAVING SUM(total_tokens) > 0
455
476
  ORDER BY total_tokens DESC, run_count DESC, model ASC
456
477
  LIMIT 12
457
- `).all(...where.params);
458
- const profileRows = db.prepare(`
478
+ `
479
+ ).all(...where.params);
480
+ const profileRows = db.prepare(
481
+ `
459
482
  SELECT
460
483
  MAX(NULLIF(profile_uid, '')) AS profile_uid,
461
484
  COALESCE(
@@ -480,7 +503,8 @@ async function readLinkUsageStatistics(paths, filter = {}) {
480
503
  HAVING SUM(total_tokens) > 0
481
504
  ORDER BY total_tokens DESC, run_count DESC, profile ASC
482
505
  LIMIT 12
483
- `).all(...where.params);
506
+ `
507
+ ).all(...where.params);
484
508
  const dailyByDate = new Map(
485
509
  dailyRows.map((row) => [readString(row, "date") ?? "", row])
486
510
  );
@@ -539,17 +563,21 @@ async function ensureProfileIdentity(paths, input) {
539
563
  const db = openDatabase(paths);
540
564
  const now = (/* @__PURE__ */ new Date()).toISOString();
541
565
  try {
542
- const existing = db.prepare(`
566
+ const existing = db.prepare(
567
+ `
543
568
  ${profileRegistrySelectSql()}
544
569
  FROM profile_registry
545
570
  WHERE profile_name = ?
546
- `).get(input.profileName);
571
+ `
572
+ ).get(input.profileName);
547
573
  if (existing) {
548
- db.prepare(`
574
+ db.prepare(
575
+ `
549
576
  UPDATE profile_registry
550
577
  SET profile_path = ?, updated_at = ?
551
578
  WHERE profile_uid = ?
552
- `).run(input.profilePath, now, readString(existing, "profile_uid") ?? "");
579
+ `
580
+ ).run(input.profilePath, now, readString(existing, "profile_uid") ?? "");
553
581
  return profileIdentityFromRow({
554
582
  ...existing,
555
583
  profile_path: input.profilePath,
@@ -557,7 +585,8 @@ async function ensureProfileIdentity(paths, input) {
557
585
  });
558
586
  }
559
587
  const profileUid = `prof_${randomUUID().replaceAll("-", "")}`;
560
- db.prepare(`
588
+ db.prepare(
589
+ `
561
590
  INSERT INTO profile_registry (
562
591
  profile_uid,
563
592
  profile_name,
@@ -565,7 +594,8 @@ async function ensureProfileIdentity(paths, input) {
565
594
  created_at,
566
595
  updated_at
567
596
  ) VALUES (?, ?, ?, ?, ?)
568
- `).run(profileUid, input.profileName, input.profilePath, now, now);
597
+ `
598
+ ).run(profileUid, input.profileName, input.profilePath, now, now);
569
599
  return {
570
600
  profileUid,
571
601
  profileName: input.profileName,
@@ -583,11 +613,13 @@ async function updateProfileMetadata(paths, input) {
583
613
  const db = openDatabase(paths);
584
614
  const now = (/* @__PURE__ */ new Date()).toISOString();
585
615
  try {
586
- const existing = db.prepare(`
616
+ const existing = db.prepare(
617
+ `
587
618
  ${profileRegistrySelectSql()}
588
619
  FROM profile_registry
589
620
  WHERE profile_name = ?
590
- `).get(input.profileName);
621
+ `
622
+ ).get(input.profileName);
591
623
  if (!existing) {
592
624
  throw new Error("profile identity not found");
593
625
  }
@@ -614,16 +646,20 @@ async function updateProfileMetadata(paths, input) {
614
646
  if (updates.length === 0) {
615
647
  return profileIdentityFromRow(existing);
616
648
  }
617
- db.prepare(`
649
+ db.prepare(
650
+ `
618
651
  UPDATE profile_registry
619
652
  SET ${updates.join(", ")}, updated_at = ?
620
653
  WHERE profile_name = ?
621
- `).run(...params, now, input.profileName);
622
- const updated = db.prepare(`
654
+ `
655
+ ).run(...params, now, input.profileName);
656
+ const updated = db.prepare(
657
+ `
623
658
  ${profileRegistrySelectSql()}
624
659
  FROM profile_registry
625
660
  WHERE profile_name = ?
626
- `).get(input.profileName);
661
+ `
662
+ ).get(input.profileName);
627
663
  return profileIdentityFromRow(updated ?? existing);
628
664
  } finally {
629
665
  db.close();
@@ -635,18 +671,22 @@ async function renameProfileIdentity(paths, input) {
635
671
  const now = (/* @__PURE__ */ new Date()).toISOString();
636
672
  let result = null;
637
673
  try {
638
- const existing = db.prepare(`
674
+ const existing = db.prepare(
675
+ `
639
676
  ${profileRegistrySelectSql()}
640
677
  FROM profile_registry
641
678
  WHERE profile_name = ?
642
- `).get(input.oldProfileName);
679
+ `
680
+ ).get(input.oldProfileName);
643
681
  if (existing) {
644
682
  const profileUid = readString(existing, "profile_uid") ?? `prof_${randomUUID().replaceAll("-", "")}`;
645
- db.prepare(`
683
+ db.prepare(
684
+ `
646
685
  UPDATE profile_registry
647
686
  SET profile_name = ?, profile_path = ?, updated_at = ?
648
687
  WHERE profile_uid = ?
649
- `).run(input.newProfileName, input.newProfilePath, now, profileUid);
688
+ `
689
+ ).run(input.newProfileName, input.newProfilePath, now, profileUid);
650
690
  result = {
651
691
  profileUid,
652
692
  profileName: input.newProfileName,
@@ -692,7 +732,8 @@ async function deleteConversationStatsForProfile(paths, input) {
692
732
  const db = openDatabase(paths);
693
733
  try {
694
734
  if (input.profileUid?.trim()) {
695
- db.prepare(`
735
+ db.prepare(
736
+ `
696
737
  DELETE FROM conversation_stats
697
738
  WHERE profile_uid = ?
698
739
  OR profile_name_snapshot = ?
@@ -700,17 +741,20 @@ async function deleteConversationStatsForProfile(paths, input) {
700
741
  (profile_name_snapshot IS NULL OR profile_name_snapshot = '')
701
742
  AND profile = ?
702
743
  )
703
- `).run(input.profileUid.trim(), input.profileName, input.profileName);
744
+ `
745
+ ).run(input.profileUid.trim(), input.profileName, input.profileName);
704
746
  return;
705
747
  }
706
- db.prepare(`
748
+ db.prepare(
749
+ `
707
750
  DELETE FROM conversation_stats
708
751
  WHERE profile_name_snapshot = ?
709
752
  OR (
710
753
  (profile_name_snapshot IS NULL OR profile_name_snapshot = '')
711
754
  AND profile = ?
712
755
  )
713
- `).run(input.profileName, input.profileName);
756
+ `
757
+ ).run(input.profileName, input.profileName);
714
758
  } finally {
715
759
  db.close();
716
760
  }
@@ -728,7 +772,9 @@ function openDatabase(paths) {
728
772
  }
729
773
  function readAppliedVersions(db) {
730
774
  const rows = db.prepare("SELECT version FROM schema_migrations").all();
731
- return new Set(rows.map((row) => readNumber(row, "version")).filter((value) => value > 0));
775
+ return new Set(
776
+ rows.map((row) => readNumber(row, "version")).filter((value) => value > 0)
777
+ );
732
778
  }
733
779
  function ensureProfileIdentitySchema(db) {
734
780
  db.exec(`
@@ -754,7 +800,9 @@ function ensureProfileIdentitySchema(db) {
754
800
  db.exec("ALTER TABLE profile_registry ADD COLUMN description TEXT;");
755
801
  }
756
802
  if (!profileColumns.has("avatar_type")) {
757
- db.exec("ALTER TABLE profile_registry ADD COLUMN avatar_type TEXT NOT NULL DEFAULT 'default';");
803
+ db.exec(
804
+ "ALTER TABLE profile_registry ADD COLUMN avatar_type TEXT NOT NULL DEFAULT 'default';"
805
+ );
758
806
  }
759
807
  if (!profileColumns.has("avatar_url")) {
760
808
  db.exec("ALTER TABLE profile_registry ADD COLUMN avatar_url TEXT;");
@@ -766,7 +814,9 @@ function ensureProfileIdentitySchema(db) {
766
814
  db.exec("ALTER TABLE conversation_stats ADD COLUMN profile_uid TEXT;");
767
815
  }
768
816
  if (!conversationColumns.has("profile_name_snapshot")) {
769
- db.exec("ALTER TABLE conversation_stats ADD COLUMN profile_name_snapshot TEXT;");
817
+ db.exec(
818
+ "ALTER TABLE conversation_stats ADD COLUMN profile_name_snapshot TEXT;"
819
+ );
770
820
  }
771
821
  db.exec(`
772
822
  CREATE INDEX IF NOT EXISTS idx_conversation_stats_profile_uid
@@ -4057,7 +4107,7 @@ async function listCronOutputFiles(profileName, jobId) {
4057
4107
  mtimeMs: fileStat.mtimeMs
4058
4108
  });
4059
4109
  }
4060
- return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path26, mtime }) => ({ path: path26, mtime }));
4110
+ return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path27, mtime }) => ({ path: path27, mtime }));
4061
4111
  }
4062
4112
  async function readCronOutput(outputPath) {
4063
4113
  const content = await readFile3(outputPath, "utf8");
@@ -4134,7 +4184,7 @@ import os2 from "os";
4134
4184
  import path5 from "path";
4135
4185
 
4136
4186
  // src/constants.ts
4137
- var LINK_VERSION = "0.4.9";
4187
+ var LINK_VERSION = "0.5.1";
4138
4188
  var LINK_COMMAND = "hermeslink";
4139
4189
  var LINK_DEFAULT_PORT = 52379;
4140
4190
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -7387,12 +7437,12 @@ function safePathSegment(value, fallback) {
7387
7437
  return safe.length > 0 ? safe.slice(0, 120) : fallback;
7388
7438
  }
7389
7439
 
7390
- // src/conversations/conversation-clear-plans.ts
7440
+ // src/conversations/conversation-archive-plans.ts
7391
7441
  import { randomUUID as randomUUID5 } from "crypto";
7392
7442
  import { mkdir as mkdir7 } from "fs/promises";
7393
7443
  import path11 from "path";
7394
- var PLAN_ID_PATTERN = /^clear_[a-f0-9]{32}$/u;
7395
- var ConversationClearPlanStore = class {
7444
+ var PLAN_ID_PATTERN = /^archive_[a-f0-9]{32}$/u;
7445
+ var ConversationArchivePlanStore = class {
7396
7446
  constructor(paths) {
7397
7447
  this.paths = paths;
7398
7448
  }
@@ -7400,12 +7450,12 @@ var ConversationClearPlanStore = class {
7400
7450
  async create(conversationIds) {
7401
7451
  const now = (/* @__PURE__ */ new Date()).toISOString();
7402
7452
  const plan = {
7403
- id: `clear_${randomUUID5().replaceAll("-", "")}`,
7453
+ id: `archive_${randomUUID5().replaceAll("-", "")}`,
7404
7454
  status: "prepared",
7405
7455
  created_at: now,
7406
7456
  updated_at: now,
7407
7457
  total_count: conversationIds.length,
7408
- deleted_count: 0,
7458
+ archived_count: 0,
7409
7459
  failed_count: 0,
7410
7460
  conversation_ids: conversationIds,
7411
7461
  conversations: []
@@ -7421,8 +7471,8 @@ var ConversationClearPlanStore = class {
7421
7471
  if (!plan) {
7422
7472
  throw new LinkHttpError(
7423
7473
  404,
7424
- "conversation_clear_plan_not_found",
7425
- "Conversation clear plan was not found"
7474
+ "conversation_archive_plan_not_found",
7475
+ "Conversation archive plan was not found"
7426
7476
  );
7427
7477
  }
7428
7478
  return plan;
@@ -7433,7 +7483,7 @@ var ConversationClearPlanStore = class {
7433
7483
  await writeJsonFile(this.planPath(normalizedPlanId), plan);
7434
7484
  }
7435
7485
  plansDir() {
7436
- return path11.join(this.paths.indexesDir, "conversation-clear-plans");
7486
+ return path11.join(this.paths.indexesDir, "conversation-archive-plans");
7437
7487
  }
7438
7488
  planPath(planId) {
7439
7489
  return path11.join(this.plansDir(), `${planId}.json`);
@@ -7442,6 +7492,71 @@ var ConversationClearPlanStore = class {
7442
7492
  function normalizePlanId(planId) {
7443
7493
  const normalized = planId.trim();
7444
7494
  if (!PLAN_ID_PATTERN.test(normalized)) {
7495
+ throw new LinkHttpError(
7496
+ 400,
7497
+ "conversation_archive_plan_id_invalid",
7498
+ "Conversation archive plan id is invalid"
7499
+ );
7500
+ }
7501
+ return normalized;
7502
+ }
7503
+
7504
+ // src/conversations/conversation-clear-plans.ts
7505
+ import { randomUUID as randomUUID6 } from "crypto";
7506
+ import { mkdir as mkdir8 } from "fs/promises";
7507
+ import path12 from "path";
7508
+ var PLAN_ID_PATTERN2 = /^clear_[a-f0-9]{32}$/u;
7509
+ var ConversationClearPlanStore = class {
7510
+ constructor(paths) {
7511
+ this.paths = paths;
7512
+ }
7513
+ paths;
7514
+ async create(conversationIds, targetStatus = "active") {
7515
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7516
+ const plan = {
7517
+ id: `clear_${randomUUID6().replaceAll("-", "")}`,
7518
+ status: "prepared",
7519
+ target_status: targetStatus,
7520
+ created_at: now,
7521
+ updated_at: now,
7522
+ total_count: conversationIds.length,
7523
+ deleted_count: 0,
7524
+ failed_count: 0,
7525
+ conversation_ids: conversationIds,
7526
+ conversations: []
7527
+ };
7528
+ await this.write(plan);
7529
+ return plan;
7530
+ }
7531
+ async read(planId) {
7532
+ const normalizedPlanId = normalizePlanId2(planId);
7533
+ const plan = await readJsonFile(
7534
+ this.planPath(normalizedPlanId)
7535
+ );
7536
+ if (!plan) {
7537
+ throw new LinkHttpError(
7538
+ 404,
7539
+ "conversation_clear_plan_not_found",
7540
+ "Conversation clear plan was not found"
7541
+ );
7542
+ }
7543
+ return plan;
7544
+ }
7545
+ async write(plan) {
7546
+ const normalizedPlanId = normalizePlanId2(plan.id);
7547
+ await mkdir8(this.plansDir(), { recursive: true, mode: 448 });
7548
+ await writeJsonFile(this.planPath(normalizedPlanId), plan);
7549
+ }
7550
+ plansDir() {
7551
+ return path12.join(this.paths.indexesDir, "conversation-clear-plans");
7552
+ }
7553
+ planPath(planId) {
7554
+ return path12.join(this.plansDir(), `${planId}.json`);
7555
+ }
7556
+ };
7557
+ function normalizePlanId2(planId) {
7558
+ const normalized = planId.trim();
7559
+ if (!PLAN_ID_PATTERN2.test(normalized)) {
7445
7560
  throw new LinkHttpError(
7446
7561
  400,
7447
7562
  "conversation_clear_plan_id_invalid",
@@ -7502,14 +7617,16 @@ var ConversationMaintenanceCoordinator = class {
7502
7617
  constructor(deps) {
7503
7618
  this.deps = deps;
7504
7619
  this.clearPlans = new ConversationClearPlanStore(deps.paths);
7620
+ this.archivePlans = new ConversationArchivePlanStore(deps.paths);
7505
7621
  }
7506
7622
  deps;
7507
7623
  clearPlans;
7508
- async prepareClearAllConversationPlan() {
7624
+ archivePlans;
7625
+ async prepareClearAllConversationPlan(targetStatus = "active") {
7509
7626
  const targets = [];
7510
7627
  for (const conversationId of await this.deps.store.listConversationIds()) {
7511
7628
  const manifest = await this.deps.store.readManifest(conversationId).catch(() => null);
7512
- if (manifest?.status !== "active") {
7629
+ if (manifest?.status !== targetStatus) {
7513
7630
  continue;
7514
7631
  }
7515
7632
  targets.push({
@@ -7519,7 +7636,8 @@ var ConversationMaintenanceCoordinator = class {
7519
7636
  }
7520
7637
  targets.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
7521
7638
  return this.clearPlans.create(
7522
- targets.map((target) => target.id)
7639
+ targets.map((target) => target.id),
7640
+ targetStatus
7523
7641
  );
7524
7642
  }
7525
7643
  async readClearAllConversationPlan(planId) {
@@ -7629,6 +7747,133 @@ var ConversationMaintenanceCoordinator = class {
7629
7747
  );
7630
7748
  return started;
7631
7749
  }
7750
+ async prepareArchiveAllConversationPlan(input = {}) {
7751
+ const excluded = new Set(
7752
+ normalizeConversationIds(input.excludeConversationIds ?? [])
7753
+ );
7754
+ const targets = [];
7755
+ for (const conversationId of await this.deps.store.listConversationIds()) {
7756
+ if (excluded.has(conversationId)) {
7757
+ continue;
7758
+ }
7759
+ const manifest = await this.deps.store.readManifest(conversationId).catch(() => null);
7760
+ if (manifest?.status !== "active") {
7761
+ continue;
7762
+ }
7763
+ targets.push({
7764
+ id: conversationId,
7765
+ updatedAt: manifest.updated_at
7766
+ });
7767
+ }
7768
+ targets.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
7769
+ return this.archivePlans.create(targets.map((target) => target.id));
7770
+ }
7771
+ async readArchiveAllConversationPlan(planId) {
7772
+ return this.archivePlans.read(planId);
7773
+ }
7774
+ async executeArchiveAllConversationPlan(planId) {
7775
+ let plan = await this.archivePlans.read(planId);
7776
+ if (plan.status === "completed") {
7777
+ return plan;
7778
+ }
7779
+ if (plan.status === "failed") {
7780
+ throw new LinkHttpError(
7781
+ 409,
7782
+ "conversation_archive_plan_already_failed",
7783
+ "Conversation archive plan has already failed"
7784
+ );
7785
+ }
7786
+ const results = [];
7787
+ if (plan.status !== "executing") {
7788
+ plan = await this.writeArchivePlan({
7789
+ ...plan,
7790
+ status: "executing",
7791
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
7792
+ conversations: results,
7793
+ archived_count: 0,
7794
+ failed_count: 0
7795
+ });
7796
+ }
7797
+ for (const conversationId of plan.conversation_ids) {
7798
+ try {
7799
+ const archived = await this.archiveConversation(conversationId);
7800
+ results.push({ ...archived, status: "archived" });
7801
+ } catch (error) {
7802
+ if (error instanceof LinkHttpError && error.code === "conversation_not_found") {
7803
+ results.push({
7804
+ conversation_id: conversationId,
7805
+ status: "archived",
7806
+ archived_at: (/* @__PURE__ */ new Date()).toISOString()
7807
+ });
7808
+ } else {
7809
+ results.push({
7810
+ conversation_id: conversationId,
7811
+ status: "failed",
7812
+ error: {
7813
+ code: error instanceof LinkHttpError ? error.code : "internal_error",
7814
+ message: error instanceof Error ? error.message : "Internal error"
7815
+ }
7816
+ });
7817
+ }
7818
+ }
7819
+ plan = await this.writeArchivePlan({
7820
+ ...plan,
7821
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
7822
+ conversations: [...results],
7823
+ archived_count: results.filter((result) => result.status === "archived").length,
7824
+ failed_count: results.filter((result) => result.status === "failed").length
7825
+ });
7826
+ }
7827
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
7828
+ return this.writeArchivePlan({
7829
+ ...plan,
7830
+ status: plan.failed_count === 0 ? "completed" : "failed",
7831
+ updated_at: completedAt,
7832
+ completed_at: completedAt
7833
+ });
7834
+ }
7835
+ async startArchiveAllConversationPlan(planId) {
7836
+ const plan = await this.archivePlans.read(planId);
7837
+ if (plan.status === "completed" || plan.status === "executing") {
7838
+ return plan;
7839
+ }
7840
+ if (plan.status === "failed") {
7841
+ throw new LinkHttpError(
7842
+ 409,
7843
+ "conversation_archive_plan_already_failed",
7844
+ "Conversation archive plan has already failed"
7845
+ );
7846
+ }
7847
+ const started = await this.writeArchivePlan({
7848
+ ...plan,
7849
+ status: "executing",
7850
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
7851
+ conversations: [],
7852
+ archived_count: 0,
7853
+ failed_count: 0
7854
+ });
7855
+ void this.executeArchiveAllConversationPlan(started.id).catch(
7856
+ async (error) => {
7857
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
7858
+ await this.writeArchivePlan({
7859
+ ...started,
7860
+ status: "failed",
7861
+ updated_at: failedAt,
7862
+ completed_at: failedAt,
7863
+ failed_count: started.total_count,
7864
+ conversations: started.conversation_ids.map((conversationId) => ({
7865
+ conversation_id: conversationId,
7866
+ status: "failed",
7867
+ error: {
7868
+ code: error instanceof LinkHttpError ? error.code : "internal_error",
7869
+ message: error instanceof Error ? error.message : "Internal error"
7870
+ }
7871
+ }))
7872
+ }).catch(() => void 0);
7873
+ }
7874
+ );
7875
+ return started;
7876
+ }
7632
7877
  async deleteConversation(conversationId) {
7633
7878
  return this.deps.withConversationLock(
7634
7879
  conversationId,
@@ -7665,8 +7910,20 @@ var ConversationMaintenanceCoordinator = class {
7665
7910
  conversations
7666
7911
  };
7667
7912
  }
7913
+ async archiveConversation(conversationId) {
7914
+ return this.deps.withConversationLock(
7915
+ conversationId,
7916
+ () => this.archiveConversationLocked(conversationId)
7917
+ );
7918
+ }
7919
+ async unarchiveConversation(conversationId) {
7920
+ return this.deps.withConversationLock(
7921
+ conversationId,
7922
+ () => this.unarchiveConversationLocked(conversationId)
7923
+ );
7924
+ }
7668
7925
  async writeBlob(conversationId, input) {
7669
- await this.deps.store.readActiveManifest(conversationId);
7926
+ await this.deps.store.readRunnableManifest(conversationId);
7670
7927
  const blob = await writeConversationBlob(
7671
7928
  this.deps.paths,
7672
7929
  conversationId,
@@ -7682,11 +7939,11 @@ var ConversationMaintenanceCoordinator = class {
7682
7939
  return blob;
7683
7940
  }
7684
7941
  async readBlob(conversationId, blobId) {
7685
- await this.deps.store.readActiveManifest(conversationId);
7942
+ await this.deps.store.readRunnableManifest(conversationId);
7686
7943
  return readConversationBlob(this.deps.paths, conversationId, blobId);
7687
7944
  }
7688
7945
  async deleteUnreferencedBlob(conversationId, blobId) {
7689
- await this.deps.store.readActiveManifest(conversationId);
7946
+ await this.deps.store.readRunnableManifest(conversationId);
7690
7947
  const snapshot = await this.deps.store.readSnapshot(conversationId);
7691
7948
  return deleteConversationBlobIfUnreferenced(
7692
7949
  this.deps.paths,
@@ -7735,8 +7992,96 @@ var ConversationMaintenanceCoordinator = class {
7735
7992
  }
7736
7993
  return parts;
7737
7994
  }
7995
+ async archiveConversationLocked(conversationId) {
7996
+ const manifest = await this.deps.store.readManifest(conversationId);
7997
+ if (manifest.status === "deleted_soft") {
7998
+ throw new LinkHttpError(
7999
+ 404,
8000
+ "conversation_not_found",
8001
+ "Conversation was not found"
8002
+ );
8003
+ }
8004
+ if (manifest.status === "archived") {
8005
+ return {
8006
+ conversation_id: conversationId,
8007
+ archived_at: manifest.archived_at ?? manifest.updated_at
8008
+ };
8009
+ }
8010
+ const archivedAt = (/* @__PURE__ */ new Date()).toISOString();
8011
+ const snapshot = await this.deps.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
8012
+ await this.deps.store.writeManifest({
8013
+ ...manifest,
8014
+ status: "archived",
8015
+ updated_at: archivedAt,
8016
+ archived_at: archivedAt
8017
+ });
8018
+ await this.deps.appendEvent(conversationId, {
8019
+ type: "conversation.archived",
8020
+ payload: {
8021
+ archived_at: archivedAt
8022
+ }
8023
+ });
8024
+ const updatedManifest = await this.deps.store.readManifest(conversationId);
8025
+ await this.deps.metadata.persistConversationStats(conversationId, snapshot);
8026
+ await this.deps.store.writeManifest({
8027
+ ...updatedManifest,
8028
+ stats: buildConversationStats(updatedManifest, snapshot)
8029
+ });
8030
+ return {
8031
+ conversation_id: conversationId,
8032
+ archived_at: archivedAt
8033
+ };
8034
+ }
8035
+ async unarchiveConversationLocked(conversationId) {
8036
+ const manifest = await this.deps.store.readManifest(conversationId);
8037
+ if (manifest.status === "deleted_soft") {
8038
+ throw new LinkHttpError(
8039
+ 404,
8040
+ "conversation_not_found",
8041
+ "Conversation was not found"
8042
+ );
8043
+ }
8044
+ if (manifest.status === "active") {
8045
+ return {
8046
+ conversation_id: conversationId,
8047
+ unarchived_at: manifest.updated_at
8048
+ };
8049
+ }
8050
+ const unarchivedAt = (/* @__PURE__ */ new Date()).toISOString();
8051
+ const snapshot = await this.deps.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
8052
+ const next = {
8053
+ ...manifest,
8054
+ status: "active",
8055
+ updated_at: unarchivedAt
8056
+ };
8057
+ delete next.archived_at;
8058
+ await this.deps.store.writeManifest(next);
8059
+ await this.deps.appendEvent(conversationId, {
8060
+ type: "conversation.unarchived",
8061
+ payload: {
8062
+ unarchived_at: unarchivedAt
8063
+ }
8064
+ });
8065
+ const updatedManifest = await this.deps.store.readManifest(conversationId);
8066
+ await this.deps.metadata.persistConversationStats(conversationId, snapshot);
8067
+ await this.deps.store.writeManifest({
8068
+ ...updatedManifest,
8069
+ stats: buildConversationStats(updatedManifest, snapshot)
8070
+ });
8071
+ return {
8072
+ conversation_id: conversationId,
8073
+ unarchived_at: unarchivedAt
8074
+ };
8075
+ }
7738
8076
  async deleteConversationLocked(conversationId) {
7739
- const manifest = await this.deps.store.readActiveManifest(conversationId);
8077
+ const manifest = await this.deps.store.readManifest(conversationId);
8078
+ if (manifest.status === "deleted_soft") {
8079
+ throw new LinkHttpError(
8080
+ 404,
8081
+ "conversation_not_found",
8082
+ "Conversation was not found"
8083
+ );
8084
+ }
7740
8085
  const snapshot = await this.deps.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
7741
8086
  const referencedBlobIds = /* @__PURE__ */ new Set([
7742
8087
  ...collectBlobIds(snapshot),
@@ -7747,7 +8092,7 @@ var ConversationMaintenanceCoordinator = class {
7747
8092
  hermesSessionIds,
7748
8093
  manifest.profile_name_snapshot ?? manifest.profile ?? "default"
7749
8094
  );
7750
- if (snapshot.runs.length > 0 && hermesDeleteResults.every((result) => result.status === "not_found")) {
8095
+ if (hermesDeleteResults.some((result) => result.status === "unknown")) {
7751
8096
  throw new LinkHttpError(
7752
8097
  502,
7753
8098
  "hermes_session_delete_not_confirmed",
@@ -7838,6 +8183,10 @@ var ConversationMaintenanceCoordinator = class {
7838
8183
  await this.clearPlans.write(plan);
7839
8184
  return plan;
7840
8185
  }
8186
+ async writeArchivePlan(plan) {
8187
+ await this.archivePlans.write(plan);
8188
+ return plan;
8189
+ }
7841
8190
  };
7842
8191
  function isVoiceAttachmentInput(attachment) {
7843
8192
  return attachment.kind === "voice" || attachment.type === "voice" || attachment.is_voice_note === true || attachment.isVoiceNote === true;
@@ -7938,14 +8287,14 @@ function isUsableLanIpv4(value) {
7938
8287
 
7939
8288
  // src/hermes/session-title.ts
7940
8289
  import { stat as stat7 } from "fs/promises";
7941
- import path12 from "path";
8290
+ import path13 from "path";
7942
8291
  async function readHermesSessionTitle(sessionId, paths, profileName) {
7943
8292
  const trimmedSessionId = sessionId.trim();
7944
8293
  if (!trimmedSessionId) {
7945
8294
  return void 0;
7946
8295
  }
7947
8296
  const resolvedProfileName = isValidProfileName(profileName) ? profileName : "default";
7948
- const dbPath = path12.join(
8297
+ const dbPath = path13.join(
7949
8298
  resolveHermesProfileDir(resolvedProfileName),
7950
8299
  "state.db"
7951
8300
  );
@@ -7966,7 +8315,7 @@ async function readHermesCompressionTip(sessionId, paths, profileName) {
7966
8315
  return void 0;
7967
8316
  }
7968
8317
  const resolvedProfileName = isValidProfileName(profileName) ? profileName : "default";
7969
- const dbPath = path12.join(
8318
+ const dbPath = path13.join(
7970
8319
  resolveHermesProfileDir(resolvedProfileName),
7971
8320
  "state.db"
7972
8321
  );
@@ -8398,7 +8747,7 @@ function stripCompressionTitleSuffix(value) {
8398
8747
  }
8399
8748
 
8400
8749
  // src/conversations/conversation-turns.ts
8401
- import { randomUUID as randomUUID6 } from "crypto";
8750
+ import { randomUUID as randomUUID7 } from "crypto";
8402
8751
  var MESSAGE_ORDER_STEP_MS = 10;
8403
8752
  var ASSISTANT_ORDER_OFFSET_MS = 1;
8404
8753
  function createAgentTurnDraft(input) {
@@ -8632,10 +8981,10 @@ function createAssistantMessage(input) {
8632
8981
  };
8633
8982
  }
8634
8983
  function createMessageId() {
8635
- return `msg_${randomUUID6().replaceAll("-", "")}`;
8984
+ return `msg_${randomUUID7().replaceAll("-", "")}`;
8636
8985
  }
8637
8986
  function createRunId() {
8638
- return `run_${randomUUID6().replaceAll("-", "")}`;
8987
+ return `run_${randomUUID7().replaceAll("-", "")}`;
8639
8988
  }
8640
8989
  function hasActiveOrQueuedRuns(snapshot) {
8641
8990
  return snapshot.runs.some(
@@ -8699,10 +9048,7 @@ var ConversationOrchestrationCoordinator = class {
8699
9048
  });
8700
9049
  }
8701
9050
  async startNextQueuedRunLocked(conversationId) {
8702
- if (!await this.deps.store.isConversationActive(conversationId)) {
8703
- return null;
8704
- }
8705
- const manifest = await this.deps.store.readActiveManifest(conversationId);
9051
+ const manifest = await this.deps.store.readRunnableManifest(conversationId);
8706
9052
  const snapshot = await this.deps.store.readSnapshot(conversationId);
8707
9053
  if (hasRunningRuns(snapshot)) {
8708
9054
  return null;
@@ -8774,7 +9120,9 @@ var ConversationOrchestrationCoordinator = class {
8774
9120
  };
8775
9121
  }
8776
9122
  async sendMessageLocked(input) {
8777
- let manifest = await this.deps.store.readActiveManifest(input.conversationId);
9123
+ let manifest = await this.deps.restoreArchivedConversationForUserContinuation(
9124
+ input.conversationId
9125
+ );
8778
9126
  let content = input.content.trim();
8779
9127
  const userAttachmentParts = await this.deps.resolveMessageAttachmentParts(
8780
9128
  manifest.id,
@@ -9502,29 +9850,45 @@ var ConversationQueryCoordinator = class {
9502
9850
  return this.listConversationsFromStore();
9503
9851
  }
9504
9852
  async listConversationPage(options = {}) {
9853
+ return this.listConversationPageForStatus("active", options);
9854
+ }
9855
+ async listArchivedConversationPage(options = {}) {
9856
+ return this.listConversationPageForStatus("archived", options);
9857
+ }
9858
+ async listConversationPageForStatus(status, options = {}) {
9505
9859
  const limit = normalizeConversationListPageLimit(options.limit);
9506
9860
  const cursor = decodeConversationListCursor(options.cursor);
9507
9861
  return this.listIndexedConversationPage({
9862
+ status,
9508
9863
  limit,
9509
9864
  cursor,
9510
- fallback: () => this.listConversationPageFromStore({ limit, cursor }),
9865
+ fallback: () => this.listConversationPageFromStore({ status, limit, cursor }),
9511
9866
  listPage: (pageCursor) => listConversationStatsPage(this.deps.paths, {
9867
+ status,
9512
9868
  limit,
9513
9869
  cursor: pageCursor
9514
9870
  })
9515
9871
  });
9516
9872
  }
9517
9873
  async searchConversationPage(options = {}) {
9874
+ return this.searchConversationPageForStatus("active", options);
9875
+ }
9876
+ async searchArchivedConversationPage(options = {}) {
9877
+ return this.searchConversationPageForStatus("archived", options);
9878
+ }
9879
+ async searchConversationPageForStatus(status, options = {}) {
9518
9880
  const query = normalizeConversationSearchQuery(options.query);
9519
9881
  if (!query) {
9520
- return this.listConversationPage(options);
9882
+ return this.listConversationPageForStatus(status, options);
9521
9883
  }
9522
9884
  const limit = normalizeConversationListPageLimit(options.limit);
9523
9885
  const cursor = decodeConversationListCursor(options.cursor);
9524
9886
  return this.listIndexedConversationPage({
9887
+ status,
9525
9888
  limit,
9526
9889
  cursor,
9527
9890
  listPage: (pageCursor) => searchConversationStatsPage(this.deps.paths, {
9891
+ status,
9528
9892
  limit,
9529
9893
  cursor: pageCursor,
9530
9894
  query
@@ -9550,6 +9914,7 @@ var ConversationQueryCoordinator = class {
9550
9914
  }
9551
9915
  usedIndex = true;
9552
9916
  const summaries = await this.summarizeIndexedConversations(
9917
+ input.status,
9553
9918
  indexedPage.records
9554
9919
  );
9555
9920
  for (const summary of summaries) {
@@ -9590,18 +9955,14 @@ var ConversationQueryCoordinator = class {
9590
9955
  }
9591
9956
  };
9592
9957
  }
9593
- async listConversationsFromStore() {
9958
+ async listConversationsFromStore(status = "active") {
9594
9959
  const summaries = [];
9595
9960
  for (const conversationId of await this.deps.store.listConversationIds()) {
9596
- const manifest = await this.deps.store.readManifest(conversationId).catch(
9597
- () => null
9598
- );
9599
- if (!manifest || manifest.status !== "active") {
9961
+ const manifest = await this.deps.store.readManifest(conversationId).catch(() => null);
9962
+ if (!manifest || manifest.status !== status) {
9600
9963
  continue;
9601
9964
  }
9602
- const snapshot = await this.deps.store.readSnapshot(conversationId).catch(
9603
- () => emptySnapshot2()
9604
- );
9965
+ const snapshot = await this.deps.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
9605
9966
  const refreshed = await this.deps.metadata.refreshTitleFromHermes(manifest, { snapshot }).catch(() => manifest);
9606
9967
  summaries.push(await this.summarizeConversation(refreshed, snapshot));
9607
9968
  }
@@ -9610,10 +9971,15 @@ var ConversationQueryCoordinator = class {
9610
9971
  );
9611
9972
  }
9612
9973
  async listConversationPageFromStore(input) {
9613
- const all = await this.listConversationsFromStore();
9614
- const startIndex = input.cursor ? all.findIndex((summary) => isAfterConversationListCursor(summary, input.cursor)) : 0;
9974
+ const all = await this.listConversationsFromStore(input.status);
9975
+ const startIndex = input.cursor ? all.findIndex(
9976
+ (summary) => isAfterConversationListCursor(summary, input.cursor)
9977
+ ) : 0;
9615
9978
  const safeStartIndex = startIndex < 0 ? all.length : startIndex;
9616
- const conversations = all.slice(safeStartIndex, safeStartIndex + input.limit);
9979
+ const conversations = all.slice(
9980
+ safeStartIndex,
9981
+ safeStartIndex + input.limit
9982
+ );
9617
9983
  const hasMore = safeStartIndex + input.limit < all.length;
9618
9984
  return {
9619
9985
  conversations,
@@ -9624,18 +9990,14 @@ var ConversationQueryCoordinator = class {
9624
9990
  }
9625
9991
  };
9626
9992
  }
9627
- async summarizeIndexedConversations(records) {
9993
+ async summarizeIndexedConversations(status, records) {
9628
9994
  const summaries = [];
9629
9995
  for (const record of records) {
9630
- const manifest = await this.deps.store.readManifest(record.conversationId).catch(
9631
- () => null
9632
- );
9633
- if (!manifest || manifest.status !== "active") {
9996
+ const manifest = await this.deps.store.readManifest(record.conversationId).catch(() => null);
9997
+ if (!manifest || manifest.status !== status) {
9634
9998
  continue;
9635
9999
  }
9636
- const snapshot = await this.deps.store.readSnapshot(record.conversationId).catch(
9637
- () => emptySnapshot2()
9638
- );
10000
+ const snapshot = await this.deps.store.readSnapshot(record.conversationId).catch(() => emptySnapshot2());
9639
10001
  const refreshed = await this.deps.metadata.refreshTitleFromHermes(manifest, { snapshot }).catch(() => manifest);
9640
10002
  summaries.push(await this.summarizeConversation(refreshed, snapshot));
9641
10003
  }
@@ -9812,20 +10174,20 @@ function hydrateAgentEventBlocks(blocks, agentEvents) {
9812
10174
  // src/conversations/conversation-store.ts
9813
10175
  import {
9814
10176
  appendFile as appendFile2,
9815
- mkdir as mkdir8,
10177
+ mkdir as mkdir9,
9816
10178
  readdir as readdir5,
9817
10179
  readFile as readFile7,
9818
10180
  rm as rm5,
9819
10181
  writeFile as writeFile2
9820
10182
  } from "fs/promises";
9821
- import path13 from "path";
10183
+ import path14 from "path";
9822
10184
  var ConversationStore = class {
9823
10185
  constructor(paths) {
9824
10186
  this.paths = paths;
9825
10187
  }
9826
10188
  paths;
9827
10189
  async ensureConversationsDir() {
9828
- await mkdir8(this.paths.conversationsDir, { recursive: true, mode: 448 });
10190
+ await mkdir9(this.paths.conversationsDir, { recursive: true, mode: 448 });
9829
10191
  }
9830
10192
  async listConversationIds() {
9831
10193
  await this.ensureConversationsDir();
@@ -9840,7 +10202,7 @@ var ConversationStore = class {
9840
10202
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
9841
10203
  }
9842
10204
  async createConversation(manifest, snapshot = createEmptySnapshot2()) {
9843
- await mkdir8(this.conversationDir(manifest.id), {
10205
+ await mkdir9(this.conversationDir(manifest.id), {
9844
10206
  recursive: true,
9845
10207
  mode: 448
9846
10208
  });
@@ -9880,7 +10242,7 @@ var ConversationStore = class {
9880
10242
  conversation_id: conversationId,
9881
10243
  created_at: now
9882
10244
  };
9883
- await mkdir8(this.conversationDir(conversationId), {
10245
+ await mkdir9(this.conversationDir(conversationId), {
9884
10246
  recursive: true,
9885
10247
  mode: 448
9886
10248
  });
@@ -9929,28 +10291,43 @@ var ConversationStore = class {
9929
10291
  }
9930
10292
  return manifest;
9931
10293
  }
10294
+ async readRunnableManifest(conversationId) {
10295
+ const manifest = await this.readManifest(conversationId);
10296
+ if (manifest.status === "deleted_soft") {
10297
+ throw new LinkHttpError(
10298
+ 404,
10299
+ "conversation_not_found",
10300
+ "Conversation was not found"
10301
+ );
10302
+ }
10303
+ return manifest;
10304
+ }
9932
10305
  async isConversationActive(conversationId) {
9933
10306
  const manifest = await this.readManifest(conversationId).catch(() => null);
9934
10307
  return manifest?.status === "active";
9935
10308
  }
10309
+ async isConversationRunnable(conversationId) {
10310
+ const manifest = await this.readManifest(conversationId).catch(() => null);
10311
+ return manifest != null && manifest.status !== "deleted_soft";
10312
+ }
9936
10313
  removeConversationAttachments(conversationId) {
9937
- return rm5(path13.join(this.conversationDir(conversationId), "attachments"), {
10314
+ return rm5(path14.join(this.conversationDir(conversationId), "attachments"), {
9938
10315
  recursive: true,
9939
10316
  force: true
9940
10317
  });
9941
10318
  }
9942
10319
  conversationDir(conversationId) {
9943
10320
  assertValidConversationId(conversationId);
9944
- return path13.join(this.paths.conversationsDir, conversationId);
10321
+ return path14.join(this.paths.conversationsDir, conversationId);
9945
10322
  }
9946
10323
  manifestPath(conversationId) {
9947
- return path13.join(this.conversationDir(conversationId), "manifest.json");
10324
+ return path14.join(this.conversationDir(conversationId), "manifest.json");
9948
10325
  }
9949
10326
  snapshotPath(conversationId) {
9950
- return path13.join(this.conversationDir(conversationId), "snapshot.json");
10327
+ return path14.join(this.conversationDir(conversationId), "snapshot.json");
9951
10328
  }
9952
10329
  eventsPath(conversationId) {
9953
- return path13.join(this.conversationDir(conversationId), "events.ndjson");
10330
+ return path14.join(this.conversationDir(conversationId), "events.ndjson");
9954
10331
  }
9955
10332
  };
9956
10333
  function createEmptySnapshot2() {
@@ -9961,13 +10338,13 @@ function isNodeError9(error, code) {
9961
10338
  }
9962
10339
 
9963
10340
  // src/conversations/hermes-session-sync.ts
9964
- import { randomUUID as randomUUID7 } from "crypto";
10341
+ import { randomUUID as randomUUID8 } from "crypto";
9965
10342
  import { readdir as readdir7, readFile as readFile9, stat as stat9 } from "fs/promises";
9966
- import path15 from "path";
10343
+ import path16 from "path";
9967
10344
 
9968
10345
  // src/conversations/delivery-import.ts
9969
10346
  import { lstat as lstat2, readFile as readFile8, readdir as readdir6, stat as stat8 } from "fs/promises";
9970
- import path14 from "path";
10347
+ import path15 from "path";
9971
10348
  var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
9972
10349
  var MAX_MEDIA_IMPORT_FAILURES = 20;
9973
10350
  var MAX_DELIVERY_FILES = 50;
@@ -10038,16 +10415,16 @@ var SUPPORTED_DELIVERY_EXTENSIONS = /* @__PURE__ */ new Set([
10038
10415
  ".m4a"
10039
10416
  ]);
10040
10417
  function resolveDeliveryStagingTarget(paths, stagingDir) {
10041
- const resolvedDir = path14.resolve(stagingDir);
10042
- const relative = path14.relative(path14.resolve(paths.conversationsDir), resolvedDir);
10043
- if (!relative || relative.startsWith("..") || path14.isAbsolute(relative)) {
10418
+ const resolvedDir = path15.resolve(stagingDir);
10419
+ const relative = path15.relative(path15.resolve(paths.conversationsDir), resolvedDir);
10420
+ if (!relative || relative.startsWith("..") || path15.isAbsolute(relative)) {
10044
10421
  throw new LinkHttpError(
10045
10422
  400,
10046
10423
  "delivery_staging_invalid",
10047
10424
  "delivery staging directory must be inside Hermes Link conversations"
10048
10425
  );
10049
10426
  }
10050
- const segments = relative.split(path14.sep);
10427
+ const segments = relative.split(path15.sep);
10051
10428
  if (segments.length !== 3 || segments[1] !== DELIVERY_STAGING_SEGMENT || !segments[0] || !segments[2]) {
10052
10429
  throw new LinkHttpError(
10053
10430
  400,
@@ -10083,7 +10460,7 @@ async function collectStagedDeliveryReferences(stagingDir) {
10083
10460
  return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isSupportedDeliveryFilename(entry.name)).sort(
10084
10461
  (left, right) => left.name.localeCompare(right.name, "en", { numeric: true })
10085
10462
  ).slice(0, MAX_DELIVERY_FILES).map((entry) => {
10086
- const sourcePath = path14.join(stagingDir, entry.name);
10463
+ const sourcePath = path15.join(stagingDir, entry.name);
10087
10464
  const mime = inferMimeType(sourcePath);
10088
10465
  return {
10089
10466
  path: sourcePath,
@@ -10269,7 +10646,7 @@ async function writeBlobFromFile(deps, conversationId, source) {
10269
10646
  }
10270
10647
  return deps.writeBlob(conversationId, {
10271
10648
  bytes: await readFile8(sourcePath),
10272
- filename: path14.basename(sourcePath),
10649
+ filename: path15.basename(sourcePath),
10273
10650
  mime: source.mime ?? inferMimeType(sourcePath)
10274
10651
  });
10275
10652
  }
@@ -10282,7 +10659,7 @@ function describeMediaImportFailure(reference, sourceKey, error) {
10282
10659
  };
10283
10660
  }
10284
10661
  function isSupportedDeliveryFilename(filename) {
10285
- return SUPPORTED_DELIVERY_EXTENSIONS.has(path14.extname(filename).toLowerCase());
10662
+ return SUPPORTED_DELIVERY_EXTENSIONS.has(path15.extname(filename).toLowerCase());
10286
10663
  }
10287
10664
  function readString8(payload, key) {
10288
10665
  const value = payload[key];
@@ -10339,7 +10716,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
10339
10716
  const candidates = [];
10340
10717
  for (const profileName of profileNames) {
10341
10718
  const profileDir = resolveHermesProfileDir(profileName);
10342
- const dbPath = path15.join(profileDir, "state.db");
10719
+ const dbPath = path16.join(profileDir, "state.db");
10343
10720
  const sessions = await listProfileSessions(dbPath).catch((error) => {
10344
10721
  result.errors.push({
10345
10722
  profile: profileName,
@@ -11575,8 +11952,8 @@ async function readJsonlMessages(profileName, sessionId) {
11575
11952
  return [];
11576
11953
  }
11577
11954
  const profileDir = resolveHermesProfileDir(profileName);
11578
- const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path15.join(profileDir, "sessions"));
11579
- const transcriptPath = path15.join(sessionsDir, `${sessionId}.jsonl`);
11955
+ const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path16.join(profileDir, "sessions"));
11956
+ const transcriptPath = path16.join(sessionsDir, `${sessionId}.jsonl`);
11580
11957
  const raw = await readFile9(transcriptPath, "utf8").catch((error) => {
11581
11958
  if (isNodeError11(error, "ENOENT")) {
11582
11959
  return "";
@@ -11731,7 +12108,7 @@ function toLinkMessage(input) {
11731
12108
  const sessionId = readString9(input.message, "session_id") ?? input.sessionId;
11732
12109
  const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
11733
12110
  return {
11734
- id: `msg_${randomUUID7().replaceAll("-", "")}`,
12111
+ id: `msg_${randomUUID8().replaceAll("-", "")}`,
11735
12112
  schema_version: 1,
11736
12113
  conversation_id: input.conversationId,
11737
12114
  role,
@@ -11903,7 +12280,7 @@ async function isFile(filePath) {
11903
12280
  });
11904
12281
  }
11905
12282
  function createConversationId() {
11906
- return `conv_${randomUUID7().replaceAll("-", "")}`;
12283
+ return `conv_${randomUUID8().replaceAll("-", "")}`;
11907
12284
  }
11908
12285
  function isoFromHermesTime(value) {
11909
12286
  const numeric = readNumber2(value);
@@ -11934,9 +12311,32 @@ function isNodeError11(error, code) {
11934
12311
  }
11935
12312
 
11936
12313
  // src/conversations/run-lifecycle.ts
12314
+ import { createHash as createHash4 } from "crypto";
11937
12315
  import { readdir as readdir8 } from "fs/promises";
11938
12316
 
11939
12317
  // src/hermes/api-server.ts
12318
+ var CAPABILITIES_CACHE_TTL_MS = 6e4;
12319
+ var capabilitiesCache = /* @__PURE__ */ new Map();
12320
+ async function readHermesApiCapabilities(options = {}) {
12321
+ const cacheKey = options.profileName ?? "default";
12322
+ const cached = options.fetchImpl ? null : capabilitiesCache.get(cacheKey) ?? null;
12323
+ if (!options.forceRefresh && cached && cached.expiresAt > Date.now()) {
12324
+ return cached.value;
12325
+ }
12326
+ const response = await callHermesApi(
12327
+ "/v1/capabilities",
12328
+ { method: "GET" },
12329
+ options
12330
+ );
12331
+ const capabilities = response.status === 404 ? legacyHermesApiCapabilities() : parseHermesApiCapabilities(await readJsonResponse(response));
12332
+ if (!options.fetchImpl) {
12333
+ capabilitiesCache.set(cacheKey, {
12334
+ expiresAt: Date.now() + CAPABILITIES_CACHE_TTL_MS,
12335
+ value: capabilities
12336
+ });
12337
+ }
12338
+ return capabilities;
12339
+ }
11940
12340
  async function listHermesModels(options = {}) {
11941
12341
  const response = await callHermesApi("/v1/models", { method: "GET" }, options);
11942
12342
  if (response.status === 404) {
@@ -12036,12 +12436,14 @@ async function runHermesCronJobAction(jobId, action, options = {}) {
12036
12436
  return { ...payload.job };
12037
12437
  }
12038
12438
  async function createHermesRun(input, options = {}) {
12439
+ const headers = new Headers({ "content-type": "application/json" });
12440
+ await applySessionKeyHeader(headers, input.session_key, options);
12039
12441
  const response = await callHermesApi(
12040
12442
  "/v1/runs",
12041
12443
  {
12042
12444
  method: "POST",
12043
12445
  body: JSON.stringify(input),
12044
- headers: { "content-type": "application/json" }
12446
+ headers
12045
12447
  },
12046
12448
  options
12047
12449
  );
@@ -12093,6 +12495,15 @@ async function streamHermesRunEvents(runId, options = {}) {
12093
12495
  });
12094
12496
  }
12095
12497
  async function streamHermesResponses(input, options = {}) {
12498
+ const requestHeaders = new Headers({
12499
+ accept: "text/event-stream",
12500
+ "content-type": "application/json"
12501
+ });
12502
+ const sessionKeyHeader = await applySessionKeyHeader(
12503
+ requestHeaders,
12504
+ input.session_key,
12505
+ options
12506
+ );
12096
12507
  const requestBody = {
12097
12508
  input: input.input,
12098
12509
  stream: true,
@@ -12102,19 +12513,32 @@ async function streamHermesResponses(input, options = {}) {
12102
12513
  ...input.session_id ? { session_id: input.session_id } : {},
12103
12514
  ...input.previous_response_id ? { previous_response_id: input.previous_response_id } : input.conversation_history && input.conversation_history.length > 0 ? { conversation_history: input.conversation_history } : {}
12104
12515
  };
12105
- const response = await callHermesApi(
12516
+ let response = await callHermesApi(
12106
12517
  "/v1/responses",
12107
12518
  {
12108
12519
  method: "POST",
12109
12520
  body: JSON.stringify(requestBody),
12110
- headers: {
12111
- accept: "text/event-stream",
12112
- "content-type": "application/json"
12113
- },
12521
+ headers: requestHeaders,
12114
12522
  signal: options.signal
12115
12523
  },
12116
12524
  options
12117
12525
  );
12526
+ if (response.status === 403 && sessionKeyHeader && await isSessionKeyRejected(response.clone())) {
12527
+ void options.logger?.warn("hermes_session_key_rejected_downgrading", {
12528
+ profile: options.profileName ?? "default"
12529
+ });
12530
+ requestHeaders.delete(sessionKeyHeader);
12531
+ response = await callHermesApi(
12532
+ "/v1/responses",
12533
+ {
12534
+ method: "POST",
12535
+ body: JSON.stringify(requestBody),
12536
+ headers: requestHeaders,
12537
+ signal: options.signal
12538
+ },
12539
+ options
12540
+ );
12541
+ }
12118
12542
  if (response.status === 404 || response.status === 503) {
12119
12543
  assertHermesRunsApiSupported(
12120
12544
  await readHermesVersion({ logger: options.logger }).catch(() => null),
@@ -12145,21 +12569,30 @@ async function streamHermesResponses(input, options = {}) {
12145
12569
  "x-hermes-session-id": response.headers.get(
12146
12570
  "x-hermes-session-id"
12147
12571
  )
12572
+ } : {},
12573
+ ...response.headers.get("x-hermes-session-key") ? {
12574
+ "x-hermes-session-key": response.headers.get(
12575
+ "x-hermes-session-key"
12576
+ )
12148
12577
  } : {}
12149
12578
  }
12150
12579
  });
12151
12580
  }
12152
12581
  async function cancelHermesRun(runId, options = {}) {
12153
- const response = await callHermesApi(
12154
- `/v1/runs/${encodeURIComponent(runId)}/cancel`,
12155
- { method: "POST" },
12156
- options
12157
- );
12582
+ const stopPath = await resolveHermesRunStopPath(runId, options);
12583
+ if (!stopPath) {
12584
+ throw new LinkHttpError(
12585
+ 501,
12586
+ "hermes_cancel_unsupported",
12587
+ "Hermes Agent does not expose a run stop endpoint; only Link-managed conversation runs can be cancelled."
12588
+ );
12589
+ }
12590
+ const response = await callHermesApi(stopPath, { method: "POST" }, options);
12158
12591
  if (response.status === 404 || response.status === 405 || response.status === 501) {
12159
12592
  throw new LinkHttpError(
12160
12593
  501,
12161
12594
  "hermes_cancel_unsupported",
12162
- "Hermes Agent does not expose a run cancel endpoint; only Link-managed conversation runs can be cancelled."
12595
+ "Hermes Agent does not expose a run stop endpoint; only Link-managed conversation runs can be cancelled."
12163
12596
  );
12164
12597
  }
12165
12598
  if (!response.ok) {
@@ -12170,10 +12603,84 @@ async function cancelHermesRun(runId, options = {}) {
12170
12603
  );
12171
12604
  }
12172
12605
  }
12173
- async function callHermesApi(path26, init, options) {
12606
+ async function resolveHermesRunStopPath(runId, options) {
12607
+ const encodedRunId = encodeURIComponent(runId);
12608
+ try {
12609
+ const capabilities = await readHermesApiCapabilities(options);
12610
+ if (capabilities.runStopPath) {
12611
+ return capabilities.runStopPath.replace("{run_id}", encodedRunId);
12612
+ }
12613
+ return null;
12614
+ } catch (error) {
12615
+ void options.logger?.debug("hermes_capabilities_probe_failed_for_stop", {
12616
+ profile: options.profileName ?? "default",
12617
+ error: error instanceof Error ? error.message : String(error)
12618
+ });
12619
+ return `/v1/runs/${encodedRunId}/stop`;
12620
+ }
12621
+ }
12622
+ async function applySessionKeyHeader(headers, sessionKey, options) {
12623
+ const normalized = sessionKey?.trim();
12624
+ if (!normalized) {
12625
+ return null;
12626
+ }
12627
+ let capabilities;
12628
+ try {
12629
+ capabilities = await readHermesApiCapabilities(options);
12630
+ } catch (error) {
12631
+ void options.logger?.debug("hermes_session_key_capabilities_unavailable", {
12632
+ profile: options.profileName ?? "default",
12633
+ error: error instanceof Error ? error.message : String(error)
12634
+ });
12635
+ return null;
12636
+ }
12637
+ if (capabilities.source !== "reported" || capabilities.authRequired !== true || !capabilities.sessionKeyHeader) {
12638
+ void options.logger?.debug("hermes_session_key_skipped", {
12639
+ profile: options.profileName ?? "default",
12640
+ source: capabilities.source,
12641
+ auth_required: capabilities.authRequired,
12642
+ supported: Boolean(capabilities.sessionKeyHeader)
12643
+ });
12644
+ return null;
12645
+ }
12646
+ headers.set(capabilities.sessionKeyHeader, normalized);
12647
+ return capabilities.sessionKeyHeader;
12648
+ }
12649
+ async function isSessionKeyRejected(response) {
12650
+ const message = await readUpstreamErrorMessage(response, "");
12651
+ return /X-Hermes-Session-Key|session key/iu.test(message);
12652
+ }
12653
+ function legacyHermesApiCapabilities() {
12654
+ return {
12655
+ source: "legacy",
12656
+ authRequired: null,
12657
+ responsesStreaming: null,
12658
+ runStopPath: null,
12659
+ sessionContinuityHeader: null,
12660
+ sessionKeyHeader: null
12661
+ };
12662
+ }
12663
+ function parseHermesApiCapabilities(payload) {
12664
+ const auth = isRecord(payload.auth) ? payload.auth : {};
12665
+ const features = isRecord(payload.features) ? payload.features : {};
12666
+ const endpoints = isRecord(payload.endpoints) ? payload.endpoints : {};
12667
+ const runStop = isRecord(endpoints.run_stop) ? endpoints.run_stop : {};
12668
+ return {
12669
+ source: "reported",
12670
+ authRequired: readBoolean2(auth, "required"),
12671
+ responsesStreaming: readBoolean2(features, "responses_streaming"),
12672
+ runStopPath: readBoolean2(features, "run_stop") === false ? null : readString10(runStop, "path"),
12673
+ sessionContinuityHeader: readString10(
12674
+ features,
12675
+ "session_continuity_header"
12676
+ ),
12677
+ sessionKeyHeader: readString10(features, "session_key_header")
12678
+ };
12679
+ }
12680
+ async function callHermesApi(path27, init, options) {
12174
12681
  const method = init.method ?? "GET";
12175
12682
  const startedAt = Date.now();
12176
- void options.logger?.debug("hermes_api_request_started", { method, path: path26 });
12683
+ void options.logger?.debug("hermes_api_request_started", { method, path: path27 });
12177
12684
  const availability = await ensureHermesApiServerAvailable({
12178
12685
  fetchImpl: options.fetchImpl,
12179
12686
  logger: options.logger,
@@ -12181,7 +12688,7 @@ async function callHermesApi(path26, init, options) {
12181
12688
  });
12182
12689
  let config = availability.configResult.apiServer;
12183
12690
  const fetcher = options.fetchImpl ?? fetch;
12184
- const request = () => fetchHermesApi(fetcher, config, path26, init, options);
12691
+ const request = () => fetchHermesApi(fetcher, config, path27, init, options);
12185
12692
  let response;
12186
12693
  try {
12187
12694
  response = await request();
@@ -12189,7 +12696,7 @@ async function callHermesApi(path26, init, options) {
12189
12696
  logHermesApiError(
12190
12697
  options.logger,
12191
12698
  method,
12192
- path26,
12699
+ path27,
12193
12700
  options.profileName,
12194
12701
  startedAt,
12195
12702
  error
@@ -12200,7 +12707,7 @@ async function callHermesApi(path26, init, options) {
12200
12707
  logHermesApiResponse(
12201
12708
  options.logger,
12202
12709
  method,
12203
- path26,
12710
+ path27,
12204
12711
  options.profileName,
12205
12712
  startedAt,
12206
12713
  response
@@ -12209,7 +12716,7 @@ async function callHermesApi(path26, init, options) {
12209
12716
  }
12210
12717
  void options.logger?.warn("hermes_api_request_retrying_after_401", {
12211
12718
  method,
12212
- path: path26,
12719
+ path: path27,
12213
12720
  profile: options.profileName ?? "default",
12214
12721
  port: config.port ?? null,
12215
12722
  duration_ms: Date.now() - startedAt
@@ -12227,7 +12734,7 @@ async function callHermesApi(path26, init, options) {
12227
12734
  logHermesApiError(
12228
12735
  options.logger,
12229
12736
  method,
12230
- path26,
12737
+ path27,
12231
12738
  options.profileName,
12232
12739
  startedAt,
12233
12740
  error
@@ -12237,7 +12744,7 @@ async function callHermesApi(path26, init, options) {
12237
12744
  logHermesApiResponse(
12238
12745
  options.logger,
12239
12746
  method,
12240
- path26,
12747
+ path27,
12241
12748
  options.profileName,
12242
12749
  startedAt,
12243
12750
  response
@@ -12247,7 +12754,7 @@ async function callHermesApi(path26, init, options) {
12247
12754
  }
12248
12755
  void options.logger?.warn("hermes_api_request_repairing_after_401", {
12249
12756
  method,
12250
- path: path26,
12757
+ path: path27,
12251
12758
  profile: options.profileName ?? "default",
12252
12759
  port: config.port ?? null,
12253
12760
  duration_ms: Date.now() - startedAt
@@ -12267,7 +12774,7 @@ async function callHermesApi(path26, init, options) {
12267
12774
  logHermesApiError(
12268
12775
  options.logger,
12269
12776
  method,
12270
- path26,
12777
+ path27,
12271
12778
  options.profileName,
12272
12779
  startedAt,
12273
12780
  error
@@ -12277,21 +12784,21 @@ async function callHermesApi(path26, init, options) {
12277
12784
  logHermesApiResponse(
12278
12785
  options.logger,
12279
12786
  method,
12280
- path26,
12787
+ path27,
12281
12788
  options.profileName,
12282
12789
  startedAt,
12283
12790
  response
12284
12791
  );
12285
12792
  return response;
12286
12793
  }
12287
- async function fetchHermesApi(fetcher, config, path26, init, options) {
12794
+ async function fetchHermesApi(fetcher, config, path27, init, options) {
12288
12795
  const headers = new Headers(init.headers);
12289
12796
  headers.set("accept", headers.get("accept") ?? "application/json");
12290
12797
  if (config.key) {
12291
12798
  headers.set("x-api-key", config.key);
12292
12799
  headers.set("authorization", `Bearer ${config.key}`);
12293
12800
  }
12294
- return await fetcher(`http://127.0.0.1:${config.port}${path26}`, {
12801
+ return await fetcher(`http://127.0.0.1:${config.port}${path27}`, {
12295
12802
  ...init,
12296
12803
  headers
12297
12804
  }).catch((error) => {
@@ -12300,10 +12807,10 @@ async function fetchHermesApi(fetcher, config, path26, init, options) {
12300
12807
  }
12301
12808
  void options.logger?.warn("hermes_api_server_connect_failed", {
12302
12809
  method: String(init.method ?? "GET").toUpperCase(),
12303
- path: path26,
12810
+ path: path27,
12304
12811
  profile: options.profileName ?? "default",
12305
12812
  port: config.port ?? null,
12306
- url: `http://127.0.0.1:${config.port}${path26}`,
12813
+ url: `http://127.0.0.1:${config.port}${path27}`,
12307
12814
  error: error instanceof Error ? error.message : String(error)
12308
12815
  });
12309
12816
  throw new LinkHttpError(
@@ -12313,10 +12820,10 @@ async function fetchHermesApi(fetcher, config, path26, init, options) {
12313
12820
  );
12314
12821
  });
12315
12822
  }
12316
- function logHermesApiResponse(logger, method, path26, profileName, startedAt, response) {
12823
+ function logHermesApiResponse(logger, method, path27, profileName, startedAt, response) {
12317
12824
  const fields = {
12318
12825
  method,
12319
- path: path26,
12826
+ path: path27,
12320
12827
  profile: profileName ?? "default",
12321
12828
  status: response.status,
12322
12829
  duration_ms: Date.now() - startedAt
@@ -12337,10 +12844,10 @@ async function logHermesApiFailureResponse(logger, fields, response) {
12337
12844
  ...upstreamError ? { upstream_error: upstreamError } : {}
12338
12845
  });
12339
12846
  }
12340
- function logHermesApiError(logger, method, path26, profileName, startedAt, error) {
12847
+ function logHermesApiError(logger, method, path27, profileName, startedAt, error) {
12341
12848
  void logger?.warn("hermes_api_request_failed", {
12342
12849
  method,
12343
- path: path26,
12850
+ path: path27,
12344
12851
  profile: profileName ?? "default",
12345
12852
  duration_ms: Date.now() - startedAt,
12346
12853
  ...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
@@ -12401,10 +12908,14 @@ function readString10(payload, key) {
12401
12908
  const value = payload[key];
12402
12909
  return typeof value === "string" && value.trim() ? value.trim() : null;
12403
12910
  }
12911
+ function readBoolean2(payload, key) {
12912
+ const value = payload[key];
12913
+ return typeof value === "boolean" ? value : null;
12914
+ }
12404
12915
 
12405
12916
  // src/conversations/history-builder.ts
12406
12917
  import { readFile as readFile10, stat as stat10 } from "fs/promises";
12407
- import path16 from "path";
12918
+ import path17 from "path";
12408
12919
  var HISTORY_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
12409
12920
  var HERMES_HISTORY_COLUMNS = [
12410
12921
  "role",
@@ -12465,13 +12976,13 @@ async function readHermesTranscriptHistory(sessionId, profileName) {
12465
12976
  }
12466
12977
  const normalizedProfileName = isValidProfileName2(profileName) ? profileName : "default";
12467
12978
  const profileDir = resolveHermesProfileDir(normalizedProfileName);
12468
- const dbPath = path16.join(profileDir, "state.db");
12979
+ const dbPath = path17.join(profileDir, "state.db");
12469
12980
  const sessionsDirConfig = await readHermesSessionsDir(normalizedProfileName).then((value) => ({
12470
12981
  sessionsDir: value.sessionsDir,
12471
12982
  configured: value.configured,
12472
12983
  configError: false
12473
12984
  })).catch(() => ({
12474
- sessionsDir: path16.join(profileDir, "sessions"),
12985
+ sessionsDir: path17.join(profileDir, "sessions"),
12475
12986
  configured: false,
12476
12987
  configError: true
12477
12988
  }));
@@ -12529,7 +13040,7 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
12529
13040
  if (!isValidSessionFileStem(sessionId)) {
12530
13041
  return empty;
12531
13042
  }
12532
- const transcriptPath = path16.join(sessionsDir, `${sessionId}.jsonl`);
13043
+ const transcriptPath = path17.join(sessionsDir, `${sessionId}.jsonl`);
12533
13044
  const raw = await readFile10(transcriptPath, "utf8").catch((error) => {
12534
13045
  if (isNodeError12(error, "ENOENT")) {
12535
13046
  return "";
@@ -12772,7 +13283,7 @@ function normalizeProfileForCompare(value) {
12772
13283
  // src/hermes/stt.ts
12773
13284
  import { execFile as execFile3 } from "child_process";
12774
13285
  import { access as access2, readFile as readFile11, stat as stat11 } from "fs/promises";
12775
- import path17 from "path";
13286
+ import path18 from "path";
12776
13287
  import { promisify as promisify3 } from "util";
12777
13288
  var execFileAsync3 = promisify3(execFile3);
12778
13289
  var STT_RESULT_PREFIX = "__HERMES_LINK_STT__";
@@ -12867,7 +13378,7 @@ async function buildHermesSttEnv(profileName) {
12867
13378
  };
12868
13379
  const devSource = await findDevHermesAgentSource();
12869
13380
  if (devSource) {
12870
- env.PYTHONPATH = [devSource, env.PYTHONPATH].filter(Boolean).join(path17.delimiter);
13381
+ env.PYTHONPATH = [devSource, env.PYTHONPATH].filter(Boolean).join(path18.delimiter);
12871
13382
  }
12872
13383
  return env;
12873
13384
  }
@@ -12914,14 +13425,14 @@ async function resolveHermesPythonCommand() {
12914
13425
  };
12915
13426
  }
12916
13427
  async function resolveExecutablePath2(command) {
12917
- if (path17.isAbsolute(command)) {
13428
+ if (path18.isAbsolute(command)) {
12918
13429
  return await isExecutableFile(command) ? command : null;
12919
13430
  }
12920
13431
  const pathEnv = process.env.PATH ?? "";
12921
13432
  const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
12922
- for (const dir of pathEnv.split(path17.delimiter)) {
13433
+ for (const dir of pathEnv.split(path18.delimiter)) {
12923
13434
  for (const extension of extensions) {
12924
- const candidate = path17.join(dir, `${command}${extension}`);
13435
+ const candidate = path18.join(dir, `${command}${extension}`);
12925
13436
  if (await isExecutableFile(candidate)) {
12926
13437
  return candidate;
12927
13438
  }
@@ -12961,8 +13472,8 @@ function shebangToPythonCommand(shebang) {
12961
13472
  }
12962
13473
  async function findDevHermesAgentSource() {
12963
13474
  const candidates = [
12964
- path17.resolve(process.cwd(), "reference/hermes-agent"),
12965
- path17.resolve(process.cwd(), "../../reference/hermes-agent")
13475
+ path18.resolve(process.cwd(), "reference/hermes-agent"),
13476
+ path18.resolve(process.cwd(), "../../reference/hermes-agent")
12966
13477
  ];
12967
13478
  for (const candidate of candidates) {
12968
13479
  if (await isDirectory(candidate)) {
@@ -12979,6 +13490,71 @@ function compactProcessOutput(value) {
12979
13490
  return compact || null;
12980
13491
  }
12981
13492
 
13493
+ // src/identity/identity.ts
13494
+ import { generateKeyPairSync, randomUUID as randomUUID9, sign } from "crypto";
13495
+ import { mkdir as mkdir10, chmod as chmod2 } from "fs/promises";
13496
+ import { z } from "zod";
13497
+ var linkIdentitySchema = z.object({
13498
+ install_id: z.string().min(1),
13499
+ link_id: z.string().min(1).nullable().optional(),
13500
+ public_key_pem: z.string().min(1),
13501
+ private_key_pem: z.string().min(1),
13502
+ created_at: z.string().min(1),
13503
+ updated_at: z.string().min(1)
13504
+ });
13505
+ async function loadIdentity(paths = resolveRuntimePaths()) {
13506
+ const value = await readJsonFile(paths.identityFile);
13507
+ if (value === null) {
13508
+ return null;
13509
+ }
13510
+ return linkIdentitySchema.parse(value);
13511
+ }
13512
+ async function ensureIdentity(paths = resolveRuntimePaths()) {
13513
+ const existing = await loadIdentity(paths);
13514
+ if (existing) {
13515
+ return existing;
13516
+ }
13517
+ await mkdir10(paths.homeDir, { recursive: true, mode: 448 });
13518
+ await chmod2(paths.homeDir, 448).catch(() => void 0);
13519
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
13520
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13521
+ const identity = {
13522
+ install_id: `install_${randomUUID9().replaceAll("-", "")}`,
13523
+ link_id: null,
13524
+ public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
13525
+ private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
13526
+ created_at: now,
13527
+ updated_at: now
13528
+ };
13529
+ await writeJsonFile(paths.identityFile, identity);
13530
+ return identity;
13531
+ }
13532
+ async function saveAssignedLinkId(linkId, paths = resolveRuntimePaths()) {
13533
+ const identity = await ensureIdentity(paths);
13534
+ const next = {
13535
+ ...identity,
13536
+ link_id: linkId,
13537
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
13538
+ };
13539
+ await writeJsonFile(paths.identityFile, next);
13540
+ return next;
13541
+ }
13542
+ function signRelayNonce(identity, nonce) {
13543
+ return signIdentityPayload(identity, nonce);
13544
+ }
13545
+ function signIdentityPayload(identity, payload) {
13546
+ const signature = sign(null, Buffer.from(payload, "utf8"), identity.private_key_pem);
13547
+ return signature.toString("base64url");
13548
+ }
13549
+ function getIdentityStatus(identity) {
13550
+ return {
13551
+ installId: identity.install_id,
13552
+ linkId: identity.link_id ?? null,
13553
+ hasPrivateKey: identity.private_key_pem.trim().length > 0,
13554
+ publicKeyPem: identity.public_key_pem
13555
+ };
13556
+ }
13557
+
12982
13558
  // src/conversations/hermes-sse.ts
12983
13559
  async function* parseSseResponse(response) {
12984
13560
  if (!response.body) {
@@ -13546,6 +14122,10 @@ var ConversationRunLifecycle = class {
13546
14122
  input: resolvedInput,
13547
14123
  instructions,
13548
14124
  session_id: hermesSessionId,
14125
+ session_key: await this.buildHermesSessionKey(
14126
+ conversationId,
14127
+ run.profile ?? "default"
14128
+ ),
13549
14129
  model: run.model,
13550
14130
  ...previousResponseId ? { previous_response_id: previousResponseId } : {},
13551
14131
  ...conversationHistory.messages.length > 0 ? { conversation_history: conversationHistory.messages } : {}
@@ -13575,7 +14155,7 @@ var ConversationRunLifecycle = class {
13575
14155
  );
13576
14156
  return;
13577
14157
  }
13578
- if (!await this.deps.isConversationActive(conversationId)) {
14158
+ if (!await this.deps.isConversationRunnable(conversationId)) {
13579
14159
  return;
13580
14160
  }
13581
14161
  const event = normalizeHermesStreamEvent(rawEvent);
@@ -13766,7 +14346,7 @@ ${attachmentLines.join("\n")}`
13766
14346
  }
13767
14347
  run.hermes_session_id = normalizedSessionId;
13768
14348
  await this.deps.writeSnapshot(conversationId, snapshot);
13769
- const manifest = await this.deps.readActiveManifest(conversationId);
14349
+ const manifest = await this.deps.readRunnableManifest(conversationId);
13770
14350
  const nextManifest = addHermesSessionIdToManifest(
13771
14351
  manifest,
13772
14352
  normalizedSessionId
@@ -13850,7 +14430,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
13850
14430
  );
13851
14431
  }
13852
14432
  async persistHermesEventLocked(conversationId, runId, event) {
13853
- if (!await this.deps.isConversationActive(conversationId)) {
14433
+ if (!await this.deps.isConversationRunnable(conversationId)) {
13854
14434
  return;
13855
14435
  }
13856
14436
  if (event.payloadType === "message.delta") {
@@ -14178,7 +14758,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
14178
14758
  });
14179
14759
  }
14180
14760
  async cancelRunLocked(conversationId, runId, options) {
14181
- const manifest = await this.deps.readActiveManifest(conversationId);
14761
+ const manifest = await this.deps.readRunnableManifest(conversationId);
14182
14762
  const snapshot = await this.deps.readSnapshot(conversationId);
14183
14763
  const run = snapshot.runs.find((item) => item.id === runId);
14184
14764
  if (!run) {
@@ -14250,6 +14830,15 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
14250
14830
  last_event_seq: event.seq
14251
14831
  };
14252
14832
  }
14833
+ async buildHermesSessionKey(conversationId, profileName) {
14834
+ const identity = await loadIdentity(this.deps.paths).catch(() => null);
14835
+ const linkScope = identity?.link_id?.trim() || identity?.install_id?.trim() || "local";
14836
+ const raw = `hermespilot:${linkScope}:profile:${profileName}:conversation:${conversationId}`;
14837
+ if (raw.length <= 200) {
14838
+ return raw;
14839
+ }
14840
+ return `hermespilot:${createHash4("sha256").update(raw).digest("hex")}`;
14841
+ }
14253
14842
  async assistantMessageIdForRun(conversationId, runId) {
14254
14843
  const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
14255
14844
  return snapshot?.runs.find((item) => item.id === runId)?.assistant_message_id;
@@ -14611,6 +15200,20 @@ function isNodeError13(error, code) {
14611
15200
 
14612
15201
  // src/conversations/conversation-service.ts
14613
15202
  var ALL_CONVERSATION_EVENTS = "conversation:*";
15203
+ function isConversationNotificationEvent(event) {
15204
+ const type = event.type.toLowerCase();
15205
+ return type === "conversation.created" || type === "conversation.updated" || isAlwaysPublishedNotificationEvent(type) || type === "message.created" || type === "message.completed" || type === "message.failed" || type === "run.completed" || type === "run.failed" || type === "run.cancelled" || type === "run.canceled" || type === "approval.requested" || readPayloadBool(event.payload, "requires_action") || readPayloadBool(event.payload, "requires_user_action") || readPayloadBool(event.payload, "requires_approval");
15206
+ }
15207
+ function isAlwaysPublishedNotificationEvent(type) {
15208
+ const normalized = type.toLowerCase();
15209
+ return normalized === "conversation.archived" || normalized === "conversation.unarchived" || normalized === "conversation.deleted";
15210
+ }
15211
+ function readPayloadBool(payload, key) {
15212
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
15213
+ return false;
15214
+ }
15215
+ return payload[key] === true;
15216
+ }
14614
15217
  var ConversationService = class {
14615
15218
  constructor(paths, logger) {
14616
15219
  this.paths = paths;
@@ -14659,8 +15262,10 @@ var ConversationService = class {
14659
15262
  statsOverride
14660
15263
  ),
14661
15264
  readActiveManifest: (conversationId) => this.store.readActiveManifest(conversationId),
15265
+ readRunnableManifest: (conversationId) => this.store.readRunnableManifest(conversationId),
14662
15266
  writeManifest: (manifest) => this.store.writeManifest(manifest),
14663
15267
  isConversationActive: (conversationId) => this.store.isConversationActive(conversationId),
15268
+ isConversationRunnable: (conversationId) => this.store.isConversationRunnable(conversationId),
14664
15269
  writeBlob: (conversationId, input) => this.maintenance.writeBlob(conversationId, input),
14665
15270
  syncCronDeliveries: () => this.syncCronDeliveries(),
14666
15271
  scheduleTitleRefresh: (conversationId) => this.metadata.scheduleGeneratedTitleRefresh(conversationId)
@@ -14676,6 +15281,9 @@ var ConversationService = class {
14676
15281
  resolveMessageAttachmentParts: (conversationId, attachments) => this.maintenance.resolveMessageAttachmentParts(
14677
15282
  conversationId,
14678
15283
  attachments
15284
+ ),
15285
+ restoreArchivedConversationForUserContinuation: (conversationId) => this.restoreArchivedConversationForUserContinuationLocked(
15286
+ conversationId
14679
15287
  )
14680
15288
  });
14681
15289
  }
@@ -14710,6 +15318,30 @@ var ConversationService = class {
14710
15318
  }
14711
15319
  }
14712
15320
  }
15321
+ async restoreArchivedConversationForUserContinuationLocked(conversationId) {
15322
+ const manifest = await this.store.readRunnableManifest(conversationId);
15323
+ if (manifest.status === "active") {
15324
+ return manifest;
15325
+ }
15326
+ const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
15327
+ const unarchivedAt = (/* @__PURE__ */ new Date()).toISOString();
15328
+ const next = {
15329
+ ...manifest,
15330
+ status: "active",
15331
+ updated_at: unarchivedAt
15332
+ };
15333
+ delete next.archived_at;
15334
+ await this.store.writeManifest(next);
15335
+ await this.appendEvent(conversationId, {
15336
+ type: "conversation.unarchived",
15337
+ payload: {
15338
+ unarchived_at: unarchivedAt,
15339
+ reason: "user_continuation"
15340
+ }
15341
+ });
15342
+ await this.metadata.persistConversationStats(conversationId, snapshot);
15343
+ return this.store.readActiveManifest(conversationId);
15344
+ }
14713
15345
  async listConversations() {
14714
15346
  return this.queries.listConversations();
14715
15347
  }
@@ -14719,6 +15351,12 @@ var ConversationService = class {
14719
15351
  searchConversationPage(input = {}) {
14720
15352
  return this.queries.searchConversationPage(input);
14721
15353
  }
15354
+ listArchivedConversationPage(input = {}) {
15355
+ return this.queries.listArchivedConversationPage(input);
15356
+ }
15357
+ searchArchivedConversationPage(input = {}) {
15358
+ return this.queries.searchArchivedConversationPage(input);
15359
+ }
14722
15360
  async getStatistics(filter = {}) {
14723
15361
  return readLinkStatistics(this.paths, filter);
14724
15362
  }
@@ -14726,15 +15364,11 @@ var ConversationService = class {
14726
15364
  const records = [];
14727
15365
  const usageFacts = [];
14728
15366
  for (const conversationId of await this.store.listConversationIds()) {
14729
- const manifest = await this.store.readManifest(conversationId).catch(
14730
- () => null
14731
- );
15367
+ const manifest = await this.store.readManifest(conversationId).catch(() => null);
14732
15368
  if (!manifest) {
14733
15369
  continue;
14734
15370
  }
14735
- const snapshot = await this.store.readSnapshot(conversationId).catch(
14736
- () => emptySnapshot2()
14737
- );
15371
+ const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
14738
15372
  records.push(
14739
15373
  toStatsIndexRecord(
14740
15374
  manifest,
@@ -14758,7 +15392,7 @@ var ConversationService = class {
14758
15392
  async createConversation(input = {}) {
14759
15393
  await this.store.ensureConversationsDir();
14760
15394
  const now = (/* @__PURE__ */ new Date()).toISOString();
14761
- const id = `conv_${randomUUID8().replaceAll("-", "")}`;
15395
+ const id = `conv_${randomUUID10().replaceAll("-", "")}`;
14762
15396
  const title = input.title?.trim() || DEFAULT_CONVERSATION_TITLE;
14763
15397
  const profile = await resolveConversationProfileTarget(
14764
15398
  this.paths,
@@ -14811,9 +15445,7 @@ var ConversationService = class {
14811
15445
  async ensureCronInboxConversation(input = {}) {
14812
15446
  const profileName = normalizeProfileName2(input.profileName);
14813
15447
  for (const conversationId of await this.store.listConversationIds()) {
14814
- const manifest = await this.store.readManifest(conversationId).catch(
14815
- () => null
14816
- );
15448
+ const manifest = await this.store.readManifest(conversationId).catch(() => null);
14817
15449
  if (manifest?.status === "active" && manifest.title === "HermesLink \u5B9A\u65F6\u4EFB\u52A1" && normalizeProfileName2(
14818
15450
  manifest.profile_name_snapshot ?? manifest.profile
14819
15451
  ) === profileName) {
@@ -14845,7 +15477,7 @@ var ConversationService = class {
14845
15477
  manifest.profile_name_snapshot ?? manifest.profile ?? input.profileName
14846
15478
  );
14847
15479
  const message = {
14848
- id: `msg_${randomUUID8().replaceAll("-", "")}`,
15480
+ id: `msg_${randomUUID10().replaceAll("-", "")}`,
14849
15481
  schema_version: 1,
14850
15482
  conversation_id: manifest.id,
14851
15483
  role: "assistant",
@@ -14915,7 +15547,7 @@ var ConversationService = class {
14915
15547
  async deliverStagedFiles(stagingDir) {
14916
15548
  const target = resolveDeliveryStagingTarget(this.paths, stagingDir);
14917
15549
  return this.withConversationLock(target.conversationId, async () => {
14918
- await this.store.readActiveManifest(target.conversationId);
15550
+ await this.store.readRunnableManifest(target.conversationId);
14919
15551
  const snapshot = await this.store.readSnapshot(target.conversationId);
14920
15552
  const run = snapshot.runs.find((item) => item.id === target.runId);
14921
15553
  if (!run) {
@@ -14961,7 +15593,9 @@ var ConversationService = class {
14961
15593
  );
14962
15594
  }
14963
15595
  return this.withConversationLock(conversationId, async () => {
14964
- const manifest = await this.store.readActiveManifest(conversationId);
15596
+ const manifest = await this.restoreArchivedConversationForUserContinuationLocked(
15597
+ conversationId
15598
+ );
14965
15599
  const snapshot = await this.store.readSnapshot(conversationId);
14966
15600
  const nextManifest = {
14967
15601
  ...manifest,
@@ -14995,7 +15629,7 @@ var ConversationService = class {
14995
15629
  }
14996
15630
  async setConversationProfile(conversationId, profileName) {
14997
15631
  return this.withConversationLock(conversationId, async () => {
14998
- const manifest = await this.store.readActiveManifest(conversationId);
15632
+ let manifest = await this.store.readRunnableManifest(conversationId);
14999
15633
  const snapshot = await this.store.readSnapshot(conversationId);
15000
15634
  if (hasRunningRuns(snapshot) || hasQueuedRuns(snapshot) || this.hasActiveRunControllerForConversation(conversationId)) {
15001
15635
  throw new LinkHttpError(
@@ -15004,6 +15638,9 @@ var ConversationService = class {
15004
15638
  "This conversation has an active or queued run. Wait for it to finish before switching profile."
15005
15639
  );
15006
15640
  }
15641
+ manifest = await this.restoreArchivedConversationForUserContinuationLocked(
15642
+ conversationId
15643
+ );
15007
15644
  const profile = await resolveConversationProfileTarget(
15008
15645
  this.paths,
15009
15646
  profileName
@@ -15098,7 +15735,9 @@ var ConversationService = class {
15098
15735
  }
15099
15736
  async renameConversation(conversationId, title) {
15100
15737
  return this.withConversationLock(conversationId, async () => {
15101
- const manifest = await this.store.readActiveManifest(conversationId);
15738
+ const manifest = await this.restoreArchivedConversationForUserContinuationLocked(
15739
+ conversationId
15740
+ );
15102
15741
  const snapshot = await this.store.readSnapshot(conversationId);
15103
15742
  return this.renameConversationLocked(conversationId, title, {
15104
15743
  manifest,
@@ -15136,6 +15775,16 @@ var ConversationService = class {
15136
15775
  this.emitter.on(ALL_CONVERSATION_EVENTS, listener);
15137
15776
  return () => this.emitter.off(ALL_CONVERSATION_EVENTS, listener);
15138
15777
  }
15778
+ async shouldPublishNotificationEvent(event) {
15779
+ if (!isConversationNotificationEvent(event)) {
15780
+ return false;
15781
+ }
15782
+ if (isAlwaysPublishedNotificationEvent(event.type)) {
15783
+ return true;
15784
+ }
15785
+ const manifest = await this.store.readManifest(event.conversation_id).catch(() => null);
15786
+ return manifest?.status === "active";
15787
+ }
15139
15788
  async sendMessage(input) {
15140
15789
  return this.orchestration.sendMessage(input);
15141
15790
  }
@@ -15154,9 +15803,7 @@ var ConversationService = class {
15154
15803
  if (active) {
15155
15804
  return this.cancelRun(active.conversationId, runId);
15156
15805
  }
15157
- const conversationId = await this.runLifecycle.findConversationIdForRun(
15158
- runId
15159
- );
15806
+ const conversationId = await this.runLifecycle.findConversationIdForRun(runId);
15160
15807
  if (!conversationId) {
15161
15808
  throw new LinkHttpError(404, "run_not_found", "Run was not found");
15162
15809
  }
@@ -15179,7 +15826,7 @@ var ConversationService = class {
15179
15826
  );
15180
15827
  }
15181
15828
  return this.withConversationLock(input.conversationId, async () => {
15182
- const manifest = await this.store.readActiveManifest(input.conversationId);
15829
+ let manifest = await this.store.readRunnableManifest(input.conversationId);
15183
15830
  const snapshot = await this.store.readSnapshot(input.conversationId);
15184
15831
  const match = findApproval(snapshot, input.approvalId);
15185
15832
  if (!match) {
@@ -15201,6 +15848,9 @@ var ConversationService = class {
15201
15848
  last_event_seq: manifest.last_event_seq
15202
15849
  };
15203
15850
  }
15851
+ manifest = await this.restoreArchivedConversationForUserContinuationLocked(
15852
+ input.conversationId
15853
+ );
15204
15854
  const resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
15205
15855
  let commandAllowlistUpdated = false;
15206
15856
  let configPath;
@@ -15307,10 +15957,17 @@ var ConversationService = class {
15307
15957
  });
15308
15958
  }
15309
15959
  async deleteConversation(conversationId) {
15960
+ this.abortActiveRunsForConversation(conversationId);
15310
15961
  return this.maintenance.deleteConversation(conversationId);
15311
15962
  }
15312
- prepareClearAllConversationPlan() {
15313
- return this.maintenance.prepareClearAllConversationPlan();
15963
+ async archiveConversation(conversationId) {
15964
+ return this.maintenance.archiveConversation(conversationId);
15965
+ }
15966
+ async unarchiveConversation(conversationId) {
15967
+ return this.maintenance.unarchiveConversation(conversationId);
15968
+ }
15969
+ prepareClearAllConversationPlan(targetStatus) {
15970
+ return this.maintenance.prepareClearAllConversationPlan(targetStatus);
15314
15971
  }
15315
15972
  readClearAllConversationPlan(planId) {
15316
15973
  return this.maintenance.readClearAllConversationPlan(planId);
@@ -15321,6 +15978,18 @@ var ConversationService = class {
15321
15978
  startClearAllConversationPlan(planId) {
15322
15979
  return this.maintenance.startClearAllConversationPlan(planId);
15323
15980
  }
15981
+ prepareArchiveAllConversationPlan(input = {}) {
15982
+ return this.maintenance.prepareArchiveAllConversationPlan(input);
15983
+ }
15984
+ readArchiveAllConversationPlan(planId) {
15985
+ return this.maintenance.readArchiveAllConversationPlan(planId);
15986
+ }
15987
+ executeArchiveAllConversationPlan(planId) {
15988
+ return this.maintenance.executeArchiveAllConversationPlan(planId);
15989
+ }
15990
+ startArchiveAllConversationPlan(planId) {
15991
+ return this.maintenance.startArchiveAllConversationPlan(planId);
15992
+ }
15324
15993
  async deleteConversations(conversationIds) {
15325
15994
  return this.maintenance.deleteConversations(conversationIds);
15326
15995
  }
@@ -15329,50 +15998,47 @@ var ConversationService = class {
15329
15998
  const profileUid = input.profileUid?.trim() || null;
15330
15999
  const deletedConversationIds = [];
15331
16000
  for (const conversationId of await this.store.listConversationIds()) {
15332
- const manifest = await this.store.readManifest(conversationId).catch(
15333
- () => null
15334
- );
16001
+ const manifest = await this.store.readManifest(conversationId).catch(() => null);
15335
16002
  if (!manifest || !conversationMatchesProfile(manifest, profileName, profileUid)) {
15336
16003
  continue;
15337
16004
  }
15338
- const deleted = await this.withConversationLock(conversationId, async () => {
15339
- const lockedManifest = await this.store.readManifest(conversationId).catch(
15340
- () => null
15341
- );
15342
- if (!lockedManifest || !conversationMatchesProfile(lockedManifest, profileName, profileUid)) {
15343
- return false;
15344
- }
15345
- this.abortActiveRunsForConversation(conversationId);
15346
- const snapshot = await this.store.readSnapshot(conversationId).catch(
15347
- () => emptySnapshot2()
15348
- );
15349
- const blobIds = /* @__PURE__ */ new Set([
15350
- ...collectBlobIds(snapshot),
15351
- ...await listConversationBlobIds(this.paths, conversationId).catch(
15352
- () => []
15353
- )
15354
- ]);
15355
- for (const blobId of blobIds) {
15356
- await pruneConversationBlobReference(
15357
- this.paths,
15358
- conversationId,
15359
- blobId
15360
- ).catch((error) => {
15361
- void this.logger.warn("profile_delete_blob_gc_failed", {
15362
- profile: profileName,
15363
- conversation_id: conversationId,
15364
- blob_id: blobId,
15365
- error: error instanceof Error ? error.message : String(error)
16005
+ const deleted = await this.withConversationLock(
16006
+ conversationId,
16007
+ async () => {
16008
+ const lockedManifest = await this.store.readManifest(conversationId).catch(() => null);
16009
+ if (!lockedManifest || !conversationMatchesProfile(lockedManifest, profileName, profileUid)) {
16010
+ return false;
16011
+ }
16012
+ this.abortActiveRunsForConversation(conversationId);
16013
+ const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
16014
+ const blobIds = /* @__PURE__ */ new Set([
16015
+ ...collectBlobIds(snapshot),
16016
+ ...await listConversationBlobIds(this.paths, conversationId).catch(
16017
+ () => []
16018
+ )
16019
+ ]);
16020
+ for (const blobId of blobIds) {
16021
+ await pruneConversationBlobReference(
16022
+ this.paths,
16023
+ conversationId,
16024
+ blobId
16025
+ ).catch((error) => {
16026
+ void this.logger.warn("profile_delete_blob_gc_failed", {
16027
+ profile: profileName,
16028
+ conversation_id: conversationId,
16029
+ blob_id: blobId,
16030
+ error: error instanceof Error ? error.message : String(error)
16031
+ });
15366
16032
  });
15367
- });
16033
+ }
16034
+ await removeConversationFiles(this.paths, conversationId);
16035
+ await removeConversationDeliveryStaging(
16036
+ this.paths,
16037
+ conversationId
16038
+ ).catch(() => void 0);
16039
+ return true;
15368
16040
  }
15369
- await removeConversationFiles(this.paths, conversationId);
15370
- await removeConversationDeliveryStaging(
15371
- this.paths,
15372
- conversationId
15373
- ).catch(() => void 0);
15374
- return true;
15375
- });
16041
+ );
15376
16042
  if (deleted) {
15377
16043
  deletedConversationIds.push(conversationId);
15378
16044
  }
@@ -15448,73 +16114,8 @@ function findApproval(snapshot, approvalId) {
15448
16114
  return null;
15449
16115
  }
15450
16116
 
15451
- // src/identity/identity.ts
15452
- import { generateKeyPairSync, randomUUID as randomUUID9, sign } from "crypto";
15453
- import { mkdir as mkdir9, chmod as chmod2 } from "fs/promises";
15454
- import { z } from "zod";
15455
- var linkIdentitySchema = z.object({
15456
- install_id: z.string().min(1),
15457
- link_id: z.string().min(1).nullable().optional(),
15458
- public_key_pem: z.string().min(1),
15459
- private_key_pem: z.string().min(1),
15460
- created_at: z.string().min(1),
15461
- updated_at: z.string().min(1)
15462
- });
15463
- async function loadIdentity(paths = resolveRuntimePaths()) {
15464
- const value = await readJsonFile(paths.identityFile);
15465
- if (value === null) {
15466
- return null;
15467
- }
15468
- return linkIdentitySchema.parse(value);
15469
- }
15470
- async function ensureIdentity(paths = resolveRuntimePaths()) {
15471
- const existing = await loadIdentity(paths);
15472
- if (existing) {
15473
- return existing;
15474
- }
15475
- await mkdir9(paths.homeDir, { recursive: true, mode: 448 });
15476
- await chmod2(paths.homeDir, 448).catch(() => void 0);
15477
- const { publicKey, privateKey } = generateKeyPairSync("ed25519");
15478
- const now = (/* @__PURE__ */ new Date()).toISOString();
15479
- const identity = {
15480
- install_id: `install_${randomUUID9().replaceAll("-", "")}`,
15481
- link_id: null,
15482
- public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
15483
- private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
15484
- created_at: now,
15485
- updated_at: now
15486
- };
15487
- await writeJsonFile(paths.identityFile, identity);
15488
- return identity;
15489
- }
15490
- async function saveAssignedLinkId(linkId, paths = resolveRuntimePaths()) {
15491
- const identity = await ensureIdentity(paths);
15492
- const next = {
15493
- ...identity,
15494
- link_id: linkId,
15495
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
15496
- };
15497
- await writeJsonFile(paths.identityFile, next);
15498
- return next;
15499
- }
15500
- function signRelayNonce(identity, nonce) {
15501
- return signIdentityPayload(identity, nonce);
15502
- }
15503
- function signIdentityPayload(identity, payload) {
15504
- const signature = sign(null, Buffer.from(payload, "utf8"), identity.private_key_pem);
15505
- return signature.toString("base64url");
15506
- }
15507
- function getIdentityStatus(identity) {
15508
- return {
15509
- installId: identity.install_id,
15510
- linkId: identity.link_id ?? null,
15511
- hasPrivateKey: identity.private_key_pem.trim().length > 0,
15512
- publicKeyPem: identity.public_key_pem
15513
- };
15514
- }
15515
-
15516
16117
  // src/security/devices.ts
15517
- import { randomBytes as randomBytes2, randomUUID as randomUUID10, timingSafeEqual, createHash as createHash4 } from "crypto";
16118
+ import { randomBytes as randomBytes2, randomUUID as randomUUID11, timingSafeEqual, createHash as createHash5 } from "crypto";
15518
16119
  var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
15519
16120
  var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
15520
16121
  var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
@@ -15532,7 +16133,7 @@ async function createDeviceSession(input, paths = resolveRuntimePaths()) {
15532
16133
  }
15533
16134
  }
15534
16135
  const device = {
15535
- id: `dev_${randomUUID10().replaceAll("-", "")}`,
16136
+ id: `dev_${randomUUID11().replaceAll("-", "")}`,
15536
16137
  label: normalizeDeviceLabel(input.label),
15537
16138
  platform: normalizeDevicePlatform(input.platform),
15538
16139
  model: normalizeDeviceModel(input.model),
@@ -15810,7 +16411,7 @@ function randomToken(prefix) {
15810
16411
  return `${prefix}${randomBytes2(24).toString("base64url")}`;
15811
16412
  }
15812
16413
  function sha256(value) {
15813
- return createHash4("sha256").update(value).digest("hex");
16414
+ return createHash5("sha256").update(value).digest("hex");
15814
16415
  }
15815
16416
  function safeEqual(left, right) {
15816
16417
  const leftBytes = Buffer.from(left);
@@ -16024,7 +16625,7 @@ function readPositiveInteger2(value) {
16024
16625
  }
16025
16626
  return void 0;
16026
16627
  }
16027
- function readBoolean2(value) {
16628
+ function readBoolean3(value) {
16028
16629
  if (typeof value === "boolean") {
16029
16630
  return value;
16030
16631
  }
@@ -16145,7 +16746,7 @@ function readMessageAttachments(value) {
16145
16746
  }
16146
16747
  const kind = readAttachmentString(record.kind);
16147
16748
  const type = readAttachmentString(record.type);
16148
- const isVoiceNote = readBoolean2(record.is_voice_note) ?? readBoolean2(record.isVoiceNote);
16749
+ const isVoiceNote = readBoolean3(record.is_voice_note) ?? readBoolean3(record.isVoiceNote);
16149
16750
  const durationMs = readPositiveInteger2(record.duration_ms) ?? readPositiveInteger2(record.durationMs);
16150
16751
  const waveform = readAttachmentWaveform2(
16151
16752
  record.waveform ?? record.waveform_samples ?? record.waveformSamples
@@ -16320,6 +16921,33 @@ function registerConversationRoutes(router, options) {
16320
16921
  page: result.page
16321
16922
  };
16322
16923
  });
16924
+ router.get("/api/v1/conversations/archived", async (ctx) => {
16925
+ await authenticateRequest(ctx, paths);
16926
+ ctx.set("cache-control", "no-store");
16927
+ const result = await conversations.listArchivedConversationPage({
16928
+ limit: readLimit(ctx.query.limit),
16929
+ cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after) ?? readQueryString(ctx.query.page_cursor)
16930
+ });
16931
+ ctx.body = {
16932
+ ok: true,
16933
+ conversations: result.conversations,
16934
+ page: result.page
16935
+ };
16936
+ });
16937
+ router.get("/api/v1/conversations/archived/search", async (ctx) => {
16938
+ await authenticateRequest(ctx, paths);
16939
+ ctx.set("cache-control", "no-store");
16940
+ const result = await conversations.searchArchivedConversationPage({
16941
+ limit: readLimit(ctx.query.limit),
16942
+ cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after) ?? readQueryString(ctx.query.page_cursor),
16943
+ query: readQueryString(ctx.query.query) ?? readQueryString(ctx.query.q) ?? readQueryString(ctx.query.keyword) ?? ""
16944
+ });
16945
+ ctx.body = {
16946
+ ok: true,
16947
+ conversations: result.conversations,
16948
+ page: result.page
16949
+ };
16950
+ });
16323
16951
  router.post("/api/v1/conversations", async (ctx) => {
16324
16952
  await authenticateRequest(ctx, paths);
16325
16953
  const body = await readJsonBody(ctx.req);
@@ -16353,14 +16981,20 @@ function registerConversationRoutes(router, options) {
16353
16981
  const response = ctx.res;
16354
16982
  let unsubscribe = () => {
16355
16983
  };
16984
+ let notificationChain = Promise.resolve();
16356
16985
  beginSseStream(ctx.req, response, {
16357
16986
  onClose: () => unsubscribe()
16358
16987
  });
16359
16988
  unsubscribe = conversations.subscribeAll((event) => {
16360
- if (notificationOnly && !isConversationNotificationEvent(event)) {
16989
+ if (!notificationOnly) {
16990
+ writeSseEvent(response, event);
16361
16991
  return;
16362
16992
  }
16363
- writeSseEvent(response, event);
16993
+ notificationChain = notificationChain.then(async () => {
16994
+ if (await conversations.shouldPublishNotificationEvent(event)) {
16995
+ writeSseEvent(response, event);
16996
+ }
16997
+ }).catch(() => void 0);
16364
16998
  });
16365
16999
  });
16366
17000
  router.get("/api/v1/conversations/:conversationId/events", async (ctx) => {
@@ -16466,7 +17100,11 @@ function registerConversationRoutes(router, options) {
16466
17100
  });
16467
17101
  router.post("/api/v1/conversations/clear-plans", async (ctx) => {
16468
17102
  await authenticateRequest(ctx, paths);
16469
- const plan = await conversations.prepareClearAllConversationPlan();
17103
+ const body = await readJsonBody(ctx.req);
17104
+ const targetStatus = readConversationClearPlanTargetStatus(body);
17105
+ const plan = await conversations.prepareClearAllConversationPlan(
17106
+ targetStatus
17107
+ );
16470
17108
  ctx.status = 201;
16471
17109
  ctx.body = {
16472
17110
  ok: true,
@@ -16495,6 +17133,46 @@ function registerConversationRoutes(router, options) {
16495
17133
  };
16496
17134
  }
16497
17135
  );
17136
+ router.post("/api/v1/conversations/archive-plans", async (ctx) => {
17137
+ await authenticateRequest(ctx, paths);
17138
+ const body = await readJsonBody(ctx.req);
17139
+ const plan = await conversations.prepareArchiveAllConversationPlan({
17140
+ excludeConversationIds: readStringArray(
17141
+ body,
17142
+ "exclude_conversation_ids",
17143
+ "excludeConversationIds"
17144
+ ) ?? []
17145
+ });
17146
+ ctx.status = 201;
17147
+ ctx.body = {
17148
+ ok: true,
17149
+ plan
17150
+ };
17151
+ });
17152
+ router.get("/api/v1/conversations/archive-plans/:planId", async (ctx) => {
17153
+ await authenticateRequest(ctx, paths);
17154
+ ctx.set("cache-control", "no-store");
17155
+ ctx.body = {
17156
+ ok: true,
17157
+ plan: await conversations.readArchiveAllConversationPlan(
17158
+ ctx.params.planId
17159
+ )
17160
+ };
17161
+ });
17162
+ router.post(
17163
+ "/api/v1/conversations/archive-plans/:planId/execute",
17164
+ async (ctx) => {
17165
+ await authenticateRequest(ctx, paths);
17166
+ const plan = await conversations.startArchiveAllConversationPlan(
17167
+ ctx.params.planId
17168
+ );
17169
+ ctx.status = plan.status === "completed" ? 200 : 202;
17170
+ ctx.body = {
17171
+ ok: true,
17172
+ plan
17173
+ };
17174
+ }
17175
+ );
16498
17176
  router.delete("/api/v1/conversations", async (ctx) => {
16499
17177
  await authenticateRequest(ctx, paths);
16500
17178
  const body = await readJsonBody(ctx.req);
@@ -16568,6 +17246,25 @@ function registerConversationRoutes(router, options) {
16568
17246
  };
16569
17247
  }
16570
17248
  );
17249
+ router.post("/api/v1/conversations/:conversationId/archive", async (ctx) => {
17250
+ await authenticateRequest(ctx, paths);
17251
+ ctx.body = {
17252
+ ok: true,
17253
+ ...await conversations.archiveConversation(ctx.params.conversationId)
17254
+ };
17255
+ });
17256
+ router.post(
17257
+ "/api/v1/conversations/:conversationId/unarchive",
17258
+ async (ctx) => {
17259
+ await authenticateRequest(ctx, paths);
17260
+ ctx.body = {
17261
+ ok: true,
17262
+ ...await conversations.unarchiveConversation(
17263
+ ctx.params.conversationId
17264
+ )
17265
+ };
17266
+ }
17267
+ );
16571
17268
  router.delete("/api/v1/conversations/:conversationId", async (ctx) => {
16572
17269
  await authenticateRequest(ctx, paths);
16573
17270
  ctx.body = {
@@ -16601,10 +17298,7 @@ function registerConversationRoutes(router, options) {
16601
17298
  ctx.set("content-type", blob.mime);
16602
17299
  ctx.set("content-length", String(blob.size));
16603
17300
  ctx.set("cache-control", "private, max-age=86400");
16604
- ctx.set(
16605
- "content-disposition",
16606
- contentDispositionInline(blob.filename)
16607
- );
17301
+ ctx.set("content-disposition", contentDispositionInline(blob.filename));
16608
17302
  ctx.body = blob.bytes;
16609
17303
  }
16610
17304
  );
@@ -16627,6 +17321,17 @@ function resolveConversationEventCursor(input) {
16627
17321
  const headerAfter = readNonNegativeIntegerHeader(input.lastEventIdHeader) ?? 0;
16628
17322
  return Math.max(queryAfter, headerAfter);
16629
17323
  }
17324
+ function readConversationClearPlanTargetStatus(body) {
17325
+ const raw = readString14(body, "target_status") ?? readString14(body, "targetStatus") ?? "active";
17326
+ if (raw === "active" || raw === "archived") {
17327
+ return raw;
17328
+ }
17329
+ throw new LinkHttpError(
17330
+ 400,
17331
+ "conversation_clear_plan_target_invalid",
17332
+ "Conversation clear plan target status is invalid"
17333
+ );
17334
+ }
16630
17335
  function readNonNegativeIntegerHeader(value) {
16631
17336
  const raw = Array.isArray(value) ? value[0] : value;
16632
17337
  if (!raw) {
@@ -16663,16 +17368,6 @@ function encodeRfc5987Value(value) {
16663
17368
  (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
16664
17369
  );
16665
17370
  }
16666
- function isConversationNotificationEvent(event) {
16667
- const type = event.type.toLowerCase();
16668
- return type === "conversation.created" || type === "conversation.updated" || type === "conversation.deleted" || type === "message.created" || type === "message.completed" || type === "message.failed" || type === "run.completed" || type === "run.failed" || type === "run.cancelled" || type === "run.canceled" || type === "approval.requested" || readPayloadBool(event.payload, "requires_action") || readPayloadBool(event.payload, "requires_user_action") || readPayloadBool(event.payload, "requires_approval");
16669
- }
16670
- function readPayloadBool(payload, key) {
16671
- if (!payload || typeof payload !== "object") {
16672
- return false;
16673
- }
16674
- return payload[key] === true;
16675
- }
16676
17371
 
16677
17372
  // src/http/middleware/error-handler.ts
16678
17373
  function createHttpErrorMiddleware(logger) {
@@ -16719,7 +17414,7 @@ function createHttpErrorMiddleware(logger) {
16719
17414
  // src/hermes/profiles.ts
16720
17415
  import { execFile as execFile4 } from "child_process";
16721
17416
  import { readdir as readdir9, readFile as readFile12, rename as rename3, stat as stat12 } from "fs/promises";
16722
- import path18 from "path";
17417
+ import path19 from "path";
16723
17418
  import { setTimeout as delay2 } from "timers/promises";
16724
17419
  import { promisify as promisify4 } from "util";
16725
17420
  import YAML2 from "yaml";
@@ -16830,7 +17525,7 @@ async function readHermesProfileCapabilities(name) {
16830
17525
  return {
16831
17526
  defaultModel: listedModels?.defaultModel ?? null,
16832
17527
  modelCount: listedModels?.models.length ?? 0,
16833
- skillCount: await countSkills(path18.join(profileDir, "skills")).catch(
17528
+ skillCount: await countSkills(path19.join(profileDir, "skills")).catch(
16834
17529
  () => 0
16835
17530
  ),
16836
17531
  toolCount: await countConfiguredTools(name).catch(() => 0)
@@ -17045,7 +17740,7 @@ async function countSkills(root) {
17045
17740
  );
17046
17741
  let count = 0;
17047
17742
  for (const entry of entries) {
17048
- const entryPath = path18.join(root, entry.name);
17743
+ const entryPath = path19.join(root, entry.name);
17049
17744
  if (entry.name === ".git" || entry.name === ".hub") {
17050
17745
  continue;
17051
17746
  }
@@ -17388,7 +18083,7 @@ function readCronJobUpdateInput(body) {
17388
18083
  if (repeat !== void 0) {
17389
18084
  input.repeat = repeat;
17390
18085
  }
17391
- const enabled = readBoolean2(body.enabled);
18086
+ const enabled = readBoolean3(body.enabled);
17392
18087
  if (enabled !== void 0) {
17393
18088
  input.enabled = enabled;
17394
18089
  }
@@ -17652,7 +18347,7 @@ function readModelConfigInput(body) {
17652
18347
  body.context_length ?? body.contextLength
17653
18348
  ),
17654
18349
  keyEnv: readString14(body, "key_env") ?? readString14(body, "keyEnv") ?? void 0,
17655
- setDefault: readBoolean2(body.set_default ?? body.setDefault),
18350
+ setDefault: readBoolean3(body.set_default ?? body.setDefault),
17656
18351
  reasoningEffort: readString14(body, "reasoning_effort") ?? readString14(body, "reasoningEffort") ?? void 0
17657
18352
  };
17658
18353
  }
@@ -17663,7 +18358,7 @@ function readModelDefaultsInput(body) {
17663
18358
  };
17664
18359
  }
17665
18360
  function shouldReloadGatewayAfterModelConfigChange(body) {
17666
- const explicit = readBoolean2(body.reload_gateway ?? body.reloadGateway) ?? (readBoolean2(body.skip_gateway_reload ?? body.skipGatewayReload) === true ? false : void 0);
18361
+ const explicit = readBoolean3(body.reload_gateway ?? body.reloadGateway) ?? (readBoolean3(body.skip_gateway_reload ?? body.skipGatewayReload) === true ? false : void 0);
17667
18362
  return explicit ?? true;
17668
18363
  }
17669
18364
  function markModelConfigAppliedWithoutGatewayReload(result) {
@@ -17742,12 +18437,12 @@ import { spawn as spawn2 } from "child_process";
17742
18437
  import { EventEmitter as EventEmitter2 } from "events";
17743
18438
  import {
17744
18439
  cp,
17745
- mkdir as mkdir10,
18440
+ mkdir as mkdir11,
17746
18441
  readFile as readFile13,
17747
18442
  rm as rm6,
17748
18443
  stat as stat13
17749
18444
  } from "fs/promises";
17750
- import path19 from "path";
18445
+ import path20 from "path";
17751
18446
  import YAML3 from "yaml";
17752
18447
  var PROFILE_CREATE_LOG_FILE = "profile-create.log";
17753
18448
  var PROFILE_CREATE_LOG_MAX_FILES = 3;
@@ -17795,7 +18490,7 @@ async function startHermesProfileCreation(input, options) {
17795
18490
  signal: null,
17796
18491
  error: null
17797
18492
  };
17798
- await mkdir10(options.paths.runDir, { recursive: true, mode: 448 });
18493
+ await mkdir11(options.paths.runDir, { recursive: true, mode: 448 });
17799
18494
  await writeProfileCreationState(options.paths, started);
17800
18495
  await writer.write(`
17801
18496
  === profile creation started ${startedAt} ===
@@ -18198,7 +18893,7 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
18198
18893
  return keys;
18199
18894
  }
18200
18895
  async function writeEnvValues(profileName, values) {
18201
- const envPath = path19.join(resolveHermesProfileDir(profileName), ".env");
18896
+ const envPath = path20.join(resolveHermesProfileDir(profileName), ".env");
18202
18897
  const existingRaw = await readFile13(envPath, "utf8").catch((error) => {
18203
18898
  if (isNodeError15(error, "ENOENT")) {
18204
18899
  return "";
@@ -18235,8 +18930,8 @@ async function writeEnvValues(profileName, values) {
18235
18930
  await atomicWriteFilePreservingMetadata(envPath, nextRaw);
18236
18931
  }
18237
18932
  async function copySkills(sourceProfile, targetProfile) {
18238
- const sourceSkills = path19.join(resolveHermesProfileDir(sourceProfile), "skills");
18239
- const targetSkills = path19.join(resolveHermesProfileDir(targetProfile), "skills");
18933
+ const sourceSkills = path20.join(resolveHermesProfileDir(sourceProfile), "skills");
18934
+ const targetSkills = path20.join(resolveHermesProfileDir(targetProfile), "skills");
18240
18935
  if (!await pathExists2(sourceSkills)) {
18241
18936
  return;
18242
18937
  }
@@ -18331,10 +19026,10 @@ async function readProfileCreationLogLines(paths) {
18331
19026
  );
18332
19027
  }
18333
19028
  function profileCreationStatePath(paths) {
18334
- return path19.join(paths.runDir, "profile-create-state.json");
19029
+ return path20.join(paths.runDir, "profile-create-state.json");
18335
19030
  }
18336
19031
  function profileCreationLogPath(paths) {
18337
- return path19.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
19032
+ return path20.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
18338
19033
  }
18339
19034
  async function clearProfileCreationLogFiles(paths) {
18340
19035
  const primary = profileCreationLogPath(paths);
@@ -18499,7 +19194,7 @@ function readProfilePermissionsInput(body) {
18499
19194
  containerDisk: readPositiveInteger2(
18500
19195
  terminal.container_disk ?? terminal.containerDisk
18501
19196
  ),
18502
- containerPersistent: readBoolean2(
19197
+ containerPersistent: readBoolean3(
18503
19198
  terminal.container_persistent ?? terminal.containerPersistent
18504
19199
  )
18505
19200
  };
@@ -18511,7 +19206,7 @@ function readProfilePermissionsInput(body) {
18511
19206
  toolsets.enabled_toolsets ?? toolsets.enabledToolsets ?? toolsets.enabled,
18512
19207
  "toolsets.enabled"
18513
19208
  ) ?? void 0,
18514
- mcpEnabled: readBoolean2(toolsets.mcp_enabled ?? toolsets.mcpEnabled)
19209
+ mcpEnabled: readBoolean3(toolsets.mcp_enabled ?? toolsets.mcpEnabled)
18515
19210
  };
18516
19211
  }
18517
19212
  if (Object.keys(input).length === 0) {
@@ -18608,7 +19303,7 @@ import {
18608
19303
  readFile as readFile14,
18609
19304
  stat as stat14
18610
19305
  } from "fs/promises";
18611
- import path20 from "path";
19306
+ import path21 from "path";
18612
19307
  import YAML4 from "yaml";
18613
19308
  var ENTRY_DELIMITER = "\n\xA7\n";
18614
19309
  var DEFAULT_MEMORY_LIMIT = 2200;
@@ -18825,7 +19520,7 @@ async function saveProviderSettings(profileName, provider, patch) {
18825
19520
  if (provider === "hindsight") {
18826
19521
  await patchJsonProviderConfig(
18827
19522
  profileName,
18828
- path20.join("hindsight", "config.json"),
19523
+ path21.join("hindsight", "config.json"),
18829
19524
  {
18830
19525
  mode: patch.mode,
18831
19526
  api_url: patch.apiUrl,
@@ -18992,7 +19687,7 @@ async function patchHermesMemoryProvider(profileName, provider) {
18992
19687
  await atomicWriteFilePreservingMetadata(configPath, document.toString());
18993
19688
  }
18994
19689
  function resolveMemoryDir(profileName) {
18995
- return path20.join(resolveHermesProfileDir(profileName), "memories");
19690
+ return path21.join(resolveHermesProfileDir(profileName), "memories");
18996
19691
  }
18997
19692
  async function readMemoryStore(profileName, target, limits) {
18998
19693
  const filePath = memoryFilePath(profileName, target);
@@ -19053,7 +19748,7 @@ async function writeMemoryEntries(profileName, target, entries) {
19053
19748
  );
19054
19749
  }
19055
19750
  function memoryFilePath(profileName, target) {
19056
- return path20.join(
19751
+ return path21.join(
19057
19752
  resolveMemoryDir(profileName),
19058
19753
  target === "user" ? "USER.md" : "MEMORY.md"
19059
19754
  );
@@ -19113,7 +19808,7 @@ async function readCustomProviderSetupSummary(profileName) {
19113
19808
  configurable: true,
19114
19809
  configured: true,
19115
19810
  configurationIssue: null,
19116
- providerConfigPath: path20.join(
19811
+ providerConfigPath: path21.join(
19117
19812
  resolveHermesProfileDir(profileName),
19118
19813
  "<provider>.json"
19119
19814
  ),
@@ -19467,7 +20162,7 @@ async function readProviderSettings(profileName, provider) {
19467
20162
  stringSetting(
19468
20163
  "dbPath",
19469
20164
  "SQLite \u6570\u636E\u5E93\u8DEF\u5F84",
19470
- config.db_path ?? path20.join(resolveHermesProfileDir(profileName), "memory_store.db")
20165
+ config.db_path ?? path21.join(resolveHermesProfileDir(profileName), "memory_store.db")
19471
20166
  ),
19472
20167
  booleanSetting("autoExtract", "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u62BD\u53D6", config.auto_extract ?? false),
19473
20168
  numberSetting("defaultTrust", "\u9ED8\u8BA4\u4FE1\u4EFB\u5206", config.default_trust ?? 0.5),
@@ -19490,7 +20185,7 @@ async function readProviderSettings(profileName, provider) {
19490
20185
  stringSetting(
19491
20186
  "workingDirectory",
19492
20187
  "\u5DE5\u4F5C\u76EE\u5F55",
19493
- path20.join(resolveHermesProfileDir(profileName), "byterover"),
20188
+ path21.join(resolveHermesProfileDir(profileName), "byterover"),
19494
20189
  false
19495
20190
  )
19496
20191
  ];
@@ -19499,16 +20194,16 @@ async function readProviderSettings(profileName, provider) {
19499
20194
  }
19500
20195
  function memoryProviderConfigPath(profileName, provider) {
19501
20196
  if (provider === "honcho") {
19502
- return path20.join(resolveHermesProfileDir(profileName), "honcho.json");
20197
+ return path21.join(resolveHermesProfileDir(profileName), "honcho.json");
19503
20198
  }
19504
20199
  if (provider === "mem0") {
19505
- return path20.join(resolveHermesProfileDir(profileName), "mem0.json");
20200
+ return path21.join(resolveHermesProfileDir(profileName), "mem0.json");
19506
20201
  }
19507
20202
  if (provider === "supermemory") {
19508
- return path20.join(resolveHermesProfileDir(profileName), "supermemory.json");
20203
+ return path21.join(resolveHermesProfileDir(profileName), "supermemory.json");
19509
20204
  }
19510
20205
  if (provider === "hindsight") {
19511
- return path20.join(
20206
+ return path21.join(
19512
20207
  resolveHermesProfileDir(profileName),
19513
20208
  "hindsight",
19514
20209
  "config.json"
@@ -19517,13 +20212,13 @@ function memoryProviderConfigPath(profileName, provider) {
19517
20212
  return null;
19518
20213
  }
19519
20214
  function customProviderConfigPath(profileName, provider) {
19520
- return path20.join(
20215
+ return path21.join(
19521
20216
  resolveHermesProfileDir(profileName),
19522
20217
  `${normalizeCustomProviderId(provider)}.json`
19523
20218
  );
19524
20219
  }
19525
20220
  function customProviderRegistryPath(profileName) {
19526
- return path20.join(
20221
+ return path21.join(
19527
20222
  resolveHermesProfileDir(profileName),
19528
20223
  CUSTOM_PROVIDER_REGISTRY_FILE
19529
20224
  );
@@ -19575,7 +20270,7 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
19575
20270
  );
19576
20271
  }
19577
20272
  async function discoverUserMemoryProviderDescriptors(profileName) {
19578
- const pluginsDir = path20.join(resolveHermesProfileDir(profileName), "plugins");
20273
+ const pluginsDir = path21.join(resolveHermesProfileDir(profileName), "plugins");
19579
20274
  const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
19580
20275
  (error) => {
19581
20276
  if (isNodeError16(error, "ENOENT")) {
@@ -19595,7 +20290,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
19595
20290
  } catch {
19596
20291
  continue;
19597
20292
  }
19598
- const providerDir = path20.join(pluginsDir, entry.name);
20293
+ const providerDir = path21.join(pluginsDir, entry.name);
19599
20294
  if (!await isMemoryProviderPluginDir(providerDir)) {
19600
20295
  continue;
19601
20296
  }
@@ -19609,7 +20304,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
19609
20304
  return descriptors;
19610
20305
  }
19611
20306
  async function isUserMemoryProviderInstalled(profileName, provider) {
19612
- const providerDir = path20.join(
20307
+ const providerDir = path21.join(
19613
20308
  resolveHermesProfileDir(profileName),
19614
20309
  "plugins",
19615
20310
  normalizeCustomProviderId(provider)
@@ -19617,7 +20312,7 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
19617
20312
  return isMemoryProviderPluginDir(providerDir);
19618
20313
  }
19619
20314
  async function isMemoryProviderPluginDir(providerDir) {
19620
- const source = await readFile14(path20.join(providerDir, "__init__.py"), "utf8").catch(
20315
+ const source = await readFile14(path21.join(providerDir, "__init__.py"), "utf8").catch(
19621
20316
  (error) => {
19622
20317
  if (isNodeError16(error, "ENOENT")) {
19623
20318
  return "";
@@ -19629,7 +20324,7 @@ async function isMemoryProviderPluginDir(providerDir) {
19629
20324
  return sample.includes("register_memory_provider") || sample.includes("MemoryProvider");
19630
20325
  }
19631
20326
  async function readPluginMetadata(providerDir) {
19632
- const raw = await readFile14(path20.join(providerDir, "plugin.yaml"), "utf8").catch(
20327
+ const raw = await readFile14(path21.join(providerDir, "plugin.yaml"), "utf8").catch(
19633
20328
  (error) => {
19634
20329
  if (isNodeError16(error, "ENOENT")) {
19635
20330
  return "";
@@ -19641,10 +20336,10 @@ async function readPluginMetadata(providerDir) {
19641
20336
  }
19642
20337
  async function resolveByteRoverCli() {
19643
20338
  const candidates = [
19644
- ...(process.env.PATH ?? "").split(path20.delimiter).filter(Boolean).map((dir) => path20.join(dir, "brv")),
19645
- path20.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
20339
+ ...(process.env.PATH ?? "").split(path21.delimiter).filter(Boolean).map((dir) => path21.join(dir, "brv")),
20340
+ path21.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
19646
20341
  "/usr/local/bin/brv",
19647
- path20.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
20342
+ path21.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
19648
20343
  ].filter(Boolean);
19649
20344
  for (const candidate of candidates) {
19650
20345
  const found = await access3(candidate).then(() => true).catch(() => false);
@@ -19705,7 +20400,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
19705
20400
  if (entries.length === 0) {
19706
20401
  return;
19707
20402
  }
19708
- const envPath = path20.join(resolveHermesProfileDir(profileName), ".env");
20403
+ const envPath = path21.join(resolveHermesProfileDir(profileName), ".env");
19709
20404
  const existingRaw = await readFile14(envPath, "utf8").catch((error) => {
19710
20405
  if (isNodeError16(error, "ENOENT")) {
19711
20406
  return "";
@@ -19779,7 +20474,7 @@ async function readActiveMemoryProvider(profileName) {
19779
20474
  return provider;
19780
20475
  }
19781
20476
  async function patchJsonProviderConfig(profileName, relativePath, patch) {
19782
- const configPath = path20.join(
20477
+ const configPath = path21.join(
19783
20478
  resolveHermesProfileDir(profileName),
19784
20479
  relativePath
19785
20480
  );
@@ -19808,7 +20503,7 @@ async function readJsonObject(filePath) {
19808
20503
  } catch {
19809
20504
  throw new HermesMemoryError(
19810
20505
  "memory_provider_config_invalid",
19811
- `${path20.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
20506
+ `${path21.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
19812
20507
  );
19813
20508
  }
19814
20509
  }
@@ -19816,7 +20511,7 @@ function booleanSetting(key, label, value) {
19816
20511
  return {
19817
20512
  key,
19818
20513
  label,
19819
- value: readBoolean3(value) ?? false,
20514
+ value: readBoolean4(value) ?? false,
19820
20515
  editable: true,
19821
20516
  kind: "boolean"
19822
20517
  };
@@ -19924,7 +20619,7 @@ function readPositiveInteger3(value) {
19924
20619
  const numberValue = typeof value === "number" ? value : typeof value === "string" ? Number(value.trim()) : NaN;
19925
20620
  return Number.isFinite(numberValue) && numberValue > 0 ? Math.floor(numberValue) : void 0;
19926
20621
  }
19927
- function readBoolean3(value) {
20622
+ function readBoolean4(value) {
19928
20623
  if (typeof value === "boolean") {
19929
20624
  return value;
19930
20625
  }
@@ -20143,15 +20838,15 @@ function readMemorySettingsPatch(body) {
20143
20838
  if (containerTag !== void 0) {
20144
20839
  input.containerTag = containerTag;
20145
20840
  }
20146
- const autoRecall = readBoolean2(body.auto_recall ?? body.autoRecall);
20841
+ const autoRecall = readBoolean3(body.auto_recall ?? body.autoRecall);
20147
20842
  if (autoRecall !== void 0) {
20148
20843
  input.autoRecall = autoRecall;
20149
20844
  }
20150
- const autoCapture = readBoolean2(body.auto_capture ?? body.autoCapture);
20845
+ const autoCapture = readBoolean3(body.auto_capture ?? body.autoCapture);
20151
20846
  if (autoCapture !== void 0) {
20152
20847
  input.autoCapture = autoCapture;
20153
20848
  }
20154
- const autoRetain = readBoolean2(body.auto_retain ?? body.autoRetain);
20849
+ const autoRetain = readBoolean3(body.auto_retain ?? body.autoRetain);
20155
20850
  if (autoRetain !== void 0) {
20156
20851
  input.autoRetain = autoRetain;
20157
20852
  }
@@ -20221,7 +20916,7 @@ function readMemorySettingsPatch(body) {
20221
20916
  if (writeFrequency) {
20222
20917
  input.writeFrequency = writeFrequency;
20223
20918
  }
20224
- const saveMessages = readBoolean2(body.save_messages ?? body.saveMessages);
20919
+ const saveMessages = readBoolean3(body.save_messages ?? body.saveMessages);
20225
20920
  if (saveMessages !== void 0) {
20226
20921
  input.saveMessages = saveMessages;
20227
20922
  }
@@ -20261,7 +20956,7 @@ function readMemorySettingsPatch(body) {
20261
20956
  if (agentId !== void 0) {
20262
20957
  input.agentId = agentId;
20263
20958
  }
20264
- const rerank = readBoolean2(body.rerank);
20959
+ const rerank = readBoolean3(body.rerank);
20265
20960
  if (rerank !== void 0) {
20266
20961
  input.rerank = rerank;
20267
20962
  }
@@ -20285,7 +20980,7 @@ function readMemorySettingsPatch(body) {
20285
20980
  if (dbPath !== void 0) {
20286
20981
  input.dbPath = dbPath;
20287
20982
  }
20288
- const autoExtract = readBoolean2(body.auto_extract ?? body.autoExtract);
20983
+ const autoExtract = readBoolean3(body.auto_extract ?? body.autoExtract);
20289
20984
  if (autoExtract !== void 0) {
20290
20985
  input.autoExtract = autoExtract;
20291
20986
  }
@@ -20354,7 +21049,7 @@ function toMemoryHttpError(error) {
20354
21049
 
20355
21050
  // src/hermes/skills.ts
20356
21051
  import { readFile as readFile15, readdir as readdir11 } from "fs/promises";
20357
- import path21 from "path";
21052
+ import path22 from "path";
20358
21053
  import YAML5 from "yaml";
20359
21054
  var HermesSkillNotFoundError = class extends Error {
20360
21055
  constructor(skillName) {
@@ -20368,7 +21063,7 @@ var EXCLUDED_SKILL_DIRS = /* @__PURE__ */ new Set([".git", ".github", ".hub"]);
20368
21063
  async function listHermesProfileSkills(profileName, paths = resolveRuntimePaths()) {
20369
21064
  const profile = await readExistingProfile(profileName, paths);
20370
21065
  const profileDir = resolveHermesProfileDir(profile.name);
20371
- const skillsRoot = path21.join(profileDir, "skills");
21066
+ const skillsRoot = path22.join(profileDir, "skills");
20372
21067
  const [skillFiles, disabled, provenance] = await Promise.all([
20373
21068
  findSkillFiles(skillsRoot),
20374
21069
  readDisabledSkillNames(resolveHermesConfigPath(profile.name)),
@@ -20459,7 +21154,7 @@ async function collectSkillFiles(directory, results) {
20459
21154
  if (EXCLUDED_SKILL_DIRS.has(entry.name)) {
20460
21155
  continue;
20461
21156
  }
20462
- const entryPath = path21.join(directory, entry.name);
21157
+ const entryPath = path22.join(directory, entry.name);
20463
21158
  if (entry.isDirectory()) {
20464
21159
  await collectSkillFiles(entryPath, results);
20465
21160
  continue;
@@ -20481,10 +21176,10 @@ async function readSkillMetadata(input) {
20481
21176
  if (raw === null) {
20482
21177
  return null;
20483
21178
  }
20484
- const skillDir = path21.dirname(input.skillFile);
21179
+ const skillDir = path22.dirname(input.skillFile);
20485
21180
  const { frontmatter, body } = parseSkillDocument(raw.slice(0, 4e3));
20486
21181
  const name = normalizeSkillName(
20487
- readString16(frontmatter.name) ?? path21.basename(skillDir)
21182
+ readString16(frontmatter.name) ?? path22.basename(skillDir)
20488
21183
  );
20489
21184
  if (!name) {
20490
21185
  return null;
@@ -20503,7 +21198,7 @@ async function readSkillMetadata(input) {
20503
21198
  enabled: !input.disabled.has(name),
20504
21199
  source: provenance.source,
20505
21200
  trust: provenance.trust,
20506
- relativePath: path21.relative(input.skillsRoot, skillDir)
21201
+ relativePath: path22.relative(input.skillsRoot, skillDir)
20507
21202
  };
20508
21203
  }
20509
21204
  function parseSkillDocument(raw) {
@@ -20524,8 +21219,8 @@ function parseSkillDocument(raw) {
20524
21219
  }
20525
21220
  }
20526
21221
  function categoryFromPath(skillsRoot, skillFile) {
20527
- const relative = path21.relative(skillsRoot, skillFile);
20528
- const parts = relative.split(path21.sep).filter(Boolean);
21222
+ const relative = path22.relative(skillsRoot, skillFile);
21223
+ const parts = relative.split(path22.sep).filter(Boolean);
20529
21224
  return parts.length >= 3 ? parts[0] : null;
20530
21225
  }
20531
21226
  function firstBodyDescription(body) {
@@ -20572,7 +21267,7 @@ async function readSkillProvenance(root) {
20572
21267
  return provenance;
20573
21268
  }
20574
21269
  async function readBundledSkillNames(root) {
20575
- const raw = await readFile15(path21.join(root, ".bundled_manifest"), "utf8").catch(
21270
+ const raw = await readFile15(path22.join(root, ".bundled_manifest"), "utf8").catch(
20576
21271
  (error) => {
20577
21272
  if (isNodeError17(error, "ENOENT")) {
20578
21273
  return "";
@@ -20595,7 +21290,7 @@ async function readBundledSkillNames(root) {
20595
21290
  return names;
20596
21291
  }
20597
21292
  async function readHubInstalledSkills(root) {
20598
- const raw = await readFile15(path21.join(root, ".hub", "lock.json"), "utf8").catch(
21293
+ const raw = await readFile15(path22.join(root, ".hub", "lock.json"), "utf8").catch(
20599
21294
  (error) => {
20600
21295
  if (isNodeError17(error, "ENOENT")) {
20601
21296
  return "";
@@ -20731,7 +21426,7 @@ function registerProfileSkillRoutes(router, options) {
20731
21426
  router.patch("/api/v1/profiles/:name/skills/:skillName", async (ctx) => {
20732
21427
  await authenticateRequest(ctx, paths);
20733
21428
  const body = await readJsonBody(ctx.req);
20734
- const enabled = readBoolean2(body.enabled);
21429
+ const enabled = readBoolean3(body.enabled);
20735
21430
  if (enabled === void 0) {
20736
21431
  throw new LinkHttpError(
20737
21432
  400,
@@ -21027,7 +21722,8 @@ function registerRunRoutes(router, options) {
21027
21722
  conversation_history: readConversationHistory(
21028
21723
  body.conversation_history ?? body.conversationHistory
21029
21724
  ),
21030
- session_id: readString14(body, "session_id") ?? readString14(body, "sessionId") ?? void 0
21725
+ session_id: readString14(body, "session_id") ?? readString14(body, "sessionId") ?? void 0,
21726
+ session_key: readString14(body, "session_key") ?? readString14(body, "sessionKey") ?? void 0
21031
21727
  },
21032
21728
  { logger, profileName: readOptionalProfileName(body) }
21033
21729
  );
@@ -21198,8 +21894,8 @@ function readModelList(payload) {
21198
21894
  // src/hermes/updates.ts
21199
21895
  import { EventEmitter as EventEmitter3 } from "events";
21200
21896
  import { spawn as spawn3 } from "child_process";
21201
- import { mkdir as mkdir11, readFile as readFile16, rm as rm7 } from "fs/promises";
21202
- import path22 from "path";
21897
+ import { mkdir as mkdir12, readFile as readFile16, rm as rm7 } from "fs/promises";
21898
+ import path23 from "path";
21203
21899
  var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
21204
21900
  var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
21205
21901
  var RELEASE_FETCH_TIMEOUT_MS = 5e3;
@@ -21262,7 +21958,7 @@ async function startHermesUpdate(options) {
21262
21958
  signal: null,
21263
21959
  error: null
21264
21960
  };
21265
- await mkdir11(options.paths.runDir, { recursive: true, mode: 448 });
21961
+ await mkdir12(options.paths.runDir, { recursive: true, mode: 448 });
21266
21962
  await writer.write(`
21267
21963
  === hermes update started ${startedAt} ===
21268
21964
  `);
@@ -21477,13 +22173,13 @@ async function readUpdateLogLines(paths) {
21477
22173
  );
21478
22174
  }
21479
22175
  function releaseCachePath(paths) {
21480
- return path22.join(paths.indexesDir, "hermes-release-check.json");
22176
+ return path23.join(paths.indexesDir, "hermes-release-check.json");
21481
22177
  }
21482
22178
  function updateStatePath(paths) {
21483
- return path22.join(paths.runDir, "hermes-update-state.json");
22179
+ return path23.join(paths.runDir, "hermes-update-state.json");
21484
22180
  }
21485
22181
  function updateLogPath(paths) {
21486
- return path22.join(paths.logsDir, UPDATE_LOG_FILE);
22182
+ return path23.join(paths.logsDir, UPDATE_LOG_FILE);
21487
22183
  }
21488
22184
  async function clearUpdateLogFiles(paths) {
21489
22185
  const primary = updateLogPath(paths);
@@ -21583,17 +22279,17 @@ function readString17(payload, key) {
21583
22279
  // src/link/updates.ts
21584
22280
  import { spawn as spawn5 } from "child_process";
21585
22281
  import { EventEmitter as EventEmitter4 } from "events";
21586
- import { mkdir as mkdir14, readFile as readFile18, rm as rm10 } from "fs/promises";
21587
- import path24 from "path";
22282
+ import { mkdir as mkdir15, readFile as readFile18, rm as rm10 } from "fs/promises";
22283
+ import path25 from "path";
21588
22284
 
21589
22285
  // src/daemon/process.ts
21590
22286
  import { spawn as spawn4 } from "child_process";
21591
- import { mkdir as mkdir13, readFile as readFile17, rm as rm9 } from "fs/promises";
21592
- import path23 from "path";
22287
+ import { mkdir as mkdir14, readFile as readFile17, rm as rm9 } from "fs/promises";
22288
+ import path24 from "path";
21593
22289
 
21594
22290
  // src/daemon/service.ts
21595
22291
  import { createServer } from "http";
21596
- import { mkdir as mkdir12, rm as rm8, writeFile as writeFile3 } from "fs/promises";
22292
+ import { mkdir as mkdir13, rm as rm8, writeFile as writeFile3 } from "fs/promises";
21597
22293
 
21598
22294
  // src/relay/control-client.ts
21599
22295
  import WebSocket from "ws";
@@ -22724,7 +23420,7 @@ function pidFilePath(paths = resolveRuntimePaths()) {
22724
23420
  return `${paths.runDir}/hermeslink.pid`;
22725
23421
  }
22726
23422
  async function writePidFile(paths) {
22727
- await mkdir12(paths.runDir, { recursive: true, mode: 448 });
23423
+ await mkdir13(paths.runDir, { recursive: true, mode: 448 });
22728
23424
  await writeFile3(pidFilePath(paths), `${process.pid}
22729
23425
  `, { mode: 384 });
22730
23426
  }
@@ -22799,8 +23495,8 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
22799
23495
  return status;
22800
23496
  }
22801
23497
  }
22802
- await mkdir13(paths.logsDir, { recursive: true, mode: 448 });
22803
- await mkdir13(paths.runDir, { recursive: true, mode: 448 });
23498
+ await mkdir14(paths.logsDir, { recursive: true, mode: 448 });
23499
+ await mkdir14(paths.runDir, { recursive: true, mode: 448 });
22804
23500
  const scriptPath = currentCliScriptPath();
22805
23501
  const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
22806
23502
  detached: true,
@@ -22818,10 +23514,10 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
22818
23514
  return await getDaemonStatus(paths);
22819
23515
  }
22820
23516
  async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
22821
- await mkdir13(paths.logsDir, { recursive: true, mode: 448 });
23517
+ await mkdir14(paths.logsDir, { recursive: true, mode: 448 });
22822
23518
  const log = createRotatingTextLogWriter({
22823
23519
  paths,
22824
- fileName: path23.basename(daemonLogFile(paths))
23520
+ fileName: path24.basename(daemonLogFile(paths))
22825
23521
  });
22826
23522
  const scriptPath = currentCliScriptPath();
22827
23523
  const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
@@ -23062,7 +23758,7 @@ async function startLinkUpdate(options) {
23062
23758
  error: null,
23063
23759
  manual_command: manualCommand
23064
23760
  };
23065
- await mkdir14(options.paths.runDir, { recursive: true, mode: 448 });
23761
+ await mkdir15(options.paths.runDir, { recursive: true, mode: 448 });
23066
23762
  await writer.write(
23067
23763
  `
23068
23764
  === link update started ${startedAt} target=${targetVersion} ===
@@ -23375,10 +24071,10 @@ async function readUpdateLogLines2(paths) {
23375
24071
  );
23376
24072
  }
23377
24073
  function updateStatePath2(paths) {
23378
- return path24.join(paths.runDir, "link-update-state.json");
24074
+ return path25.join(paths.runDir, "link-update-state.json");
23379
24075
  }
23380
24076
  function updateLogPath2(paths) {
23381
- return path24.join(paths.logsDir, UPDATE_LOG_FILE2);
24077
+ return path25.join(paths.logsDir, UPDATE_LOG_FILE2);
23382
24078
  }
23383
24079
  async function clearUpdateLogFiles2(paths) {
23384
24080
  const primary = updateLogPath2(paths);
@@ -23442,7 +24138,7 @@ function readString18(payload, key) {
23442
24138
  }
23443
24139
 
23444
24140
  // src/pairing/pairing.ts
23445
- import path25 from "path";
24141
+ import path26 from "path";
23446
24142
  import { rm as rm11 } from "fs/promises";
23447
24143
 
23448
24144
  // src/relay/bootstrap.ts
@@ -23782,10 +24478,10 @@ async function loadRequiredIdentity2(paths) {
23782
24478
  }
23783
24479
  return identity;
23784
24480
  }
23785
- async function postServerJson(serverBaseUrl, path26, body, options) {
24481
+ async function postServerJson(serverBaseUrl, path27, body, options) {
23786
24482
  let response;
23787
24483
  try {
23788
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
24484
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path27}`, {
23789
24485
  method: "POST",
23790
24486
  headers: {
23791
24487
  accept: "application/json",
@@ -23833,10 +24529,10 @@ function pairingErrorSnapshot(stage, error) {
23833
24529
  occurred_at: (/* @__PURE__ */ new Date()).toISOString()
23834
24530
  };
23835
24531
  }
23836
- async function patchServerJson(serverBaseUrl, path26, token, body, options) {
24532
+ async function patchServerJson(serverBaseUrl, path27, token, body, options) {
23837
24533
  let response;
23838
24534
  try {
23839
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
24535
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path27}`, {
23840
24536
  method: "PATCH",
23841
24537
  headers: {
23842
24538
  accept: "application/json",
@@ -23884,10 +24580,10 @@ function createPairingNetworkError(input) {
23884
24580
  );
23885
24581
  }
23886
24582
  function pairingClaimPath(sessionId, paths) {
23887
- return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
24583
+ return path26.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
23888
24584
  }
23889
24585
  function pairingSessionPath(sessionId, paths) {
23890
- return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
24586
+ return path26.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
23891
24587
  }
23892
24588
  function qrPreferredUrls(routes) {
23893
24589
  return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
@@ -23939,6 +24635,10 @@ function registerSystemRoutes(router, options) {
23939
24635
  statistics: true,
23940
24636
  conversations: true,
23941
24637
  conversation_events: true,
24638
+ conversation_archive: true,
24639
+ conversation_unarchive: true,
24640
+ conversation_archive_plan: true,
24641
+ conversation_archived_list: true,
23942
24642
  conversation_delete: true,
23943
24643
  conversation_bulk_delete: true,
23944
24644
  conversation_clear_plan: true,
@@ -24142,19 +24842,17 @@ function registerSystemRoutes(router, options) {
24142
24842
  })),
24143
24843
  readDeviceSummary(paths),
24144
24844
  listHermesProfiles(paths).catch(() => []),
24145
- readHermesUpdateCheck({ paths, logger }).catch(
24146
- (error) => ({
24147
- ok: true,
24148
- local: {
24149
- version: null,
24150
- raw: error instanceof Error ? error.message : String(error)
24151
- },
24152
- remote: null,
24153
- update_available: false,
24154
- check_state: "unavailable",
24155
- issue: error instanceof Error ? error.message : String(error)
24156
- })
24157
- ),
24845
+ readHermesUpdateCheck({ paths, logger }).catch((error) => ({
24846
+ ok: true,
24847
+ local: {
24848
+ version: null,
24849
+ raw: error instanceof Error ? error.message : String(error)
24850
+ },
24851
+ remote: null,
24852
+ update_available: false,
24853
+ check_state: "unavailable",
24854
+ issue: error instanceof Error ? error.message : String(error)
24855
+ })),
24158
24856
  readHermesUpdateStatus(paths).catch((error) => ({
24159
24857
  ok: true,
24160
24858
  state: "failed",
@@ -24378,7 +25076,7 @@ async function readActiveCronJobCount(profiles, logger) {
24378
25076
  }, 0);
24379
25077
  }
24380
25078
  function isActiveCronJob(job) {
24381
- const enabled = readBoolean2(job.enabled) ?? true;
25079
+ const enabled = readBoolean3(job.enabled) ?? true;
24382
25080
  if (!enabled) {
24383
25081
  return false;
24384
25082
  }
@@ -25074,10 +25772,10 @@ export {
25074
25772
  saveConfig,
25075
25773
  parseLogLevel,
25076
25774
  normalizeLanHost,
25077
- ConversationService,
25078
25775
  loadIdentity,
25079
25776
  ensureIdentity,
25080
25777
  getIdentityStatus,
25778
+ ConversationService,
25081
25779
  hasActiveDevices,
25082
25780
  detectRuntimeEnvironment,
25083
25781
  preparePairing,