@hermespilot/link 0.8.1-beta.2 → 0.8.1-beta.3

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 { createHash as createHash8, randomUUID as randomUUID12 } from "crypto";
7
+ import { createHash as createHash9, randomUUID as randomUUID12 } from "crypto";
8
8
  import path27 from "path";
9
9
 
10
10
  // src/database/link-database.ts
@@ -7714,7 +7714,7 @@ function isConversationMissingError(error) {
7714
7714
  }
7715
7715
 
7716
7716
  // src/constants.ts
7717
- var LINK_VERSION = "0.8.1-beta.2";
7717
+ var LINK_VERSION = "0.8.1-beta.3";
7718
7718
  var LINK_COMMAND = "hermeslink";
7719
7719
  var LINK_DEFAULT_PORT = 52379;
7720
7720
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -10687,6 +10687,13 @@ function delay3(ms) {
10687
10687
  }
10688
10688
 
10689
10689
  // src/hermes/tui-gateway-rpc.ts
10690
+ var TuiGatewaySessionBusyError = class extends LinkHttpError {
10691
+ constructor(message, details = { profileName: "default" }) {
10692
+ super(409, "tui_gateway_session_busy", message);
10693
+ this.details = details;
10694
+ }
10695
+ details;
10696
+ };
10690
10697
  var CONNECT_TIMEOUT_MS = 15e3;
10691
10698
  var REQUEST_TIMEOUT_MS = 12e4;
10692
10699
  var USAGE_REQUEST_TIMEOUT_MS = 1e4;
@@ -10699,12 +10706,60 @@ var WS_HEARTBEAT_INTERVAL_MS = 6e4;
10699
10706
  var WS_RECONNECT_BASE_DELAY_MS = 1e3;
10700
10707
  var WS_RECONNECT_MAX_DELAY_MS = 15e3;
10701
10708
  var INTERRUPT_DRAIN_TIMEOUT_MS = 15e3;
10709
+ var SESSION_BUSY_RETRY_TIMEOUT_MS = 6e3;
10710
+ var SESSION_BUSY_RETRY_INTERVAL_MS = 150;
10702
10711
  var PROFILE_NAME_PATTERN2 = /^[a-zA-Z0-9._-]{1,64}$/u;
10703
10712
  var backendEntries = /* @__PURE__ */ new Map();
10704
10713
  var backendStarts = /* @__PURE__ */ new Map();
10705
10714
  var clients = /* @__PURE__ */ new Map();
10706
10715
  var sessionRefs = /* @__PURE__ */ new Map();
10707
10716
  var backendReaper = null;
10717
+ function isTuiGatewaySessionBusyError(error) {
10718
+ if (error instanceof TuiGatewaySessionBusyError) {
10719
+ return true;
10720
+ }
10721
+ const message = error instanceof Error ? error.message : String(error);
10722
+ return /session busy/i.test(message);
10723
+ }
10724
+ function createTuiGatewayRpcError(error) {
10725
+ const message = error?.message ?? "Hermes RPC failed";
10726
+ if (error?.code === 4009 && /session busy/i.test(message)) {
10727
+ return new TuiGatewaySessionBusyError(message);
10728
+ }
10729
+ return new Error(message);
10730
+ }
10731
+ function withBusyContext(error, details) {
10732
+ if (error instanceof TuiGatewaySessionBusyError) {
10733
+ return new TuiGatewaySessionBusyError(error.message, {
10734
+ ...error.details,
10735
+ ...details
10736
+ });
10737
+ }
10738
+ if (isTuiGatewaySessionBusyError(error)) {
10739
+ return new TuiGatewaySessionBusyError(
10740
+ error instanceof Error ? error.message : String(error),
10741
+ details
10742
+ );
10743
+ }
10744
+ return error instanceof Error ? error : new Error(String(error));
10745
+ }
10746
+ async function withSessionBusyRetry(call, signal) {
10747
+ const deadline = Date.now() + SESSION_BUSY_RETRY_TIMEOUT_MS;
10748
+ for (; ; ) {
10749
+ try {
10750
+ return await call();
10751
+ } catch (error) {
10752
+ if (!isTuiGatewaySessionBusyError(error) || Date.now() >= deadline) {
10753
+ throw error;
10754
+ }
10755
+ await delay4(
10756
+ Math.min(SESSION_BUSY_RETRY_INTERVAL_MS, deadline - Date.now()),
10757
+ void 0,
10758
+ { signal }
10759
+ );
10760
+ }
10761
+ }
10762
+ }
10708
10763
  async function streamTuiGatewayRun(input) {
10709
10764
  const profileName = normalizeProfileName2(input.profileName);
10710
10765
  const client = await getTuiGatewayClient({
@@ -10762,6 +10817,77 @@ async function interruptTuiGatewaySession(input) {
10762
10817
  });
10763
10818
  await client.interruptSession(sessionId);
10764
10819
  }
10820
+ async function readTuiGatewayLiveSession(input) {
10821
+ const runtimeSessionId = input.runtimeSessionId?.trim();
10822
+ const storedSessionId = input.storedSessionId?.trim();
10823
+ if (!runtimeSessionId && !storedSessionId) {
10824
+ return null;
10825
+ }
10826
+ const profileName = normalizeProfileName2(input.profileName);
10827
+ const client = await getTuiGatewayClient({
10828
+ profileName,
10829
+ paths: input.paths,
10830
+ logger: input.logger
10831
+ });
10832
+ try {
10833
+ const result = await client.request(
10834
+ "session.active_list",
10835
+ runtimeSessionId ? { current_session_id: runtimeSessionId } : {},
10836
+ 1e4
10837
+ );
10838
+ const sessions = Array.isArray(result.sessions) ? result.sessions : [];
10839
+ for (const item of sessions) {
10840
+ const liveSession = normalizeTuiGatewayLiveSessionRecord(item);
10841
+ if (!liveSession) {
10842
+ continue;
10843
+ }
10844
+ const runtimeMatches = runtimeSessionId !== void 0 && liveSession.runtimeSessionId === runtimeSessionId;
10845
+ const storedMatches = storedSessionId !== void 0 && liveSession.storedSessionId === storedSessionId;
10846
+ if (!runtimeMatches && !storedMatches) {
10847
+ continue;
10848
+ }
10849
+ return liveSession;
10850
+ }
10851
+ } catch (error) {
10852
+ void input.logger?.debug("tui_gateway_live_session_read_failed", {
10853
+ profile: profileName,
10854
+ runtime_session_id: runtimeSessionId ?? null,
10855
+ stored_session_id: storedSessionId ?? null,
10856
+ error: error instanceof Error ? error.message : String(error)
10857
+ });
10858
+ }
10859
+ return null;
10860
+ }
10861
+ function normalizeTuiGatewayLiveSessionRecord(value) {
10862
+ const record = toRecord3(value);
10863
+ const runtimeId = readString5(record, "id");
10864
+ if (!runtimeId) {
10865
+ return null;
10866
+ }
10867
+ const storedId = readString5(record, "session_key") ?? readString5(record, "sessionKey");
10868
+ const status = (readString5(record, "status") ?? "unknown").toLowerCase();
10869
+ const inflight = typeof record.inflight === "object" && record.inflight !== null ? toRecord3(record.inflight) : void 0;
10870
+ const running = readBoolean(record.running) || status === "working" || status === "waiting" || status === "starting" || inflight !== void 0 && Object.keys(inflight).length > 0;
10871
+ const title = readString5(record, "title");
10872
+ const preview = readString5(record, "preview");
10873
+ const model = readString5(record, "model");
10874
+ const lastActiveAt = readFiniteNumber(record.last_active);
10875
+ const startedAt = readFiniteNumber(record.started_at);
10876
+ return {
10877
+ runtimeSessionId: runtimeId,
10878
+ storedSessionId: storedId ?? runtimeId,
10879
+ status,
10880
+ running,
10881
+ ...readBoolean(record.current) ? { current: true } : {},
10882
+ ...title ? { title } : {},
10883
+ ...preview ? { preview } : {},
10884
+ ...model ? { model } : {},
10885
+ ...lastActiveAt !== void 0 ? { lastActiveAt } : {},
10886
+ ...startedAt !== void 0 ? { startedAt } : {},
10887
+ ...inflight ? { inflight } : {},
10888
+ raw: record
10889
+ };
10890
+ }
10765
10891
  async function dispatchTuiGatewayCommand(input) {
10766
10892
  const profileName = normalizeProfileName2(input.profileName);
10767
10893
  const client = await getTuiGatewayClient({
@@ -11009,32 +11135,50 @@ async function setTuiGatewaySessionModelConfig(input) {
11009
11135
  }
11010
11136
  const raw = [];
11011
11137
  if (model) {
11012
- const result = await client.request(
11013
- "config.set",
11014
- {
11015
- session_id: started.runtimeSessionId,
11016
- key: "model",
11017
- value: formatSessionModelConfigValue(
11018
- model,
11019
- input.provider,
11020
- hasNativeSessionModelConfig
11021
- ),
11022
- confirm_expensive_model: true
11023
- },
11024
- REQUEST_TIMEOUT_MS
11025
- );
11138
+ const result = await withSessionBusyRetry(
11139
+ () => client.request(
11140
+ "config.set",
11141
+ {
11142
+ session_id: started.runtimeSessionId,
11143
+ key: "model",
11144
+ value: formatSessionModelConfigValue(
11145
+ model,
11146
+ input.provider,
11147
+ hasNativeSessionModelConfig
11148
+ ),
11149
+ confirm_expensive_model: true
11150
+ },
11151
+ REQUEST_TIMEOUT_MS
11152
+ )
11153
+ ).catch((error) => {
11154
+ throw withBusyContext(error, {
11155
+ profileName,
11156
+ runtimeSessionId: started.runtimeSessionId,
11157
+ storedSessionId: started.storedSessionId,
11158
+ method: "config.set:model"
11159
+ });
11160
+ });
11026
11161
  raw.push(result);
11027
11162
  }
11028
11163
  if (reasoningEffort) {
11029
- const result = await client.request(
11030
- "config.set",
11031
- {
11032
- session_id: started.runtimeSessionId,
11033
- key: "reasoning",
11034
- value: reasoningEffort
11035
- },
11036
- REQUEST_TIMEOUT_MS
11037
- );
11164
+ const result = await withSessionBusyRetry(
11165
+ () => client.request(
11166
+ "config.set",
11167
+ {
11168
+ session_id: started.runtimeSessionId,
11169
+ key: "reasoning",
11170
+ value: reasoningEffort
11171
+ },
11172
+ REQUEST_TIMEOUT_MS
11173
+ )
11174
+ ).catch((error) => {
11175
+ throw withBusyContext(error, {
11176
+ profileName,
11177
+ runtimeSessionId: started.runtimeSessionId,
11178
+ storedSessionId: started.storedSessionId,
11179
+ method: "config.set:reasoning"
11180
+ });
11181
+ });
11038
11182
  raw.push(result);
11039
11183
  }
11040
11184
  rememberSessionRef(profileName, input.hermesSessionId, started);
@@ -11706,12 +11850,21 @@ var TuiGatewayClient = class {
11706
11850
  cleanup();
11707
11851
  };
11708
11852
  input.signal?.addEventListener("abort", abort, { once: true });
11709
- void this.request(
11710
- "prompt.submit",
11711
- { session_id: input.sessionId, text: input.text },
11712
- PROMPT_SUBMIT_TIMEOUT_MS
11853
+ void withSessionBusyRetry(
11854
+ () => this.request(
11855
+ "prompt.submit",
11856
+ { session_id: input.sessionId, text: input.text },
11857
+ PROMPT_SUBMIT_TIMEOUT_MS
11858
+ ),
11859
+ input.signal
11713
11860
  ).catch((error) => {
11714
- queue.pushError(error);
11861
+ queue.pushError(
11862
+ withBusyContext(error, {
11863
+ profileName: this.profileName,
11864
+ runtimeSessionId: input.sessionId,
11865
+ method: "prompt.submit"
11866
+ })
11867
+ );
11715
11868
  });
11716
11869
  queue.closed.finally(cleanup).catch(() => void 0);
11717
11870
  return queue;
@@ -11845,7 +11998,7 @@ var TuiGatewayClient = class {
11845
11998
  this.pending.delete(frame.id);
11846
11999
  clearTimeout(pending.timer);
11847
12000
  if (frame.error) {
11848
- pending.reject(new Error(frame.error.message ?? "Hermes RPC failed"));
12001
+ pending.reject(createTuiGatewayRpcError(frame.error));
11849
12002
  } else {
11850
12003
  pending.resolve(frame.result);
11851
12004
  }
@@ -12546,6 +12699,19 @@ function readString5(payload, key) {
12546
12699
  const value = payload[key];
12547
12700
  return typeof value === "string" && value.trim() ? value.trim() : null;
12548
12701
  }
12702
+ function readBoolean(value) {
12703
+ return value === true || value === "true" || value === 1;
12704
+ }
12705
+ function readFiniteNumber(value) {
12706
+ if (typeof value === "number" && Number.isFinite(value)) {
12707
+ return value;
12708
+ }
12709
+ if (typeof value === "string" && value.trim()) {
12710
+ const parsed = Number.parseFloat(value);
12711
+ return Number.isFinite(parsed) ? parsed : void 0;
12712
+ }
12713
+ return void 0;
12714
+ }
12549
12715
  function readStringList2(value) {
12550
12716
  if (!Array.isArray(value)) {
12551
12717
  return [];
@@ -17481,7 +17647,10 @@ var ConversationOrchestrationCoordinator = class {
17481
17647
  );
17482
17648
  }
17483
17649
  startRunWorkerAndDrain(conversationId, runId, input) {
17484
- void this.deps.runLifecycle.startRunWorker(conversationId, runId, input).catch(async (error) => {
17650
+ let shouldDrainQueue = true;
17651
+ void this.deps.runLifecycle.startRunWorker(conversationId, runId, input).then((outcome) => {
17652
+ shouldDrainQueue = outcome.shouldDrainQueue;
17653
+ }).catch(async (error) => {
17485
17654
  if (isConversationNotFoundError(error)) {
17486
17655
  return;
17487
17656
  }
@@ -17507,6 +17676,9 @@ var ConversationOrchestrationCoordinator = class {
17507
17676
  });
17508
17677
  }
17509
17678
  }).finally(() => {
17679
+ if (!shouldDrainQueue) {
17680
+ return;
17681
+ }
17510
17682
  void this.startNextQueuedRun(conversationId).catch((error) => {
17511
17683
  void this.deps.logger.warn("conversation_queue_drain_failed", {
17512
17684
  conversation_id: conversationId,
@@ -20514,7 +20686,7 @@ function isNodeError11(error, code) {
20514
20686
  }
20515
20687
 
20516
20688
  // src/conversations/hermes-session-sync.ts
20517
- import { randomUUID as randomUUID11 } from "crypto";
20689
+ import { createHash as createHash6, randomUUID as randomUUID11 } from "crypto";
20518
20690
  import { readdir as readdir7, readFile as readFile11, stat as stat12 } from "fs/promises";
20519
20691
  import path21 from "path";
20520
20692
 
@@ -21074,6 +21246,131 @@ async function syncHermesCronSessionIntoConversations(paths, logger, input) {
21074
21246
  reprojected: false
21075
21247
  };
21076
21248
  }
21249
+ async function syncHermesConversationMessages(paths, logger, input) {
21250
+ const store = input.store ?? new ConversationStore(paths);
21251
+ const manifest = await store.readActiveManifest(input.conversationId);
21252
+ const snapshot = await store.readSnapshot(input.conversationId);
21253
+ const targets = collectHermesSessionDeleteTargets(manifest, snapshot);
21254
+ const result = {
21255
+ conversation_id: input.conversationId,
21256
+ hermes_session_ids: targets.map((target) => target.sessionId),
21257
+ appended_count: 0,
21258
+ changed: false,
21259
+ errors: []
21260
+ };
21261
+ if (targets.length === 0) {
21262
+ return result;
21263
+ }
21264
+ const candidates = await collectConversationSyncCandidates(targets);
21265
+ const knownExactKeys = collectKnownHermesRowKeys(snapshot);
21266
+ const representedMessages = collectRepresentedMessageSignatures(snapshot);
21267
+ const projectedMessages = [];
21268
+ const candidateProfiles = /* @__PURE__ */ new Map();
21269
+ for (const candidate of candidates) {
21270
+ try {
21271
+ const candidateMessages = await readHermesLineageMessages(candidate);
21272
+ if (candidateMessages.length === 0) {
21273
+ continue;
21274
+ }
21275
+ const profile = await resolveConversationProfileTarget(
21276
+ paths,
21277
+ candidate.profileName
21278
+ );
21279
+ candidateProfiles.set(conversationSyncCandidateKey(candidate), {
21280
+ candidate,
21281
+ profileUid: profile.profileUid,
21282
+ profileName: profile.profileName
21283
+ });
21284
+ projectedMessages.push(
21285
+ ...toLinkMessages({
21286
+ conversationId: input.conversationId,
21287
+ profileName: profile.profileName,
21288
+ profileUid: profile.profileUid,
21289
+ profileDisplayName: profile.profileDisplayName,
21290
+ sessionId: candidate.session.id,
21291
+ messages: candidateMessages
21292
+ })
21293
+ );
21294
+ } catch (error) {
21295
+ result.errors.push({
21296
+ profile: candidate.profileName,
21297
+ session_id: candidate.session.id,
21298
+ message: error instanceof Error ? error.message : String(error)
21299
+ });
21300
+ }
21301
+ }
21302
+ const appendedMessages = [];
21303
+ for (const message of projectedMessages.sort(compareLinkMessagesByCreatedAt)) {
21304
+ const exactKeys = collectMessageHermesRowKeys(message);
21305
+ if (exactKeys.length > 0 && exactKeys.every((key) => knownExactKeys.has(key))) {
21306
+ consumeRepresentedMessageSignature(representedMessages, message);
21307
+ continue;
21308
+ }
21309
+ if (consumeRepresentedMessageSignature(representedMessages, message)) {
21310
+ exactKeys.forEach((key) => knownExactKeys.add(key));
21311
+ continue;
21312
+ }
21313
+ appendedMessages.push(message);
21314
+ exactKeys.forEach((key) => knownExactKeys.add(key));
21315
+ }
21316
+ if (appendedMessages.length === 0) {
21317
+ return result;
21318
+ }
21319
+ await store.writeSnapshot(input.conversationId, {
21320
+ ...snapshot,
21321
+ messages: [...snapshot.messages, ...appendedMessages]
21322
+ });
21323
+ await hydrateImportedConversationMedia({
21324
+ paths,
21325
+ store,
21326
+ logger,
21327
+ conversationId: input.conversationId
21328
+ });
21329
+ const hydratedSnapshot = await store.readSnapshot(input.conversationId);
21330
+ const appendedIds = new Set(appendedMessages.map((message) => message.id));
21331
+ const hydratedAppendedMessages = hydratedSnapshot.messages.filter(
21332
+ (message) => appendedIds.has(message.id)
21333
+ );
21334
+ let nextManifest = await store.readManifest(input.conversationId);
21335
+ for (const profile of candidateProfiles.values()) {
21336
+ nextManifest = mergeHermesLineageIntoManifest({
21337
+ manifest: nextManifest,
21338
+ candidate: profile.candidate,
21339
+ snapshot: hydratedSnapshot,
21340
+ profileUid: profile.profileUid,
21341
+ profileName: profile.profileName,
21342
+ updatedAt: isoFromHermesTime(profile.candidate.session.last_active) ?? nextManifest.updated_at
21343
+ });
21344
+ }
21345
+ const stats = buildConversationStats(nextManifest, hydratedSnapshot);
21346
+ nextManifest = {
21347
+ ...nextManifest,
21348
+ stats,
21349
+ updated_at: latestTimestamp(
21350
+ nextManifest.updated_at,
21351
+ latestMessageTimestamp(hydratedAppendedMessages)
21352
+ )
21353
+ };
21354
+ await store.writeManifest(nextManifest);
21355
+ await upsertConversationStats(paths, toStatsIndexRecord(nextManifest, stats));
21356
+ const appendEvent = input.appendEvent ?? ((conversationId, event) => store.appendEvent(conversationId, event));
21357
+ let lastEventSeq;
21358
+ for (const message of hydratedAppendedMessages) {
21359
+ const event = await appendEvent(input.conversationId, {
21360
+ type: "message.created",
21361
+ message_id: message.id,
21362
+ payload: { message, imported_from: "hermes" },
21363
+ raw: message.raw
21364
+ });
21365
+ lastEventSeq = event.seq;
21366
+ }
21367
+ result.appended_count = hydratedAppendedMessages.length;
21368
+ result.changed = hydratedAppendedMessages.length > 0;
21369
+ if (lastEventSeq !== void 0) {
21370
+ result.last_event_seq = lastEventSeq;
21371
+ }
21372
+ return result;
21373
+ }
21077
21374
  async function importHermesSession(input) {
21078
21375
  const { paths, store, logger, candidate } = input;
21079
21376
  const profile = await resolveConversationProfileTarget(
@@ -21285,6 +21582,143 @@ function findKnownConversationIdsForCandidate(known, candidate) {
21285
21582
  }
21286
21583
  return conversationIds;
21287
21584
  }
21585
+ async function collectConversationSyncCandidates(targets) {
21586
+ const sessionIdsByProfile = /* @__PURE__ */ new Map();
21587
+ for (const target of targets) {
21588
+ const profileName = target.profileName.trim() || DEFAULT_PROFILE_NAME;
21589
+ const sessionId = target.sessionId.trim();
21590
+ if (!sessionId) {
21591
+ continue;
21592
+ }
21593
+ const current = sessionIdsByProfile.get(profileName) ?? /* @__PURE__ */ new Set();
21594
+ current.add(sessionId);
21595
+ sessionIdsByProfile.set(profileName, current);
21596
+ }
21597
+ const candidates = [];
21598
+ const seenCandidates = /* @__PURE__ */ new Set();
21599
+ for (const [profileName, targetSessionIds] of sessionIdsByProfile) {
21600
+ const profileDir = resolveHermesProfileDir(profileName);
21601
+ const dbPath = path21.join(profileDir, "state.db");
21602
+ const coveredSessionIds = /* @__PURE__ */ new Set();
21603
+ const sessions = await listProfileSessions(dbPath).catch(() => []);
21604
+ for (const session of sessions) {
21605
+ if (isDeletedSession(session) || isHiddenSession(session)) {
21606
+ continue;
21607
+ }
21608
+ const candidate = {
21609
+ profileName,
21610
+ profileDir,
21611
+ dbPath,
21612
+ session
21613
+ };
21614
+ const candidateSessionIds = lineageSessionIds(candidate);
21615
+ if (!candidateSessionIds.some(
21616
+ (sessionId) => targetSessionIds.has(sessionId)
21617
+ )) {
21618
+ continue;
21619
+ }
21620
+ for (const sessionId of candidateSessionIds) {
21621
+ if (targetSessionIds.has(sessionId)) {
21622
+ coveredSessionIds.add(sessionId);
21623
+ }
21624
+ }
21625
+ const key = conversationSyncCandidateKey(candidate);
21626
+ if (!seenCandidates.has(key)) {
21627
+ seenCandidates.add(key);
21628
+ candidates.push(candidate);
21629
+ }
21630
+ }
21631
+ for (const sessionId of targetSessionIds) {
21632
+ if (coveredSessionIds.has(sessionId)) {
21633
+ continue;
21634
+ }
21635
+ const candidate = {
21636
+ profileName,
21637
+ profileDir,
21638
+ dbPath,
21639
+ session: { id: sessionId }
21640
+ };
21641
+ const key = conversationSyncCandidateKey(candidate);
21642
+ if (!seenCandidates.has(key)) {
21643
+ seenCandidates.add(key);
21644
+ candidates.push(candidate);
21645
+ }
21646
+ }
21647
+ }
21648
+ return candidates;
21649
+ }
21650
+ function conversationSyncCandidateKey(candidate) {
21651
+ return `${candidate.profileName}\0${candidate.session.id}`;
21652
+ }
21653
+ function collectKnownHermesRowKeys(snapshot) {
21654
+ const keys = /* @__PURE__ */ new Set();
21655
+ for (const message of snapshot.messages) {
21656
+ collectMessageHermesRowKeys(message).forEach((key) => keys.add(key));
21657
+ }
21658
+ return keys;
21659
+ }
21660
+ function collectMessageHermesRowKeys(message) {
21661
+ const keys = [];
21662
+ readHermesRawMessageRows(message.raw).forEach((row, index) => {
21663
+ keys.push(hermesRowKey(row, index));
21664
+ });
21665
+ const hermes = toRecord11(message.hermes);
21666
+ const sessionId = readString11(hermes, "session_id") ?? "";
21667
+ const messageIds = Array.isArray(hermes.message_ids) ? hermes.message_ids : hermes.message_id === void 0 || hermes.message_id === null ? [] : [hermes.message_id];
21668
+ for (const messageId of messageIds) {
21669
+ if (messageId === void 0 || messageId === null) {
21670
+ continue;
21671
+ }
21672
+ keys.push(hermesMessageIdKey(sessionId, messageId));
21673
+ }
21674
+ return [...new Set(keys)];
21675
+ }
21676
+ function collectRepresentedMessageSignatures(snapshot) {
21677
+ const counts = /* @__PURE__ */ new Map();
21678
+ for (const message of snapshot.messages) {
21679
+ const signature = representedMessageSignature(message);
21680
+ if (!signature) {
21681
+ continue;
21682
+ }
21683
+ counts.set(signature, (counts.get(signature) ?? 0) + 1);
21684
+ }
21685
+ return counts;
21686
+ }
21687
+ function consumeRepresentedMessageSignature(counts, message) {
21688
+ const signature = representedMessageSignature(message);
21689
+ if (!signature) {
21690
+ return false;
21691
+ }
21692
+ const count = counts.get(signature) ?? 0;
21693
+ if (count <= 0) {
21694
+ return false;
21695
+ }
21696
+ if (count === 1) {
21697
+ counts.delete(signature);
21698
+ } else {
21699
+ counts.set(signature, count - 1);
21700
+ }
21701
+ return true;
21702
+ }
21703
+ function representedMessageSignature(message) {
21704
+ const text = messageText(message);
21705
+ if (!text) {
21706
+ return null;
21707
+ }
21708
+ return `${message.role}:${hashText(text)}`;
21709
+ }
21710
+ function hashText(value) {
21711
+ return createHash6("sha256").update(value).digest("hex");
21712
+ }
21713
+ function compareLinkMessagesByCreatedAt(left, right) {
21714
+ return Date.parse(left.created_at) - Date.parse(right.created_at) || left.id.localeCompare(right.id);
21715
+ }
21716
+ function latestMessageTimestamp(messages2) {
21717
+ return messages2.reduce(
21718
+ (latest, message) => latestTimestamp(latest, message.updated_at || message.created_at),
21719
+ messages2[0]?.updated_at ?? (/* @__PURE__ */ new Date()).toISOString()
21720
+ );
21721
+ }
21288
21722
  function lineageSessionIds(candidate) {
21289
21723
  return normalizeSessionIds([
21290
21724
  candidate.session._lineage_root_id,
@@ -21574,9 +22008,12 @@ function appendHermesRowOnce(rows, seen, row) {
21574
22008
  }
21575
22009
  function hermesRowKey(row, fallbackIndex) {
21576
22010
  if (row.id !== void 0 && row.id !== null) {
21577
- return `id:${row.id}`;
22011
+ return hermesMessageIdKey(readString11(row, "session_id") ?? "", row.id);
21578
22012
  }
21579
- return `fallback:${fallbackIndex}:${row.role ?? ""}:${row.timestamp ?? ""}:${normalizeContent2(row.content)}`;
22013
+ return `fallback:${readString11(row, "session_id") ?? ""}:${fallbackIndex}:${row.role ?? ""}:${row.timestamp ?? ""}:${normalizeContent2(row.content)}`;
22014
+ }
22015
+ function hermesMessageIdKey(sessionId, messageId) {
22016
+ return `id:${sessionId}:${String(messageId)}`;
21580
22017
  }
21581
22018
  function hasHermesToolMetadata(row) {
21582
22019
  return normalizeMessageRole(row.role) === "tool" || readHermesToolCalls(row).length > 0 || Boolean(readString11(row, "tool_call_id")) || Boolean(readString11(row, "tool_name"));
@@ -22629,13 +23066,13 @@ function toRecord11(value) {
22629
23066
  return typeof value === "object" && value !== null ? value : {};
22630
23067
  }
22631
23068
  function isDeletedSession(session) {
22632
- return readBoolean(session.deleted) || readBoolean(session.is_deleted) || Boolean(readString11(session, "deleted_at")) || ["deleted", "removed"].includes(readString11(session, "status") ?? "");
23069
+ return readBoolean2(session.deleted) || readBoolean2(session.is_deleted) || Boolean(readString11(session, "deleted_at")) || ["deleted", "removed"].includes(readString11(session, "status") ?? "");
22633
23070
  }
22634
23071
  function isHiddenSession(session) {
22635
23072
  const source = readString11(session, "source")?.toLowerCase();
22636
23073
  const status = readString11(session, "status")?.toLowerCase();
22637
23074
  const visibility = readString11(session, "visibility")?.toLowerCase();
22638
- return Boolean(source && HIDDEN_SESSION_SOURCES.has(source)) || readBoolean(session.hidden) || readBoolean(session.archived) || Boolean(readString11(session, "archived_at")) || status === "hidden" || status === "archived" || visibility === "hidden" || visibility === "hide";
23075
+ return Boolean(source && HIDDEN_SESSION_SOURCES.has(source)) || readBoolean2(session.hidden) || readBoolean2(session.archived) || Boolean(readString11(session, "archived_at")) || status === "hidden" || status === "archived" || visibility === "hidden" || visibility === "hide";
22639
23076
  }
22640
23077
  function readTableColumns2(db, tableName) {
22641
23078
  try {
@@ -22676,7 +23113,7 @@ function readString11(payload, key) {
22676
23113
  function readNumber3(value) {
22677
23114
  return typeof value === "number" && Number.isFinite(value) ? value : null;
22678
23115
  }
22679
- function readBoolean(value) {
23116
+ function readBoolean2(value) {
22680
23117
  if (value === true || value === 1) {
22681
23118
  return true;
22682
23119
  }
@@ -22894,7 +23331,7 @@ function isNodeError14(error, code) {
22894
23331
  }
22895
23332
 
22896
23333
  // src/conversations/run-lifecycle.ts
22897
- import { createHash as createHash7 } from "crypto";
23334
+ import { createHash as createHash8 } from "crypto";
22898
23335
  import { readdir as readdir9 } from "fs/promises";
22899
23336
 
22900
23337
  // src/hermes/api-server.ts
@@ -23344,9 +23781,9 @@ function parseHermesApiCapabilities(payload) {
23344
23781
  const runStop = isRecord2(endpoints.run_stop) ? endpoints.run_stop : {};
23345
23782
  return {
23346
23783
  source: "reported",
23347
- authRequired: readBoolean2(auth, "required"),
23348
- responsesStreaming: readBoolean2(features, "responses_streaming"),
23349
- runStopPath: readBoolean2(features, "run_stop") === false ? null : readString13(runStop, "path"),
23784
+ authRequired: readBoolean3(auth, "required"),
23785
+ responsesStreaming: readBoolean3(features, "responses_streaming"),
23786
+ runStopPath: readBoolean3(features, "run_stop") === false ? null : readString13(runStop, "path"),
23350
23787
  sessionContinuityHeader: readString13(
23351
23788
  features,
23352
23789
  "session_continuity_header"
@@ -23588,7 +24025,7 @@ function readString13(payload, key) {
23588
24025
  const value = payload[key];
23589
24026
  return typeof value === "string" && value.trim() ? value.trim() : null;
23590
24027
  }
23591
- function readBoolean2(payload, key) {
24028
+ function readBoolean3(payload, key) {
23592
24029
  const value = payload[key];
23593
24030
  return typeof value === "boolean" ? value : null;
23594
24031
  }
@@ -25964,7 +26401,7 @@ function isNodeError17(error, code) {
25964
26401
  }
25965
26402
 
25966
26403
  // src/conversations/run-tool-event-ids.ts
25967
- import { createHash as createHash6 } from "crypto";
26404
+ import { createHash as createHash7 } from "crypto";
25968
26405
  var RunToolEventIdCoalescer = class {
25969
26406
  scope;
25970
26407
  ordinal = 0;
@@ -26129,7 +26566,7 @@ function stableStringify2(value) {
26129
26566
  }
26130
26567
  }
26131
26568
  function hashStableValue(value) {
26132
- return createHash6("sha256").update(value).digest("hex").slice(0, 16);
26569
+ return createHash7("sha256").update(value).digest("hex").slice(0, 16);
26133
26570
  }
26134
26571
  function readString17(payload, key) {
26135
26572
  const value = payload[key];
@@ -26646,7 +27083,7 @@ var ConversationRunLifecycle = class {
26646
27083
  const snapshot = await this.deps.readSnapshot(conversationId);
26647
27084
  const run = snapshot.runs.find((item) => item.id === runId);
26648
27085
  if (!run || run.status !== "running") {
26649
- return;
27086
+ return { shouldDrainQueue: true };
26650
27087
  }
26651
27088
  const controller = new AbortController();
26652
27089
  this.deps.activeRunControllers.set(runId, { conversationId, controller });
@@ -27000,7 +27437,7 @@ var ConversationRunLifecycle = class {
27000
27437
  return null;
27001
27438
  });
27002
27439
  if (!response) {
27003
- return;
27440
+ return { shouldDrainQueue: true };
27004
27441
  }
27005
27442
  await this.consumeHermesEventStream({
27006
27443
  backend,
@@ -27022,7 +27459,15 @@ var ConversationRunLifecycle = class {
27022
27459
  reason: "cancelled by app"
27023
27460
  })
27024
27461
  );
27025
- return;
27462
+ return { shouldDrainQueue: true };
27463
+ }
27464
+ if (isTuiGatewaySessionBusyError(error)) {
27465
+ await this.markRunBlockedByTuiGatewayBusy({
27466
+ conversationId,
27467
+ runId,
27468
+ error
27469
+ });
27470
+ return { shouldDrainQueue: false };
27026
27471
  }
27027
27472
  throw error;
27028
27473
  } finally {
@@ -27031,6 +27476,148 @@ var ConversationRunLifecycle = class {
27031
27476
  }
27032
27477
  unregisterDeliveryContext(runId);
27033
27478
  }
27479
+ return { shouldDrainQueue: true };
27480
+ }
27481
+ async markRunBlockedByTuiGatewayBusy(input) {
27482
+ const details = input.error instanceof TuiGatewaySessionBusyError ? input.error.details : void 0;
27483
+ const snapshot = await this.deps.readSnapshot(input.conversationId);
27484
+ const run = snapshot.runs.find((item) => item.id === input.runId);
27485
+ if (!run) {
27486
+ return;
27487
+ }
27488
+ const liveSession = await readTuiGatewayLiveSession({
27489
+ profileName: run.profile,
27490
+ runtimeSessionId: details?.runtimeSessionId ?? run.hermes_rpc_session_id,
27491
+ storedSessionId: details?.storedSessionId ?? run.hermes_session_id,
27492
+ logger: this.deps.logger,
27493
+ paths: this.deps.paths
27494
+ }).catch((error) => {
27495
+ void this.deps.logger.debug("tui_gateway_busy_live_probe_failed", {
27496
+ conversation_id: input.conversationId,
27497
+ run_id: input.runId,
27498
+ error: error instanceof Error ? error.message : String(error)
27499
+ });
27500
+ return null;
27501
+ });
27502
+ await this.markRunBlockedByTuiGatewayBusyLocked({
27503
+ conversationId: input.conversationId,
27504
+ runId: input.runId,
27505
+ error: input.error,
27506
+ liveSession,
27507
+ runtimeSessionId: liveSession?.runtimeSessionId ?? details?.runtimeSessionId ?? run.hermes_rpc_session_id,
27508
+ storedSessionId: liveSession?.storedSessionId ?? details?.storedSessionId ?? run.hermes_session_id,
27509
+ method: details?.method
27510
+ });
27511
+ await this.deps.syncConversationMessages(input.conversationId).catch(
27512
+ (error) => {
27513
+ void this.deps.logger.warn("tui_gateway_busy_message_sync_failed", {
27514
+ conversation_id: input.conversationId,
27515
+ run_id: input.runId,
27516
+ error: error instanceof Error ? error.message : String(error)
27517
+ });
27518
+ }
27519
+ );
27520
+ }
27521
+ async markRunBlockedByTuiGatewayBusyLocked(input) {
27522
+ await this.deps.withConversationLock(input.conversationId, async () => {
27523
+ const snapshot = await this.deps.readSnapshot(input.conversationId);
27524
+ const run = snapshot.runs.find((item) => item.id === input.runId);
27525
+ if (!run || run.status !== "running") {
27526
+ return;
27527
+ }
27528
+ const now = (/* @__PURE__ */ new Date()).toISOString();
27529
+ const assistant = snapshot.messages.find(
27530
+ (item) => item.id === run.assistant_message_id
27531
+ );
27532
+ const errorMessage5 = input.error instanceof Error ? input.error.message : String(input.error);
27533
+ run.hermes_backend = "tui_gateway";
27534
+ run.hermes_rpc_session_id = input.runtimeSessionId;
27535
+ if (input.storedSessionId) {
27536
+ run.hermes_session_id = input.storedSessionId;
27537
+ }
27538
+ run.error_detail = "tui_gateway_session_busy";
27539
+ run.error_message = errorMessage5;
27540
+ if (assistant) {
27541
+ assistant.status = "streaming";
27542
+ assistant.updated_at = now;
27543
+ assistant.hermes = {
27544
+ ...assistant.hermes ?? {},
27545
+ upstream_busy: true,
27546
+ upstream_busy_at: now,
27547
+ upstream_busy_message: errorMessage5,
27548
+ ...input.method ? { upstream_busy_method: input.method } : {},
27549
+ ...input.runtimeSessionId ? { upstream_runtime_session_id: input.runtimeSessionId } : {},
27550
+ ...input.storedSessionId ? { upstream_session_id: input.storedSessionId } : {},
27551
+ ...input.liveSession ? {
27552
+ upstream_status: input.liveSession.status,
27553
+ upstream_running: input.liveSession.running,
27554
+ ...input.liveSession.preview ? { upstream_preview: input.liveSession.preview } : {}
27555
+ } : {}
27556
+ };
27557
+ }
27558
+ await this.deps.writeSnapshot(input.conversationId, snapshot);
27559
+ if (input.storedSessionId) {
27560
+ const manifest = await this.deps.readRunnableManifest(
27561
+ input.conversationId
27562
+ );
27563
+ const nextManifest = addHermesSessionIdForProfileToManifest(
27564
+ manifest,
27565
+ input.storedSessionId,
27566
+ run.profile
27567
+ );
27568
+ if (nextManifest !== manifest) {
27569
+ await this.deps.writeManifest(nextManifest);
27570
+ }
27571
+ }
27572
+ if (assistant) {
27573
+ await this.deps.appendEvent(input.conversationId, {
27574
+ type: "message.updated",
27575
+ message_id: assistant.id,
27576
+ run_id: run.id,
27577
+ payload: {
27578
+ message: assistant,
27579
+ reason: "tui_gateway_session_busy",
27580
+ upstream_busy: true,
27581
+ ...input.liveSession ? {
27582
+ upstream_session: sanitizeLiveSessionForEvent(
27583
+ input.liveSession
27584
+ )
27585
+ } : {}
27586
+ }
27587
+ });
27588
+ }
27589
+ const event = await this.deps.appendEvent(input.conversationId, {
27590
+ type: "run.upstream_busy",
27591
+ message_id: assistant?.id,
27592
+ run_id: run.id,
27593
+ payload: {
27594
+ run,
27595
+ reason: "tui_gateway_session_busy",
27596
+ error: errorMessage5,
27597
+ ...input.method ? { method: input.method } : {},
27598
+ ...input.liveSession ? {
27599
+ upstream_session: sanitizeLiveSessionForEvent(
27600
+ input.liveSession
27601
+ )
27602
+ } : {
27603
+ upstream_session: {
27604
+ ...input.runtimeSessionId ? { id: input.runtimeSessionId } : {},
27605
+ ...input.storedSessionId ? { session_key: input.storedSessionId } : {}
27606
+ }
27607
+ }
27608
+ }
27609
+ });
27610
+ await this.deps.persistConversationStats(input.conversationId, snapshot);
27611
+ void this.deps.logger.warn("conversation_run_upstream_busy", {
27612
+ conversation_id: input.conversationId,
27613
+ run_id: input.runId,
27614
+ event_seq: event.seq,
27615
+ runtime_session_id: input.runtimeSessionId ?? null,
27616
+ stored_session_id: input.storedSessionId ?? null,
27617
+ upstream_status: input.liveSession?.status ?? null,
27618
+ method: input.method ?? null
27619
+ });
27620
+ });
27034
27621
  }
27035
27622
  async ensureUsageProbeBeforeRun(input) {
27036
27623
  if (!shouldUseHermesUsageProbe(input.backend)) {
@@ -27256,6 +27843,9 @@ var ConversationRunLifecycle = class {
27256
27843
  await this.cancelRunAfterAbort(input.conversationId, input.runId);
27257
27844
  return;
27258
27845
  }
27846
+ if (isTuiGatewaySessionBusyError(error)) {
27847
+ throw error;
27848
+ }
27259
27849
  streamError = error;
27260
27850
  await this.deps.logger.warn("tui_gateway_event_stream_interrupted", {
27261
27851
  backend: input.backend,
@@ -29040,7 +29630,7 @@ ${details.join("\n")}` : localizedEmptyHermesResponseMessage(language);
29040
29630
  if (raw.length <= 200) {
29041
29631
  return raw;
29042
29632
  }
29043
- return `hermespilot:${createHash7("sha256").update(raw).digest("hex")}`;
29633
+ return `hermespilot:${createHash8("sha256").update(raw).digest("hex")}`;
29044
29634
  }
29045
29635
  async assistantMessageIdForRun(conversationId, runId) {
29046
29636
  const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
@@ -29672,6 +30262,21 @@ function expirePendingMessageApprovals(message, resolvedAt) {
29672
30262
  }
29673
30263
  return expired;
29674
30264
  }
30265
+ function sanitizeLiveSessionForEvent(liveSession) {
30266
+ return {
30267
+ id: liveSession.runtimeSessionId,
30268
+ session_key: liveSession.storedSessionId,
30269
+ status: liveSession.status,
30270
+ running: liveSession.running,
30271
+ ...liveSession.current ? { current: true } : {},
30272
+ ...liveSession.title ? { title: liveSession.title } : {},
30273
+ ...liveSession.preview ? { preview: liveSession.preview } : {},
30274
+ ...liveSession.model ? { model: liveSession.model } : {},
30275
+ ...liveSession.lastActiveAt !== void 0 ? { last_active: liveSession.lastActiveAt } : {},
30276
+ ...liveSession.startedAt !== void 0 ? { started_at: liveSession.startedAt } : {},
30277
+ ...liveSession.inflight ? { inflight: liveSession.inflight } : {}
30278
+ };
30279
+ }
29675
30280
  function previewText2(message) {
29676
30281
  if (!message) {
29677
30282
  return null;
@@ -29680,15 +30285,15 @@ function previewText2(message) {
29680
30285
  return text ? text.slice(0, 512) : null;
29681
30286
  }
29682
30287
  function runNotificationSourceEventId(conversationId, runId, eventKind) {
29683
- const digest = createHash7("sha256").update(`${conversationId}:${runId}:${eventKind}`).digest("hex").slice(0, 24);
30288
+ const digest = createHash8("sha256").update(`${conversationId}:${runId}:${eventKind}`).digest("hex").slice(0, 24);
29684
30289
  return `${conversationId}:${eventKind}:${digest}`;
29685
30290
  }
29686
30291
  function approvalNotificationSourceEventId(conversationId, runId, approvalId) {
29687
- const digest = createHash7("sha256").update(`${conversationId}:${runId}:${approvalId}:approval_required`).digest("hex").slice(0, 24);
30292
+ const digest = createHash8("sha256").update(`${conversationId}:${runId}:${approvalId}:approval_required`).digest("hex").slice(0, 24);
29688
30293
  return `${conversationId}:approval_required:${digest}`;
29689
30294
  }
29690
30295
  function inputRequestNotificationSourceEventId(conversationId, runId, inputRequestId) {
29691
- const digest = createHash7("sha256").update(`${conversationId}:${runId}:${inputRequestId}:input_required`).digest("hex").slice(0, 24);
30296
+ const digest = createHash8("sha256").update(`${conversationId}:${runId}:${inputRequestId}:input_required`).digest("hex").slice(0, 24);
29692
30297
  return `${conversationId}:input_required:${digest}`;
29693
30298
  }
29694
30299
  async function closeSseIterator(iterator) {
@@ -29799,6 +30404,7 @@ var ConversationService = class {
29799
30404
  isConversationRunnable: (conversationId) => this.store.isConversationRunnable(conversationId),
29800
30405
  writeBlob: (conversationId, input) => this.maintenance.writeBlob(conversationId, input),
29801
30406
  syncCronDeliveries: () => this.syncCronDeliveries(),
30407
+ syncConversationMessages: (conversationId) => this.syncHermesConversationMessages(conversationId),
29802
30408
  scheduleTitleRefresh: (conversationId) => this.metadata.scheduleGeneratedTitleRefresh(conversationId)
29803
30409
  });
29804
30410
  this.orchestration = new ConversationOrchestrationCoordinator({
@@ -30377,6 +30983,16 @@ var ConversationService = class {
30377
30983
  }
30378
30984
  }
30379
30985
  }
30986
+ async syncHermesConversationMessages(conversationId) {
30987
+ return this.withConversationLock(
30988
+ conversationId,
30989
+ () => syncHermesConversationMessages(this.paths, this.logger, {
30990
+ conversationId,
30991
+ store: this.store,
30992
+ appendEvent: (targetConversationId, event) => this.appendEvent(targetConversationId, event)
30993
+ })
30994
+ );
30995
+ }
30380
30996
  async deliverStagedFiles(stagingDir) {
30381
30997
  const target = resolveDeliveryStagingTarget(this.paths, stagingDir);
30382
30998
  return this.withConversationLock(target.conversationId, async () => {
@@ -30446,6 +31062,16 @@ var ConversationService = class {
30446
31062
  };
30447
31063
  }
30448
31064
  async getMessages(conversationId, options = {}) {
31065
+ if (options.syncHermes === true && !options.beforeMessageId) {
31066
+ await this.syncHermesConversationMessages(conversationId).catch(
31067
+ (error) => {
31068
+ void this.logger.warn("hermes_conversation_message_sync_failed", {
31069
+ conversation_id: conversationId,
31070
+ error: error instanceof Error ? error.message : String(error)
31071
+ });
31072
+ }
31073
+ );
31074
+ }
30449
31075
  return this.queries.getMessages(conversationId, options);
30450
31076
  }
30451
31077
  async setConversationModel(conversationId, input) {
@@ -31490,7 +32116,7 @@ function findApproval(snapshot, approvalId) {
31490
32116
  return null;
31491
32117
  }
31492
32118
  function cronNotificationSourceEventId(conversationId, jobId, outputPath, eventKind) {
31493
- const digest = createHash8("sha256").update(`${conversationId}:${jobId}:${outputPath}:${eventKind}`).digest("hex").slice(0, 24);
32119
+ const digest = createHash9("sha256").update(`${conversationId}:${jobId}:${outputPath}:${eventKind}`).digest("hex").slice(0, 24);
31494
32120
  return `${conversationId}:${eventKind}:${digest}`;
31495
32121
  }
31496
32122
  function conversationHermesSessionIds(manifest) {
@@ -31918,7 +32544,7 @@ function approvalRestartText(language, zh, en) {
31918
32544
  }
31919
32545
 
31920
32546
  // src/security/devices.ts
31921
- import { randomBytes as randomBytes3, randomUUID as randomUUID13, timingSafeEqual, createHash as createHash9 } from "crypto";
32547
+ import { randomBytes as randomBytes3, randomUUID as randomUUID13, timingSafeEqual, createHash as createHash10 } from "crypto";
31922
32548
  var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
31923
32549
  var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
31924
32550
  var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
@@ -32214,7 +32840,7 @@ function randomToken(prefix) {
32214
32840
  return `${prefix}${randomBytes3(24).toString("base64url")}`;
32215
32841
  }
32216
32842
  function sha256(value) {
32217
- return createHash9("sha256").update(value).digest("hex");
32843
+ return createHash10("sha256").update(value).digest("hex");
32218
32844
  }
32219
32845
  function safeEqual(left, right) {
32220
32846
  const leftBytes = Buffer.from(left);
@@ -32494,7 +33120,7 @@ function readPositiveInteger2(value) {
32494
33120
  }
32495
33121
  return void 0;
32496
33122
  }
32497
- function readBoolean3(value) {
33123
+ function readBoolean4(value) {
32498
33124
  if (typeof value === "boolean") {
32499
33125
  return value;
32500
33126
  }
@@ -32615,7 +33241,7 @@ function readMessageAttachments(value) {
32615
33241
  }
32616
33242
  const kind = readAttachmentString(record.kind);
32617
33243
  const type = readAttachmentString(record.type);
32618
- const isVoiceNote = readBoolean3(record.is_voice_note) ?? readBoolean3(record.isVoiceNote);
33244
+ const isVoiceNote = readBoolean4(record.is_voice_note) ?? readBoolean4(record.isVoiceNote);
32619
33245
  const durationMs = readPositiveInteger2(record.duration_ms) ?? readPositiveInteger2(record.durationMs);
32620
33246
  const waveform = readAttachmentWaveform2(
32621
33247
  record.waveform ?? record.waveform_samples ?? record.waveformSamples
@@ -32909,7 +33535,8 @@ function registerConversationRoutes(router, options) {
32909
33535
  ctx.set("cache-control", "no-store");
32910
33536
  const result = await conversations.getMessages(ctx.params.conversationId, {
32911
33537
  limit: readLimit(ctx.query.limit),
32912
- beforeMessageId: readQueryString(ctx.query.before_message_id) ?? readQueryString(ctx.query.before)
33538
+ beforeMessageId: readQueryString(ctx.query.before_message_id) ?? readQueryString(ctx.query.before),
33539
+ syncHermes: readConversationMessagesSyncHermes(ctx.query)
32913
33540
  });
32914
33541
  ctx.body = {
32915
33542
  ok: true,
@@ -33496,6 +34123,13 @@ function registerConversationRoutes(router, options) {
33496
34123
  }
33497
34124
  );
33498
34125
  }
34126
+ function readConversationMessagesSyncHermes(query) {
34127
+ const explicit = readBoolean4(query.sync_hermes) ?? readBoolean4(query.syncHermes);
34128
+ if (explicit !== void 0) {
34129
+ return explicit;
34130
+ }
34131
+ return readQueryString(query.sync)?.toLowerCase() === "hermes";
34132
+ }
33499
34133
  async function prepareConversationListRead(conversations, logger, auth, options = {}) {
33500
34134
  if (options.syncHermesSessions) {
33501
34135
  await conversations.syncHermesSessions().catch((error) => {
@@ -33525,7 +34159,7 @@ function readConversationListCursor(query) {
33525
34159
  }
33526
34160
  function readConversationListForce(query) {
33527
34161
  const raw = Array.isArray(query.force) ? query.force[0] : query.force;
33528
- return readBoolean3(raw) === true;
34162
+ return readBoolean4(raw) === true;
33529
34163
  }
33530
34164
  function readConversationWorkspaceFilter(query) {
33531
34165
  const workspace = readQueryString(query.workspace) ?? readQueryString(query.workspace_id);
@@ -34145,7 +34779,7 @@ function readCronJobUpdateInput(body) {
34145
34779
  if (repeat !== void 0) {
34146
34780
  input.repeat = repeat;
34147
34781
  }
34148
- const enabled = readBoolean3(body.enabled);
34782
+ const enabled = readBoolean4(body.enabled);
34149
34783
  if (enabled !== void 0) {
34150
34784
  input.enabled = enabled;
34151
34785
  }
@@ -34215,7 +34849,7 @@ function assertCronJobId(jobId) {
34215
34849
  }
34216
34850
 
34217
34851
  // src/http/routes/model-configs.ts
34218
- import { createHash as createHash10 } from "crypto";
34852
+ import { createHash as createHash11 } from "crypto";
34219
34853
 
34220
34854
  // src/model-catalog/catalog.ts
34221
34855
  import { randomInt } from "crypto";
@@ -35444,7 +36078,7 @@ function modelProviderIdFromParts(input) {
35444
36078
  input.keyEnv ?? "",
35445
36079
  input.authType ?? ""
35446
36080
  ].join("");
35447
- return `mp_${createHash10("sha256").update(identity).digest("hex").slice(0, 18)}`;
36081
+ return `mp_${createHash11("sha256").update(identity).digest("hex").slice(0, 18)}`;
35448
36082
  }
35449
36083
  function mergeCredentialState(left, right) {
35450
36084
  if (left === "configured" || right === "configured") {
@@ -35743,7 +36377,7 @@ function readModelConfigInput(body) {
35743
36377
  body.context_length ?? body.contextLength
35744
36378
  ),
35745
36379
  keyEnv: readString21(body, "key_env") ?? readString21(body, "keyEnv") ?? void 0,
35746
- setDefault: readBoolean3(body.set_default ?? body.setDefault),
36380
+ setDefault: readBoolean4(body.set_default ?? body.setDefault),
35747
36381
  reasoningEffort: readString21(body, "reasoning_effort") ?? readString21(body, "reasoningEffort") ?? void 0,
35748
36382
  reasoningSupportPolicy: readString21(body, "reasoning_support_policy") ?? readString21(body, "reasoningSupportPolicy") ?? readString21(body, "reasoning_support") ?? readString21(body, "reasoningSupport") ?? void 0,
35749
36383
  supportsVision: readNullableBoolean2(
@@ -35812,14 +36446,14 @@ function readModelConfigImportInput(body) {
35812
36446
  provider: readString21(body, "provider") ?? readString21(body, "provider_key") ?? readString21(body, "providerKey") ?? void 0,
35813
36447
  baseUrl: readString21(body, "base_url") ?? readString21(body, "baseUrl") ?? void 0,
35814
36448
  apiMode: readString21(body, "api_mode") ?? readString21(body, "apiMode") ?? void 0,
35815
- setDefault: readBoolean3(body.set_default ?? body.setDefault)
36449
+ setDefault: readBoolean4(body.set_default ?? body.setDefault)
35816
36450
  };
35817
36451
  }
35818
36452
  function shouldReloadGatewayAfterModelConfigChange(body, options = {}) {
35819
- if (readBoolean3(body.skip_gateway_reload ?? body.skipGatewayReload) === true) {
36453
+ if (readBoolean4(body.skip_gateway_reload ?? body.skipGatewayReload) === true) {
35820
36454
  return false;
35821
36455
  }
35822
- if (readBoolean3(body.reload_gateway ?? body.reloadGateway) === true) {
36456
+ if (readBoolean4(body.reload_gateway ?? body.reloadGateway) === true) {
35823
36457
  return true;
35824
36458
  }
35825
36459
  return options.defaultReload === true;
@@ -36055,13 +36689,20 @@ hermeslink logs -f
36055
36689
  hermeslink logs --all --level debug -f
36056
36690
  \`\`\`
36057
36691
 
36058
- If the daemon appears stuck, suggest:
36692
+ If the daemon appears stuck, tell the user to run this in a separate computer
36693
+ terminal, outside of Hermes Agent:
36059
36694
 
36060
36695
  \`\`\`bash
36061
36696
  hermeslink restart
36062
36697
  \`\`\`
36063
36698
 
36064
- Explain that restarting Hermes Link may briefly disconnect the mobile app.
36699
+ Never execute \`hermeslink restart\` yourself through Hermes Agent, terminal
36700
+ tools, shell tools, code execution, or any other in-session tool call.
36701
+ Restarting Hermes Link from inside the current HermesPilot App run can kill the
36702
+ Link/tui_gateway process that is carrying the response, interrupt the event
36703
+ stream, and leave the current run unrecoverable. If a restart is needed, only
36704
+ instruct the user to run it manually in their own terminal, then wait for the
36705
+ mobile app to reconnect.
36065
36706
 
36066
36707
  ## Troubleshooting Flow
36067
36708
 
@@ -36078,6 +36719,11 @@ Never reveal API keys, access tokens, refresh tokens, private keys, or full .env
36078
36719
 
36079
36720
  Do not recommend exposing port 52379 directly to the public internet without TLS, VPN, Tailscale, WireGuard, or another access-control layer.
36080
36721
 
36722
+ Do not execute Hermes Link service-control commands yourself from inside a
36723
+ Hermes Agent session. This includes \`hermeslink restart\`, \`hermeslink stop\`,
36724
+ \`hermeslink uninstall\`, and Link package update commands. Provide the command
36725
+ for the user to run in an external terminal instead.
36726
+
36081
36727
  Do not modify Hermes profiles, delete user data, edit config files, or kill processes unless the user explicitly asks.
36082
36728
  `;
36083
36729
  async function ensureHermesLinkSkillInstalledForProfiles(options = {}) {
@@ -37135,14 +37781,14 @@ function readProfilePermissionsInput(body) {
37135
37781
  containerDisk: readPositiveInteger2(
37136
37782
  terminal.container_disk ?? terminal.containerDisk
37137
37783
  ),
37138
- containerPersistent: readBoolean3(
37784
+ containerPersistent: readBoolean4(
37139
37785
  terminal.container_persistent ?? terminal.containerPersistent
37140
37786
  )
37141
37787
  };
37142
37788
  }
37143
37789
  const sudo = readOptionalObject(body, "sudo");
37144
37790
  if (sudo) {
37145
- const clear = readBoolean3(sudo.clear ?? sudo.remove ?? sudo.delete);
37791
+ const clear = readBoolean4(sudo.clear ?? sudo.remove ?? sudo.delete);
37146
37792
  const sudoInput = {
37147
37793
  password: readRawString2(sudo, "password") ?? readRawString2(sudo, "sudo_password") ?? readRawString2(sudo, "sudoPassword") ?? void 0,
37148
37794
  clear: clear === true ? true : void 0
@@ -37158,7 +37804,7 @@ function readProfilePermissionsInput(body) {
37158
37804
  toolsets.enabled_toolsets ?? toolsets.enabledToolsets ?? toolsets.enabled,
37159
37805
  "toolsets.enabled"
37160
37806
  ) ?? void 0,
37161
- mcpEnabled: readBoolean3(toolsets.mcp_enabled ?? toolsets.mcpEnabled)
37807
+ mcpEnabled: readBoolean4(toolsets.mcp_enabled ?? toolsets.mcpEnabled)
37162
37808
  };
37163
37809
  }
37164
37810
  if (Object.keys(input).length === 0) {
@@ -38767,7 +39413,7 @@ function booleanSetting(key, label, value) {
38767
39413
  return {
38768
39414
  key,
38769
39415
  label,
38770
- value: readBoolean4(value) ?? false,
39416
+ value: readBoolean5(value) ?? false,
38771
39417
  editable: true,
38772
39418
  kind: "boolean"
38773
39419
  };
@@ -38885,7 +39531,7 @@ function readPositiveInteger4(value) {
38885
39531
  const numberValue = typeof value === "number" ? value : typeof value === "string" ? Number(value.trim()) : NaN;
38886
39532
  return Number.isFinite(numberValue) && numberValue > 0 ? Math.floor(numberValue) : void 0;
38887
39533
  }
38888
- function readBoolean4(value) {
39534
+ function readBoolean5(value) {
38889
39535
  if (typeof value === "boolean") {
38890
39536
  return value;
38891
39537
  }
@@ -39151,15 +39797,15 @@ function readMemorySettingsPatch(body, options = {}) {
39151
39797
  if (containerTag !== void 0) {
39152
39798
  input.containerTag = containerTag;
39153
39799
  }
39154
- const autoRecall = readBoolean3(body.auto_recall ?? body.autoRecall);
39800
+ const autoRecall = readBoolean4(body.auto_recall ?? body.autoRecall);
39155
39801
  if (autoRecall !== void 0) {
39156
39802
  input.autoRecall = autoRecall;
39157
39803
  }
39158
- const autoCapture = readBoolean3(body.auto_capture ?? body.autoCapture);
39804
+ const autoCapture = readBoolean4(body.auto_capture ?? body.autoCapture);
39159
39805
  if (autoCapture !== void 0) {
39160
39806
  input.autoCapture = autoCapture;
39161
39807
  }
39162
- const autoRetain = readBoolean3(body.auto_retain ?? body.autoRetain);
39808
+ const autoRetain = readBoolean4(body.auto_retain ?? body.autoRetain);
39163
39809
  if (autoRetain !== void 0) {
39164
39810
  input.autoRetain = autoRetain;
39165
39811
  }
@@ -39229,7 +39875,7 @@ function readMemorySettingsPatch(body, options = {}) {
39229
39875
  if (writeFrequency) {
39230
39876
  input.writeFrequency = writeFrequency;
39231
39877
  }
39232
- const saveMessages = readBoolean3(body.save_messages ?? body.saveMessages);
39878
+ const saveMessages = readBoolean4(body.save_messages ?? body.saveMessages);
39233
39879
  if (saveMessages !== void 0) {
39234
39880
  input.saveMessages = saveMessages;
39235
39881
  }
@@ -39269,7 +39915,7 @@ function readMemorySettingsPatch(body, options = {}) {
39269
39915
  if (agentId !== void 0) {
39270
39916
  input.agentId = agentId;
39271
39917
  }
39272
- const rerank = readBoolean3(body.rerank);
39918
+ const rerank = readBoolean4(body.rerank);
39273
39919
  if (rerank !== void 0) {
39274
39920
  input.rerank = rerank;
39275
39921
  }
@@ -39293,7 +39939,7 @@ function readMemorySettingsPatch(body, options = {}) {
39293
39939
  if (dbPath !== void 0) {
39294
39940
  input.dbPath = dbPath;
39295
39941
  }
39296
- const autoExtract = readBoolean3(body.auto_extract ?? body.autoExtract);
39942
+ const autoExtract = readBoolean4(body.auto_extract ?? body.autoExtract);
39297
39943
  if (autoExtract !== void 0) {
39298
39944
  input.autoExtract = autoExtract;
39299
39945
  }
@@ -39760,7 +40406,7 @@ function registerProfileSkillRoutes(router, options) {
39760
40406
  router.patch("/api/v1/profiles/:name/skills/:skillName", async (ctx) => {
39761
40407
  await authenticateRequest(ctx, paths);
39762
40408
  const body = await readJsonBody(ctx.req);
39763
- const enabled = readBoolean3(body.enabled);
40409
+ const enabled = readBoolean4(body.enabled);
39764
40410
  if (enabled === void 0) {
39765
40411
  throw new LinkHttpError(
39766
40412
  400,
@@ -41564,6 +42210,7 @@ async function observePublicRoute(options) {
41564
42210
  "content-type": "application/json",
41565
42211
  ...options.relayBootstrapToken ? { authorization: `Bearer ${options.relayBootstrapToken}` } : {}
41566
42212
  },
42213
+ signal: options.signal,
41567
42214
  body: JSON.stringify({
41568
42215
  install_id: options.installId,
41569
42216
  link_id: options.linkId,
@@ -41849,7 +42496,8 @@ async function reportLinkStatusToServer(options = {}) {
41849
42496
  publicKeyPem: identity.public_key_pem,
41850
42497
  observePublicRoute: true,
41851
42498
  configuredLanHost: config.lanHost,
41852
- fetchImpl: options.fetchImpl
42499
+ fetchImpl: options.fetchImpl,
42500
+ signal: options.signal
41853
42501
  });
41854
42502
  const routes = await mergeLastReportedPublicRoutes(paths, discoveredRoutes);
41855
42503
  const systemInfo = readLinkSystemInfo();
@@ -41880,7 +42528,8 @@ async function reportLinkStatusToServer(options = {}) {
41880
42528
  ...payload,
41881
42529
  public_key_pem: identity.public_key_pem,
41882
42530
  signature
41883
- })
42531
+ }),
42532
+ signal: options.signal
41884
42533
  }
41885
42534
  );
41886
42535
  const body = await response.json().catch(() => null);
@@ -41928,19 +42577,28 @@ function startLanIpMonitor(options) {
41928
42577
  let running = false;
41929
42578
  let closed = false;
41930
42579
  let current = Promise.resolve();
42580
+ let currentAbortController = null;
41931
42581
  const check = (context = {}) => {
41932
42582
  if (running || closed) {
41933
42583
  return current;
41934
42584
  }
41935
42585
  running = true;
42586
+ const abortController = new AbortController();
42587
+ currentAbortController = abortController;
41936
42588
  current = (async () => {
41937
42589
  try {
41938
- await checkLanIpChange(options, context);
42590
+ await checkLanIpChange({ ...options, signal: abortController.signal }, context);
41939
42591
  } catch (error) {
42592
+ if (closed && isAbortError3(error)) {
42593
+ return;
42594
+ }
41940
42595
  void options.logger.warn("lan_ip_monitor_failed", {
41941
42596
  error: error instanceof Error ? error.message : String(error)
41942
42597
  });
41943
42598
  } finally {
42599
+ if (currentAbortController === abortController) {
42600
+ currentAbortController = null;
42601
+ }
41944
42602
  running = false;
41945
42603
  }
41946
42604
  })();
@@ -41959,6 +42617,7 @@ function startLanIpMonitor(options) {
41959
42617
  async close() {
41960
42618
  closed = true;
41961
42619
  clearInterval(timer);
42620
+ currentAbortController?.abort();
41962
42621
  await current.catch(() => void 0);
41963
42622
  }
41964
42623
  };
@@ -41979,7 +42638,8 @@ async function checkLanIpChange(options, context = {}) {
41979
42638
  publicKeyPem: identity.public_key_pem,
41980
42639
  observePublicRoute: context.observePublicRoute === true,
41981
42640
  configuredLanHost: config.lanHost,
41982
- fetchImpl: options.fetchImpl
42641
+ fetchImpl: options.fetchImpl,
42642
+ signal: options.signal
41983
42643
  });
41984
42644
  const routes = await mergeLastReportedPublicRoutes(options.paths, discoveredRoutes);
41985
42645
  if (context.publishToRelay) {
@@ -42004,7 +42664,8 @@ async function checkLanIpChange(options, context = {}) {
42004
42664
  const result = await reportLinkStatusToServer({
42005
42665
  paths: options.paths,
42006
42666
  fetchImpl: options.fetchImpl,
42007
- routes
42667
+ routes,
42668
+ signal: options.signal
42008
42669
  });
42009
42670
  if (result) {
42010
42671
  options.onNetworkRoutes?.(routes);
@@ -42022,6 +42683,9 @@ async function checkLanIpChange(options, context = {}) {
42022
42683
  });
42023
42684
  }
42024
42685
  }
42686
+ function isAbortError3(error) {
42687
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
42688
+ }
42025
42689
 
42026
42690
  // src/daemon/process-guard.ts
42027
42691
  var installed = false;
@@ -44898,7 +45562,7 @@ async function readActiveCronJobCount(profiles, logger) {
44898
45562
  }, 0);
44899
45563
  }
44900
45564
  function isActiveCronJob(job) {
44901
- const enabled = readBoolean3(job.enabled) ?? true;
45565
+ const enabled = readBoolean4(job.enabled) ?? true;
44902
45566
  if (!enabled) {
44903
45567
  return false;
44904
45568
  }