@hermespilot/link 0.8.4 → 0.8.5

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.
@@ -7721,7 +7721,7 @@ function isConversationMissingError(error) {
7721
7721
  }
7722
7722
 
7723
7723
  // src/constants.ts
7724
- var LINK_VERSION = "0.8.4";
7724
+ var LINK_VERSION = "0.8.5";
7725
7725
  var LINK_COMMAND = "hermeslink";
7726
7726
  var LINK_DEFAULT_PORT = 52379;
7727
7727
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -10966,6 +10966,8 @@ async function dispatchTuiGatewayCommand(input) {
10966
10966
  output: readString5(result, "output") ?? void 0,
10967
10967
  notice: readString5(result, "notice") ?? void 0,
10968
10968
  message: readString5(result, "message") ?? void 0,
10969
+ runtimeSessionId: started.runtimeSessionId,
10970
+ storedSessionId: started.storedSessionId,
10969
10971
  raw: result
10970
10972
  };
10971
10973
  }
@@ -14555,6 +14557,9 @@ function applyAppMessagePresentation(message) {
14555
14557
  }
14556
14558
  const text = messageText(message);
14557
14559
  if (isHermesImportedMessage(message)) {
14560
+ if (message.role === "user" && isHermesGoalContinuationPrompt(text)) {
14561
+ return { ...message, visibility: "internal" };
14562
+ }
14558
14563
  const strippedText = stripLeadingHermesInternalNote(text);
14559
14564
  if (strippedText !== text) {
14560
14565
  return strippedText ? withText(message, strippedText) : { ...message, visibility: "internal" };
@@ -14600,6 +14605,9 @@ function linkLifecycleMarkerPresentation(message) {
14600
14605
  function isHermesImportedMessage(message) {
14601
14606
  return message.hermes?.imported_from === "hermes" || HERMES_IMPORTED_MESSAGE_FORMATS.has(message.raw?.format ?? "");
14602
14607
  }
14608
+ function isHermesGoalContinuationPrompt(text) {
14609
+ return text.replace(/\r\n/gu, "\n").startsWith("[Continuing toward your standing goal]\nGoal:");
14610
+ }
14603
14611
  function isHermesInternalSystemStyleMarker(text) {
14604
14612
  const normalized = text.trim();
14605
14613
  return normalized.startsWith(
@@ -15932,6 +15940,9 @@ function collectHermesSessionIds(manifest, snapshot) {
15932
15940
  return normalizeHermesSessionIds([
15933
15941
  manifest.hermes_session_id,
15934
15942
  ...manifest.hermes_session_ids ?? [],
15943
+ manifest.hermes_lineage?.root_session_id,
15944
+ manifest.hermes_lineage?.current_session_id,
15945
+ ...manifest.hermes_lineage?.session_ids ?? [],
15935
15946
  ...Object.values(manifest.profile_session_ids ?? {}),
15936
15947
  ...snapshot.runs.map((run) => run.hermes_session_id)
15937
15948
  ]);
@@ -15960,6 +15971,11 @@ function collectHermesSessionDeleteTargets(manifest, snapshot) {
15960
15971
  for (const sessionId of manifest.hermes_session_ids ?? []) {
15961
15972
  add(sessionId, defaultProfile);
15962
15973
  }
15974
+ add(manifest.hermes_lineage?.root_session_id, defaultProfile);
15975
+ add(manifest.hermes_lineage?.current_session_id, defaultProfile);
15976
+ for (const sessionId of manifest.hermes_lineage?.session_ids ?? []) {
15977
+ add(sessionId, defaultProfile);
15978
+ }
15963
15979
  for (const [profileName, sessionId] of Object.entries(
15964
15980
  manifest.profile_session_ids ?? {}
15965
15981
  )) {
@@ -18245,6 +18261,13 @@ var ConversationOrchestrationCoordinator = class {
18245
18261
  attachments: userAttachmentParts,
18246
18262
  language: normalizeLanguage(input.language)
18247
18263
  }) : null;
18264
+ if (goalRequest?.hermesSessionId) {
18265
+ manifest = addHermesSessionIdForProfileToManifest(
18266
+ manifest,
18267
+ goalRequest.hermesSessionId,
18268
+ runtime.profileName
18269
+ );
18270
+ }
18248
18271
  if ((hasRunningRuns(snapshot) || queuedRunCount(snapshot) > 0) && queuedRunCount(snapshot) >= MAX_CONVERSATION_QUEUED_RUNS) {
18249
18272
  throw new LinkHttpError(
18250
18273
  409,
@@ -18459,6 +18482,7 @@ var ConversationOrchestrationCoordinator = class {
18459
18482
  maxTurns: readGoalProgressFromStatusText(result.notice ?? result.output ?? "").maxTurns
18460
18483
  }),
18461
18484
  kickoffInput: result.message.trim(),
18485
+ ...result.storedSessionId ? { hermesSessionId: result.storedSessionId } : {},
18462
18486
  ...result.notice ? { notice: result.notice } : {},
18463
18487
  ...result.output ? { output: result.output } : {}
18464
18488
  };
@@ -20240,6 +20264,40 @@ var ContextCompressionCoordinator = class {
20240
20264
  hasActiveCompression(snapshot) {
20241
20265
  return snapshot.runs.some((run) => isActiveCompressionRun(run));
20242
20266
  }
20267
+ async repairDetachedRunningCompressions(conversationId) {
20268
+ const snapshot = await this.deps.store.readSnapshot(conversationId).catch(() => null);
20269
+ const candidates = snapshot?.runs.filter(
20270
+ (run) => isActiveCompressionRun(run) && !this.activeOperations.has(run.context_compression.operation_id)
20271
+ ) ?? [];
20272
+ let repaired = false;
20273
+ for (const run of candidates) {
20274
+ const operation = run.context_compression;
20275
+ if (!operation) {
20276
+ continue;
20277
+ }
20278
+ const markerId = run.assistant_message_id;
20279
+ await this.finishCompression({
20280
+ conversationId,
20281
+ runId: run.id,
20282
+ markerId,
20283
+ operationId: operation.operation_id,
20284
+ generation: operation.generation,
20285
+ status: "failed",
20286
+ error: new LinkHttpError(
20287
+ 502,
20288
+ "context_compression_detached",
20289
+ localizedText(
20290
+ normalizeLanguage2(run.language),
20291
+ "Hermes Link \u5DF2\u5931\u53BB\u8FD9\u6B21\u4E0A\u4E0B\u6587\u538B\u7F29\u7684\u5B9E\u65F6\u8FDE\u63A5\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
20292
+ "Hermes Link lost the realtime connection for this context compression. Try again shortly."
20293
+ )
20294
+ ),
20295
+ language: normalizeLanguage2(run.language)
20296
+ });
20297
+ repaired = true;
20298
+ }
20299
+ return repaired;
20300
+ }
20243
20301
  async startLocked(input) {
20244
20302
  const focus = normalizeFocus(input.focus);
20245
20303
  const language = normalizeLanguage2(input.language);
@@ -21675,15 +21733,6 @@ async function syncHermesConversationMessages(paths, logger, input) {
21675
21733
  for (const candidate of candidates) {
21676
21734
  try {
21677
21735
  const candidateMessages = await readHermesLineageMessages(candidate);
21678
- if (candidateMessages.length === 0) {
21679
- continue;
21680
- }
21681
- const missingCandidateMessages = candidateMessages.filter(
21682
- (message, index) => !isRepresentedHermesRow(representedRows, message, index)
21683
- );
21684
- if (missingCandidateMessages.length === 0) {
21685
- continue;
21686
- }
21687
21736
  const profile = await resolveConversationProfileTarget(
21688
21737
  paths,
21689
21738
  candidate.profileName
@@ -21693,6 +21742,15 @@ async function syncHermesConversationMessages(paths, logger, input) {
21693
21742
  profileUid: profile.profileUid,
21694
21743
  profileName: profile.profileName
21695
21744
  });
21745
+ if (candidateMessages.length === 0) {
21746
+ continue;
21747
+ }
21748
+ const missingCandidateMessages = candidateMessages.filter(
21749
+ (message, index) => !isRepresentedHermesRow(representedRows, message, index)
21750
+ );
21751
+ if (missingCandidateMessages.length === 0) {
21752
+ continue;
21753
+ }
21696
21754
  projectedMessages.push(
21697
21755
  ...toLinkMessages({
21698
21756
  conversationId: input.conversationId,
@@ -21726,13 +21784,55 @@ async function syncHermesConversationMessages(paths, logger, input) {
21726
21784
  exactKeys.forEach((key) => knownExactKeys.add(key));
21727
21785
  }
21728
21786
  if (appendedMessages.length === 0) {
21787
+ let nextManifest2 = candidateProfiles.size > 0 || duplicateCleanup.removedCount > 0 ? await store.readManifest(input.conversationId) : null;
21788
+ const currentManifest = nextManifest2;
21789
+ let lineageChanged = false;
21790
+ if (nextManifest2) {
21791
+ for (const profile of candidateProfiles.values()) {
21792
+ nextManifest2 = mergeHermesLineageIntoManifest({
21793
+ manifest: nextManifest2,
21794
+ candidate: profile.candidate,
21795
+ snapshot,
21796
+ profileUid: profile.profileUid,
21797
+ profileName: profile.profileName,
21798
+ updatedAt: isoFromHermesTime(profile.candidate.session.last_active) ?? nextManifest2.updated_at
21799
+ });
21800
+ }
21801
+ lineageChanged = !manifestEquivalent(currentManifest, nextManifest2);
21802
+ if (duplicateCleanup.removedCount > 0) {
21803
+ nextManifest2 = {
21804
+ ...nextManifest2,
21805
+ stats: buildConversationStats(nextManifest2, snapshot)
21806
+ };
21807
+ }
21808
+ }
21729
21809
  if (duplicateCleanup.removedCount > 0) {
21730
21810
  await store.writeSnapshot(input.conversationId, snapshot);
21731
- const currentManifest = await store.readManifest(input.conversationId);
21732
- const stats2 = buildConversationStats(currentManifest, snapshot);
21733
- const nextManifest2 = { ...currentManifest, stats: stats2 };
21811
+ }
21812
+ if (nextManifest2 && currentManifest && (duplicateCleanup.removedCount > 0 || lineageChanged)) {
21813
+ const stats2 = buildConversationStats(nextManifest2, snapshot);
21814
+ nextManifest2 = { ...nextManifest2, stats: stats2 };
21734
21815
  await store.writeManifest(nextManifest2);
21735
21816
  await upsertConversationStats(paths, toStatsIndexRecord(nextManifest2, stats2));
21817
+ result.hermes_session_ids = manifestHermesSessionIds(nextManifest2);
21818
+ result.changed = true;
21819
+ if (lineageChanged) {
21820
+ const appendEvent2 = input.appendEvent ?? ((conversationId, event2) => store.appendEvent(conversationId, event2));
21821
+ const event = await appendEvent2(input.conversationId, {
21822
+ type: "conversation.updated",
21823
+ payload: {
21824
+ source: "hermes_sync",
21825
+ hermes_session_id: nextManifest2.hermes_session_id,
21826
+ hermes_session_ids: manifestHermesSessionIds(nextManifest2),
21827
+ conversation: await summarizeSyncedConversation(
21828
+ paths,
21829
+ nextManifest2,
21830
+ snapshot
21831
+ )
21832
+ }
21833
+ });
21834
+ result.last_event_seq = event.seq;
21835
+ }
21736
21836
  }
21737
21837
  return result;
21738
21838
  }
@@ -21773,6 +21873,7 @@ async function syncHermesConversationMessages(paths, logger, input) {
21773
21873
  };
21774
21874
  await store.writeManifest(nextManifest);
21775
21875
  await upsertConversationStats(paths, toStatsIndexRecord(nextManifest, stats));
21876
+ result.hermes_session_ids = manifestHermesSessionIds(nextManifest);
21776
21877
  const appendEvent = input.appendEvent ?? ((conversationId, event) => store.appendEvent(conversationId, event));
21777
21878
  let lastEventSeq;
21778
21879
  for (const message of hydratedAppendedMessages) {
@@ -22162,6 +22263,18 @@ function isStrictImportedDuplicateRunMessage(paths, snapshot, message) {
22162
22263
  if (!isMessageInsideRunCleanupWindow(message, run)) {
22163
22264
  return false;
22164
22265
  }
22266
+ if (isImportedMessageCoveredByRunWindow(message, run)) {
22267
+ if (message.role === "user") {
22268
+ return snapshot.messages.some(
22269
+ (item) => item.id === run.trigger_message_id
22270
+ );
22271
+ }
22272
+ if (message.role === "assistant" || message.role === "tool") {
22273
+ return snapshot.messages.some(
22274
+ (item) => item.id === run.assistant_message_id
22275
+ );
22276
+ }
22277
+ }
22165
22278
  if (message.role === "user") {
22166
22279
  const user = snapshot.messages.find(
22167
22280
  (item) => item.id === run.trigger_message_id
@@ -22177,6 +22290,20 @@ function isStrictImportedDuplicateRunMessage(paths, snapshot, message) {
22177
22290
  return false;
22178
22291
  });
22179
22292
  }
22293
+ function isImportedMessageCoveredByRunWindow(message, run) {
22294
+ const rows = readHermesRawMessageRows(message.raw);
22295
+ if (rows.length === 0) {
22296
+ return false;
22297
+ }
22298
+ const windows = collectRunHermesMessageWindows(run);
22299
+ if (windows.length === 0) {
22300
+ return false;
22301
+ }
22302
+ return rows.every((row) => {
22303
+ const role = normalizeMessageRole(row.role);
22304
+ return windows.some((window) => isRowInsideRunWindow(window, row, role));
22305
+ });
22306
+ }
22180
22307
  function isDuplicateImportedRunUser(paths, user, imported) {
22181
22308
  const importedText = normalizedDuplicateText(imported);
22182
22309
  if (!importedText) {
@@ -22249,7 +22376,7 @@ function normalizedDuplicateTextValue(value) {
22249
22376
  return value.replace(/\r\n/gu, "\n").trim();
22250
22377
  }
22251
22378
  function collectRunHermesMessageWindows(run) {
22252
- const before = normalizeWatermarkPoint(
22379
+ let before = normalizeWatermarkPoint(
22253
22380
  run.hermes_message_watermark?.before,
22254
22381
  run.hermes_session_id
22255
22382
  );
@@ -22260,10 +22387,15 @@ function collectRunHermesMessageWindows(run) {
22260
22387
  run.hermes_message_watermark?.after,
22261
22388
  run.hermes_session_id
22262
22389
  );
22263
- const sessionId = before.session_id?.trim() || run.hermes_session_id?.trim();
22390
+ const beforeSessionId = before.session_id?.trim();
22391
+ const afterSessionId = after?.session_id?.trim();
22392
+ const sessionId = beforeSessionId && isPlaceholderHermesSessionId(beforeSessionId) && afterSessionId && !isPlaceholderHermesSessionId(afterSessionId) ? afterSessionId : beforeSessionId || run.hermes_session_id?.trim();
22264
22393
  if (!sessionId) {
22265
22394
  return [];
22266
22395
  }
22396
+ if (before.session_id !== sessionId) {
22397
+ before = { ...before, session_id: sessionId };
22398
+ }
22267
22399
  return [
22268
22400
  {
22269
22401
  runId: run.id,
@@ -22387,6 +22519,9 @@ function isRowInsideRunWindow(window, row, role) {
22387
22519
  const upperTimeMs = hermesTimestampMillis(window.after?.max_timestamp) ?? (allowTerminalGrace && window.completedAtMs !== void 0 ? window.completedAtMs + 2 * 60 * 1e3 : allowOpenEnded ? Number.POSITIVE_INFINITY : void 0);
22388
22520
  return upperTimeMs !== void 0 && (beforeTimeMs !== void 0 ? rowTimeMs > lowerTimeMs : rowTimeMs >= lowerTimeMs) && rowTimeMs <= upperTimeMs;
22389
22521
  }
22522
+ function isPlaceholderHermesSessionId(sessionId) {
22523
+ return sessionId.startsWith("hp_");
22524
+ }
22390
22525
  function isVoicePart(part) {
22391
22526
  return part.kind === "voice" || part.is_voice_note === true;
22392
22527
  }
@@ -22474,6 +22609,16 @@ function lineageSessionIds(candidate) {
22474
22609
  candidate.session.id
22475
22610
  ]);
22476
22611
  }
22612
+ function manifestHermesSessionIds(manifest) {
22613
+ return normalizeSessionIds([
22614
+ manifest.hermes_session_id,
22615
+ ...manifest.hermes_session_ids ?? [],
22616
+ manifest.hermes_lineage?.root_session_id,
22617
+ manifest.hermes_lineage?.current_session_id,
22618
+ ...manifest.hermes_lineage?.session_ids ?? [],
22619
+ ...Object.values(manifest.profile_session_ids ?? {})
22620
+ ]);
22621
+ }
22477
22622
  function lineageTitle(candidate) {
22478
22623
  const explicitLineageTitle = normalizeOptionalTitle(
22479
22624
  candidate.session._lineage_title
@@ -22735,6 +22880,13 @@ function mergeHermesLineageIntoManifest(input) {
22735
22880
  function manifestEquivalent(left, right) {
22736
22881
  return stableJson(left) === stableJson(right);
22737
22882
  }
22883
+ async function summarizeSyncedConversation(paths, manifest, snapshot) {
22884
+ return toSummary(
22885
+ manifest,
22886
+ snapshot,
22887
+ await readConversationProfileSummary(paths, manifest)
22888
+ );
22889
+ }
22738
22890
  function collectImportedHermesPrefix(snapshot) {
22739
22891
  const rows = [];
22740
22892
  const seen = /* @__PURE__ */ new Set();
@@ -29665,6 +29817,309 @@ ${resolved}` : resolved;
29665
29817
  return true;
29666
29818
  });
29667
29819
  }
29820
+ async repairStaleAutomaticContextCompression(conversationId) {
29821
+ const terminalRepaired = await this.deps.withConversationLock(
29822
+ conversationId,
29823
+ async () => {
29824
+ const snapshot = await this.deps.readSnapshot(conversationId);
29825
+ const cleared = [];
29826
+ for (const run of snapshot.runs) {
29827
+ const status = staleAutomaticContextCompressionStatus(run);
29828
+ if (!status) {
29829
+ continue;
29830
+ }
29831
+ const result = this.clearAutomaticContextCompressionInSnapshot({
29832
+ snapshot,
29833
+ run,
29834
+ status,
29835
+ completedAt: run.completed_at ?? (/* @__PURE__ */ new Date()).toISOString()
29836
+ });
29837
+ if (result) {
29838
+ cleared.push({ run, ...result });
29839
+ }
29840
+ }
29841
+ if (cleared.length === 0) {
29842
+ return false;
29843
+ }
29844
+ await this.deps.writeSnapshot(conversationId, snapshot);
29845
+ for (const item of cleared) {
29846
+ await this.appendAutomaticContextCompressionClearedEvent({
29847
+ conversationId,
29848
+ run: item.run,
29849
+ marker: item.marker,
29850
+ metadata: item.metadata
29851
+ });
29852
+ }
29853
+ await this.deps.persistConversationStats(conversationId, snapshot);
29854
+ return true;
29855
+ }
29856
+ );
29857
+ const detachedRunningRepaired = await this.repairDetachedRunningAutomaticContextCompression(conversationId);
29858
+ return terminalRepaired || detachedRunningRepaired;
29859
+ }
29860
+ async repairStaleConversationRuns(conversationId) {
29861
+ const compressionRepaired = await this.repairStaleAutomaticContextCompression(conversationId);
29862
+ const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
29863
+ const candidates = snapshot?.runs.filter(
29864
+ (run) => run.status === "running" && run.kind !== "compression" && run.context_compression?.status !== "compressing" && !isRunningAutomaticContextCompression(run)
29865
+ ) ?? [];
29866
+ let repaired = compressionRepaired;
29867
+ let shouldDrainQueue = compressionRepaired;
29868
+ let pending = false;
29869
+ for (const run of candidates) {
29870
+ const result = await this.repairDetachedRunningRun(conversationId, run);
29871
+ if (result.repaired) {
29872
+ repaired = true;
29873
+ }
29874
+ if (result.shouldDrainQueue) {
29875
+ shouldDrainQueue = true;
29876
+ }
29877
+ if (result.pending) {
29878
+ pending = true;
29879
+ }
29880
+ }
29881
+ return { repaired, shouldDrainQueue, pending };
29882
+ }
29883
+ async repairDetachedRunningAutomaticContextCompression(conversationId) {
29884
+ const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
29885
+ const candidates = snapshot?.runs.filter(
29886
+ (run) => isRunningAutomaticContextCompression(run)
29887
+ ) ?? [];
29888
+ let repaired = false;
29889
+ for (const run of candidates) {
29890
+ const previousSessionId = run.context_compression?.previous_session_id?.trim() || run.hermes_session_id;
29891
+ const nextSessionId = await readHermesCompressionTip(
29892
+ previousSessionId,
29893
+ this.deps.paths,
29894
+ run.profile
29895
+ ).catch(() => void 0);
29896
+ if (!nextSessionId || nextSessionId === previousSessionId) {
29897
+ continue;
29898
+ }
29899
+ const finished = await this.finishAutomaticContextCompressionFromEvent({
29900
+ conversationId,
29901
+ runId: run.id,
29902
+ previousSessionId,
29903
+ nextSessionId
29904
+ });
29905
+ if (!finished) {
29906
+ continue;
29907
+ }
29908
+ this.abortAndForgetActiveRunController(run.id);
29909
+ await this.completeRun(conversationId, run.id);
29910
+ repaired = true;
29911
+ }
29912
+ return repaired;
29913
+ }
29914
+ async repairDetachedRunningRun(conversationId, candidate) {
29915
+ const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
29916
+ const run = snapshot?.runs.find((item) => item.id === candidate.id);
29917
+ if (!run || run.status !== "running") {
29918
+ return { repaired: false, shouldDrainQueue: false };
29919
+ }
29920
+ const backend = run.hermes_backend ?? (run.hermes_run_id ? "runs" : run.hermes_rpc_session_id ? "tui_gateway" : void 0);
29921
+ const language = run.language === "en" ? "en" : "zh-CN";
29922
+ if (backend === "runs" && run.hermes_run_id) {
29923
+ const terminal = await this.readRunTerminalEventOnce({
29924
+ conversationId,
29925
+ run,
29926
+ language
29927
+ });
29928
+ if (terminal.event) {
29929
+ this.abortAndForgetActiveRunController(run.id);
29930
+ const handled = await this.handleNormalizedHermesEvent({
29931
+ backend: "runs",
29932
+ conversationId,
29933
+ runId: run.id,
29934
+ event: terminal.event,
29935
+ profileName: run.profile,
29936
+ cronJobIdsBeforeRun: null
29937
+ });
29938
+ if (handled) {
29939
+ this.deps.scheduleTitleRefresh(conversationId);
29940
+ return { repaired: true, shouldDrainQueue: true };
29941
+ }
29942
+ }
29943
+ if (terminal.pending) {
29944
+ return { repaired: false, shouldDrainQueue: false, pending: true };
29945
+ }
29946
+ }
29947
+ if (backend === "tui_gateway" && run.hermes_rpc_session_id) {
29948
+ const liveSession = await readTuiGatewayLiveSession({
29949
+ profileName: run.profile,
29950
+ runtimeSessionId: run.hermes_rpc_session_id,
29951
+ storedSessionId: run.hermes_session_id,
29952
+ logger: this.deps.logger,
29953
+ paths: this.deps.paths
29954
+ }).catch((error) => {
29955
+ void this.deps.logger.debug("stale_tui_gateway_live_probe_failed", {
29956
+ conversation_id: conversationId,
29957
+ run_id: run.id,
29958
+ hermes_rpc_session_id: run.hermes_rpc_session_id,
29959
+ error: error instanceof Error ? error.message : String(error)
29960
+ });
29961
+ return null;
29962
+ });
29963
+ if (liveSession?.running) {
29964
+ await this.deps.logger.debug("stale_run_left_running_live_session", {
29965
+ conversation_id: conversationId,
29966
+ run_id: run.id,
29967
+ hermes_rpc_session_id: run.hermes_rpc_session_id,
29968
+ upstream_status: liveSession.status
29969
+ });
29970
+ return { repaired: false, shouldDrainQueue: false, pending: true };
29971
+ }
29972
+ }
29973
+ if (await this.runHasDisplayableAssistantOutput(conversationId, run.id)) {
29974
+ this.abortAndForgetActiveRunController(run.id);
29975
+ await this.completeRun(conversationId, run.id);
29976
+ this.deps.scheduleTitleRefresh(conversationId);
29977
+ return { repaired: true, shouldDrainQueue: true };
29978
+ }
29979
+ const recovered = await this.recoverDetachedRunFromHermesTranscript({
29980
+ conversationId,
29981
+ run
29982
+ });
29983
+ if (recovered) {
29984
+ this.abortAndForgetActiveRunController(run.id);
29985
+ this.deps.scheduleTitleRefresh(conversationId);
29986
+ return { repaired: true, shouldDrainQueue: true };
29987
+ }
29988
+ const stillRunning = await this.deps.readSnapshot(conversationId).then(
29989
+ (latest) => latest.runs.some(
29990
+ (item) => item.id === run.id && item.status === "running"
29991
+ )
29992
+ ).catch(() => false);
29993
+ if (!stillRunning || this.deps.activeRunControllers.has(run.id)) {
29994
+ return { repaired: false, shouldDrainQueue: false };
29995
+ }
29996
+ await this.failRun(
29997
+ conversationId,
29998
+ run.id,
29999
+ detachedRunRecoveryFailedMessage(language)
30000
+ );
30001
+ return { repaired: true, shouldDrainQueue: true };
30002
+ }
30003
+ abortAndForgetActiveRunController(runId) {
30004
+ const active = this.deps.activeRunControllers.get(runId);
30005
+ active?.controller.abort();
30006
+ this.deps.activeRunControllers.delete(runId);
30007
+ }
30008
+ async readRunTerminalEventOnce(input) {
30009
+ const hermesRunId = input.run.hermes_run_id;
30010
+ if (!hermesRunId) {
30011
+ return { event: null, pending: false };
30012
+ }
30013
+ let status;
30014
+ try {
30015
+ status = await readHermesRunStatus(hermesRunId, {
30016
+ logger: this.deps.logger,
30017
+ profileName: input.run.profile,
30018
+ language: input.language
30019
+ });
30020
+ } catch (error) {
30021
+ await this.deps.logger.warn("stale_run_status_probe_failed", {
30022
+ conversation_id: input.conversationId,
30023
+ run_id: input.run.id,
30024
+ hermes_run_id: hermesRunId,
30025
+ error: error instanceof Error ? error.message : String(error)
30026
+ });
30027
+ return { event: null, pending: false };
30028
+ }
30029
+ if (!status) {
30030
+ return { event: null, pending: false };
30031
+ }
30032
+ if (status.session_id) {
30033
+ await this.rememberRunHermesSessionId(
30034
+ input.conversationId,
30035
+ input.run.id,
30036
+ status.session_id
30037
+ );
30038
+ }
30039
+ return {
30040
+ event: hermesRunStatusToTerminalEvent(status),
30041
+ pending: !isTerminalRunStatus(status.status)
30042
+ };
30043
+ }
30044
+ async recoverDetachedRunFromHermesTranscript(input) {
30045
+ const recoveredText = await readRunFinalAssistantText({
30046
+ profileName: input.run.profile,
30047
+ hermesSessionId: input.run.hermes_session_id,
30048
+ runStartedAt: input.run.started_at
30049
+ }).catch(async (error) => {
30050
+ await this.deps.logger.warn("stale_run_transcript_recovery_read_failed", {
30051
+ conversation_id: input.conversationId,
30052
+ run_id: input.run.id,
30053
+ hermes_session_id: input.run.hermes_session_id,
30054
+ error: error instanceof Error ? error.message : String(error)
30055
+ });
30056
+ return null;
30057
+ });
30058
+ if (!recoveredText?.trim()) {
30059
+ return false;
30060
+ }
30061
+ await this.persistHermesEvent(
30062
+ input.conversationId,
30063
+ input.run.id,
30064
+ transcriptRecoveryDeltaEvent(input.run.hermes_session_id, recoveredText)
30065
+ );
30066
+ await this.completeRun(
30067
+ input.conversationId,
30068
+ input.run.id,
30069
+ transcriptRecoveryCompletedEvent(input.run.hermes_session_id)
30070
+ );
30071
+ await this.deps.logger.info("stale_run_transcript_recovered", {
30072
+ conversation_id: input.conversationId,
30073
+ run_id: input.run.id,
30074
+ hermes_session_id: input.run.hermes_session_id,
30075
+ content_length: recoveredText.length
30076
+ });
30077
+ return true;
30078
+ }
30079
+ clearAutomaticContextCompressionInSnapshot(input) {
30080
+ if (input.run.context_compression?.status !== "compressing" || input.run.context_compression.source !== "auto") {
30081
+ return null;
30082
+ }
30083
+ const marker = findContextCompressionMarker(
30084
+ input.snapshot,
30085
+ input.run.context_compression.operation_id
30086
+ );
30087
+ if (!marker) {
30088
+ return null;
30089
+ }
30090
+ const metadata = {
30091
+ ...input.run.context_compression,
30092
+ status: input.status,
30093
+ completed_at: input.completedAt
30094
+ };
30095
+ input.run.context_compression = metadata;
30096
+ updateContextCompressionMarker({
30097
+ message: marker,
30098
+ metadata,
30099
+ language: input.run.language === "en" ? "en" : "zh-CN"
30100
+ });
30101
+ return { marker, metadata };
30102
+ }
30103
+ async appendAutomaticContextCompressionClearedEvent(input) {
30104
+ await this.deps.appendEvent(input.conversationId, {
30105
+ type: contextCompressionClearedEventType(input.metadata.status),
30106
+ message_id: input.marker.id,
30107
+ run_id: input.run.id,
30108
+ payload: {
30109
+ message: input.marker,
30110
+ run: input.run,
30111
+ operation: input.metadata,
30112
+ automatic: true,
30113
+ session_rotated: false
30114
+ },
30115
+ ...input.event ? {
30116
+ raw: {
30117
+ format: "hermes-run-event",
30118
+ payload: input.event.rawPayload
30119
+ }
30120
+ } : {}
30121
+ });
30122
+ }
29668
30123
  async runHasAssistantOutput(conversationId, runId) {
29669
30124
  const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
29670
30125
  const run = snapshot?.runs.find((item) => item.id === runId);
@@ -29676,6 +30131,17 @@ ${resolved}` : resolved;
29676
30131
  }
29677
30132
  return messageText2(assistant).length > 0 || (assistant.agent_events?.length ?? 0) > 0 || (assistant.approvals?.length ?? 0) > 0 || assistant.parts.some((part) => part.type !== "text");
29678
30133
  }
30134
+ async runHasDisplayableAssistantOutput(conversationId, runId) {
30135
+ const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
30136
+ const run = snapshot?.runs.find((item) => item.id === runId);
30137
+ const assistant = snapshot?.messages.find(
30138
+ (item) => item.id === run?.assistant_message_id
30139
+ );
30140
+ if (!assistant) {
30141
+ return false;
30142
+ }
30143
+ return messageText2(assistant).trim().length > 0 || assistant.parts.some((part) => part.type !== "text");
30144
+ }
29679
30145
  async appendAssistantTextFromCompletedResponse(conversationId, runId, event) {
29680
30146
  const terminalText = extractResponseAssistantText(event.payload);
29681
30147
  if (!terminalText?.trim()) {
@@ -30228,6 +30694,12 @@ ${details.join("\n")}` : localizedEmptyHermesResponseMessage(language);
30228
30694
  );
30229
30695
  }
30230
30696
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
30697
+ const clearedCompression = this.clearAutomaticContextCompressionInSnapshot({
30698
+ snapshot,
30699
+ run,
30700
+ status: "completed",
30701
+ completedAt
30702
+ });
30231
30703
  const usage = readUsage(source?.payload);
30232
30704
  const responseId = readResponseId(source?.payload);
30233
30705
  const hermesRunId = readRunId(source?.payload);
@@ -30328,6 +30800,15 @@ ${details.join("\n")}` : localizedEmptyHermesResponseMessage(language);
30328
30800
  runId,
30329
30801
  expiredInputRequests
30330
30802
  );
30803
+ if (clearedCompression) {
30804
+ await this.appendAutomaticContextCompressionClearedEvent({
30805
+ conversationId,
30806
+ run,
30807
+ marker: clearedCompression.marker,
30808
+ metadata: clearedCompression.metadata,
30809
+ event: source
30810
+ });
30811
+ }
30331
30812
  if (assistant) {
30332
30813
  await this.deps.appendEvent(conversationId, {
30333
30814
  type: "message.completed",
@@ -30404,6 +30885,12 @@ ${details.join("\n")}` : localizedEmptyHermesResponseMessage(language);
30404
30885
  }
30405
30886
  run.status = "failed";
30406
30887
  run.completed_at = (/* @__PURE__ */ new Date()).toISOString();
30888
+ const clearedCompression = this.clearAutomaticContextCompressionInSnapshot({
30889
+ snapshot,
30890
+ run,
30891
+ status: "failed",
30892
+ completedAt: run.completed_at
30893
+ });
30407
30894
  run.error_message = message;
30408
30895
  run.error_detail = source ? readErrorMessage4(source.payload) ?? void 0 : void 0;
30409
30896
  const language = run.language === "en" ? "en" : "zh-CN";
@@ -30454,6 +30941,15 @@ ${details.join("\n")}` : localizedEmptyHermesResponseMessage(language);
30454
30941
  runId,
30455
30942
  expiredInputRequests
30456
30943
  );
30944
+ if (clearedCompression) {
30945
+ await this.appendAutomaticContextCompressionClearedEvent({
30946
+ conversationId,
30947
+ run,
30948
+ marker: clearedCompression.marker,
30949
+ metadata: clearedCompression.metadata,
30950
+ event: source
30951
+ });
30952
+ }
30457
30953
  const contextUsage = contextUsagePayloadForSnapshot(snapshot);
30458
30954
  await this.deps.appendEvent(conversationId, {
30459
30955
  type: "run.failed",
@@ -30682,6 +31178,12 @@ ${details.join("\n")}` : localizedEmptyHermesResponseMessage(language);
30682
31178
  const cancelledAt = (/* @__PURE__ */ new Date()).toISOString();
30683
31179
  run.status = "cancelled";
30684
31180
  run.completed_at = cancelledAt;
31181
+ const clearedCompression = this.clearAutomaticContextCompressionInSnapshot({
31182
+ snapshot,
31183
+ run,
31184
+ status: "cancelled",
31185
+ completedAt: cancelledAt
31186
+ });
30685
31187
  run.error_message = options.reason;
30686
31188
  const cancelledByGoalPause = options.reason === "goal paused by app";
30687
31189
  const assistant = snapshot.messages.find(
@@ -30723,6 +31225,14 @@ ${details.join("\n")}` : localizedEmptyHermesResponseMessage(language);
30723
31225
  runId,
30724
31226
  cancelledInputRequests
30725
31227
  );
31228
+ if (clearedCompression) {
31229
+ await this.appendAutomaticContextCompressionClearedEvent({
31230
+ conversationId,
31231
+ run,
31232
+ marker: clearedCompression.marker,
31233
+ metadata: clearedCompression.metadata
31234
+ });
31235
+ }
30726
31236
  if (assistant) {
30727
31237
  await this.deps.appendEvent(conversationId, {
30728
31238
  type: "message.completed",
@@ -30975,6 +31485,36 @@ function contextCompressionGeneration(message) {
30975
31485
  function readPreviousSessionId(payload) {
30976
31486
  return readString21(payload, "old_session_id") ?? readString21(payload, "oldSessionId") ?? readString21(payload, "previous_session_id") ?? readString21(payload, "previousSessionId");
30977
31487
  }
31488
+ function contextCompressionClearedEventType(status) {
31489
+ if (status === "completed") {
31490
+ return "context_compression.completed";
31491
+ }
31492
+ if (status === "cancelled") {
31493
+ return "context_compression.cancelled";
31494
+ }
31495
+ if (status === "timed_out") {
31496
+ return "context_compression.timed_out";
31497
+ }
31498
+ return "context_compression.failed";
31499
+ }
31500
+ function staleAutomaticContextCompressionStatus(run) {
31501
+ if (run.context_compression?.status !== "compressing" || run.context_compression.source !== "auto") {
31502
+ return null;
31503
+ }
31504
+ if (run.status === "completed") {
31505
+ return "completed";
31506
+ }
31507
+ if (run.status === "failed") {
31508
+ return "failed";
31509
+ }
31510
+ if (run.status === "cancelled") {
31511
+ return "cancelled";
31512
+ }
31513
+ return null;
31514
+ }
31515
+ function isRunningAutomaticContextCompression(run) {
31516
+ return run.status === "running" && run.context_compression?.status === "compressing" && run.context_compression.source === "auto";
31517
+ }
30978
31518
  function readNextSessionId(payload) {
30979
31519
  const info = toRecord19(payload.info);
30980
31520
  return readString21(payload, "session_key") ?? readString21(payload, "sessionKey") ?? readString21(payload, "stored_session_id") ?? readString21(payload, "storedSessionId") ?? readString21(payload, "session_id") ?? readString21(payload, "sessionId") ?? readString21(info, "session_key") ?? readString21(info, "sessionKey");
@@ -31410,6 +31950,62 @@ function transcriptRecoveryCompletedEvent(hermesSessionId) {
31410
31950
  }
31411
31951
  };
31412
31952
  }
31953
+ function hermesRunStatusToTerminalEvent(status) {
31954
+ if (!status) {
31955
+ return null;
31956
+ }
31957
+ const normalizedStatus = status.status.trim().toLowerCase();
31958
+ if (isCompletedRunStatus(normalizedStatus)) {
31959
+ return {
31960
+ eventName: "run.completed",
31961
+ payloadType: "run.completed",
31962
+ payload: {
31963
+ type: "run.completed",
31964
+ run_id: status.run_id,
31965
+ output: status.output,
31966
+ usage: status.usage,
31967
+ status: status.status
31968
+ },
31969
+ rawPayload: status.raw
31970
+ };
31971
+ }
31972
+ if (isFailedRunStatus(normalizedStatus)) {
31973
+ return {
31974
+ eventName: "run.failed",
31975
+ payloadType: "run.failed",
31976
+ payload: {
31977
+ type: "run.failed",
31978
+ run_id: status.run_id,
31979
+ error: {
31980
+ message: readStatusErrorMessage(status.error) ?? "Hermes run failed"
31981
+ },
31982
+ status: status.status,
31983
+ usage: status.usage
31984
+ },
31985
+ rawPayload: status.raw
31986
+ };
31987
+ }
31988
+ if (isCancelledRunStatus(normalizedStatus)) {
31989
+ return {
31990
+ eventName: "run.cancelled",
31991
+ payloadType: "run.cancelled",
31992
+ payload: {
31993
+ type: "run.cancelled",
31994
+ run_id: status.run_id,
31995
+ status: status.status
31996
+ },
31997
+ rawPayload: status.raw
31998
+ };
31999
+ }
32000
+ return null;
32001
+ }
32002
+ function detachedRunRecoveryFailedMessage(language) {
32003
+ return localizedRunText(
32004
+ language,
32005
+ "Hermes Link \u5DF2\u5931\u53BB\u8FD9\u6B21\u8FD0\u884C\u7684\u5B9E\u65F6\u8FDE\u63A5\uFF0C\u4E14\u65E0\u6CD5\u4ECE Hermes \u672C\u5730\u8BB0\u5F55\u6062\u590D\u6700\u7EC8\u7ED3\u679C\u3002\u8BF7\u91CD\u65B0\u53D1\u9001\uFF0C\u6216\u5728 Hermes Desktop \u4E2D\u67E5\u770B\u8BE5\u4F1A\u8BDD\u3002",
32006
+ "Hermes Link lost the realtime connection for this run and could not recover the final result from local Hermes records. Send the message again, or check this conversation in Hermes Desktop."
32007
+ );
32008
+ }
31413
32009
  function readResponseId(payload) {
31414
32010
  if (!payload) {
31415
32011
  return null;
@@ -31432,6 +32028,10 @@ function isFailedRunStatus(status) {
31432
32028
  function isCancelledRunStatus(status) {
31433
32029
  return status === "cancelled" || status === "canceled" || status === "stopped" || status === "aborted";
31434
32030
  }
32031
+ function isTerminalRunStatus(status) {
32032
+ const normalized = status.trim().toLowerCase();
32033
+ return isCompletedRunStatus(normalized) || isFailedRunStatus(normalized) || isCancelledRunStatus(normalized);
32034
+ }
31435
32035
  function isTerminalConversationGoalStatus(status) {
31436
32036
  return status === "done" || status === "paused" || status === "cleared";
31437
32037
  }
@@ -31536,6 +32136,13 @@ function isNodeError18(error, code) {
31536
32136
  // src/conversations/conversation-service.ts
31537
32137
  var ALL_CONVERSATION_EVENTS = "conversation:*";
31538
32138
  var HERMES_ARCHIVE_STATE_SYNC_ID = "hermes-agent-archive-state-v1";
32139
+ var STALE_RUN_REPAIR_RETRY_DELAYS_MS = [
32140
+ 2e3,
32141
+ 5e3,
32142
+ 15e3,
32143
+ 3e4,
32144
+ 6e4
32145
+ ];
31539
32146
  function runtimeMatchesConfiguredModel(runtime, model) {
31540
32147
  return runtime.model.id === model.id && (runtime.model.provider_key ?? runtime.model.provider ?? "") === model.provider && normalizeModelEndpoint(runtime.model.base_url) === normalizeModelEndpoint(model.baseUrl) && (runtime.model.api_mode ?? "") === (model.apiMode ?? "");
31541
32148
  }
@@ -31656,6 +32263,8 @@ var ConversationService = class {
31656
32263
  runLifecycle;
31657
32264
  hermesSessionSyncPromise = null;
31658
32265
  cronDeliverySyncPromise = null;
32266
+ staleRunRepairRetryAttempts = /* @__PURE__ */ new Map();
32267
+ staleRunRepairRetryTimers = /* @__PURE__ */ new Map();
31659
32268
  async withConversationLock(conversationId, task) {
31660
32269
  const previous = this.conversationLocks.get(conversationId) ?? Promise.resolve();
31661
32270
  let release;
@@ -31700,19 +32309,34 @@ var ConversationService = class {
31700
32309
  return this.store.readActiveManifest(conversationId);
31701
32310
  }
31702
32311
  async listConversations() {
31703
- return this.queries.listConversations();
32312
+ return this.repairStaleConversationSummaries(
32313
+ await this.queries.listConversations(),
32314
+ () => this.queries.listConversations()
32315
+ );
31704
32316
  }
31705
- listConversationPage(input = {}) {
31706
- return this.queries.listConversationPage(input);
32317
+ async listConversationPage(input = {}) {
32318
+ return this.repairStaleConversationListPage(
32319
+ await this.queries.listConversationPage(input),
32320
+ () => this.queries.listConversationPage(input)
32321
+ );
31707
32322
  }
31708
- searchConversationPage(input = {}) {
31709
- return this.queries.searchConversationPage(input);
32323
+ async searchConversationPage(input = {}) {
32324
+ return this.repairStaleConversationListPage(
32325
+ await this.queries.searchConversationPage(input),
32326
+ () => this.queries.searchConversationPage(input)
32327
+ );
31710
32328
  }
31711
- listArchivedConversationPage(input = {}) {
31712
- return this.queries.listArchivedConversationPage(input);
32329
+ async listArchivedConversationPage(input = {}) {
32330
+ return this.repairStaleConversationListPage(
32331
+ await this.queries.listArchivedConversationPage(input),
32332
+ () => this.queries.listArchivedConversationPage(input)
32333
+ );
31713
32334
  }
31714
- searchArchivedConversationPage(input = {}) {
31715
- return this.queries.searchArchivedConversationPage(input);
32335
+ async searchArchivedConversationPage(input = {}) {
32336
+ return this.repairStaleConversationListPage(
32337
+ await this.queries.searchArchivedConversationPage(input),
32338
+ () => this.queries.searchArchivedConversationPage(input)
32339
+ );
31716
32340
  }
31717
32341
  async getStatistics(filter = {}) {
31718
32342
  return readLinkStatistics(this.paths, filter);
@@ -32277,14 +32901,15 @@ var ConversationService = class {
32277
32901
  }
32278
32902
  async getMessages(conversationId, options = {}) {
32279
32903
  if (options.syncHermes === true && !options.beforeMessageId) {
32280
- await this.syncHermesConversationMessages(conversationId).catch(
32281
- (error) => {
32282
- void this.logger.warn("hermes_conversation_message_sync_failed", {
32283
- conversation_id: conversationId,
32284
- error: error instanceof Error ? error.message : String(error)
32285
- });
32286
- }
32287
- );
32904
+ await this.repairAndSyncConversation(conversationId, "messages_read");
32905
+ } else if (!options.beforeMessageId) {
32906
+ const repaired = await this.repairStaleConversationRuns(conversationId);
32907
+ if (repaired) {
32908
+ await this.syncHermesConversationMessagesBestEffort(
32909
+ conversationId,
32910
+ "messages_read_after_repair"
32911
+ );
32912
+ }
32288
32913
  }
32289
32914
  return this.queries.getMessages(conversationId, options);
32290
32915
  }
@@ -32310,6 +32935,7 @@ var ConversationService = class {
32310
32935
  "model_id or reasoning_effort is required"
32311
32936
  );
32312
32937
  }
32938
+ await this.repairAndSyncConversationBeforeWrite(conversationId);
32313
32939
  return this.withConversationLock(conversationId, async () => {
32314
32940
  let manifest = await this.restoreArchivedConversationForUserContinuationLocked(
32315
32941
  conversationId
@@ -32429,6 +33055,7 @@ var ConversationService = class {
32429
33055
  });
32430
33056
  }
32431
33057
  async setConversationProfile(conversationId, profileName) {
33058
+ await this.repairAndSyncConversationBeforeWrite(conversationId);
32432
33059
  return this.withConversationLock(conversationId, async () => {
32433
33060
  let manifest = await this.store.readRunnableManifest(conversationId);
32434
33061
  const snapshot = await this.store.readSnapshot(conversationId);
@@ -32585,6 +33212,7 @@ var ConversationService = class {
32585
33212
  };
32586
33213
  }
32587
33214
  async listEvents(conversationId, after = 0) {
33215
+ await this.repairStaleConversationRuns(conversationId);
32588
33216
  return (await this.queries.listEvents(conversationId, after)).map(toAppVisibleConversationEvent).filter((event) => event !== null);
32589
33217
  }
32590
33218
  subscribe(conversationId, listener) {
@@ -32622,15 +33250,19 @@ var ConversationService = class {
32622
33250
  return manifest?.status === "active";
32623
33251
  }
32624
33252
  async sendMessage(input) {
33253
+ await this.repairAndSyncConversationBeforeWrite(input.conversationId);
32625
33254
  return this.orchestration.sendMessage(input);
32626
33255
  }
32627
33256
  async setGoalPaused(conversationId, paused, language) {
33257
+ await this.repairAndSyncConversationBeforeWrite(conversationId);
32628
33258
  return this.orchestration.setGoalPaused(conversationId, paused, language);
32629
33259
  }
32630
33260
  async clearGoal(conversationId, language) {
33261
+ await this.repairAndSyncConversationBeforeWrite(conversationId);
32631
33262
  return this.orchestration.clearGoal(conversationId, language);
32632
33263
  }
32633
33264
  async startContextCompression(input) {
33265
+ await this.repairAndSyncConversationBeforeWrite(input.conversationId);
32634
33266
  return this.contextCompression.start(input);
32635
33267
  }
32636
33268
  async cancelRun(conversationId, runId) {
@@ -32659,6 +33291,117 @@ var ConversationService = class {
32659
33291
  }
32660
33292
  return this.cancelRun(conversationId, runId);
32661
33293
  }
33294
+ async repairStaleConversationRuns(conversationId) {
33295
+ const compressionRepaired = await this.contextCompression.repairDetachedRunningCompressions(conversationId).catch((error) => {
33296
+ void this.logger.warn("conversation_stale_compression_repair_failed", {
33297
+ conversation_id: conversationId,
33298
+ error: error instanceof Error ? error.message : String(error)
33299
+ });
33300
+ return false;
33301
+ });
33302
+ const result = await this.runLifecycle.repairStaleConversationRuns(conversationId).catch((error) => {
33303
+ void this.logger.warn("conversation_stale_run_repair_failed", {
33304
+ conversation_id: conversationId,
33305
+ error: error instanceof Error ? error.message : String(error)
33306
+ });
33307
+ return null;
33308
+ });
33309
+ const hasPendingRepair = result?.pending === true;
33310
+ const repaired = compressionRepaired || result?.repaired === true || result?.shouldDrainQueue === true;
33311
+ const shouldDrainQueue = compressionRepaired || result?.shouldDrainQueue || await this.conversationQueueNeedsDrain(conversationId);
33312
+ if (!shouldDrainQueue) {
33313
+ if (hasPendingRepair) {
33314
+ this.scheduleStaleRunRepairRetry(conversationId);
33315
+ } else {
33316
+ this.clearStaleRunRepairRetry(conversationId);
33317
+ }
33318
+ return repaired;
33319
+ }
33320
+ if (hasPendingRepair) {
33321
+ this.scheduleStaleRunRepairRetry(conversationId);
33322
+ } else {
33323
+ this.clearStaleRunRepairRetry(conversationId);
33324
+ }
33325
+ void this.orchestration.startNextQueuedRun(conversationId).catch((error) => {
33326
+ void this.logger.warn("conversation_queue_drain_failed", {
33327
+ conversation_id: conversationId,
33328
+ error: error instanceof Error ? error.message : String(error)
33329
+ });
33330
+ });
33331
+ return true;
33332
+ }
33333
+ async repairAndSyncConversationBeforeWrite(conversationId) {
33334
+ await this.repairAndSyncConversation(conversationId, "conversation_write");
33335
+ }
33336
+ async repairAndSyncConversation(conversationId, source) {
33337
+ await this.repairStaleConversationRuns(conversationId);
33338
+ await this.syncHermesConversationMessagesBestEffort(conversationId, source);
33339
+ await this.repairStaleConversationRuns(conversationId);
33340
+ }
33341
+ async syncHermesConversationMessagesBestEffort(conversationId, source) {
33342
+ await this.syncHermesConversationMessages(conversationId).catch(
33343
+ (error) => {
33344
+ void this.logger.warn("hermes_conversation_message_sync_failed", {
33345
+ source,
33346
+ conversation_id: conversationId,
33347
+ error: error instanceof Error ? error.message : String(error)
33348
+ });
33349
+ }
33350
+ );
33351
+ }
33352
+ async conversationQueueNeedsDrain(conversationId) {
33353
+ const snapshot = await this.store.readSnapshot(conversationId).catch(() => null);
33354
+ return Boolean(
33355
+ snapshot && hasQueuedRuns(snapshot) && !hasRunningRuns(snapshot)
33356
+ );
33357
+ }
33358
+ async repairStaleConversationSummaries(summaries, reread) {
33359
+ let repaired = false;
33360
+ for (const summary of summaries) {
33361
+ if (!conversationSummaryNeedsStaleRunRepair(summary)) {
33362
+ continue;
33363
+ }
33364
+ await this.repairStaleConversationRuns(summary.id);
33365
+ repaired = true;
33366
+ }
33367
+ if (!repaired) {
33368
+ return summaries;
33369
+ }
33370
+ return reread();
33371
+ }
33372
+ async repairStaleConversationListPage(page, reread) {
33373
+ let repaired = false;
33374
+ for (const summary of page.conversations) {
33375
+ if (!conversationSummaryNeedsStaleRunRepair(summary)) {
33376
+ continue;
33377
+ }
33378
+ await this.repairStaleConversationRuns(summary.id);
33379
+ repaired = true;
33380
+ }
33381
+ return repaired ? reread() : page;
33382
+ }
33383
+ scheduleStaleRunRepairRetry(conversationId) {
33384
+ if (this.staleRunRepairRetryTimers.has(conversationId)) {
33385
+ return;
33386
+ }
33387
+ const attempt = this.staleRunRepairRetryAttempts.get(conversationId) ?? 0;
33388
+ const delayMs = STALE_RUN_REPAIR_RETRY_DELAYS_MS[Math.min(attempt, STALE_RUN_REPAIR_RETRY_DELAYS_MS.length - 1)];
33389
+ this.staleRunRepairRetryAttempts.set(conversationId, attempt + 1);
33390
+ const timer = setTimeout(() => {
33391
+ this.staleRunRepairRetryTimers.delete(conversationId);
33392
+ void this.repairStaleConversationRuns(conversationId);
33393
+ }, delayMs);
33394
+ timer.unref?.();
33395
+ this.staleRunRepairRetryTimers.set(conversationId, timer);
33396
+ }
33397
+ clearStaleRunRepairRetry(conversationId) {
33398
+ this.staleRunRepairRetryAttempts.delete(conversationId);
33399
+ const timer = this.staleRunRepairRetryTimers.get(conversationId);
33400
+ if (timer) {
33401
+ clearTimeout(timer);
33402
+ this.staleRunRepairRetryTimers.delete(conversationId);
33403
+ }
33404
+ }
32662
33405
  async guideQueuedRun(conversationId, runId) {
32663
33406
  return this.orchestration.guideQueuedRun(conversationId, runId);
32664
33407
  }
@@ -33636,6 +34379,9 @@ function liveActivityPhaseForEvent(event, run, contextOperation) {
33636
34379
  if (type === "context_compression.timed_out") {
33637
34380
  return "context_compression_timed_out";
33638
34381
  }
34382
+ if (type === "context_compression.cancelled" || type === "context_compression.canceled") {
34383
+ return "cancelled";
34384
+ }
33639
34385
  if (type === "run.completed") {
33640
34386
  return "completed";
33641
34387
  }
@@ -33771,6 +34517,13 @@ function readInteger4(value, key) {
33771
34517
  }
33772
34518
  return Math.max(0, Math.trunc(parsed));
33773
34519
  }
34520
+ function conversationSummaryNeedsStaleRunRepair(summary) {
34521
+ const eventStream = summary.event_stream;
34522
+ if (!eventStream) {
34523
+ return false;
34524
+ }
34525
+ return eventStream.has_active_runs || eventStream.reason === "context_compression" || eventStream.reason === "active_run" || eventStream.reason === "queued_run" || eventStream.reason === "unknown";
34526
+ }
33774
34527
  function approvalRestartText(language, zh, en) {
33775
34528
  return language === "en" ? en : zh;
33776
34529
  }
package/dist/cli/index.js CHANGED
@@ -55,7 +55,7 @@ import {
55
55
  stopDaemonProcess,
56
56
  summarizeUsageProbeEnsure,
57
57
  translate
58
- } from "../chunk-VVHNBT4E.js";
58
+ } from "../chunk-QT55LT5X.js";
59
59
 
60
60
  // src/cli/index.ts
61
61
  import { Command } from "commander";
@@ -626,6 +626,8 @@ declare class ConversationService {
626
626
  private readonly runLifecycle;
627
627
  private hermesSessionSyncPromise;
628
628
  private cronDeliverySyncPromise;
629
+ private readonly staleRunRepairRetryAttempts;
630
+ private readonly staleRunRepairRetryTimers;
629
631
  constructor(paths: RuntimePaths, logger: FileLogger);
630
632
  private withConversationLock;
631
633
  private restoreArchivedConversationForUserContinuationLocked;
@@ -753,6 +755,15 @@ declare class ConversationService {
753
755
  startContextCompression(input: StartContextCompressionInput): Promise<ContextCompressionResult>;
754
756
  cancelRun(conversationId: string, runId: string): Promise<CancelRunResult>;
755
757
  cancelRunById(runId: string): Promise<CancelRunResult>;
758
+ private repairStaleConversationRuns;
759
+ private repairAndSyncConversationBeforeWrite;
760
+ private repairAndSyncConversation;
761
+ private syncHermesConversationMessagesBestEffort;
762
+ private conversationQueueNeedsDrain;
763
+ private repairStaleConversationSummaries;
764
+ private repairStaleConversationListPage;
765
+ private scheduleStaleRunRepairRetry;
766
+ private clearStaleRunRepairRetry;
756
767
  guideQueuedRun(conversationId: string, runId: string): Promise<QueuedRunActionResult>;
757
768
  cancelQueuedRun(conversationId: string, runId: string): Promise<QueuedRunActionResult>;
758
769
  resolveApproval(input: {
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-VVHNBT4E.js";
3
+ } from "../chunk-QT55LT5X.js";
4
4
  export {
5
5
  createApp
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hermespilot/link",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",