@hermespilot/link 0.8.5-beta.0 → 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.5-beta.0";
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(
@@ -18253,6 +18261,13 @@ var ConversationOrchestrationCoordinator = class {
18253
18261
  attachments: userAttachmentParts,
18254
18262
  language: normalizeLanguage(input.language)
18255
18263
  }) : null;
18264
+ if (goalRequest?.hermesSessionId) {
18265
+ manifest = addHermesSessionIdForProfileToManifest(
18266
+ manifest,
18267
+ goalRequest.hermesSessionId,
18268
+ runtime.profileName
18269
+ );
18270
+ }
18256
18271
  if ((hasRunningRuns(snapshot) || queuedRunCount(snapshot) > 0) && queuedRunCount(snapshot) >= MAX_CONVERSATION_QUEUED_RUNS) {
18257
18272
  throw new LinkHttpError(
18258
18273
  409,
@@ -18467,6 +18482,7 @@ var ConversationOrchestrationCoordinator = class {
18467
18482
  maxTurns: readGoalProgressFromStatusText(result.notice ?? result.output ?? "").maxTurns
18468
18483
  }),
18469
18484
  kickoffInput: result.message.trim(),
18485
+ ...result.storedSessionId ? { hermesSessionId: result.storedSessionId } : {},
18470
18486
  ...result.notice ? { notice: result.notice } : {},
18471
18487
  ...result.output ? { output: result.output } : {}
18472
18488
  };
@@ -20248,6 +20264,40 @@ var ContextCompressionCoordinator = class {
20248
20264
  hasActiveCompression(snapshot) {
20249
20265
  return snapshot.runs.some((run) => isActiveCompressionRun(run));
20250
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
+ }
20251
20301
  async startLocked(input) {
20252
20302
  const focus = normalizeFocus(input.focus);
20253
20303
  const language = normalizeLanguage2(input.language);
@@ -21736,6 +21786,7 @@ async function syncHermesConversationMessages(paths, logger, input) {
21736
21786
  if (appendedMessages.length === 0) {
21737
21787
  let nextManifest2 = candidateProfiles.size > 0 || duplicateCleanup.removedCount > 0 ? await store.readManifest(input.conversationId) : null;
21738
21788
  const currentManifest = nextManifest2;
21789
+ let lineageChanged = false;
21739
21790
  if (nextManifest2) {
21740
21791
  for (const profile of candidateProfiles.values()) {
21741
21792
  nextManifest2 = mergeHermesLineageIntoManifest({
@@ -21747,6 +21798,7 @@ async function syncHermesConversationMessages(paths, logger, input) {
21747
21798
  updatedAt: isoFromHermesTime(profile.candidate.session.last_active) ?? nextManifest2.updated_at
21748
21799
  });
21749
21800
  }
21801
+ lineageChanged = !manifestEquivalent(currentManifest, nextManifest2);
21750
21802
  if (duplicateCleanup.removedCount > 0) {
21751
21803
  nextManifest2 = {
21752
21804
  ...nextManifest2,
@@ -21757,13 +21809,30 @@ async function syncHermesConversationMessages(paths, logger, input) {
21757
21809
  if (duplicateCleanup.removedCount > 0) {
21758
21810
  await store.writeSnapshot(input.conversationId, snapshot);
21759
21811
  }
21760
- if (nextManifest2 && currentManifest && (duplicateCleanup.removedCount > 0 || !manifestEquivalent(currentManifest, nextManifest2))) {
21812
+ if (nextManifest2 && currentManifest && (duplicateCleanup.removedCount > 0 || lineageChanged)) {
21761
21813
  const stats2 = buildConversationStats(nextManifest2, snapshot);
21762
21814
  nextManifest2 = { ...nextManifest2, stats: stats2 };
21763
21815
  await store.writeManifest(nextManifest2);
21764
21816
  await upsertConversationStats(paths, toStatsIndexRecord(nextManifest2, stats2));
21765
21817
  result.hermes_session_ids = manifestHermesSessionIds(nextManifest2);
21766
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
+ }
21767
21836
  }
21768
21837
  return result;
21769
21838
  }
@@ -22194,6 +22263,18 @@ function isStrictImportedDuplicateRunMessage(paths, snapshot, message) {
22194
22263
  if (!isMessageInsideRunCleanupWindow(message, run)) {
22195
22264
  return false;
22196
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
+ }
22197
22278
  if (message.role === "user") {
22198
22279
  const user = snapshot.messages.find(
22199
22280
  (item) => item.id === run.trigger_message_id
@@ -22209,6 +22290,20 @@ function isStrictImportedDuplicateRunMessage(paths, snapshot, message) {
22209
22290
  return false;
22210
22291
  });
22211
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
+ }
22212
22307
  function isDuplicateImportedRunUser(paths, user, imported) {
22213
22308
  const importedText = normalizedDuplicateText(imported);
22214
22309
  if (!importedText) {
@@ -22281,7 +22376,7 @@ function normalizedDuplicateTextValue(value) {
22281
22376
  return value.replace(/\r\n/gu, "\n").trim();
22282
22377
  }
22283
22378
  function collectRunHermesMessageWindows(run) {
22284
- const before = normalizeWatermarkPoint(
22379
+ let before = normalizeWatermarkPoint(
22285
22380
  run.hermes_message_watermark?.before,
22286
22381
  run.hermes_session_id
22287
22382
  );
@@ -22292,10 +22387,15 @@ function collectRunHermesMessageWindows(run) {
22292
22387
  run.hermes_message_watermark?.after,
22293
22388
  run.hermes_session_id
22294
22389
  );
22295
- 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();
22296
22393
  if (!sessionId) {
22297
22394
  return [];
22298
22395
  }
22396
+ if (before.session_id !== sessionId) {
22397
+ before = { ...before, session_id: sessionId };
22398
+ }
22299
22399
  return [
22300
22400
  {
22301
22401
  runId: run.id,
@@ -22419,6 +22519,9 @@ function isRowInsideRunWindow(window, row, role) {
22419
22519
  const upperTimeMs = hermesTimestampMillis(window.after?.max_timestamp) ?? (allowTerminalGrace && window.completedAtMs !== void 0 ? window.completedAtMs + 2 * 60 * 1e3 : allowOpenEnded ? Number.POSITIVE_INFINITY : void 0);
22420
22520
  return upperTimeMs !== void 0 && (beforeTimeMs !== void 0 ? rowTimeMs > lowerTimeMs : rowTimeMs >= lowerTimeMs) && rowTimeMs <= upperTimeMs;
22421
22521
  }
22522
+ function isPlaceholderHermesSessionId(sessionId) {
22523
+ return sessionId.startsWith("hp_");
22524
+ }
22422
22525
  function isVoicePart(part) {
22423
22526
  return part.kind === "voice" || part.is_voice_note === true;
22424
22527
  }
@@ -22777,6 +22880,13 @@ function mergeHermesLineageIntoManifest(input) {
22777
22880
  function manifestEquivalent(left, right) {
22778
22881
  return stableJson(left) === stableJson(right);
22779
22882
  }
22883
+ async function summarizeSyncedConversation(paths, manifest, snapshot) {
22884
+ return toSummary(
22885
+ manifest,
22886
+ snapshot,
22887
+ await readConversationProfileSummary(paths, manifest)
22888
+ );
22889
+ }
22780
22890
  function collectImportedHermesPrefix(snapshot) {
22781
22891
  const rows = [];
22782
22892
  const seen = /* @__PURE__ */ new Set();
@@ -29747,16 +29857,36 @@ ${resolved}` : resolved;
29747
29857
  const detachedRunningRepaired = await this.repairDetachedRunningAutomaticContextCompression(conversationId);
29748
29858
  return terminalRepaired || detachedRunningRepaired;
29749
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
+ }
29750
29883
  async repairDetachedRunningAutomaticContextCompression(conversationId) {
29751
29884
  const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
29752
29885
  const candidates = snapshot?.runs.filter(
29753
- (run) => isRunningAutomaticContextCompression(run) && !this.deps.activeRunControllers.has(run.id)
29886
+ (run) => isRunningAutomaticContextCompression(run)
29754
29887
  ) ?? [];
29755
29888
  let repaired = false;
29756
29889
  for (const run of candidates) {
29757
- if (this.deps.activeRunControllers.has(run.id)) {
29758
- continue;
29759
- }
29760
29890
  const previousSessionId = run.context_compression?.previous_session_id?.trim() || run.hermes_session_id;
29761
29891
  const nextSessionId = await readHermesCompressionTip(
29762
29892
  previousSessionId,
@@ -29775,11 +29905,177 @@ ${resolved}` : resolved;
29775
29905
  if (!finished) {
29776
29906
  continue;
29777
29907
  }
29908
+ this.abortAndForgetActiveRunController(run.id);
29778
29909
  await this.completeRun(conversationId, run.id);
29779
29910
  repaired = true;
29780
29911
  }
29781
29912
  return repaired;
29782
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
+ }
29783
30079
  clearAutomaticContextCompressionInSnapshot(input) {
29784
30080
  if (input.run.context_compression?.status !== "compressing" || input.run.context_compression.source !== "auto") {
29785
30081
  return null;
@@ -29835,6 +30131,17 @@ ${resolved}` : resolved;
29835
30131
  }
29836
30132
  return messageText2(assistant).length > 0 || (assistant.agent_events?.length ?? 0) > 0 || (assistant.approvals?.length ?? 0) > 0 || assistant.parts.some((part) => part.type !== "text");
29837
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
+ }
29838
30145
  async appendAssistantTextFromCompletedResponse(conversationId, runId, event) {
29839
30146
  const terminalText = extractResponseAssistantText(event.payload);
29840
30147
  if (!terminalText?.trim()) {
@@ -31643,6 +31950,62 @@ function transcriptRecoveryCompletedEvent(hermesSessionId) {
31643
31950
  }
31644
31951
  };
31645
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
+ }
31646
32009
  function readResponseId(payload) {
31647
32010
  if (!payload) {
31648
32011
  return null;
@@ -31665,6 +32028,10 @@ function isFailedRunStatus(status) {
31665
32028
  function isCancelledRunStatus(status) {
31666
32029
  return status === "cancelled" || status === "canceled" || status === "stopped" || status === "aborted";
31667
32030
  }
32031
+ function isTerminalRunStatus(status) {
32032
+ const normalized = status.trim().toLowerCase();
32033
+ return isCompletedRunStatus(normalized) || isFailedRunStatus(normalized) || isCancelledRunStatus(normalized);
32034
+ }
31668
32035
  function isTerminalConversationGoalStatus(status) {
31669
32036
  return status === "done" || status === "paused" || status === "cleared";
31670
32037
  }
@@ -31769,6 +32136,13 @@ function isNodeError18(error, code) {
31769
32136
  // src/conversations/conversation-service.ts
31770
32137
  var ALL_CONVERSATION_EVENTS = "conversation:*";
31771
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
+ ];
31772
32146
  function runtimeMatchesConfiguredModel(runtime, model) {
31773
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 ?? "");
31774
32148
  }
@@ -31889,6 +32263,8 @@ var ConversationService = class {
31889
32263
  runLifecycle;
31890
32264
  hermesSessionSyncPromise = null;
31891
32265
  cronDeliverySyncPromise = null;
32266
+ staleRunRepairRetryAttempts = /* @__PURE__ */ new Map();
32267
+ staleRunRepairRetryTimers = /* @__PURE__ */ new Map();
31892
32268
  async withConversationLock(conversationId, task) {
31893
32269
  const previous = this.conversationLocks.get(conversationId) ?? Promise.resolve();
31894
32270
  let release;
@@ -31933,19 +32309,34 @@ var ConversationService = class {
31933
32309
  return this.store.readActiveManifest(conversationId);
31934
32310
  }
31935
32311
  async listConversations() {
31936
- return this.queries.listConversations();
32312
+ return this.repairStaleConversationSummaries(
32313
+ await this.queries.listConversations(),
32314
+ () => this.queries.listConversations()
32315
+ );
31937
32316
  }
31938
- listConversationPage(input = {}) {
31939
- 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
+ );
31940
32322
  }
31941
- searchConversationPage(input = {}) {
31942
- 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
+ );
31943
32328
  }
31944
- listArchivedConversationPage(input = {}) {
31945
- 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
+ );
31946
32334
  }
31947
- searchArchivedConversationPage(input = {}) {
31948
- 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
+ );
31949
32340
  }
31950
32341
  async getStatistics(filter = {}) {
31951
32342
  return readLinkStatistics(this.paths, filter);
@@ -32510,19 +32901,15 @@ var ConversationService = class {
32510
32901
  }
32511
32902
  async getMessages(conversationId, options = {}) {
32512
32903
  if (options.syncHermes === true && !options.beforeMessageId) {
32513
- await this.syncHermesConversationMessages(conversationId).catch(
32514
- (error) => {
32515
- void this.logger.warn("hermes_conversation_message_sync_failed", {
32516
- conversation_id: conversationId,
32517
- error: error instanceof Error ? error.message : String(error)
32518
- });
32519
- }
32520
- );
32521
- }
32522
- if (!options.beforeMessageId) {
32523
- await this.runLifecycle.repairStaleAutomaticContextCompression(
32524
- conversationId
32525
- );
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
+ }
32526
32913
  }
32527
32914
  return this.queries.getMessages(conversationId, options);
32528
32915
  }
@@ -32548,6 +32935,7 @@ var ConversationService = class {
32548
32935
  "model_id or reasoning_effort is required"
32549
32936
  );
32550
32937
  }
32938
+ await this.repairAndSyncConversationBeforeWrite(conversationId);
32551
32939
  return this.withConversationLock(conversationId, async () => {
32552
32940
  let manifest = await this.restoreArchivedConversationForUserContinuationLocked(
32553
32941
  conversationId
@@ -32667,6 +33055,7 @@ var ConversationService = class {
32667
33055
  });
32668
33056
  }
32669
33057
  async setConversationProfile(conversationId, profileName) {
33058
+ await this.repairAndSyncConversationBeforeWrite(conversationId);
32670
33059
  return this.withConversationLock(conversationId, async () => {
32671
33060
  let manifest = await this.store.readRunnableManifest(conversationId);
32672
33061
  const snapshot = await this.store.readSnapshot(conversationId);
@@ -32823,6 +33212,7 @@ var ConversationService = class {
32823
33212
  };
32824
33213
  }
32825
33214
  async listEvents(conversationId, after = 0) {
33215
+ await this.repairStaleConversationRuns(conversationId);
32826
33216
  return (await this.queries.listEvents(conversationId, after)).map(toAppVisibleConversationEvent).filter((event) => event !== null);
32827
33217
  }
32828
33218
  subscribe(conversationId, listener) {
@@ -32860,15 +33250,19 @@ var ConversationService = class {
32860
33250
  return manifest?.status === "active";
32861
33251
  }
32862
33252
  async sendMessage(input) {
33253
+ await this.repairAndSyncConversationBeforeWrite(input.conversationId);
32863
33254
  return this.orchestration.sendMessage(input);
32864
33255
  }
32865
33256
  async setGoalPaused(conversationId, paused, language) {
33257
+ await this.repairAndSyncConversationBeforeWrite(conversationId);
32866
33258
  return this.orchestration.setGoalPaused(conversationId, paused, language);
32867
33259
  }
32868
33260
  async clearGoal(conversationId, language) {
33261
+ await this.repairAndSyncConversationBeforeWrite(conversationId);
32869
33262
  return this.orchestration.clearGoal(conversationId, language);
32870
33263
  }
32871
33264
  async startContextCompression(input) {
33265
+ await this.repairAndSyncConversationBeforeWrite(input.conversationId);
32872
33266
  return this.contextCompression.start(input);
32873
33267
  }
32874
33268
  async cancelRun(conversationId, runId) {
@@ -32897,6 +33291,117 @@ var ConversationService = class {
32897
33291
  }
32898
33292
  return this.cancelRun(conversationId, runId);
32899
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
+ }
32900
33405
  async guideQueuedRun(conversationId, runId) {
32901
33406
  return this.orchestration.guideQueuedRun(conversationId, runId);
32902
33407
  }
@@ -33874,6 +34379,9 @@ function liveActivityPhaseForEvent(event, run, contextOperation) {
33874
34379
  if (type === "context_compression.timed_out") {
33875
34380
  return "context_compression_timed_out";
33876
34381
  }
34382
+ if (type === "context_compression.cancelled" || type === "context_compression.canceled") {
34383
+ return "cancelled";
34384
+ }
33877
34385
  if (type === "run.completed") {
33878
34386
  return "completed";
33879
34387
  }
@@ -34009,6 +34517,13 @@ function readInteger4(value, key) {
34009
34517
  }
34010
34518
  return Math.max(0, Math.trunc(parsed));
34011
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
+ }
34012
34527
  function approvalRestartText(language, zh, en) {
34013
34528
  return language === "en" ? en : zh;
34014
34529
  }
package/dist/cli/index.js CHANGED
@@ -55,7 +55,7 @@ import {
55
55
  stopDaemonProcess,
56
56
  summarizeUsageProbeEnsure,
57
57
  translate
58
- } from "../chunk-JJBIYFGY.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-JJBIYFGY.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.5-beta.0",
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",