diff --git a/agent/error_surface.py b/agent/error_surface.py index 730a880954..4ae25a2cdf 100644 --- a/agent/error_surface.py +++ b/agent/error_surface.py @@ -38,6 +38,7 @@ _REASON_TO_LAYER = { "auth": LAYER_AUTH, "auth_permanent": LAYER_AUTH, "billing": LAYER_BILLING, "billing_unverified": LAYER_BILLING, "loop_error": LAYER_GATEWAY, "interpreter_shutdown": LAYER_GATEWAY, "session_busy": LAYER_GATEWAY, "gateway_connection": LAYER_GATEWAY, "host_error": LAYER_GATEWAY, "workspace_unavailable": LAYER_GATEWAY, + "launch_credentials_unavailable": LAYER_GATEWAY, "truncated": LAYER_PROVIDER, "empty_response": LAYER_PROVIDER, "invalid_response": LAYER_PROVIDER, "context_overflow": LAYER_PROVIDER, # a bigger-window model IS the fix, so Switch provider applies } diff --git a/agent/turn_failure_copy.py b/agent/turn_failure_copy.py index c2bed81efa..cbfc575730 100644 --- a/agent/turn_failure_copy.py +++ b/agent/turn_failure_copy.py @@ -22,6 +22,20 @@ SITE_FAILURE_CODES = frozenset({ }) +def launch_credentials_copy(error: Any) -> str: + """The refusal for a session whose own launch/route API key changed or is gone (``RuntimeStoreError`` + or an RPC refusal carrying ``field``/``detail``). A session never sends a different key than it + started with, so /retry repeats the refusal: the copy names /new on every surface.""" + from agent.i18n import t + + data = getattr(error, "data", None) + data = data if isinstance(data, dict) else {} + field = getattr(error, "field", None) or data.get("field") + detail = getattr(error, "detail", None) or data.get("detail") + named = f" ({field}: {detail or 'unavailable'})" if field else "" + return t("gateway.errors.launch_credentials_unavailable", detail=named) + + def stamp_failure(result: dict[str, Any], reason: str, retryable: bool) -> dict[str, Any]: """Stamp the UI verdict fields on a terminal result (in place; returns it).""" result["failure_reason"] = reason diff --git a/apps/desktop/src/i18n/ar_assistant.ts b/apps/desktop/src/i18n/ar_assistant.ts index 20bb8fbc65..6ade574b20 100644 --- a/apps/desktop/src/i18n/ar_assistant.ts +++ b/apps/desktop/src/i18n/ar_assistant.ts @@ -68,6 +68,10 @@ export const arAssistant = { workspace_unavailable: { title: 'مجلد هذه المحادثة غير موجود', body: 'نُقل المجلد الذي تعمل فيه هذه المحادثة أو حُذف، لذا لم تُنفَّذ رسالتك. أعد إنشاء المجلد، أو استخدم «Move to project» لتعيين مجلد آخر للمحادثة، ثم أعد المحاولة.' + }, + launch_credentials_unavailable: { + title: 'تغيّر مفتاح API لهذه المحادثة', + body: 'بدأت هذه المحادثة بمفتاح API تم تغييره أو حذفه منذ ذلك الحين، لذا لم تُرسل رسالتك. المحادثة المفتوحة لا تنتقل أبدًا إلى مفتاح مختلف من تلقاء نفسها. استخدم «بدء جلسة جديدة» لاستخدام مفتاحك الحالي.' } }, errorRetry: 'إعادة المحاولة', diff --git a/apps/desktop/src/i18n/de.ts b/apps/desktop/src/i18n/de.ts index ac9cdf49c9..35c882893a 100644 --- a/apps/desktop/src/i18n/de.ts +++ b/apps/desktop/src/i18n/de.ts @@ -5,6 +5,7 @@ import { deBoot } from './de_boot' import { deClarify } from './de_clarify' import { deConnectorAccounts } from './de_connector_accounts' import { deConnectorCard } from './de_connector_card' +import { deErrorCodes } from './de_error_codes' import { deErrorLayers } from './de_error_layers' import { deFirstRun } from './de_first_run' import { deLocalModels } from './de_local_models' @@ -4884,6 +4885,7 @@ export const deOverrides = { responseStopped: 'Antwort gestoppt', ...deErrorLayers, errorCodes: { + ...deErrorCodes, auth: { title: provider => `${provider} hat Ihre Anmeldung abgelehnt`, body: provider => @@ -4987,14 +4989,6 @@ export const deOverrides = { title: 'Hermes ist in einer Schleife hängen geblieben', body: 'Die Antwort hat dieselben Schritte wiederholt, daher hat Hermes sie gestoppt. Versuchen Sie es erneut oder starten Sie einen neuen Chat, wenn es wieder passiert.' }, - cron_session_read_only: { - title: 'Dieser Chat gehört zu einer geplanten Aufgabe', - body: 'Er führt nur diese Aufgabe aus, daher wurde Ihre Nachricht nicht ausgeführt. Starten Sie einen neuen Chat, um sie zu senden.' - }, - workspace_unavailable: { - title: 'Der Ordner dieses Chats fehlt', - body: 'Der Ordner, in dem dieser Chat arbeitet, wurde verschoben oder gelöscht, daher wurde Ihre Nachricht nicht ausgeführt. Legen Sie den Ordner neu an oder geben Sie dem Chat mit „In Projekt verschieben“ einen anderen, und versuchen Sie es dann erneut.' - }, SESSION_NOT_OWNED: { title: 'Dieser Chat ist woanders offen', body: 'Dieser Chat ist gerade in einem anderen Hermes-Fenster oder Terminal geöffnet. Schließen Sie ihn dort und senden Sie Ihre Nachricht erneut, oder starten Sie hier einen neuen Chat.' diff --git a/apps/desktop/src/i18n/de_error_codes.ts b/apps/desktop/src/i18n/de_error_codes.ts new file mode 100644 index 0000000000..8c61085b7f --- /dev/null +++ b/apps/desktop/src/i18n/de_error_codes.ts @@ -0,0 +1,19 @@ +import type { Translations } from './types' + +type ThreadCopy = Translations['assistant']['thread'] + +// The error card's per-code title and body (lib/error-surface-copy.ts), keyed by error_surface.code. +export const deErrorCodes: Partial = { + cron_session_read_only: { + title: 'Dieser Chat gehört zu einer geplanten Aufgabe', + body: 'Er führt nur diese Aufgabe aus, daher wurde Ihre Nachricht nicht ausgeführt. Starten Sie einen neuen Chat, um sie zu senden.' + }, + workspace_unavailable: { + title: 'Der Ordner dieses Chats fehlt', + body: 'Der Ordner, in dem dieser Chat arbeitet, wurde verschoben oder gelöscht, daher wurde Ihre Nachricht nicht ausgeführt. Legen Sie den Ordner neu an oder geben Sie dem Chat mit „In Projekt verschieben“ einen anderen, und versuchen Sie es dann erneut.' + }, + launch_credentials_unavailable: { + title: 'Der API-Schlüssel dieses Chats hat sich geändert', + body: 'Dieser Chat wurde mit einem API-Schlüssel gestartet, der inzwischen geändert oder entfernt wurde, daher wurde Ihre Nachricht nicht gesendet. Ein offener Chat wechselt nie von selbst zu einem anderen Schlüssel. Wählen Sie „Neue Session starten“, um Ihren aktuellen Schlüssel zu verwenden.' + } +} diff --git a/apps/desktop/src/i18n/en_error_codes.ts b/apps/desktop/src/i18n/en_error_codes.ts index 37850be0fa..1e626d9e8d 100644 --- a/apps/desktop/src/i18n/en_error_codes.ts +++ b/apps/desktop/src/i18n/en_error_codes.ts @@ -112,6 +112,10 @@ export const enErrorCodes: ThreadCopy['errorCodes'] = { title: "This chat's folder is gone", body: 'The folder this chat works in was moved or deleted, so your message was not run. Recreate the folder, or use Move to project to give the chat another one, then retry.' }, + launch_credentials_unavailable: { + title: "This chat's API key changed", + body: 'This chat started with an API key that has since changed or been removed, so your message was not sent. An open chat never switches to a different key on its own. Start a new session to use your current key.' + }, SESSION_OWNER_UNCONFIRMED: { title: 'Your message may already be queued', body: 'The gateway did not confirm your message in time. If it was queued, it runs once the earlier turn finishes or is resolved. Check the chat before sending it again.' diff --git a/apps/desktop/src/i18n/es.ts b/apps/desktop/src/i18n/es.ts index cd77eb152d..cb14b27773 100644 --- a/apps/desktop/src/i18n/es.ts +++ b/apps/desktop/src/i18n/es.ts @@ -6,6 +6,7 @@ import { esBoot } from './es_boot' import { esClarify } from './es_clarify' import { esConnectorAccounts } from './es_connector_accounts' import { esConnectorCard } from './es_connector_card' +import { esErrorCodes } from './es_error_codes' import { esErrorLayers } from './es_error_layers' import { esFirstRun } from './es_first_run' import { esLocalModels } from './es_local_models' @@ -4873,6 +4874,7 @@ export const esOverrides = { responseStopped: 'Respuesta detenida', ...esErrorLayers, errorCodes: { + ...esErrorCodes, auth: { title: (provider: string) => `${provider} rechazó tu inicio de sesión`, body: (provider: string) => @@ -4974,14 +4976,6 @@ export const esOverrides = { title: 'Hermes se quedó atascado en un bucle', body: 'La respuesta repetía los mismos pasos, así que Hermes la detuvo. Reinténtalo o empieza un chat nuevo si vuelve a ocurrir.' }, - cron_session_read_only: { - title: 'Este chat pertenece a una tarea programada', - body: 'Solo ejecuta esa tarea, así que tu mensaje no se ejecutó. Empieza un chat nuevo para enviarlo.' - }, - workspace_unavailable: { - title: 'La carpeta de este chat ya no existe', - body: 'La carpeta en la que trabaja este chat se movió o se eliminó, así que tu mensaje no se ejecutó. Vuelve a crear la carpeta o usa «Mover a proyecto» para darle otra al chat, y vuelve a intentarlo.' - }, SESSION_NOT_OWNED: { title: 'Este chat está abierto en otro sitio', body: 'Este chat está abierto en otra ventana de Hermes o en un terminal. Ciérralo allí y vuelve a enviar el mensaje, o empieza un chat nuevo aquí.' diff --git a/apps/desktop/src/i18n/es_error_codes.ts b/apps/desktop/src/i18n/es_error_codes.ts new file mode 100644 index 0000000000..e8cbbda288 --- /dev/null +++ b/apps/desktop/src/i18n/es_error_codes.ts @@ -0,0 +1,19 @@ +import type { Translations } from './types' + +type ThreadCopy = Translations['assistant']['thread'] + +// The error card's per-code title and body (lib/error-surface-copy.ts), keyed by error_surface.code. +export const esErrorCodes: Partial = { + cron_session_read_only: { + title: 'Este chat pertenece a una tarea programada', + body: 'Solo ejecuta esa tarea, así que tu mensaje no se ejecutó. Empieza un chat nuevo para enviarlo.' + }, + workspace_unavailable: { + title: 'La carpeta de este chat ya no existe', + body: 'La carpeta en la que trabaja este chat se movió o se eliminó, así que tu mensaje no se ejecutó. Vuelve a crear la carpeta o usa «Mover a proyecto» para darle otra al chat, y vuelve a intentarlo.' + }, + launch_credentials_unavailable: { + title: 'La clave de API de este chat cambió', + body: 'Este chat se inició con una clave de API que desde entonces cambió o se eliminó, así que tu mensaje no se envió. Un chat abierto nunca cambia por sí solo a otra clave. Usa «Iniciar sesión nueva» para usar tu clave actual.' + } +} diff --git a/apps/desktop/src/i18n/fr.ts b/apps/desktop/src/i18n/fr.ts index 05bfa1f7bb..cd089f49f0 100644 --- a/apps/desktop/src/i18n/fr.ts +++ b/apps/desktop/src/i18n/fr.ts @@ -6,6 +6,7 @@ import { frBoot } from './fr_boot' import { frClarify } from './fr_clarify' import { frConnectorAccounts } from './fr_connector_accounts' import { frConnectorCard } from './fr_connector_card' +import { frErrorCodes } from './fr_error_codes' import { frErrorLayers } from './fr_error_layers' import { frFirstRun } from './fr_first_run' import { frLocalModels } from './fr_local_models' @@ -4895,6 +4896,7 @@ export const frOverrides = { responseStopped: 'Réponse interrompue', ...frErrorLayers, errorCodes: { + ...frErrorCodes, auth: { title: provider => `${provider} a refusé votre connexion`, body: provider => @@ -4994,14 +4996,6 @@ export const frOverrides = { title: 'Hermes est resté bloqué dans une boucle', body: 'La réponse répétait les mêmes étapes ; Hermes l’a donc arrêtée. Réessayez ou démarrez une nouvelle conversation.' }, - cron_session_read_only: { - title: 'Cette conversation appartient à une tâche planifiée', - body: 'Elle n’exécute que cette tâche ; votre message n’a donc pas été exécuté. Démarrez une nouvelle conversation pour l’envoyer.' - }, - workspace_unavailable: { - title: 'Le dossier de cette conversation a disparu', - body: "Le dossier dans lequel travaille cette conversation a été déplacé ou supprimé ; votre message n'a donc pas été exécuté. Recréez le dossier ou utilisez « Déplacer vers un projet » pour lui en attribuer un autre, puis réessayez." - }, SESSION_NOT_OWNED: { title: 'Cette conversation est ouverte ailleurs', body: 'Cette conversation est déjà ouverte dans une autre fenêtre Hermes ou un terminal. Fermez-la là-bas puis réessayez, ou démarrez-en une nouvelle ici.' diff --git a/apps/desktop/src/i18n/fr_error_codes.ts b/apps/desktop/src/i18n/fr_error_codes.ts new file mode 100644 index 0000000000..099ab0245f --- /dev/null +++ b/apps/desktop/src/i18n/fr_error_codes.ts @@ -0,0 +1,19 @@ +import type { Translations } from './types' + +type ThreadCopy = Translations['assistant']['thread'] + +// The error card's per-code title and body (lib/error-surface-copy.ts), keyed by error_surface.code. +export const frErrorCodes: Partial = { + cron_session_read_only: { + title: 'Cette conversation appartient à une tâche planifiée', + body: 'Elle n’exécute que cette tâche ; votre message n’a donc pas été exécuté. Démarrez une nouvelle conversation pour l’envoyer.' + }, + workspace_unavailable: { + title: 'Le dossier de cette conversation a disparu', + body: "Le dossier dans lequel travaille cette conversation a été déplacé ou supprimé ; votre message n'a donc pas été exécuté. Recréez le dossier ou utilisez « Déplacer vers un projet » pour lui en attribuer un autre, puis réessayez." + }, + launch_credentials_unavailable: { + title: 'La clé API de cette conversation a changé', + body: "Cette conversation a démarré avec une clé API qui a depuis été modifiée ou supprimée ; votre message n'a donc pas été envoyé. Une conversation ouverte ne passe jamais d'elle-même à une autre clé. Utilisez « Démarrer une nouvelle session » pour utiliser votre clé actuelle." + } +} diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 32f9180d40..b79975b60a 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -7,6 +7,7 @@ import { jaBoot } from './ja_boot' import { jaCatalog, jaSkillDeepLink } from './ja_catalog' import { jaClarify } from './ja_clarify' import { jaConnectors } from './ja_connectors' +import { jaErrorCodes } from './ja_error_codes' import { jaErrorLayers } from './ja_error_layers' import { jaFirstRun } from './ja_first_run' import { jaLocalModels } from './ja_local_models' @@ -3315,6 +3316,7 @@ export const jaOverrides = { errorGenericProvider: 'AI サービス', ...jaErrorLayers, errorCodes: { + ...jaErrorCodes, provider_policy_blocked: { title: 'アカウント設定によりこのモデルはブロックされています', body: provider => @@ -3365,17 +3367,9 @@ export const jaOverrides = { body: provider => `Hermes は ${provider} との安全な接続を検証できませんでした。ネットワークやプロキシの設定を確認するか、プロバイダーを切り替えて再送してください。` }, - cron_session_read_only: { - title: 'このチャットはスケジュールされたジョブ用です', - body: 'このチャットはそのジョブの実行専用のため、メッセージは実行されませんでした。送信するには新しいチャットを開始してください。' - }, usage_window: { title: '含まれる利用量を使い切りました', body: 'プランに含まれる利用量はリセットまで使い切りました。続けるには /topup でクレジットを追加してください。' - }, - workspace_unavailable: { - title: 'このチャットのフォルダーが見つかりません', - body: 'このチャットの作業フォルダーが移動または削除されたため、メッセージは実行されませんでした。フォルダーを作り直すか、「Move to project」で別のフォルダーを割り当ててから、もう一度お試しください。' } }, errorRetry: '再試行', diff --git a/apps/desktop/src/i18n/ja_error_codes.ts b/apps/desktop/src/i18n/ja_error_codes.ts new file mode 100644 index 0000000000..7d63d2b0a3 --- /dev/null +++ b/apps/desktop/src/i18n/ja_error_codes.ts @@ -0,0 +1,19 @@ +import type { Translations } from './types' + +type ThreadCopy = Translations['assistant']['thread'] + +// The error card's per-code title and body (lib/error-surface-copy.ts), keyed by error_surface.code. +export const jaErrorCodes: Partial = { + cron_session_read_only: { + title: 'このチャットはスケジュールされたジョブ用です', + body: 'このチャットはそのジョブの実行専用のため、メッセージは実行されませんでした。送信するには新しいチャットを開始してください。' + }, + workspace_unavailable: { + title: 'このチャットのフォルダーが見つかりません', + body: 'このチャットの作業フォルダーが移動または削除されたため、メッセージは実行されませんでした。フォルダーを作り直すか、「Move to project」で別のフォルダーを割り当ててから、もう一度お試しください。' + }, + launch_credentials_unavailable: { + title: 'このチャットの API キーが変更されました', + body: 'このチャットの開始時の API キーがその後変更または削除されたため、メッセージは送信されませんでした。開いているチャットが自動で別のキーに切り替わることはありません。「新しいセッションを開始」で現在のキーを使ってください。' + } +} diff --git a/apps/desktop/src/i18n/ru_error_codes.ts b/apps/desktop/src/i18n/ru_error_codes.ts index ecb423e8c4..807febc1ce 100644 --- a/apps/desktop/src/i18n/ru_error_codes.ts +++ b/apps/desktop/src/i18n/ru_error_codes.ts @@ -12,6 +12,10 @@ export const ruErrorCodes: Partial = { title: 'Папка этого чата пропала', body: 'Папка, в которой работает этот чат, была перемещена или удалена, поэтому ваше сообщение не было выполнено. Создайте папку заново или назначьте чату другую через «Переместить в проект», затем повторите попытку.' }, + launch_credentials_unavailable: { + title: 'Ключ API этого чата изменился', + body: 'Этот чат был начат с ключом API, который с тех пор изменили или удалили, поэтому ваше сообщение не отправлено. Открытый чат никогда сам не переключается на другой ключ. Нажмите «Start new session», чтобы использовать текущий ключ.' + }, usage_window: { title: 'Включённый объём исчерпан', body: 'Включённый объём вашего плана исчерпан до сброса. Пополните кредиты в /topup, чтобы продолжить.' diff --git a/apps/desktop/src/i18n/zh-hant_assistant.ts b/apps/desktop/src/i18n/zh-hant_assistant.ts index 967cc86fcf..2400a29905 100644 --- a/apps/desktop/src/i18n/zh-hant_assistant.ts +++ b/apps/desktop/src/i18n/zh-hant_assistant.ts @@ -108,6 +108,10 @@ export const zhHantAssistant = { workspace_unavailable: { title: '此聊天的資料夾已不存在', body: '此聊天使用的資料夾已被移動或刪除,因此你的訊息未被執行。請重新建立該資料夾,或使用「Move to project」為聊天指定其他資料夾,然後重試。' + }, + launch_credentials_unavailable: { + title: '此聊天的 API 金鑰已變更', + body: '此聊天開始時使用的 API 金鑰後來已被變更或刪除,因此你的訊息未送出。開啟中的聊天不會自行切換到其他金鑰。請使用「開始新工作階段」以使用目前的金鑰。' } }, errorLayers: { diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index c61f1f8479..4c89aea3cf 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -6,6 +6,7 @@ import { introZh } from './intro-zh' import { zhCatalog, zhSkillDeepLink } from './zh_catalog' import { zhClarify } from './zh_clarify' import { zhConnectorCard } from './zh_connector_card' +import { zhErrorCodes } from './zh_error_codes' import { zhErrorLayers } from './zh_error_layers' import { zhFirstRun } from './zh_first_run' import { zhLocalModels } from './zh_local_models' @@ -4301,6 +4302,7 @@ export const zhOverrides = { errorGenericProvider: 'AI 服务', ...zhErrorLayers, errorCodes: { + ...zhErrorCodes, provider_policy_blocked: { title: '账户设置阻止了此模型', body: provider => `${provider} 无法按你账户的数据或隐私设置路由此请求。请选择其他模型或切换服务商。` @@ -4345,17 +4347,9 @@ export const zhOverrides = { title: '安全连接失败', body: provider => `Hermes 无法验证与 ${provider} 的安全连接。请检查网络或代理设置,或切换服务商后重新发送。` }, - cron_session_read_only: { - title: '此聊天属于一个定时任务', - body: '它只运行该任务,因此你的消息未被执行。请新建聊天后再发送。' - }, usage_window: { title: '包含用量已用完', body: '你的方案包含的用量已用完,将在重置后恢复。在 /topup 中添加额度即可继续。' - }, - workspace_unavailable: { - title: '此聊天的文件夹已不存在', - body: '此聊天使用的文件夹已被移动或删除,因此你的消息未被执行。请重新创建该文件夹,或使用“移动到项目”为聊天指定其他文件夹,然后重试。' } }, errorRetry: '重试', diff --git a/apps/desktop/src/i18n/zh_error_codes.ts b/apps/desktop/src/i18n/zh_error_codes.ts new file mode 100644 index 0000000000..767478bb82 --- /dev/null +++ b/apps/desktop/src/i18n/zh_error_codes.ts @@ -0,0 +1,19 @@ +import type { Translations } from './types' + +type ThreadCopy = Translations['assistant']['thread'] + +// The error card's per-code title and body (lib/error-surface-copy.ts), keyed by error_surface.code. +export const zhErrorCodes: Partial = { + cron_session_read_only: { + title: '此聊天属于一个定时任务', + body: '它只运行该任务,因此你的消息未被执行。请新建聊天后再发送。' + }, + workspace_unavailable: { + title: '此聊天的文件夹已不存在', + body: '此聊天使用的文件夹已被移动或删除,因此你的消息未被执行。请重新创建该文件夹,或使用“移动到项目”为聊天指定其他文件夹,然后重试。' + }, + launch_credentials_unavailable: { + title: '此聊天的 API 密钥已更改', + body: '此聊天开始时使用的 API 密钥后来已被更改或删除,因此你的消息未发送。打开的聊天不会自行切换到其他密钥。请使用“开始新会话”以使用当前密钥。' + } +} diff --git a/apps/desktop/src/lib/error-surface.ts b/apps/desktop/src/lib/error-surface.ts index 252bcb5049..a64a6d2d61 100644 --- a/apps/desktop/src/lib/error-surface.ts +++ b/apps/desktop/src/lib/error-surface.ts @@ -56,6 +56,9 @@ export const ERROR_CODE_KEYS = [ // The chat's working folder was moved or deleted: nothing ran, and nothing will until it is back // or the chat moves to another folder. 'workspace_unavailable', + // The API key this chat started with changed or was removed (gateway: a chat never switches keys + // on its own): Retry repeats it; a new chat runs on the current key. + 'launch_credentials_unavailable', // The owner did not confirm a forwarded send in time; it may already be queued: no Retry. 'SESSION_OWNER_UNCONFIRMED', 'disk_full', @@ -348,6 +351,7 @@ const CODE_PLANS: Partial>> = { cron_session_read_only: { retry: false, startNewSession: true }, // Retry is the next click once the folder is back or the chat was moved (Move to project). workspace_unavailable: { retry: true, startNewSession: true }, + launch_credentials_unavailable: { retry: false, startNewSession: true }, content_policy_blocked: { editMessage: true, retry: false }, context_overflow: { compress: true, retry: false, startNewSession: true }, disk_full: { openHermesFolder: true, retry: true }, diff --git a/apps/desktop/src/store/notifications.test.ts b/apps/desktop/src/store/notifications.test.ts index 4d406b7184..843e605618 100644 --- a/apps/desktop/src/store/notifications.test.ts +++ b/apps/desktop/src/store/notifications.test.ts @@ -200,3 +200,29 @@ test('an unrelated invalid_params keeps the generic toast', () => { ) expect(lastMessage()).toBe('invalid_params') }) + +// #143133: a model pick or option change on a chat whose launch API key changed showed the raw +// `launch_credentials_unavailable` code. Every retry repeats it; a new chat runs on the current key. +test('a launch-credentials refusal says the key changed and offers a new chat', () => { + notifyError( + new JsonRpcGatewayError('launch_credentials_unavailable', { + code: 4001, + data: { + reason: 'launch_credentials_unavailable', + field: 'model.api_key', + detail: 'changed in config.yaml since this session started' + } + }), + en.shell.modelOptions.updateFailed + ) + + const toast = $notifications.get()[0] + expect(toast?.message).toBe(en.assistant.thread.errorCodes.launch_credentials_unavailable.body) + expect(toast?.message).not.toContain('launch_credentials_unavailable') + expect(toast?.action?.label).toBe(en.assistant.thread.errorStartNewSession) + + // A deferred pick's refusal event carries only the bare code. + clearNotifications() + notifyError(new Error('launch_credentials_unavailable'), en.desktop.modelSwitchFailed) + expect(lastMessage()).toBe(en.assistant.thread.errorCodes.launch_credentials_unavailable.body) +}) diff --git a/apps/desktop/src/store/notifications.ts b/apps/desktop/src/store/notifications.ts index 6655e82703..b27405f8a5 100644 --- a/apps/desktop/src/store/notifications.ts +++ b/apps/desktop/src/store/notifications.ts @@ -249,6 +249,19 @@ function staleRewindRefusal(error: unknown): 'compacted' | 'stale' | null { return typeof detail === 'string' && detail.startsWith('compacted:') ? 'compacted' : 'stale' } +// The owner refuses every model pick and turn on a chat whose launch API key changed or was removed +// (gateway/session_policy_credentials.py): `data.reason` on an RPC error, or the bare code a deferred +// pick's refusal event carries. +function isLaunchCredentialsRefusal(error: unknown): boolean { + const data = error && typeof error === 'object' ? (error as { data?: { reason?: unknown } }).data : undefined + + if (data && typeof data === 'object' && data.reason === 'launch_credentials_unavailable') { + return true + } + + return error instanceof Error && error.message.trim() === 'launch_credentials_unavailable' +} + // Exported so flows that surface errors inline (e.g. ConfirmDialog's onConfirm // rethrow) can reuse the same IPC-unwrapping + summarizing as notifyError. export function readableError( @@ -263,6 +276,19 @@ export function readableError( return { message: translateNow(`promptNotices.${key}`), category: 'other' } } + if (isLaunchCredentialsRefusal(error)) { + // Same sentence and button as the chat's error card: a model pick, retry or option change on + // this chat repeats the refusal until the user starts a new one. + return { + message: translateNow('assistant.thread.errorCodes.launch_credentials_unavailable.body'), + action: { + label: translateNow('assistant.thread.errorStartNewSession'), + onClick: () => void import('@/store/profile').then(({ requestFreshSession }) => requestFreshSession()) + }, + category: 'other' + } + } + const raw = error instanceof Error ? error.message : typeof error === 'string' ? error : fallback const unwrapped = raw.match(/Error invoking remote method '[^']+': Error: (.+)$/)?.[1] ?? raw const cleaned = cleanErrorText(unwrapped) diff --git a/gateway/run_turn_persistence.py b/gateway/run_turn_persistence.py index a8926f3ac5..f70c48ec72 100644 --- a/gateway/run_turn_persistence.py +++ b/gateway/run_turn_persistence.py @@ -657,10 +657,11 @@ class GatewayTurnPersistenceMixin: status_hint = t("gateway.errors.hint_usage_limit") elif status_code == 400: status_hint = t("gateway.errors.hint_rejected") - elif getattr(e, "reason", None) == "launch_credentials_unavailable" and getattr(e, "field", None): - # The session's own frozen route credential is gone or changed: name it, not the model. - status_hint = f" {e.reason} ({e.field}: {e.detail or 'unavailable'})." from gateway.session_results import record_unexecuted_failure + if getattr(e, "reason", None) == "launch_credentials_unavailable": + # The session's own route key is gone or changed: /retry repeats it, /new is the way out. + from agent.turn_failure_copy import launch_credentials_copy + return record_unexecuted_failure(launch_credentials_copy(e), 'launch_credentials_unavailable') return record_unexecuted_failure(self._hmwa_add_failed_turn_notice( t("gateway.errors.generic_failed_with_hint", hint=status_hint), PARTIAL_FAILED_TURN_NOTICE, )) diff --git a/gateway/run_turn_runner.py b/gateway/run_turn_runner.py index f942316f2b..5457f2adfd 100644 --- a/gateway/run_turn_runner.py +++ b/gateway/run_turn_runner.py @@ -1172,7 +1172,7 @@ class TurnRunner(GatewayTurnProgressMixin, GatewaySessionAgentMixin): def run_sync(self): from contextlib import ExitStack - from gateway.session_policy import bind_turn_workspace, policy_for_source, policy_scope, workspace_unavailable_result + from gateway.session_policy import bind_turn_workspace, policy_for_source, policy_scope, refused_turn_result from gateway.session_api_turn import api_policy_scope from gateway.session_authorities import active_authority from hermes_state_runtime import RuntimeStoreError @@ -1181,9 +1181,9 @@ class TurnRunner(GatewayTurnProgressMixin, GatewaySessionAgentMixin): try: scope.enter_context(policy_scope(policy, authority=active_authority(self._runner))) except RuntimeStoreError as exc: - if exc.reason != 'workspace_unavailable': + result = refused_turn_result(policy, exc) + if result is None: raise - result = workspace_unavailable_result(policy) else: scope.enter_context(api_policy_scope()) bind_turn_workspace(policy, self._ctx.session_id) diff --git a/gateway/session_policy.py b/gateway/session_policy.py index ab3aace258..f5f301ece3 100644 --- a/gateway/session_policy.py +++ b/gateway/session_policy.py @@ -1062,6 +1062,20 @@ def workspace_unavailable_result(policy) -> dict: "workspace_unavailable", False) +def refused_turn_result(policy, exc) -> dict | None: + """A failed, never-run turn for an owner refusal at scope entry that an unchanged resend repeats + (gone working folder, changed launch key); None for any other refusal.""" + if exc.reason == 'workspace_unavailable': + return workspace_unavailable_result(policy) + if exc.reason != 'launch_credentials_unavailable': + return None + from agent.turn_failure_copy import launch_credentials_copy, stamp_failure + text = launch_credentials_copy(exc) + return stamp_failure({"final_response": text, "messages": [], "api_calls": 0, "tools": [], + "failed": True, "completed": False, "error": text}, + "launch_credentials_unavailable", False) + + @contextmanager def policy_scope(policy, *, authority=None): if policy is None: diff --git a/gateway/session_results.py b/gateway/session_results.py index 875f48b7c0..3f718b7003 100644 --- a/gateway/session_results.py +++ b/gateway/session_results.py @@ -7,14 +7,18 @@ execution_result: ContextVar[dict | None] = ContextVar("execution_result", defau from hermes_state_terminal import RESULT_PREFIX as _RESULT_PREFIX, load_result -def record_unexecuted_failure(reply): +def record_unexecuted_failure(reply, reason=None): """The admitted turn never ran because it failed (agent initialization raised, history unreadable): commit a failed result for the executing admission, so no surface settles it - ``completed`` with the apology as the turn's output. Returns ``reply`` unchanged.""" + ``completed`` with the apology as the turn's output. A *reason* is stamped as the turn's + non-retryable failure code, so clients render that code's card. Returns ``reply`` unchanged.""" captured = execution_result.get() if captured is not None and 'result' not in captured: captured['result'] = {'final_response': '', 'messages': [], 'failed': True, 'completed': False, 'error': str(reply or 'The admitted turn failed.')} + if reason: + from agent.turn_failure_copy import stamp_failure + stamp_failure(captured['result'], reason, False) return reply diff --git a/hermes_cli/cli_chat_error_copy.py b/hermes_cli/cli_chat_error_copy.py index 7319da9ca0..cbf6b33b69 100644 --- a/hermes_cli/cli_chat_error_copy.py +++ b/hermes_cli/cli_chat_error_copy.py @@ -41,6 +41,11 @@ def refusal_text(error: BaseException | str) -> str: (``gateway/session_controls.py``), and ``hermes_cli.gateway_client.rpc_error`` carries the bounded scalars onto ``GatewayRPCError.data``. A refusal from anywhere else has no ``data`` and reads exactly as before.""" + if str(error) == "launch_credentials_unavailable": + # The chat's launch key changed: every retry repeats the refusal, so name the way out. + from agent.turn_failure_copy import launch_credentials_copy + + return launch_credentials_copy(error) data = getattr(error, "data", None) data = data if isinstance(data, dict) else {} field, detail = str(data.get("field") or "").strip(), str(data.get("detail") or "").strip() diff --git a/hermes_cli/cli_model_switch_mixin.py b/hermes_cli/cli_model_switch_mixin.py index 322bb02fa4..91745db9d6 100644 --- a/hermes_cli/cli_model_switch_mixin.py +++ b/hermes_cli/cli_model_switch_mixin.py @@ -835,6 +835,11 @@ class CLIModelSwitchMixin: # rather than a dead session (#50163). for _k, _v in _cli_snapshot.items(): setattr(self, _k, _v) + if str(exc) == "launch_credentials_unavailable": + # No model switch can fix a chat whose launch key changed; say what does. + from hermes_cli.cli_chat_error_copy import refusal_text + _cprint(f" {refusal_text(exc)}") + return False _cprint(f" {t('cli.model.switch_failed', model=result.new_model, error=exc, old_model=old_model)}") return False finally: diff --git a/hermes_cli/gateway_client.py b/hermes_cli/gateway_client.py index 45e459da7a..788837b3e4 100644 --- a/hermes_cli/gateway_client.py +++ b/hermes_cli/gateway_client.py @@ -18,7 +18,8 @@ GATEWAY_WS_TICKET_PREFIX = "hermes-gateway-ticket." DEFAULT_RPC_TIMEOUT = 30 # A typed ``--api-key`` on a resume that the owner does not bind (``rebind_launch_key``): every CLI says this. RESUME_KEY_MISMATCH = ("--api-key on resume must be the key this session was launched with " - "(a session launched without --api-key keeps its profile credentials).") + "(a session launched without --api-key keeps its profile credentials). " + "A session never switches keys: start a new one (without --resume) to use a different key.") # Canonical ``/compress`` answers only after the summary call(s) and the commit: a long session's # summary routinely takes more than 30 s (``auxiliary.compression.timeout`` defaults to 120 s per # request, ``compression.context_total_ceiling_seconds`` to 600 s). Same budget as Desktop's diff --git a/locales/_keys.desktop.json b/locales/_keys.desktop.json index 4fc576b0ba..3f16db355c 100644 --- a/locales/_keys.desktop.json +++ b/locales/_keys.desktop.json @@ -245,6 +245,8 @@ "assistant.thread.errorCodes.free_tier_route.title", "assistant.thread.errorCodes.invalid_response.body", "assistant.thread.errorCodes.invalid_response.title", + "assistant.thread.errorCodes.launch_credentials_unavailable.body", + "assistant.thread.errorCodes.launch_credentials_unavailable.title", "assistant.thread.errorCodes.loop_error.body", "assistant.thread.errorCodes.loop_error.title", "assistant.thread.errorCodes.model_not_found.body", diff --git a/locales/_keys.tui.json b/locales/_keys.tui.json index c20b009147..5b846c39c9 100644 --- a/locales/_keys.tui.json +++ b/locales/_keys.tui.json @@ -1314,6 +1314,7 @@ "userMessages.promptTimeout.vaultCode", "userMessages.promptTimeout.vaultSaveLogin", "userMessages.promptTimeout.vaultUnlockPrompt", + "userMessages.rpc.launchCredentialsChanged", "userMessages.rpc.notConnected", "userMessages.rpc.sessionNotFound", "userMessages.rpc.shellBackendRefused", diff --git a/locales/af.tui.yaml b/locales/af.tui.yaml index fa4984ad1b..e03a58c122 100644 --- a/locales/af.tui.yaml +++ b/locales/af.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "Wagwoord-prompt gesluit: geen antwoord betyds userMessages.promptTimeout.vaultCode: "Verifikasiekode-prompt gesluit: geen antwoord betyds nie, so die aanmelding is oorgeslaan. Stuur jou versoek weer wanneer jy die kode het." userMessages.promptTimeout.vaultSaveLogin: "Stoor-aanmelding-prompt gesluit: geen antwoord betyds nie, so niks is gestoor nie. Stuur jou versoek weer wanneer jy gereed is." userMessages.promptTimeout.vaultUnlockPrompt: "Ontsluit-prompt gesluit: geen antwoord betyds nie, so die wagwoordbestuurder het gesluit gebly. Stuur jou versoek weer wanneer jy gereed is om dit te ontsluit." +userMessages.rpc.launchCredentialsChanged: "Hierdie klets kan nie meer die API-sleutel gebruik waarmee dit begin is nie (dit is verander of verwyder), daarom is dit nie gedoen nie. 'n Klets skakel nooit self na 'n ander sleutel oor nie. Tik /new om 'n nuwe klets met jou huidige sleutel te begin." userMessages.rpc.notConnected: "Hermes is nie op die oomblik verbind nie, so dit is nie gestuur nie. Dit herverbind outomaties; wag 'n oomblik en probeer weer, of tik /logs as dit aanhou." userMessages.rpc.sessionNotFound: "Hierdie klets is nie meer aan die agterkant gekoppel nie (dit was onaktief, of die agterkant het herbegin). Jou geskiedenis is gestoor: tik /resume om dit weer oop te maak." userMessages.rpc.shellBackendRefused: "!-opdragte loop op die gateway-gasheer, maar hierdie sessie se terminaal-backend is {0}, so die opdrag is nie uitgevoer nie. Vra eerder die agent om dit uit te voer; die agent se terminaal loop binne {0}." diff --git a/locales/af.yaml b/locales/af.yaml index 57caa321c6..4888ed6e1c 100644 --- a/locales/af.yaml +++ b/locales/af.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ Ek kon nie met die KI-modeldiens verbind nie, so hierdie boodskap is nie verwerk nie. Gebruik /login om weer aan te meld, of /model om 'n ander model te kies. As dit aanhou misluk, voer `hermes doctor` op die gasheer uit." cron_session_read_only: "⚠️ Hierdie klets behoort aan 'n geskeduleerde taak en voer net daardie taak uit, so jou boodskap is nie uitgevoer nie. Begin 'n nuwe klets om dit te stuur." workspace_unavailable: "⚠️ Hierdie klets se werkvouer bestaan nie meer nie ({path}), so jou boodskap is nie uitgevoer nie. Skep die vouer weer, of skuif die klets na 'n ander een (`/worktree new` in die CLI, “Move to project” in die rekenaarprogram), en stuur dan jou boodskap weer." + launch_credentials_unavailable: "⚠️ Hierdie klets kan nie meer die API-sleutel gebruik waarmee dit begin is nie{detail}, daarom kan niks daarin loop nie. 'n Oop klets skakel nooit self na 'n ander sleutel oor nie, so /retry misluk op dieselfde manier. Gebruik /new om 'n nuwe klets met jou huidige sleutel te begin." skills: disabled: "Die **{name}**-vaardigheid is geïnstalleer, maar gedeaktiveer.\nAktiveer dit met: `hermes skills config`" not_installed: "Die **{name}**-vaardigheid is beskikbaar, maar nie geïnstalleer nie.\nInstalleer dit met: `hermes skills install {install_name}`" diff --git a/locales/ar.tui.yaml b/locales/ar.tui.yaml index fc3641735f..c85b84f27f 100644 --- a/locales/ar.tui.yaml +++ b/locales/ar.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "أُغلقت مطالبة كلمة المرو userMessages.promptTimeout.vaultCode: "أُغلقت مطالبة رمز التحقّق: لم يصل ردّ في الوقت المحدّد، لذا تم تخطّي تسجيل الدخول. أرسل طلبك مجددًا عندما يتوفّر لديك الرمز." userMessages.promptTimeout.vaultSaveLogin: "أُغلقت مطالبة حفظ بيانات تسجيل الدخول: لم يصل ردّ في الوقت المحدّد، لذا لم يُحفظ شيء. أرسل طلبك مجددًا عندما تكون مستعدًا." userMessages.promptTimeout.vaultUnlockPrompt: "أُغلقت مطالبة فتح القفل: لم يصل ردّ في الوقت المحدّد، لذا بقي مدير كلمات المرور مقفلًا. أرسل طلبك مجددًا عندما تكون مستعدًا لفتح قفله." +userMessages.rpc.launchCredentialsChanged: "لم تعد هذه المحادثة قادرة على استخدام مفتاح API الذي بدأت به (تم تغييره أو حذفه)، لذلك لم يُنفَّذ ذلك. المحادثة لا تنتقل أبدًا إلى مفتاح مختلف من تلقاء نفسها. اكتب /new لبدء محادثة جديدة بمفتاحك الحالي." userMessages.rpc.notConnected: "Hermes غير متصل حاليًا، لذا لم يُرسَل ذلك. يعيد الاتصال تلقائيًا؛ انتظر قليلًا وحاول مرة أخرى، أو اكتب /logs إن استمرّ الأمر." userMessages.rpc.sessionNotFound: "لم تعد هذه المحادثة مرتبطة بالخادم الخلفي (كانت خاملة أو أُعيد تشغيل الخادم الخلفي). سجلّك محفوظ: اكتب /resume لإعادة فتحها." userMessages.rpc.shellBackendRefused: "تعمل أوامر ! على مضيف البوابة، لكن واجهة الطرفية لهذه الجلسة هي {0}، لذلك لم يُنفَّذ الأمر. اطلب من الوكيل تنفيذه بدلاً من ذلك؛ طرفية الوكيل تعمل داخل {0}." diff --git a/locales/ar.yaml b/locales/ar.yaml index e1b14b4dc0..6cc7f73356 100644 --- a/locales/ar.yaml +++ b/locales/ar.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ تعذّر الاتصال بخدمة نموذج الذكاء الاصطناعي، لذا لم تُعالج هذه الرسالة. استخدم /login لتسجيل الدخول مجددًا، أو /model لاختيار نموذج آخر. إن استمرّ الفشل، شغّل `hermes doctor` على المضيف." cron_session_read_only: "⚠️ هذه المحادثة تابعة لمهمة مجدولة ولا تشغّل سوى تلك المهمة، لذا لم تُنفَّذ رسالتك. ابدأ محادثة جديدة لإرسالها." workspace_unavailable: "⚠️ لم يعد مجلد عمل هذه المحادثة موجوداً ({path})، لذا لم تُنفَّذ رسالتك. أعد إنشاء المجلد، أو انقل المحادثة إلى مجلد آخر (`/worktree new` في واجهة سطر الأوامر، أو «Move to project» في تطبيق سطح المكتب)، ثم أرسل رسالتك مجدداً." + launch_credentials_unavailable: "⚠️ لم تعد هذه المحادثة قادرة على استخدام مفتاح API الذي بدأت به{detail}، لذلك لا يمكن تشغيل أي شيء فيها. المحادثة المفتوحة لا تنتقل أبدًا إلى مفتاح مختلف من تلقاء نفسها، لذا سيفشل /retry بالطريقة نفسها. استخدم /new لبدء محادثة جديدة بمفتاحك الحالي." skills: disabled: "مهارة **{name}** مثبّتة لكنها معطّلة.\nفعّلها بـ: `hermes skills config`" not_installed: "مهارة **{name}** متاحة لكنها غير مثبّتة.\nثبّتها بـ: `hermes skills install {install_name}`" diff --git a/locales/de.tui.yaml b/locales/de.tui.yaml index 1bd2aaedf6..c1fe8da4fc 100644 --- a/locales/de.tui.yaml +++ b/locales/de.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "Passwort-Abfrage geschlossen: keine Antwort in userMessages.promptTimeout.vaultCode: "Abfrage des Bestätigungscodes geschlossen: keine Antwort in der vorgegebenen Zeit, die Anmeldung wurde übersprungen. Senden Sie Ihre Anfrage erneut, wenn Sie den Code haben." userMessages.promptTimeout.vaultSaveLogin: "Abfrage zum Speichern der Anmeldung geschlossen: keine Antwort in der vorgegebenen Zeit, es wurde nichts gespeichert. Senden Sie Ihre Anfrage erneut, wenn Sie bereit sind." userMessages.promptTimeout.vaultUnlockPrompt: "Entsperr-Abfrage geschlossen: keine Antwort in der vorgegebenen Zeit, der Passwort-Manager blieb gesperrt. Senden Sie Ihre Anfrage erneut, wenn Sie ihn entsperren möchten." +userMessages.rpc.launchCredentialsChanged: "Dieser Chat kann den API-Schlüssel, mit dem er gestartet wurde, nicht mehr verwenden (er wurde geändert oder entfernt), daher wurde das nicht ausgeführt. Ein Chat wechselt nie von selbst zu einem anderen Schlüssel. Geben Sie /new ein, um einen neuen Chat mit Ihrem aktuellen Schlüssel zu starten." userMessages.rpc.notConnected: "Hermes ist gerade nicht verbunden, daher wurde das nicht gesendet. Die Verbindung wird automatisch wiederhergestellt; warten Sie einen Moment und versuchen Sie es erneut, oder geben Sie /logs ein, falls das Problem bestehen bleibt." userMessages.rpc.sessionNotFound: "Dieser Chat ist nicht mehr mit dem Backend verbunden (er war inaktiv oder das Backend wurde neu gestartet). Ihr Verlauf ist gespeichert: Geben Sie /resume ein, um ihn wieder zu öffnen." userMessages.rpc.shellBackendRefused: "!-Befehle laufen auf dem Gateway-Host, aber das Terminal-Backend dieser Sitzung ist {0}, daher wurde der Befehl nicht ausgeführt. Bitten Sie stattdessen den Agenten, ihn auszuführen; das Terminal des Agenten läuft in {0}." diff --git a/locales/de.yaml b/locales/de.yaml index 837e4a09b2..380e83bbec 100644 --- a/locales/de.yaml +++ b/locales/de.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ Ich konnte keine Verbindung zum KI-Modelldienst herstellen, daher wurde diese Nachricht nicht verarbeitet. Verwenden Sie /login, um sich erneut anzumelden, oder /model, um ein anderes Modell zu wählen. Falls es weiterhin fehlschlägt, führen Sie `hermes doctor` auf dem Host aus." cron_session_read_only: "⚠️ Dieser Chat gehört zu einer geplanten Aufgabe und führt nur diese aus, daher wurde deine Nachricht nicht ausgeführt. Starte einen neuen Chat, um sie zu senden." workspace_unavailable: "⚠️ Der Arbeitsordner dieses Chats existiert nicht mehr ({path}), daher wurde deine Nachricht nicht ausgeführt. Lege den Ordner neu an oder verschiebe den Chat in einen anderen (`/worktree new` in der CLI, „In Projekt verschieben“ in der Desktop-App) und sende deine Nachricht dann erneut." + launch_credentials_unavailable: "⚠️ Dieser Chat kann den API-Schlüssel, mit dem er gestartet wurde, nicht mehr verwenden{detail}, daher kann darin nichts mehr ausgeführt werden. Ein offener Chat wechselt nie von selbst zu einem anderen Schlüssel, deshalb scheitert /retry genauso. Starte mit /new einen neuen Chat mit deinem aktuellen Schlüssel." skills: disabled: "Der Skill **{name}** ist installiert, aber deaktiviert.\nAktivieren Sie ihn mit: `hermes skills config`" not_installed: "Der Skill **{name}** ist verfügbar, aber nicht installiert.\nInstallieren Sie ihn mit: `hermes skills install {install_name}`" diff --git a/locales/en.yaml b/locales/en.yaml index 6334414bbf..01e7024865 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -705,6 +705,7 @@ gateway: no_credentials: "⚠️ I couldn't connect to the AI model service, so this message wasn't processed. Use /login to sign in again, or /model to pick a different model. If it keeps failing, run `hermes doctor` on the host." cron_session_read_only: "⚠️ This chat belongs to a scheduled job and only runs that job, so your message was not run. Start a new chat to send it." workspace_unavailable: "⚠️ This chat's working folder no longer exists ({path}), so your message was not run. Recreate the folder, or move the chat to another one (`/worktree new` in the CLI, Move to project in the desktop app), then send your message again." + launch_credentials_unavailable: "⚠️ This chat can't use the API key it was started with any more{detail}, so nothing can run in it. An open chat never switches to a different key on its own, so /retry fails the same way. Use /new to start a new chat on your current key." skills: disabled: "The **{name}** skill is installed but disabled.\nEnable it with: `hermes skills config`" diff --git a/locales/es.tui.yaml b/locales/es.tui.yaml index 79c1659d20..bb3c0f0dbb 100644 --- a/locales/es.tui.yaml +++ b/locales/es.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "El prompt de contraseña se cerró: no hubo re userMessages.promptTimeout.vaultCode: "El prompt del código de verificación se cerró: no hubo respuesta a tiempo, así que se omitió el inicio de sesión. Envía tu solicitud de nuevo cuando tengas el código." userMessages.promptTimeout.vaultSaveLogin: "El prompt para guardar el inicio de sesión se cerró: no hubo respuesta a tiempo, así que no se guardó nada. Envía tu solicitud de nuevo cuando estés listo." userMessages.promptTimeout.vaultUnlockPrompt: "El prompt de desbloqueo se cerró: no hubo respuesta a tiempo, así que el gestor de contraseñas siguió bloqueado. Envía tu solicitud de nuevo cuando estés listo para desbloquearlo." +userMessages.rpc.launchCredentialsChanged: "Este chat ya no puede usar la clave de API con la que se inició (cambió o se eliminó), así que no se hizo. Un chat nunca cambia por sí solo a otra clave. Escribe /new para empezar un chat nuevo con tu clave actual." userMessages.rpc.notConnected: "Hermes no está conectado ahora mismo, así que eso no se envió. Se reconecta automáticamente; espera un momento e inténtalo de nuevo, o escribe /logs si el problema persiste." userMessages.rpc.sessionNotFound: "Este chat ya no está conectado al backend (estuvo inactivo o el backend se reinició). Tu historial está guardado: escribe /resume para reabrirlo." userMessages.rpc.shellBackendRefused: "Los comandos ! se ejecutan en el host del gateway, pero el backend de terminal de esta sesión es {0}, así que el comando no se ejecutó. Pide al agente que lo ejecute; la terminal del agente se ejecuta dentro de {0}." diff --git a/locales/es.yaml b/locales/es.yaml index e8982bbb57..549254932a 100644 --- a/locales/es.yaml +++ b/locales/es.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ No pude conectar con el servicio del modelo de IA, así que este mensaje no se procesó. Usa /login para iniciar sesión de nuevo, o /model para elegir otro modelo. Si sigue fallando, ejecuta `hermes doctor` en el host." cron_session_read_only: "⚠️ Este chat pertenece a una tarea programada y solo ejecuta esa tarea, así que tu mensaje no se ejecutó. Empieza un chat nuevo para enviarlo." workspace_unavailable: "⚠️ La carpeta de trabajo de este chat ya no existe ({path}), así que tu mensaje no se ejecutó. Vuelve a crear la carpeta o mueve el chat a otra (`/worktree new` en la CLI, «Mover a proyecto» en la app de escritorio) y vuelve a enviar tu mensaje." + launch_credentials_unavailable: "⚠️ Este chat ya no puede usar la clave de API con la que se inició{detail}, así que ya no se puede ejecutar nada en él. Un chat abierto nunca cambia por sí solo a otra clave, así que /retry falla de la misma forma. Usa /new para empezar un chat nuevo con tu clave actual." skills: disabled: "La skill **{name}** está instalada pero desactivada.\nActívala con: `hermes skills config`" not_installed: "La skill **{name}** está disponible pero no instalada.\nInstálala con: `hermes skills install {install_name}`" diff --git a/locales/fr.tui.yaml b/locales/fr.tui.yaml index fb4eb3d055..9d60b21cb9 100644 --- a/locales/fr.tui.yaml +++ b/locales/fr.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "Invite de mot de passe fermée : aucune répon userMessages.promptTimeout.vaultCode: "Invite de code de vérification fermée : aucune réponse à temps, la connexion a donc été ignorée. Renvoyez votre demande quand vous aurez le code." userMessages.promptTimeout.vaultSaveLogin: "Invite d'enregistrement des identifiants fermée : aucune réponse à temps, rien n'a donc été enregistré. Renvoyez votre demande quand vous serez prêt." userMessages.promptTimeout.vaultUnlockPrompt: "Invite de déverrouillage fermée : aucune réponse à temps, le gestionnaire de mots de passe est donc resté verrouillé. Renvoyez votre demande quand vous serez prêt à le déverrouiller." +userMessages.rpc.launchCredentialsChanged: "Cette conversation ne peut plus utiliser la clé API avec laquelle elle a démarré (elle a été modifiée ou supprimée), l'action n'a donc pas été effectuée. Une conversation ne passe jamais d'elle-même à une autre clé. Tapez /new pour démarrer une nouvelle conversation avec votre clé actuelle." userMessages.rpc.notConnected: "Hermes n'est pas connecté pour le moment, le message n'a donc pas été envoyé. La reconnexion est automatique ; patientez un instant et réessayez, ou tapez /logs si le problème persiste." userMessages.rpc.sessionNotFound: "Ce chat n'est plus rattaché au backend (il était inactif ou le backend a redémarré). Votre historique est sauvegardé : tapez /resume pour le rouvrir." userMessages.rpc.shellBackendRefused: "Les commandes ! s'exécutent sur l'hôte du gateway, mais le backend de terminal de cette session est {0} : la commande n'a donc pas été exécutée. Demandez plutôt à l'agent de l'exécuter ; le terminal de l'agent tourne dans {0}." diff --git a/locales/fr.yaml b/locales/fr.yaml index a8efd419e3..da45c04207 100644 --- a/locales/fr.yaml +++ b/locales/fr.yaml @@ -808,6 +808,7 @@ gateway: no_credentials: "⚠️ Je n'ai pas pu me connecter au service de modèle IA ; ce message n'a donc pas été traité. Utilisez /login pour vous reconnecter, ou /model pour choisir un autre modèle. Si l'échec persiste, lancez `hermes doctor` sur l'hôte." cron_session_read_only: "⚠️ Cette conversation appartient à une tâche planifiée et n'exécute que cette tâche ; votre message n'a donc pas été exécuté. Démarrez une nouvelle conversation pour l'envoyer." workspace_unavailable: "⚠️ Le dossier de travail de cette conversation n'existe plus ({path}) ; votre message n'a donc pas été exécuté. Recréez le dossier ou déplacez la conversation vers un autre (`/worktree new` dans la CLI, « Déplacer vers un projet » dans l'application de bureau), puis renvoyez votre message." + launch_credentials_unavailable: "⚠️ Cette conversation ne peut plus utiliser la clé API avec laquelle elle a été démarrée{detail}, plus rien ne peut donc s'y exécuter. Une conversation ouverte ne passe jamais d'elle-même à une autre clé, donc /retry échoue de la même façon. Utilisez /new pour démarrer une nouvelle conversation avec votre clé actuelle." skills: disabled: |- La skill **{name}** est installée mais désactivée. diff --git a/locales/ga.tui.yaml b/locales/ga.tui.yaml index 2ab8d77b92..0db8523abd 100644 --- a/locales/ga.tui.yaml +++ b/locales/ga.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "Dúnadh leid an phasfhocail: níor tugadh frea userMessages.promptTimeout.vaultCode: "Dúnadh leid an chóid fhíoraithe: níor tugadh freagra in am, mar sin scipeáladh an síniú isteach. Seol d'iarratas arís nuair a bheidh an cód agat." userMessages.promptTimeout.vaultSaveLogin: "Dúnadh leid shábháil na logála isteach: níor tugadh freagra in am, mar sin níor sábháladh aon rud. Seol d'iarratas arís nuair a bheidh tú réidh." userMessages.promptTimeout.vaultUnlockPrompt: "Dúnadh leid na díghlasála: níor tugadh freagra in am, mar sin d'fhan an bainisteoir pasfhocal faoi ghlas. Seol d'iarratas arís nuair a bheidh tú réidh lena dhíghlasáil." +userMessages.rpc.launchCredentialsChanged: "Ní féidir leis an gcomhrá seo an eochair API ar tosaíodh leis a úsáid a thuilleadh (athraíodh nó baineadh í), mar sin ní dhearnadh é sin. Ní athraíonn comhrá go heochair eile as a stuaim féin. Clóscríobh /new chun comhrá nua a thosú le d'eochair reatha." userMessages.rpc.notConnected: "Níl Hermes ceangailte faoi láthair, mar sin níor seoladh é sin. Athcheanglaíonn sé go huathoibríoch; fan nóiméad agus bain triail eile as, nó clóscríobh /logs má leanann sé seo ar aghaidh." userMessages.rpc.sessionNotFound: "Níl an comhrá seo ceangailte leis an gcúlchóras a thuilleadh (bhí sé díomhaoin nó atosaíodh an cúlchóras). Tá do stair sábháilte: clóscríobh /resume chun é a athoscailt." userMessages.rpc.shellBackendRefused: "Ritheann orduithe ! ar óstríomhaire an gateway, ach is é {0} inneall teirminéil an tseisiúin seo, mar sin níor ritheadh an t-ordú. Iarr ar an ngníomhaire é a rith ina ionad; ritheann teirminéal an ghníomhaire laistigh de {0}." diff --git a/locales/ga.yaml b/locales/ga.yaml index 33a550ea88..d424d583d9 100644 --- a/locales/ga.yaml +++ b/locales/ga.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ Níorbh fhéidir liom ceangal le seirbhís na samhla AI, mar sin níor próiseáladh an teachtaireacht seo. Úsáid /login chun síniú isteach arís, nó /model chun samhail eile a roghnú. Má leanann sé ag teip, rith `hermes doctor` ar an óstach." cron_session_read_only: "⚠️ Baineann an comhrá seo le jab sceidealaithe agus ní ritheann sé ach an jab sin, mar sin níor ritheadh do theachtaireacht. Tosaigh comhrá nua chun í a sheoladh." workspace_unavailable: "⚠️ Níl fillteán oibre an chomhrá seo ann a thuilleadh ({path}), mar sin níor ritheadh do theachtaireacht. Athchruthaigh an fillteán, nó bog an comhrá go ceann eile (`/worktree new` sa CLI, “Move to project” san aip deisce), agus ansin seol do theachtaireacht arís." + launch_credentials_unavailable: "⚠️ Ní féidir leis an gcomhrá seo an eochair API ar tosaíodh leis a úsáid a thuilleadh{detail}, mar sin ní féidir aon rud a rith ann. Ní athraíonn comhrá oscailte go heochair eile as a stuaim féin, mar sin teipeann ar /retry ar an mbealach céanna. Úsáid /new chun comhrá nua a thosú le d'eochair reatha." skills: disabled: "Tá an scil **{name}** suiteáilte ach díchumasaithe.\nCumasaigh í le: `hermes skills config`" not_installed: "Tá an scil **{name}** ar fáil ach níl sí suiteáilte.\nSuiteáil í le: `hermes skills install {install_name}`" diff --git a/locales/hu.tui.yaml b/locales/hu.tui.yaml index 84fb94e9b0..ea0016d1b5 100644 --- a/locales/hu.tui.yaml +++ b/locales/hu.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "A jelszó bekérése bezárult: nem érkezett userMessages.promptTimeout.vaultCode: "Az ellenőrző kód bekérése bezárult: nem érkezett válasz időben, így a bejelentkezés kimaradt. Küldd el újra a kérésedet, amikor megvan a kód." userMessages.promptTimeout.vaultSaveLogin: "A bejelentkezés mentésének bekérése bezárult: nem érkezett válasz időben, így semmi nem lett mentve. Küldd el újra a kérésedet, amikor készen állsz." userMessages.promptTimeout.vaultUnlockPrompt: "A feloldás bekérése bezárult: nem érkezett válasz időben, így a jelszókezelő zárolva maradt. Küldd el újra a kérésedet, amikor készen állsz a feloldására." +userMessages.rpc.launchCredentialsChanged: "Ez a csevegés már nem tudja használni azt az API-kulcsot, amellyel elindult (megváltozott vagy törölték), ezért ez nem történt meg. Egy csevegés soha nem vált magától másik kulcsra. Írd be a /new parancsot, hogy új csevegést indíts a jelenlegi kulcsoddal." userMessages.rpc.notConnected: "A Hermes most nincs csatlakozva, így ez nem lett elküldve. Automatikusan újracsatlakozik; várj egy kicsit, és próbáld újra, vagy ha a hiba nem múlik, írd be: /logs." userMessages.rpc.sessionNotFound: "Ez a beszélgetés már nem kapcsolódik a backendhez (tétlen volt, vagy a backend újraindult). Az előzményeid mentve vannak: a /resume paranccsal nyithatod újra." userMessages.rpc.shellBackendRefused: "A !-parancsok a gateway gépén futnak, de ennek a munkamenetnek a terminál-backendje {0}, ezért a parancs nem futott le. Kérd meg inkább az ügynököt, hogy futtassa; az ügynök terminálja a(z) {0} belsejében fut." diff --git a/locales/hu.yaml b/locales/hu.yaml index 0e243b71ad..1064ffce16 100644 --- a/locales/hu.yaml +++ b/locales/hu.yaml @@ -707,6 +707,7 @@ gateway: no_credentials: "⚠️ Nem tudtam kapcsolódni az AI-modellszolgáltatáshoz, ezért ez az üzenet nem lett feldolgozva. Jelentkezz be újra a /login paranccsal, vagy válassz másik modellt a /model paranccsal. Ha továbbra is hibázik, futtasd a `hermes doctor` parancsot a gépen." cron_session_read_only: "⚠️ Ez a csevegés egy ütemezett feladathoz tartozik, és csak azt futtatja, ezért az üzeneted nem futott le. Indíts új csevegést az elküldéséhez." workspace_unavailable: "⚠️ Ennek a csevegésnek a munkamappája már nem létezik ({path}), ezért az üzeneted nem futott le. Hozd létre újra a mappát, vagy helyezd át a csevegést egy másikba (`/worktree new` a CLI-ben, „Move to project” az asztali alkalmazásban), majd küldd el újra az üzenetet." + launch_credentials_unavailable: "⚠️ Ez a csevegés már nem tudja használni azt az API-kulcsot, amellyel elindult{detail}, ezért semmi sem futhat benne. Egy nyitott csevegés soha nem vált magától másik kulcsra, így a /retry ugyanígy hibázik. A /new paranccsal indíts új csevegést a jelenlegi kulcsoddal." skills: disabled: "A(z) **{name}** skill telepítve van, de ki van kapcsolva.\nBekapcsolás: `hermes skills config`" not_installed: "A(z) **{name}** skill elérhető, de nincs telepítve.\nTelepítés: `hermes skills install {install_name}`" diff --git a/locales/it.tui.yaml b/locales/it.tui.yaml index 2df5afb273..c8afde9bf3 100644 --- a/locales/it.tui.yaml +++ b/locales/it.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "Richiesta della password chiusa: nessuna rispo userMessages.promptTimeout.vaultCode: "Richiesta del codice di verifica chiusa: nessuna risposta in tempo, quindi l'accesso è stato saltato. Invia di nuovo la richiesta quando hai il codice." userMessages.promptTimeout.vaultSaveLogin: "Richiesta di salvataggio dell'accesso chiusa: nessuna risposta in tempo, quindi non è stato salvato nulla. Invia di nuovo la richiesta quando sei pronto." userMessages.promptTimeout.vaultUnlockPrompt: "Richiesta di sblocco chiusa: nessuna risposta in tempo, quindi il gestore di password è rimasto bloccato. Invia di nuovo la richiesta quando sei pronto a sbloccarlo." +userMessages.rpc.launchCredentialsChanged: "Questa chat non può più usare la chiave API con cui è stata avviata (è stata cambiata o rimossa), quindi l'operazione non è stata eseguita. Una chat non passa mai da sola a una chiave diversa. Digita /new per avviare una nuova chat con la tua chiave attuale." userMessages.rpc.notConnected: "Hermes al momento non è connesso, quindi il messaggio non è stato inviato. Si riconnette da solo; attendi un istante e riprova, oppure digita /logs se il problema persiste." userMessages.rpc.sessionNotFound: "Questa chat non è più collegata al backend (era inattiva o il backend si è riavviato). La cronologia è salvata: digita /resume per riaprirla." userMessages.rpc.shellBackendRefused: "I comandi ! vengono eseguiti sull'host del gateway, ma il backend di terminale di questa sessione è {0}, quindi il comando non è stato eseguito. Chiedi invece all'agente di eseguirlo; il terminale dell'agente gira dentro {0}." diff --git a/locales/it.yaml b/locales/it.yaml index 807632e115..a46413bf6d 100644 --- a/locales/it.yaml +++ b/locales/it.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ Non sono riuscito a connettermi al servizio del modello AI, quindi questo messaggio non è stato elaborato. Usa /login per accedere di nuovo, oppure /model per scegliere un altro modello. Se continua a fallire, esegui `hermes doctor` sull'host." cron_session_read_only: "⚠️ Questa chat appartiene a un'attività pianificata ed esegue solo quella, quindi il tuo messaggio non è stato eseguito. Avvia una nuova chat per inviarlo." workspace_unavailable: "⚠️ La cartella di lavoro di questa chat non esiste più ({path}), quindi il tuo messaggio non è stato eseguito. Ricrea la cartella o sposta la chat in un'altra (`/worktree new` nella CLI, «Move to project» nell'app desktop), poi invia di nuovo il messaggio." + launch_credentials_unavailable: "⚠️ Questa chat non può più usare la chiave API con cui è stata avviata{detail}, quindi non può più eseguire nulla. Una chat aperta non passa mai da sola a una chiave diversa, quindi /retry fallisce allo stesso modo. Usa /new per avviare una nuova chat con la tua chiave attuale." skills: disabled: "La skill **{name}** è installata ma disattivata.\nAttivala con: `hermes skills config`" not_installed: "La skill **{name}** è disponibile ma non installata.\nInstallala con: `hermes skills install {install_name}`" diff --git a/locales/ja.tui.yaml b/locales/ja.tui.yaml index 7c88bcf73c..2e0185da58 100644 --- a/locales/ja.tui.yaml +++ b/locales/ja.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "パスワード入力プロンプトを閉じ userMessages.promptTimeout.vaultCode: "確認コード入力プロンプトを閉じました: 時間内に入力がなかったため、サインインはスキップされました。コードが手元に届いたら、もう一度リクエストを送ってください。" userMessages.promptTimeout.vaultSaveLogin: "ログイン保存プロンプトを閉じました: 時間内に応答がなかったため、何も保存されていません。準備ができたら、もう一度リクエストを送ってください。" userMessages.promptTimeout.vaultUnlockPrompt: "ロック解除プロンプトを閉じました: 時間内に入力がなかったため、パスワードマネージャーはロックされたままです。解除の準備ができたら、もう一度リクエストを送ってください。" +userMessages.rpc.launchCredentialsChanged: "このチャットは開始時の API キーをもう使えないため(変更または削除されました)、実行されませんでした。チャットが自動で別のキーに切り替わることはありません。/new と入力して、現在のキーで新しいチャットを始めてください。" userMessages.rpc.notConnected: "Hermes に接続していないため送信されませんでした。自動的に再接続しますので、少し待ってから再度お試しください。続く場合は /logs と入力してください。" userMessages.rpc.sessionNotFound: "このチャットはバックエンドから切り離されています(アイドル状態が続いたか、バックエンドが再起動しました)。履歴は保存されています。/resume と入力すると再び開けます。" userMessages.rpc.shellBackendRefused: "! コマンドはゲートウェイのホストで実行されますが、このセッションのターミナルバックエンドは {0} のため、コマンドは実行されませんでした。代わりにエージェントに実行を依頼してください。エージェントのターミナルは {0} 内で動作します。" diff --git a/locales/ja.yaml b/locales/ja.yaml index a3f8dbe808..ebc2b1a984 100644 --- a/locales/ja.yaml +++ b/locales/ja.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ AI モデルサービスに接続できなかったため、このメッセージは処理されませんでした。/login で再度サインインするか、/model で別のモデルを選んでください。失敗が続く場合は、ホストで `hermes doctor` を実行してください。" cron_session_read_only: "⚠️ このチャットはスケジュールされたジョブ専用で、そのジョブしか実行しないため、メッセージは実行されませんでした。送信するには新しいチャットを開始してください。" workspace_unavailable: "⚠️ このチャットの作業フォルダーが見つかりません({path})。そのためメッセージは実行されませんでした。フォルダーを作り直すか、チャットを別のフォルダーへ移動してから(CLI では `/worktree new`、デスクトップアプリでは「Move to project」)、もう一度送信してください。" + launch_credentials_unavailable: "⚠️ このチャットは開始時の API キーをもう使えないため{detail}、このチャットでは何も実行できません。開いているチャットが自動で別のキーに切り替わることはないので、/retry でも同じように失敗します。/new で現在のキーを使う新しいチャットを始めてください。" skills: disabled: "**{name}** スキルはインストールされていますが無効です。\n有効化するには: `hermes skills config`" not_installed: "**{name}** スキルは利用可能ですがインストールされていません。\nインストールするには: `hermes skills install {install_name}`" diff --git a/locales/ko.tui.yaml b/locales/ko.tui.yaml index 56b7162a1b..17d0089b87 100644 --- a/locales/ko.tui.yaml +++ b/locales/ko.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "비밀번호 프롬프트가 닫혔어요: 제 userMessages.promptTimeout.vaultCode: "인증 코드 프롬프트가 닫혔어요: 제한 시간 안에 답이 없어서 로그인을 건너뛰었어요. 코드가 준비되면 요청을 다시 보내세요." userMessages.promptTimeout.vaultSaveLogin: "로그인 저장 프롬프트가 닫혔어요: 제한 시간 안에 답이 없어서 아무것도 저장하지 않았어요. 준비되면 요청을 다시 보내세요." userMessages.promptTimeout.vaultUnlockPrompt: "잠금 해제 프롬프트가 닫혔어요: 제한 시간 안에 답이 없어서 비밀번호 관리자가 잠긴 상태로 남았어요. 잠금을 해제할 준비가 되면 요청을 다시 보내세요." +userMessages.rpc.launchCredentialsChanged: "이 채팅은 시작할 때 사용한 API 키를 더 이상 사용할 수 없어서(변경되었거나 삭제됨) 실행되지 않았습니다. 채팅은 스스로 다른 키로 바뀌지 않습니다. /new를 입력해 현재 키로 새 채팅을 시작하세요." userMessages.rpc.notConnected: "지금 Hermes가 연결되어 있지 않아서 전송되지 않았어요. 자동으로 다시 연결되니 잠시 후 다시 시도하고, 계속되면 /logs 를 입력하세요." userMessages.rpc.sessionNotFound: "이 채팅은 더 이상 백엔드에 연결되어 있지 않아요 (유휴 상태였거나 백엔드가 다시 시작됐어요). 기록은 저장되어 있으니 /resume 을 입력해 다시 열어 주세요." userMessages.rpc.shellBackendRefused: "! 명령은 게이트웨이 호스트에서 실행되지만 이 세션의 터미널 백엔드는 {0}이므로 명령을 실행하지 않았습니다. 대신 에이전트에게 실행을 요청하세요. 에이전트의 터미널은 {0} 안에서 실행됩니다." diff --git a/locales/ko.yaml b/locales/ko.yaml index 173abfcf74..e34a69affd 100644 --- a/locales/ko.yaml +++ b/locales/ko.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ AI 모델 서비스에 연결할 수 없어서 이 메시지는 처리되지 않았어요. /login으로 다시 로그인하거나, /model로 다른 모델을 선택하세요. 계속 실패하면 호스트에서 `hermes doctor`를 실행하세요." cron_session_read_only: "⚠️ 이 채팅은 예약된 작업 전용이며 그 작업만 실행하므로 메시지가 실행되지 않았습니다. 보내려면 새 채팅을 시작하세요." workspace_unavailable: "⚠️ 이 채팅의 작업 폴더가 더 이상 없습니다({path}). 그래서 메시지가 실행되지 않았습니다. 폴더를 다시 만들거나 채팅을 다른 폴더로 옮긴 뒤(CLI에서는 `/worktree new`, 데스크톱 앱에서는 'Move to project') 메시지를 다시 보내세요." + launch_credentials_unavailable: "⚠️ 이 채팅은 시작할 때 사용한 API 키를 더 이상 사용할 수 없어서{detail} 이 채팅에서는 아무것도 실행할 수 없습니다. 열려 있는 채팅은 스스로 다른 키로 바뀌지 않으므로 /retry도 같은 이유로 실패합니다. /new로 현재 키를 사용하는 새 채팅을 시작하세요." skills: disabled: "**{name}** 스킬은 설치되어 있지만 비활성화되어 있어요.\n활성화 방법: `hermes skills config`" not_installed: "**{name}** 스킬은 사용할 수 있지만 설치되어 있지 않아요.\n설치 방법: `hermes skills install {install_name}`" diff --git a/locales/pt.tui.yaml b/locales/pt.tui.yaml index f2b83cbec2..c9d6c4e9f8 100644 --- a/locales/pt.tui.yaml +++ b/locales/pt.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "Prompt de senha fechado: sem resposta a tempo, userMessages.promptTimeout.vaultCode: "Prompt de código de verificação fechado: sem resposta a tempo, então o login foi pulado. Envie sua solicitação novamente quando tiver o código." userMessages.promptTimeout.vaultSaveLogin: "Prompt de salvar login fechado: sem resposta a tempo, então nada foi salvo. Envie sua solicitação novamente quando estiver pronto." userMessages.promptTimeout.vaultUnlockPrompt: "Prompt de desbloqueio fechado: sem resposta a tempo, então o gerenciador de senhas continuou bloqueado. Envie sua solicitação novamente quando estiver pronto para desbloqueá-lo." +userMessages.rpc.launchCredentialsChanged: "Este chat não pode mais usar a chave de API com que foi iniciado (ela foi alterada ou removida), então isso não foi feito. Um chat nunca muda sozinho para outra chave. Digite /new para começar um novo chat com sua chave atual." userMessages.rpc.notConnected: "O Hermes não está conectado agora, então isso não foi enviado. Ele reconecta automaticamente; aguarde um momento e tente novamente, ou digite /logs se persistir." userMessages.rpc.sessionNotFound: "Este chat não está mais vinculado ao backend (ficou ocioso ou o backend reiniciou). Seu histórico está salvo: digite /resume para reabri-lo." userMessages.rpc.shellBackendRefused: "Os comandos ! são executados no host do gateway, mas o backend de terminal desta sessão é {0}, então o comando não foi executado. Peça ao agente para executá-lo; o terminal do agente roda dentro de {0}." diff --git a/locales/pt.yaml b/locales/pt.yaml index 79a0da9015..ad1b44aff7 100644 --- a/locales/pt.yaml +++ b/locales/pt.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ Não consegui me conectar ao serviço do modelo de IA, então esta mensagem não foi processada. Use /login para entrar novamente ou /model para escolher outro modelo. Se continuar falhando, execute `hermes doctor` no host." cron_session_read_only: "⚠️ Este chat pertence a uma tarefa agendada e só executa essa tarefa, por isso a sua mensagem não foi executada. Inicie um novo chat para enviá-la." workspace_unavailable: "⚠️ A pasta de trabalho deste chat já não existe ({path}), por isso a sua mensagem não foi executada. Recrie a pasta ou mova o chat para outra (`/worktree new` na CLI, «Move to project» na aplicação de ambiente de trabalho) e envie a mensagem novamente." + launch_credentials_unavailable: "⚠️ Este chat não pode mais usar a chave de API com que foi iniciado{detail}, então nada pode ser executado nele. Um chat aberto nunca muda sozinho para outra chave, então /retry falha da mesma forma. Use /new para começar um novo chat com sua chave atual." skills: disabled: "A skill **{name}** está instalada, mas desativada.\nAtive-a com: `hermes skills config`" not_installed: "A skill **{name}** está disponível, mas não instalada.\nInstale-a com: `hermes skills install {install_name}`" diff --git a/locales/ru.tui.yaml b/locales/ru.tui.yaml index cea20b14e4..88af2249ff 100644 --- a/locales/ru.tui.yaml +++ b/locales/ru.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "Запрос пароля закрыт: отв userMessages.promptTimeout.vaultCode: "Запрос кода подтверждения закрыт: ответ не получен вовремя, поэтому вход пропущен. Отправьте запрос снова, когда у вас будет код." userMessages.promptTimeout.vaultSaveLogin: "Запрос на сохранение логина закрыт: ответ не получен вовремя, поэтому ничего не сохранено. Отправьте запрос снова, когда будете готовы." userMessages.promptTimeout.vaultUnlockPrompt: "Запрос разблокировки закрыт: ответ не получен вовремя, поэтому менеджер паролей остался заблокирован. Отправьте запрос снова, когда будете готовы его разблокировать." +userMessages.rpc.launchCredentialsChanged: "Этот чат больше не может использовать ключ API, с которым он был начат (ключ изменили или удалили), поэтому действие не выполнено. Чат никогда сам не переключается на другой ключ. Введите /new, чтобы начать новый чат с текущим ключом." userMessages.rpc.notConnected: "Hermes сейчас не подключён, поэтому это не было отправлено. Он переподключается автоматически; подождите немного и повторите, а если проблема сохраняется — введите /logs." userMessages.rpc.sessionNotFound: "Этот чат больше не привязан к бэкенду (он простаивал или бэкенд перезапустился). История сохранена: введите /resume, чтобы открыть его снова." userMessages.rpc.shellBackendRefused: "Команды ! выполняются на хосте шлюза, но терминальный бэкенд этой сессии — {0}, поэтому команда не выполнена. Попросите агента выполнить её; терминал агента работает внутри {0}." diff --git a/locales/ru.yaml b/locales/ru.yaml index e38a0961e4..c3f1b8de85 100644 --- a/locales/ru.yaml +++ b/locales/ru.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ Мне не удалось подключиться к сервису ИИ-модели, поэтому сообщение не было обработано. Используйте /login, чтобы войти заново, или /model, чтобы выбрать другую модель. Если ошибка повторяется, выполните `hermes doctor` на хосте." cron_session_read_only: "⚠️ Этот чат относится к запланированной задаче и выполняет только её, поэтому ваше сообщение не было выполнено. Начните новый чат, чтобы отправить его." workspace_unavailable: "⚠️ Рабочая папка этого чата больше не существует ({path}), поэтому ваше сообщение не было выполнено. Создайте папку заново или перенесите чат в другую (`/worktree new` в CLI, «Переместить в проект» в настольном приложении), затем отправьте сообщение ещё раз." + launch_credentials_unavailable: "⚠️ Этот чат больше не может использовать ключ API, с которым он был начат{detail}, поэтому в нём ничего не может выполняться. Открытый чат никогда сам не переключается на другой ключ, поэтому /retry завершится так же. Используйте /new, чтобы начать новый чат с текущим ключом." skills: disabled: "Навык **{name}** установлен, но выключен.\nВключите его командой: `hermes skills config`" not_installed: "Навык **{name}** доступен, но не установлен.\nУстановите его командой: `hermes skills install {install_name}`" diff --git a/locales/tr.tui.yaml b/locales/tr.tui.yaml index c126a069a5..4698495a2f 100644 --- a/locales/tr.tui.yaml +++ b/locales/tr.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "Parola istemi kapandı: zamanında yanıt gelm userMessages.promptTimeout.vaultCode: "Doğrulama kodu istemi kapandı: zamanında yanıt gelmediği için giriş atlandı. Kod elinizde olduğunda isteğinizi yeniden gönderin." userMessages.promptTimeout.vaultSaveLogin: "Giriş kaydetme istemi kapandı: zamanında yanıt gelmediği için hiçbir şey kaydedilmedi. Hazır olduğunuzda isteğinizi yeniden gönderin." userMessages.promptTimeout.vaultUnlockPrompt: "Kilit açma istemi kapandı: zamanında yanıt gelmediği için parola yöneticisi kilitli kaldı. Kilidi açmaya hazır olduğunuzda isteğinizi yeniden gönderin." +userMessages.rpc.launchCredentialsChanged: "Bu sohbet, başlatıldığı API anahtarını artık kullanamıyor (değiştirildi veya kaldırıldı), bu yüzden işlem yapılmadı. Bir sohbet asla kendiliğinden başka bir anahtara geçmez. Mevcut anahtarınla yeni bir sohbet başlatmak için /new yaz." userMessages.rpc.notConnected: "Hermes şu anda bağlı değil, bu yüzden mesaj gönderilmedi. Otomatik olarak yeniden bağlanır; biraz bekleyip yeniden deneyin ya da sorun sürerse /logs yazın." userMessages.rpc.sessionNotFound: "Bu sohbet artık arka uca bağlı değil (boşta kaldı veya arka uç yeniden başlatıldı). Geçmişiniz kaydedildi: yeniden açmak için /resume yazın." userMessages.rpc.shellBackendRefused: "! komutları gateway ana makinesinde çalışır, ancak bu oturumun terminal arka ucu {0} olduğundan komut çalıştırılmadı. Bunun yerine ajandan çalıştırmasını isteyin; ajanın terminali {0} içinde çalışır." diff --git a/locales/tr.yaml b/locales/tr.yaml index a95dc8ed35..1d295af27c 100644 --- a/locales/tr.yaml +++ b/locales/tr.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ Yapay zekâ model servisine bağlanamadım; bu nedenle mesaj işlenmedi. Yeniden giriş yapmak için /login, farklı bir model seçmek için /model kullanın. Sorun sürerse ana makinede `hermes doctor` çalıştırın." cron_session_read_only: "⚠️ Bu sohbet zamanlanmış bir göreve ait ve yalnızca o görevi çalıştırıyor, bu yüzden mesajın çalıştırılmadı. Göndermek için yeni bir sohbet başlat." workspace_unavailable: "⚠️ Bu sohbetin çalışma klasörü artık yok ({path}), bu yüzden mesajın çalıştırılmadı. Klasörü yeniden oluştur ya da sohbeti başka bir klasöre taşı (CLI'da `/worktree new`, masaüstü uygulamasında “Move to project”), sonra mesajını yeniden gönder." + launch_credentials_unavailable: "⚠️ Bu sohbet, başlatıldığı API anahtarını artık kullanamıyor{detail}, bu yüzden içinde hiçbir şey çalıştırılamaz. Açık bir sohbet asla kendiliğinden başka bir anahtara geçmez, bu nedenle /retry de aynı şekilde başarısız olur. Mevcut anahtarınla yeni bir sohbet başlatmak için /new kullan." skills: disabled: "**{name}** skill'i kurulu ancak devre dışı.\nEtkinleştirmek için: `hermes skills config`" not_installed: "**{name}** skill'i mevcut ancak kurulu değil.\nKurmak için: `hermes skills install {install_name}`" diff --git a/locales/uk.tui.yaml b/locales/uk.tui.yaml index dad1841460..f5b3efdda0 100644 --- a/locales/uk.tui.yaml +++ b/locales/uk.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "Запит пароля закрито: від userMessages.promptTimeout.vaultCode: "Запит коду підтвердження закрито: відповіді вчасно не було, тому вхід пропущено. Надішліть запит знову, коли матимете код." userMessages.promptTimeout.vaultSaveLogin: "Запит на збереження логіна закрито: відповіді вчасно не було, тому нічого не збережено. Надішліть запит знову, коли будете готові." userMessages.promptTimeout.vaultUnlockPrompt: "Запит на розблокування закрито: відповіді вчасно не було, тому менеджер паролів залишився заблокованим. Надішліть запит знову, коли будете готові його розблокувати." +userMessages.rpc.launchCredentialsChanged: "Цей чат більше не може використовувати ключ API, з яким його було розпочато (ключ змінили або видалили), тому дію не виконано. Чат ніколи сам не перемикається на інший ключ. Введіть /new, щоб почати новий чат із поточним ключем." userMessages.rpc.notConnected: "Hermes зараз не підключено, тому це не надіслано. Він перепідключається автоматично; зачекайте трохи та спробуйте ще раз або введіть /logs, якщо проблема не зникає." userMessages.rpc.sessionNotFound: "Цей чат більше не прив'язаний до бекенду (він простоював або бекенд перезапустився). Історію збережено: введіть /resume, щоб відкрити його знову." userMessages.rpc.shellBackendRefused: "Команди ! виконуються на хості шлюзу, але термінальний бекенд цієї сесії — {0}, тому команду не виконано. Попросіть агента виконати її; термінал агента працює всередині {0}." diff --git a/locales/uk.yaml b/locales/uk.yaml index 22ebc9b947..251992da2f 100644 --- a/locales/uk.yaml +++ b/locales/uk.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ Я не зміг з'єднатися із сервісом AI-моделі, тому це повідомлення не оброблено. Скористайтеся /login, щоб увійти знову, або /model, щоб вибрати іншу модель. Якщо помилка повторюється, виконайте `hermes doctor` на хості." cron_session_read_only: "⚠️ Цей чат належить до запланованого завдання й виконує лише його, тому ваше повідомлення не було виконано. Почніть новий чат, щоб надіслати його." workspace_unavailable: "⚠️ Робочої теки цього чату більше не існує ({path}), тому ваше повідомлення не було виконано. Створіть теку знову або перенесіть чат до іншої (`/worktree new` у CLI, «Move to project» в настільному застосунку), а потім надішліть повідомлення ще раз." + launch_credentials_unavailable: "⚠️ Цей чат більше не може використовувати ключ API, з яким його було розпочато{detail}, тому в ньому нічого не може виконуватися. Відкритий чат ніколи сам не перемикається на інший ключ, тож /retry завершиться так само. Скористайтеся /new, щоб почати новий чат із поточним ключем." skills: disabled: "Навичку **{name}** встановлено, але вимкнено.\nУвімкніть її командою: `hermes skills config`" not_installed: "Навичка **{name}** доступна, але не встановлена.\nВстановіть її командою: `hermes skills install {install_name}`" diff --git a/locales/zh-hant.tui.yaml b/locales/zh-hant.tui.yaml index bb97ed1a55..0bb1da194a 100644 --- a/locales/zh-hant.tui.yaml +++ b/locales/zh-hant.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "密碼提示已關閉:未及時回應,該 userMessages.promptTimeout.vaultCode: "驗證碼提示已關閉:未及時回應,登入已被略過。拿到驗證碼後請重新送出你的請求。" userMessages.promptTimeout.vaultSaveLogin: "儲存登入資訊的提示已關閉:未及時回應,因此未儲存任何內容。準備好後請重新送出你的請求。" userMessages.promptTimeout.vaultUnlockPrompt: "解鎖提示已關閉:未及時回應,密碼管理器仍保持鎖定。準備好解鎖時請重新送出你的請求。" +userMessages.rpc.launchCredentialsChanged: "此聊天無法再使用它開始時的 API 金鑰(已被變更或刪除),因此未執行該操作。聊天不會自行切換到其他金鑰。輸入 /new 以目前的金鑰開始新聊天。" userMessages.rpc.notConnected: "Hermes 目前未連線,所以剛才的內容沒有送出。它會自動重新連線;請稍候再試,如果問題持續,請輸入 /logs。" userMessages.rpc.sessionNotFound: "此聊天已不再連接到後端(長時間閒置或後端已重新啟動)。你的歷史記錄已儲存:輸入 /resume 重新開啟。" userMessages.rpc.shellBackendRefused: "! 指令在閘道主機上執行,但此工作階段的終端後端是 {0},因此未執行該指令。請改請代理執行;代理的終端在 {0} 內執行。" diff --git a/locales/zh-hant.yaml b/locales/zh-hant.yaml index c41b6b9e5f..293aa5c864 100644 --- a/locales/zh-hant.yaml +++ b/locales/zh-hant.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ 我無法連上 AI 模型服務,因此這則訊息未被處理。請使用 /login 重新登入,或使用 /model 選擇其他模型。若持續失敗,請在主機上執行 `hermes doctor`。" cron_session_read_only: "⚠️ 此聊天屬於一個排程工作,只會執行該工作,因此你的訊息未被執行。請開始新的聊天後再傳送。" workspace_unavailable: "⚠️ 此聊天的工作資料夾已不存在({path}),因此你的訊息未被執行。請重新建立該資料夾,或將聊天移至其他資料夾(CLI 中使用 `/worktree new`,桌面應用程式中使用「Move to project」),然後再次傳送訊息。" + launch_credentials_unavailable: "⚠️ 此聊天無法再使用它開始時的 API 金鑰{detail},因此無法在其中執行任何內容。開啟中的聊天不會自行切換到其他金鑰,所以 /retry 也會以同樣方式失敗。請使用 /new 以目前的金鑰開始新聊天。" skills: disabled: "**{name}** 技能已安裝但已停用。\n啟用方式:`hermes skills config`" not_installed: "**{name}** 技能可用但尚未安裝。\n安裝方式:`hermes skills install {install_name}`" diff --git a/locales/zh.tui.yaml b/locales/zh.tui.yaml index 51863c1a3d..032c90f4d7 100644 --- a/locales/zh.tui.yaml +++ b/locales/zh.tui.yaml @@ -1337,6 +1337,7 @@ userMessages.promptTimeout.sudo: "密码提示已关闭:未及时回应,该 userMessages.promptTimeout.vaultCode: "验证码提示已关闭:未及时回应,登录已被跳过。拿到验证码后请重新发送你的请求。" userMessages.promptTimeout.vaultSaveLogin: "保存登录信息的提示已关闭:未及时回应,因此未保存任何内容。准备好后请重新发送你的请求。" userMessages.promptTimeout.vaultUnlockPrompt: "解锁提示已关闭:未及时回应,密码管理器仍保持锁定。准备好解锁时请重新发送你的请求。" +userMessages.rpc.launchCredentialsChanged: "此聊天无法再使用它开始时的 API 密钥(已被更改或删除),因此未执行该操作。聊天不会自行切换到其他密钥。输入 /new 以当前密钥开始新聊天。" userMessages.rpc.notConnected: "Hermes 目前未连接,所以刚才的内容没有发送。它会自动重连;请稍等片刻再试,如果问题持续,请输入 /logs。" userMessages.rpc.sessionNotFound: "此聊天已不再连接到后端(长时间空闲或后端已重启)。你的历史记录已保存:输入 /resume 重新打开。" userMessages.rpc.shellBackendRefused: "! 命令在网关主机上运行,但此会话的终端后端是 {0},因此未执行该命令。请改为让代理运行;代理的终端在 {0} 内运行。" diff --git a/locales/zh.yaml b/locales/zh.yaml index 8988bdcd8e..8f525aedf8 100644 --- a/locales/zh.yaml +++ b/locales/zh.yaml @@ -697,6 +697,7 @@ gateway: no_credentials: "⚠️ 我无法连接 AI 模型服务,因此这条消息未被处理。请使用 /login 重新登录,或用 /model 选择其他模型。如果持续失败,请在主机上运行 `hermes doctor`。" cron_session_read_only: "⚠️ 此聊天属于一个定时任务,只会运行该任务,因此你的消息未被执行。请新建聊天后再发送。" workspace_unavailable: "⚠️ 此聊天的工作文件夹已不存在({path}),因此你的消息未被执行。请重新创建该文件夹,或将聊天移到其他文件夹(CLI 中使用 `/worktree new`,桌面应用中使用“移动到项目”),然后重新发送消息。" + launch_credentials_unavailable: "⚠️ 此聊天无法再使用它开始时的 API 密钥{detail},因此无法在其中运行任何内容。打开的聊天不会自行切换到其他密钥,所以 /retry 也会以同样方式失败。请使用 /new 以当前密钥开始新聊天。" skills: disabled: "**{name}** 技能已安装但被禁用。\n启用方式:`hermes skills config`" not_installed: "**{name}** 技能可用但尚未安装。\n安装方式:`hermes skills install {install_name}`" diff --git a/tests/gateway/test_launch_credentials_refusal_copy.py b/tests/gateway/test_launch_credentials_refusal_copy.py new file mode 100644 index 0000000000..aa7497578b --- /dev/null +++ b/tests/gateway/test_launch_credentials_refusal_copy.py @@ -0,0 +1,54 @@ +"""A chat whose own route key changed since it started is refused by design (a session never sends a +different key than it started with). The refusal must name the way out (/new), never /retry.""" +import json +from types import SimpleNamespace + + +def test_turn_on_a_chat_whose_route_key_changed_names_new_chat_not_retry(tmp_path, monkeypatch): + """The refusal at turn entry used to escape as an exception: every surface showed "Something went + wrong ... Use /retry", which can never succeed, plus a false "some actions may have run" notice.""" + from agent.error_surface import build_error_surface_from_result + from gateway import session_authorities as authorities, session_policy + from gateway.run_turn_runner import TurnRunner + from gateway.session_results import execution_result + from tests.gateway.test_config_policy_recovery import _bound_session + + cfg = {'model': {'provider': 'custom', 'api_key': 'route-token', 'base_url': 'http://127.0.0.1:1234/v1'}} + policy, owner, config_path = _bound_session(tmp_path, monkeypatch, cfg) + cfg['model']['api_key'] = 'rotated-token' + config_path.write_text(json.dumps(cfg)) + turn = TurnRunner(SimpleNamespace(), SimpleNamespace(source=None, session_id='session', session_key=None)) + monkeypatch.setattr(session_policy, 'policy_for_source', lambda runner, source: policy) + monkeypatch.setattr(authorities, 'active_authority', lambda runner: owner()) # a restarted owner + + token = execution_result.set({}) + try: + result = turn.run_sync() + captured = execution_result.get() + finally: + execution_result.reset(token) + + assert result['failed'] and not result['completed'] and result['api_calls'] == 0, result + assert result['failure_reason'] == 'launch_credentials_unavailable' and result['failure_retryable'] is False + text = result['final_response'] + assert '/new' in text and 'model.api_key' in text, text + assert 'Use /retry to try again' not in text and 'Some actions may already have run' not in text, text + assert 'route-token' not in text and 'rotated-token' not in text + assert captured['result'] == result # what the classic CLI viewer reads off the receipt + surface = build_error_surface_from_result(result) + assert (surface['layer'], surface['code'], surface['retryable']) == ('gateway', 'launch_credentials_unavailable', False) + + +def test_cli_refusal_of_a_control_on_that_chat_names_new_chat(): + """The classic CLI's /model (and /compress, /worktree) printed the bare reason code for the same + refusal; any retry or model pick repeats it.""" + from hermes_cli.cli_chat_error_copy import refusal_text + from hermes_cli.gateway_client import GatewayRPCError + + refused = GatewayRPCError('launch_credentials_unavailable', code=4001, data={ + 'reason': 'launch_credentials_unavailable', 'field': 'model.api_key', + 'detail': 'changed in config.yaml since this session started'}) + text = refusal_text(refused) + assert '/new' in text and 'model.api_key' in text, text + assert refusal_text(GatewayRPCError('invalid_params', data={'field': 'toolsets', 'detail': 'bad'})) == \ + 'invalid_params (toolsets: bad)' # every other refusal reads as before diff --git a/ui-tui/src/__tests__/userMessages.test.ts b/ui-tui/src/__tests__/userMessages.test.ts index 1d3dc5be21..fcfb046677 100644 --- a/ui-tui/src/__tests__/userMessages.test.ts +++ b/ui-tui/src/__tests__/userMessages.test.ts @@ -117,6 +117,13 @@ describe('describeRpcError', () => { ).toContain('/resume') }) + it('names /new for a refusal on a chat whose launch API key changed, never the bare code', () => { + const text = describeRpcError(new JsonRpcGatewayError('launch_credentials_unavailable', { code: 4001 })) + + expect(text).toContain('/new') + expect(text).not.toContain('launch_credentials_unavailable') + }) + it('records the raw wire text it replaced in the log sink', () => { const lines: string[] = [] setRpcErrorLogSink(line => lines.push(line)) diff --git a/ui-tui/src/app/userMessages.ts b/ui-tui/src/app/userMessages.ts index 503e47f5b7..d36740812a 100644 --- a/ui-tui/src/app/userMessages.ts +++ b/ui-tui/src/app/userMessages.ts @@ -136,6 +136,7 @@ const RPC_ERROR_ROWS: RpcErrorRow[] = [ () => t('userMessages.rpc.sessionNotFound') ], [(_code, text) => NOT_CONNECTED_RE.test(text), () => t('userMessages.rpc.notConnected')], + [(_code, text) => text.trim() === 'launch_credentials_unavailable', () => t('userMessages.rpc.launchCredentialsChanged')], [(_code, text) => TIMED_OUT_RE.exec(text), m => t('userMessages.rpc.timedOut', m?.[1] ?? '?')] ] diff --git a/ui-tui/src/i18n/en/userMessages.ts b/ui-tui/src/i18n/en/userMessages.ts index 6223be8747..fcc0073cff 100644 --- a/ui-tui/src/i18n/en/userMessages.ts +++ b/ui-tui/src/i18n/en/userMessages.ts @@ -47,6 +47,9 @@ export const userMessagesEn = { 'The terminal UI and the Hermes backend are out of sync (different versions). Run /update, or exit and run `hermes update`, then start the TUI again.', sessionNotFound: 'This chat is no longer attached to the backend (it was idle or the backend restarted). Your history is saved: type /resume to reopen it.', + /** The owner refused a control on a chat whose launch API key changed or was removed. */ + launchCredentialsChanged: + "This chat can't use the API key it was started with any more (it changed or was removed), so that was not done. A chat never switches to a different key on its own. Type /new to start a new chat on your current key.", notConnected: 'Hermes is not connected right now, so that was not sent. It reconnects automatically; wait a moment and try again, or type /logs if this persists.', /** `{0}` = timeout in seconds. */