feat(agents): session self-service tools

New grouped sessions tool (patch: label/pin/archive/model/thinking; group
CRUD; owner-gated; no reset/delete/compact). sessions_spawn gains visible +
worktree via sessions.create with admission reservation and abort-confirmed
rollback; a started run with no run id is aborted and deleted rather than
left orphaned. subagents tool reads the unified task_runs ledger tree-scoped
(subagent/acp/media/cron) and cancels detached runs, enforcing the
controlScope gate for leaf callers. session_status gains a cost line.
Agent-origin model patches record a fallback marker; runs failing on
auth/billing/model_not_found revert to the last working model (transient
errors never revert), and an independent thinkingLevel change realigns the
marker so revert cannot clobber it.

Refs #107237
This commit is contained in:
Peter Steinberger
2026-07-14 14:01:55 -07:00
parent 1fd4009e41
commit e905a682f7
66 changed files with 3003 additions and 266 deletions
@@ -1066,6 +1066,7 @@ internal val nativeStringResourceIds: Map<String, Int> =
"Session History" to R.string.native_edeead3f1961f9b8,
"Session Search" to R.string.native_5a7a1c2ec168fbbf,
"Session Send" to R.string.native_35458ac899311b76,
"Session Settings" to R.string.native_7703e4288a7ca2db,
"Session Status" to R.string.native_30e08bdeea992a84,
"Session Target" to R.string.native_88e6e8384a7fc7ff,
"Session activity" to R.string.native_4e35d595e6975128,
@@ -1344,6 +1345,7 @@ internal val nativeStringResourceIds: Map<String, Int> =
"close" to R.string.native_310ff200149b44a3,
"console" to R.string.native_93d8874c8c86f0fc,
"delete" to R.string.native_6197595503f01ee2,
"delete group" to R.string.native_698ef855b198408f,
"describe" to R.string.native_c52028f34e378d1e,
"dialog" to R.string.native_6940189433657105,
"document" to R.string.native_43cc23fa52b87b4c,
@@ -1356,6 +1358,7 @@ internal val nativeStringResourceIds: Map<String, Int> =
"events" to R.string.native_862417b9e7c3720b,
"focus" to R.string.native_c51faa148557a08c,
"generate" to R.string.native_24cacf5004bf68ae,
"groups" to R.string.native_4ed379d418bb8629,
"hide" to R.string.native_1f5bb9791a310e13,
"image" to R.string.native_6105d6cc76af4003,
"import" to R.string.native_d942f64886578d87,
@@ -1389,6 +1392,7 @@ internal val nativeStringResourceIds: Map<String, Int> =
"reasoning" to R.string.native_0c4d01e81bb3d1fd,
"reject" to R.string.native_084e990a49ba65e6,
"remove" to R.string.native_7e5608ab610017af,
"rename group" to R.string.native_5fa1d457ddfcd26c,
"requested \${relativeDeviceTime(it)}" to R.string.native_d0d0de0039c7990b,
"restart" to R.string.native_3ace60b0a0c1b6c9,
"role add" to R.string.native_c58ab237bd246028,
@@ -1401,6 +1405,7 @@ internal val nativeStringResourceIds: Map<String, Int> =
"screenshot" to R.string.native_4441146b0fe1d5c6,
"search" to R.string.native_2419329067823cab,
"send" to R.string.native_27ce1d1bf4270020,
"set groups" to R.string.native_8169e4741bbe5fa4,
"snapshot" to R.string.native_16a0eeb0791b6c92,
"soon" to R.string.native_4a754148b88a68e1,
"start" to R.string.native_cced28c6dc3f99c2,
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"نشاط الجلسة"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"إعدادات التشغيل الآلي غير صالحة."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"قائمة السماح"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"حول"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"لم يتم العثور على رمز QR للإعداد في هذه الصورة. اختر رمز QR الذي أنشأه openclaw qr، أو أدخل رمز الإعداد يدويًا."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تصفّح الجلسات"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · الموقع: دائمًا"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"التحميل من Gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"يحتاج إلى"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"جاهز"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تنتظر المذكّرات أول إدخال لها."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"الصفحة المباشرة"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"الاستماع إلى كلمات التنبيه"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"حدّد Gateway موفّرًا غير معروف %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"متابعة Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"يشير رمز الإعداد إلى Gateway بعيد غير آمن. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway غير متصل."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"صِل Gateway للبدء"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"إشعار أمان"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تجاهل تحذير الصورة المشتركة"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"خلال %1$s ساعة"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"موافقة Gateway معلّقة. شغّل الأمر التالي على مضيف Gateway:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"مطلوب وصول المسؤول"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تثبيت النموذج"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"مسح البحث"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ممكّنة للوكلاء المؤهلين."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sitzungsaktivität"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Die Automation hat eine ungültige Konfiguration."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Zulassungsliste"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Info"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"In diesem Bild wurde kein Einrichtungs-QR-Code gefunden. Wähle den von openclaw qr generierten QR-Code oder gib den Einrichtungscode manuell ein."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sitzungen durchsuchen"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Standort: Immer"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Vom Gateway laden"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Erforderlich"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Bereit"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Das Tagebuch wartet auf seinen ersten Eintrag."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Live-Seite"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Auf Aktivierungswörter warten"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway hat unbekannten Anbieter %1$s ausgewählt"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Android folgen · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Der Einrichtungscode verweist auf eine unsichere Remote-Gateway. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway getrennt."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway verbinden, um zu starten"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sicherheitshinweis"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Warnung zu geteiltem Bild schließen"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"In %1$s Std."</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Die Gateway-Genehmigung steht noch aus. Führen Sie auf dem Gateway-Host Folgendes aus:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Administratorzugriff erforderlich"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Modell anheften"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Suche löschen"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Für berechtigte Agenten aktiviert."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Actividad de la sesión"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"La automatización tiene una configuración no válida."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Lista de permitidos"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Acerca de"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"No se encontró ningún código QR de configuración en esa imagen. Selecciona el código QR generado por openclaw qr o introduce manualmente el código de configuración."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Explorar sesiones"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Ubicación: Siempre"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Cargar desde el gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Necesita"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Listo"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"El diario está esperando su primera entrada."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Página en vivo"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Escuchar palabras de activación"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway seleccionó un proveedor desconocido: %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Seguir Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"El código de configuración apunta a un Gateway remoto no seguro. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway desconectado."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Conecta el Gateway para comenzar"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Aviso de seguridad"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Descartar advertencia de imagen compartida"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"En %1$s h"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"La aprobación del Gateway está pendiente. Ejecuta esto en el host del Gateway:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Se requiere acceso de administrador"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Fijar modelo"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Borrar búsqueda"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Activada para los agentes elegibles."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"فعالیت جلسه"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"پیکربندی اجرای خودکار نامعتبر است."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"فهرست مجاز"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"درباره"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"هیچ کد QR راه‌اندازی در این تصویر پیدا نشد. کد QR ایجادشده با openclaw qr را انتخاب کنید، یا کد راه‌اندازی را به‌صورت دستی وارد کنید."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"مرور نشست‌ها"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · مکان: همیشه"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"بارگیری از Gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"نیاز دارد"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"آماده"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"دفترچه منتظر نخستین ورودی خود است."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"صفحه زنده"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"گوش‌دادن به واژه‌های بیدارباش"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway ارائه‌دهنده ناشناخته %1$s را انتخاب کرد"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"دنبال کردن Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"کد راه‌اندازی به یک gateway راه‌دور ناامن اشاره دارد. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway قطع شده است."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"برای شروع، Gateway را متصل کنید"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"اعلان امنیتی"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"رد کردن هشدار تصویر اشتراکی"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s ساعت دیگر"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"تأیید Gateway در انتظار است. این دستور را روی میزبان Gateway اجرا کنید:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"دسترسی مدیر لازم است"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"سنجاق کردن مدل"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"پاک‌کردن جست‌وجو"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"برای عامل‌های واجد شرایط فعال است."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Activité de la session"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"La configuration de lautomatisation nest pas valide."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Liste dautorisation"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"À propos"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Aucun code QR de configuration na été trouvé dans cette image. Choisissez le code QR généré par openclaw qr ou saisissez manuellement le code de configuration."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Parcourir les sessions"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Localisation : Toujours"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Charger depuis le Gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Requis"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Prêt"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Le journal attend sa première entrée."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Page en direct"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Écouter les mots dactivation"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway a sélectionné un fournisseur inconnu %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Suivre Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Le code de configuration pointe vers une Gateway distante non sécurisée. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway déconnecté."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Connectez le Gateway pour commencer"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Avis de sécurité"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ignorer l\'avertissement d\'image partagée"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dans %1$s h"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Lapprobation du Gateway est en attente. Exécutez ceci sur lhôte du Gateway :"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Accès administrateur requis"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Épingler le modèle"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Effacer la recherche"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Activée pour les agents éligibles."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"सेशन गतिविधि"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ऑटोमेशन का कॉन्फ़िगरेशन अमान्य है।"</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"अनुमति सूची"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"परिचय"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"उस इमेज में कोई सेटअप QR कोड नहीं मिला। openclaw qr से जनरेट किया गया QR चुनें या सेटअप कोड मैन्युअल रूप से दर्ज करें।"</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"सत्र ब्राउज़ करें"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · स्थान: हमेशा"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway से लोड करें"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"आवश्यक"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"तैयार"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"डायरी अपनी पहली प्रविष्टि की प्रतीक्षा कर रही है।"</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"लाइव पेज"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"वेक वर्ड सुनें"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway ने अज्ञात प्रदाता %1$s चुना"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Android का अनुसरण करें · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"सेटअप कोड एक असुरक्षित रिमोट gateway की ओर इशारा करता है। %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway डिस्कनेक्ट हो गया।"</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"शुरू करने के लिए Gateway कनेक्ट करें"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"सुरक्षा सूचना"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"साझा-छवि चेतावनी खारिज करें"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$sघंटे में"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway की स्वीकृति लंबित है। इसे gateway होस्ट पर चलाएँ:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Admin एक्सेस आवश्यक"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"मॉडल पिन करें"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"खोज साफ़ करें"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"पात्र एजेंट के लिए सक्षम।"</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Aktivitas sesi"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Automasi memiliki konfigurasi yang tidak valid."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Daftar izin"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tentang"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kode QR penyiapan tidak ditemukan dalam gambar tersebut. Pilih QR yang dibuat oleh openclaw qr, atau masukkan kode penyiapan secara manual."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Telusuri Sesi"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Lokasi: Selalu"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Muat dari gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Perlu"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Siap"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Diary sedang menunggu entri pertamanya."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Halaman langsung"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dengarkan kata aktivasi"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway memilih penyedia tidak dikenal %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ikuti Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kode penyiapan mengarah ke gateway jarak jauh yang tidak aman. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway terputus."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Hubungkan Gateway untuk memulai"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Pemberitahuan keamanan"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tutup peringatan gambar yang dibagikan"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dalam %1$s jam"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Persetujuan Gateway tertunda. Jalankan ini di host gateway:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Akses admin diperlukan"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sematkan model"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Hapus Pencarian"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Diaktifkan untuk agen yang memenuhi syarat."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Attività della sessione"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"L\'automazione ha una configurazione non valida."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Elenco consentiti"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Informazioni"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nell\'immagine non è stato trovato alcun codice QR di configurazione. Scegli il QR generato da openclaw qr oppure inserisci manualmente il codice di configurazione."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sfoglia sessioni"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Posizione: Sempre"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Carica dal Gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Richiede"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Pronto"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Il diario è in attesa della sua prima voce."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Pagina live"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ascolta le parole di attivazione"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway ha selezionato il provider sconosciuto %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Segui Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Il codice di configurazione punta a un gateway remoto non sicuro. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway disconnesso."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Connetti il gateway per iniziare"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Avviso di sicurezza"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ignora avviso immagine condivisa"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tra %1$sh"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"L\'approvazione del Gateway è in sospeso. Esegui questo comando sull\'host del Gateway:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Accesso amministratore richiesto"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Fissa modello"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Cancella ricerca"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Abilitata per gli agenti idonei."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"セッションアクティビティ"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"自動化の設定が無効です。"</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"許可リスト"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"情報"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"この画像にセットアップQRコードが見つかりませんでした。openclaw qr で生成されたQRを選択するか、セットアップコードを手動で入力してください。"</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"セッションを参照"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · 位置情報:常に許可"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway から読み込む"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"必要"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"準備完了"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"日記は最初のエントリを待っています。"</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ライブページ"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ウェイクワードを聞き取る"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway が不明なプロバイダー %1$s を選択しました"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Androidに従う · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"セットアップコードが安全でないリモートGatewayを指しています。%1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway が切断されました。"</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"開始するにはGatewayに接続してください"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"セキュリティに関する通知"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"共有画像の警告を閉じる"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s時間後"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gatewayの承認待ちです。Gatewayホストで次を実行してください:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"管理者アクセスが必要です"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"モデルをピン留め"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"検索をクリア"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"対象となるエージェントに対して有効です。"</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"세션 활동"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"자동화 구성이 잘못되었습니다."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"허용 목록"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"정보"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"해당 이미지에서 설정 QR 코드를 찾을 수 없습니다. openclaw qr로 생성한 QR을 선택하거나 설정 코드를 직접 입력하세요."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"세션 둘러보기"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · 위치: 항상"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway에서 불러오기"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"필요"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"준비됨"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"일기가 첫 항목을 기다리고 있습니다."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"라이브 페이지"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"호출어 감지"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway에서 알 수 없는 제공자 %1$s을(를) 선택했습니다"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Android 설정 따르기 · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"설정 코드가 안전하지 않은 원격 gateway를 가리킵니다. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway 연결이 끊어졌습니다."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"시작하려면 Gateway를 연결하세요"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"보안 알림"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"공유 이미지 경고 닫기"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s시간 후"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway 승인이 대기 중입니다. Gateway 호스트에서 다음을 실행하세요:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"관리자 권한 필요"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"모델 고정"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"검색 지우기"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"적격 에이전트에 대해 활성화되었습니다."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sessieactiviteit"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"De automatisering heeft een ongeldige configuratie."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Toegestaan-lijst"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Over"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Er is geen installatie-QR-code gevonden in die afbeelding. Kies de QR-code die is gegenereerd door openclaw qr of voer de installatiecode handmatig in."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sessies bekijken"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Locatie: Altijd"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Laden vanuit Gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Vereist"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gereed"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Het dagboek wacht op het eerste item."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Livepagina"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Luisteren naar activeringswoorden"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway heeft onbekende provider %1$s geselecteerd"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Volg Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"De setupcode verwijst naar een onveilige externe Gateway. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway losgekoppeld."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Verbind de Gateway om te beginnen"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Beveiligingsmelding"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Waarschuwing gedeelde afbeelding sluiten"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Over %1$su"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Goedkeuring van Gateway is in behandeling. Voer dit uit op de Gateway-host:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Beheerderstoegang vereist"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Model vastzetten"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Zoekopdracht wissen"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ingeschakeld voor geschikte agents."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Aktywność sesji"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Automatyzacja ma nieprawidłową konfigurację."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Lista dozwolonych"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Informacje"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Na tym obrazie nie znaleziono kodu QR konfiguracji. Wybierz kod QR wygenerowany przez openclaw qr lub wprowadź kod konfiguracji ręcznie."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Przeglądaj sesje"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Lokalizacja: Zawsze"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Wczytaj z Gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Wymaga"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gotowe"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dziennik czeka na pierwszy wpis."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Strona na żywo"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nasłuchuj słów aktywujących"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway wybrał nieznanego dostawcę %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Śledź Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kod konfiguracji wskazuje na niezabezpieczony zdalny Gateway. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway rozłączony."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Połącz Gateway, aby rozpocząć"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Informacja o bezpieczeństwie"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Odrzuć ostrzeżenie o udostępnionym obrazie"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Za %1$s godz."</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Oczekiwanie na zatwierdzenie Gateway. Uruchom to polecenie na hoście Gateway:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Wymagany dostęp administratora"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Przypnij model"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Wyczyść wyszukiwanie"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Włączono dla kwalifikujących się agentów."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Atividade da sessão"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"A automação tem uma configuração inválida."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Lista de permissões"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sobre"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nenhum código QR de configuração foi encontrado nessa imagem. Escolha o QR gerado por openclaw qr ou insira o código de configuração manualmente."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Explorar sessões"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Localização: Sempre"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Carregar do gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Necessários"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Pronto"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"O diário está aguardando sua primeira entrada."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Página ao vivo"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ouvir palavras de ativação"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"O Gateway selecionou o provedor desconhecido %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Seguir Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"O código de configuração aponta para um gateway remoto inseguro. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway desconectado."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Conecte o Gateway para começar"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Aviso de segurança"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dispensar aviso de imagem compartilhada"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Em %1$sh"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"A aprovação do Gateway está pendente. Execute isto no host do Gateway:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Acesso de administrador necessário"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Fixar modelo"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Limpar busca"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ativada para agentes qualificados."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Активность сеанса"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Конфигурация автоматизации недействительна."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Список разрешений"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"О приложении"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"На этом изображении не найден QR-код настройки. Выберите QR-код, созданный с помощью openclaw qr, или введите код настройки вручную."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Просмотреть сеансы"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Геопозиция: всегда"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Загрузить из Gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Требуется"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Готово"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Дневник ожидает первой записи."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Страница в реальном времени"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Распознавать фразы активации"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway выбрал неизвестного поставщика %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Следовать Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Код настройки указывает на небезопасный удаленный Gateway. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway отключен."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Подключите Gateway, чтобы начать"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Уведомление о безопасности"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Закрыть предупреждение об общем изображении"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Через %1$s ч."</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ожидается одобрение Gateway. Выполните следующую команду на хосте Gateway:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Требуется доступ администратора"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Закрепить модель"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Очистить поиск"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Включено для подходящих агентов."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sessionsaktivitet"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Automatiseringen har en ogiltig konfiguration."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tillåtelselista"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Om"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ingen QR-kod för konfiguration hittades i bilden. Välj QR-koden som genererades av openclaw qr eller ange konfigurationskoden manuellt."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Bläddra bland sessioner"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Plats: Alltid"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Läs in från Gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Behöver"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Klar"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dagboken väntar på sin första post."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Livesida"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Lyssna efter aktiveringsord"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway valde okänd leverantör %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Följ Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Installationskoden pekar på en osäker fjärr-Gateway. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway frånkopplad."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Anslut Gateway för att börja"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Säkerhetsmeddelande"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Avfärda varning om delad bild"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Om %1$s tim"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Godkännande av Gateway väntar. Kör detta på Gateway-värden:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Administratörsåtkomst krävs"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Fäst modell"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Rensa sökning"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Aktiverad för behöriga agenter."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"กิจกรรมเซสชัน"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ระบบอัตโนมัติมีการกำหนดค่าที่ไม่ถูกต้อง"</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"รายการที่อนุญาต"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"เกี่ยวกับ"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ไม่พบคิวอาร์โค้ดสำหรับตั้งค่าในรูปภาพนั้น โปรดเลือกคิวอาร์ที่สร้างโดย openclaw qr หรือป้อนรหัสตั้งค่าด้วยตนเอง"</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"เรียกดูเซสชัน"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · ตำแหน่งที่ตั้ง: ตลอดเวลา"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"โหลดจาก Gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ต้องการ"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"พร้อม"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ไดอารีกำลังรอรายการแรก"</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"หน้าไลฟ์"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ฟังคำปลุก"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway เลือกผู้ให้บริการที่ไม่รู้จัก %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ติดตาม Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"รหัสตั้งค่าชี้ไปยัง Gateway ระยะไกลที่ไม่ปลอดภัย %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway ถูกตัดการเชื่อมต่อ"</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"เชื่อมต่อ Gateway เพื่อเริ่มต้น"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ประกาศด้านความปลอดภัย"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ปิดคำเตือนรูปภาพที่แชร์"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ในอีก %1$s ชม."</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"การอนุมัติ Gateway อยู่ระหว่างรอดำเนินการ เรียกใช้คำสั่งนี้บนโฮสต์ Gateway:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ต้องมีสิทธิ์ผู้ดูแลระบบ"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ปักหมุดโมเดล"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"ล้างการค้นหา"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"เปิดใช้งานสำหรับเอเจนต์ที่มีสิทธิ์"</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Oturum etkinliği"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Otomasyonun yapılandırması geçersiz."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"İzin verilenler listesi"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Hakkında"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Bu görüntüde kurulum QR kodu bulunamadı. openclaw qr tarafından oluşturulan QR kodunu seçin veya kurulum kodunu manuel olarak girin."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Oturumlara Göz At"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Konum: Her Zaman"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway\'den yükle"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gerekenler"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Hazır"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Günlük ilk kaydını bekliyor."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Canlı sayfa"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Uyandırma sözcüklerini dinle"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway bilinmeyen sağlayıcıyı seçti: %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Android\'i takip et · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kurulum kodu güvenli olmayan bir uzak gateway\'i işaret ediyor. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway bağlantısı kesildi."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Başlamak için Gateway\'i bağlayın"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Güvenlik bildirimi"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Paylaşılan görüntü uyarısını kapat"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s sa. içinde"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway onayı bekleniyor. Gateway ana makinesinde şunu çalıştırın:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Yönetici erişimi gerekli"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Modeli sabitle"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Aramayı Temizle"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Uygun aracılar için etkin."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Активність сеансу"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Автоматизація має недійсну конфігурацію."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Список дозволених"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Про застосунок"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"На цьому зображенні не знайдено QR-коду налаштування. Виберіть QR-код, згенерований за допомогою openclaw qr, або введіть код налаштування вручну."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Переглянути сеанси"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Місцезнаходження: Завжди"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Завантажити з Gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Потрібно"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Готово"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Щоденник очікує на перший запис."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Сторінка наживо"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Слухати слова активації"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway вибрав невідомого постачальника %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Слідувати Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Код налаштування вказує на небезпечний віддалений gateway. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway відключено."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Підключіть Gateway, щоб почати"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Повідомлення про безпеку"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Закрити попередження про спільне зображення"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Через %1$s год"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Очікується схвалення Gateway. Виконайте цю команду на хості Gateway:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Потрібен доступ адміністратора"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Закріпити модель"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Очистити пошук"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Увімкнено для відповідних агентів."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Hoạt động phiên"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tác vụ tự động có cấu hình không hợp lệ."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Danh sách cho phép"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Giới thiệu"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Không tìm thấy mã QR thiết lập trong hình ảnh đó. Hãy chọn mã QR do openclaw qr tạo hoặc nhập mã thiết lập theo cách thủ công."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Duyệt các phiên"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Vị trí: Luôn luôn"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Tải từ Gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Cần"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sẵn sàng"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Nhật ký đang chờ mục đầu tiên."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Trang trực tiếp"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Lắng nghe từ đánh thức"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway đã chọn nhà cung cấp không xác định %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Theo Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Mã thiết lập trỏ đến một gateway từ xa không an toàn. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway đã ngắt kết nối."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Kết nối Gateway để bắt đầu"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Thông báo bảo mật"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Bỏ qua cảnh báo hình ảnh được chia sẻ"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Sau %1$s giờ"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Đang chờ phê duyệt Gateway. Chạy lệnh này trên máy chủ Gateway:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Cần quyền quản trị"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ghim mô hình"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Xóa tìm kiếm"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Đã bật cho các agent đủ điều kiện."</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"会话活动"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"自动化配置无效。"</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"允许列表"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"关于"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"在该图片中未找到设置二维码。请选择由 openclaw qr 生成的二维码,或手动输入设置代码。"</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"浏览会话"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · 位置:始终允许"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"从 Gateway 加载"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"需要"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"就绪"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"日记正在等待第一条记录。"</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"实时页面"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"监听唤醒词"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway 选择了未知提供商 %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"跟随 Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"设置代码指向不安全的远程 Gateway。%1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway 已断开连接。"</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"连接 Gateway 以开始"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"安全提示"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"关闭共享图片警告"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s 小时后"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway 批准尚待处理。请在 Gateway 主机上运行:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"需要管理员访问权限"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"固定模型"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"清除搜索"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"已对符合条件的代理启用。"</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"工作階段活動"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"自動化的設定無效。"</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"允許清單"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"關於"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"在該圖片中找不到設定用 QR code。請選擇 openclaw qr 所產生的 QR,或手動輸入設定代碼。"</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"瀏覽工作階段"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · 位置:永遠允許"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"從 Gateway 載入"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"需要"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"就緒"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"日記正在等待第一筆項目。"</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"即時頁面"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"聆聽喚醒詞"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway 選取了未知的供應商 %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"依循 Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"設定碼指向不安全的遠端 Gateway。%1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway 已中斷連線。"</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"連接 Gateway 以開始"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"安全性通知"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"關閉分享圖片警告"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"%1$s 小時後"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway 核准待處理。請在 Gateway 主機上執行:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"需要管理員存取權"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"釘選模型"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"清除搜尋"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"已對符合資格的代理程式啟用。"</string>
@@ -442,6 +442,7 @@
<string name="native_4e35d595e6975128" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session activity"</string>
<string name="native_4e8c72cab82e0711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Automation has an invalid configuration."</string>
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Allowlist"</string>
<string name="native_4ed379d418bb8629" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"groups"</string>
<string name="native_4efca0d10c5feb8e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"About"</string>
<string name="native_4f1bc100d1dccc1e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"No setup QR code was found in that image. Choose the QR generated by openclaw qr, or enter the setup code manually."</string>
<string name="native_4f993457edb69110" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Browse Sessions"</string>
@@ -542,6 +543,7 @@
<string name="native_5f09a378bea71719" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">" · Location: Always"</string>
<string name="native_5f2c3c8856593678" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Load from gateway"</string>
<string name="native_5f9bbdd124f822a2" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Needs"</string>
<string name="native_5fa1d457ddfcd26c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"rename group"</string>
<string name="native_5fa7aac5375c5815" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Ready"</string>
<string name="native_5fc1927ca948f31c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"The diary is waiting for its first entry."</string>
<string name="native_600c9c27960112a6" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Live page"</string>
@@ -598,6 +600,7 @@
<string name="native_695c03727ff0251f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Listen for wake words"</string>
<string name="native_6965fdd9e5a18c87" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"camera snap"</string>
<string name="native_6974b6348bd6f124" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway selected unknown provider %1$s"</string>
<string name="native_698ef855b198408f" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"delete group"</string>
<string name="native_69909e834a13b2a8" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Follow Android · %1$s"</string>
<string name="native_69e36568cd8b4659" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"channel"</string>
<string name="native_6a58d83c8b7683d3" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"This skill is blocked by the gateway allowlist. Allowlist changes stay on desktop or CLI."</string>
@@ -690,6 +693,7 @@
<string name="native_76ddc3b6e89f8deb" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Setup code points to an insecure remote gateway. %1$s %2$s"</string>
<string name="native_76ed42d22129dc35" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Computer"</string>
<string name="native_76fc9d5d79736bfd" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway disconnected."</string>
<string name="native_7703e4288a7ca2db" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Session Settings"</string>
<string name="native_7718167ea9c39713" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Connect gateway to start"</string>
<string name="native_7728331d12fcfc1a" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Security notice"</string>
<string name="native_7765ad1890a192a8" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Dismiss shared-image warning"</string>
@@ -738,6 +742,7 @@
<string name="native_7fe837d777c45948" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"In %1$sh"</string>
<string name="native_8026a36a8868431c" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway approval is pending. Run this on the gateway host:"</string>
<string name="native_812fc837108361ba" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Admin access required"</string>
<string name="native_8169e4741bbe5fa4" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"set groups"</string>
<string name="native_8193ee8a22d25208" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Pin model"</string>
<string name="native_81c2c4f30a4bec68" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Clear Search"</string>
<string name="native_81d38aca5306940b" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Enabled for eligible agents."</string>
@@ -494,6 +494,45 @@
"model"
]
},
"sessions": {
"emoji": "🗂️",
"title": "Session Settings",
"actions": {
"patch": {
"label": "update",
"detailKeys": [
"sessionKey",
"label",
"pinned",
"archived",
"model",
"thinkingLevel"
]
},
"group_list": {
"label": "groups"
},
"group_set": {
"label": "set groups",
"detailKeys": [
"names"
]
},
"group_rename": {
"label": "rename group",
"detailKeys": [
"name",
"to"
]
},
"group_delete": {
"label": "delete group",
"detailKeys": [
"name"
]
}
}
},
"sessions_list": {
"emoji": "🗂️",
"title": "Sessions",
+3
View File
@@ -161,6 +161,7 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
"src/agents/openai-completions-compat.ts: resolveOpenAICompletionsCompatDefaults",
"src/agents/openai-transport-stream.ts: testing",
"src/agents/run-wait.ts: testing",
"src/agents/session-model-auto-revert.ts: reconcileAgentPatchedSessionModel",
"src/agents/session-suspension.ts: testing",
"src/agents/session-write-lock.ts: resetSessionWriteLockStateForTest",
"src/agents/session-write-lock.ts: testing",
@@ -201,6 +202,7 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
"src/agents/tools/model-config.helpers.ts: hasDirectProviderApiKeyAuthForTool",
"src/agents/tools/sessions-resolution.ts: testing",
"src/agents/tools/sessions-send-tool.a2a.ts: testing",
"src/agents/tools/sessions-spawn-visible-admission.ts: VisibleChildReservation",
"src/agents/tools/video-generate-tool.ts: resolveVideoGenerationModelConfigForTool",
"src/agents/tools/web-fetch.ts: sanitizeWebFetchUrl",
"src/agents/utils/tools-manager.ts: testing",
@@ -345,6 +347,7 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
"src/skills/lifecycle/upload-store.ts: createSkillUploadStore",
"src/skills/runtime/refresh.ts: resetSkillsRefreshForTest",
"src/skills/runtime/remote-skills.ts: resetRemoteNodeSkillsForTests",
"src/status/status-runtime-lines.ts: resolveSessionCostLine",
"src/system-agent/agent-turn.ts: runSystemAgentTurnWithDeps",
"src/tasks/detached-task-runtime.ts: resetDetachedTaskLifecycleRuntimeForTests",
"src/tasks/detached-task-runtime.ts: setDetachedTaskLifecycleRuntime",
@@ -30,6 +30,7 @@ export const CORE_TOOL_FACTORY_DESCRIPTORS = [
{ name: "nodes", family: "openclaw" },
{ name: "pdf", family: "openclaw" },
{ name: "session_status", family: "openclaw" },
{ name: "sessions", family: "openclaw" },
{ name: "sessions_history", family: "openclaw" },
{ name: "sessions_list", family: "openclaw" },
{ name: "sessions_search", family: "openclaw" },
+12 -15
View File
@@ -46,7 +46,7 @@ import type { AnyAgentTool } from "./tools/common.js";
import { createComputerTool } from "./tools/computer-tool.js";
import { createCronTool, type CronCreatorToolAllowlistEntry } from "./tools/cron-tool.js";
import { createEmbeddedCallGateway } from "./tools/embedded-gateway-stub.js";
import { wrapToolWithGatewayCallerIdentity } from "./tools/gateway-caller-context.js";
import { createGatewayToolCallerWrapper } from "./tools/gateway-caller-context.js";
import { createGatewayTool } from "./tools/gateway-tool.js";
import {
createCreateGoalTool,
@@ -66,6 +66,7 @@ import { createSessionsListTool } from "./tools/sessions-list-tool.js";
import { createSessionsSearchTool } from "./tools/sessions-search-tool.js";
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
import { createSessionsTool } from "./tools/sessions-tool.js";
import { createSessionsYieldTool } from "./tools/sessions-yield-tool.js";
import { createConfiguredSkillWorkshopTool } from "./tools/skill-workshop-tool-factory.js";
import { createSubagentsTool } from "./tools/subagents-tool.js";
@@ -477,6 +478,11 @@ export function createOpenClawTools(
? { selfRemoveOnlyJobId: options.cronSelfRemoveOnlyJobId }
: {}),
}),
createSessionsTool({
agentSessionKey: options?.runSessionKey ?? options?.agentSessionKey,
sandboxed: options?.sandboxed,
config: resolvedConfig,
}),
]),
...(!embedded && taskSuggestionSessionKey && options?.taskSuggestionDeliveryMode === "gateway"
? createTaskSuggestionTools({
@@ -578,6 +584,9 @@ export function createOpenClawTools(
agentAccountId: options?.agentAccountId,
agentTo: options?.agentTo,
agentThreadId: options?.agentThreadId,
currentMessagingTarget: options?.currentMessagingTarget,
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
agentGroupId: options?.agentGroupId,
agentGroupChannel: options?.agentGroupChannel,
agentGroupSpace: options?.agentGroupSpace,
@@ -597,6 +606,7 @@ export function createOpenClawTools(
}),
createSubagentsTool({
agentSessionKey: options?.agentSessionKey,
config: resolvedConfig,
}),
createSessionStatusTool({
agentSessionKey: options?.agentSessionKey,
@@ -636,20 +646,7 @@ export function createOpenClawTools(
options?.recordToolPrepStage?.("openclaw-tools:client-capabilities");
const hookAgentId = options?.requesterAgentIdOverride ?? sessionAgentId;
const gatewayCallerIdentity =
hookAgentId && options?.agentSessionKey?.trim()
? {
agentId: hookAgentId,
sessionKey: options.agentSessionKey.trim(),
turnSourceChannel: options.agentChannel,
turnSourceTo:
options.currentMessagingTarget ?? options.currentChannelId ?? options.agentTo,
turnSourceAccountId: options.agentAccountId,
turnSourceThreadId: options.currentThreadTs ?? options.agentThreadId,
}
: undefined;
const wrapGatewayCallerIdentity = (tool: AnyAgentTool) =>
wrapToolWithGatewayCallerIdentity(tool, gatewayCallerIdentity);
const wrapGatewayCallerIdentity = createGatewayToolCallerWrapper(hookAgentId, options);
if (options?.wrapBeforeToolCallHook === false) {
return allTools.map(wrapGatewayCallerIdentity);
+211
View File
@@ -0,0 +1,211 @@
/** One-run rollback for agent-selected session models. */
import {
appendTranscriptMessage,
loadSessionEntry,
patchSessionEntry,
} from "../config/sessions/session-accessor.js";
import {
createAgentPatchedSessionModelFallback,
type AgentPatchedSessionModelFallback,
} from "../config/sessions/session-model-fallback.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { FailoverReason } from "./embedded-agent-helpers/types.js";
import { resolveFailoverReasonFromError } from "./failover-error.js";
import { resolveSessionModelRef } from "./session-model-ref.js";
// Revert only when the chosen model is definitively unusable. Transient
// provider states (rate_limit/overloaded/timeout/server_error) hit working
// models too; reverting on them would undo a valid choice.
const REVERT_REASONS = new Set<FailoverReason>([
"auth",
"auth_permanent",
"billing",
"model_not_found",
]);
type SessionModelRunOutcome =
| { success: true }
| { success: false; error?: unknown; reason?: FailoverReason };
export async function reconcileAgentPatchedSessionModel(params: {
cfg: OpenClawConfig;
agentId?: string;
sessionKey: string;
storePath?: string;
outcome: SessionModelRunOutcome;
expectedMarkerTs?: number;
validatedFallback?: AgentPatchedSessionModelFallback;
now?: number;
}): Promise<"cleared" | "promoted" | "reverted" | "kept" | "none"> {
const reason = params.outcome.success
? undefined
: (params.outcome.reason ?? resolveFailoverReasonFromError(params.outcome.error));
if (!params.outcome.success && (!reason || !REVERT_REASONS.has(reason))) {
return "kept";
}
let note: string | undefined;
let sessionId: string | undefined;
let result: "cleared" | "promoted" | "reverted" | "none" = "none";
await patchSessionEntry(
{
agentId: params.agentId,
sessionKey: params.sessionKey,
storePath: params.storePath,
},
(entry) => {
const marker = entry.modelFallback;
if (marker?.source !== "agent-patch") {
return null;
}
if (params.expectedMarkerTs !== undefined && marker.ts !== params.expectedMarkerTs) {
if (
params.outcome.success &&
params.validatedFallback &&
marker.ts > params.expectedMarkerTs &&
params.expectedMarkerTs > (marker.lastValidatedPatchTs ?? -1)
) {
result = "promoted";
return {
modelFallback: {
...params.validatedFallback,
ts: marker.ts,
lastValidatedPatchTs: params.expectedMarkerTs,
},
};
}
return null;
}
sessionId = entry.sessionId;
if (params.outcome.success) {
result = "cleared";
return { modelFallback: undefined };
}
const failed = resolveSessionModelRef(params.cfg, entry, params.agentId);
result = "reverted";
note = `System note: model ${failed.provider}/${failed.model} failed; reverted to ${marker.prevProvider}/${marker.prevModel}.`;
return {
model: marker.prevModel,
modelProvider: marker.prevProvider,
modelOverride: marker.prevModelOverride,
providerOverride: marker.prevProviderOverride,
modelOverrideSource: marker.prevModelOverrideSource,
modelOverrideFallbackOriginProvider: marker.prevModelOverrideFallbackOriginProvider,
modelOverrideFallbackOriginModel: marker.prevModelOverrideFallbackOriginModel,
authProfileOverride: marker.prevAuthProfileOverride,
authProfileOverrideSource: marker.prevAuthProfileOverrideSource,
authProfileOverrideCompactionCount: marker.prevAuthProfileOverrideCompactionCount,
thinkingLevel: marker.prevThinkingLevel,
modelFallback: undefined,
liveModelSwitchPending: undefined,
};
},
);
if (note && sessionId) {
try {
const timestamp = params.now ?? Date.now();
await appendTranscriptMessage(
{
agentId: params.agentId,
sessionId,
sessionKey: params.sessionKey,
storePath: params.storePath,
},
{
config: params.cfg,
message: {
role: "custom" as const,
customType: "openclaw.system-note",
content: note,
display: true,
timestamp,
},
...(params.now === undefined ? {} : { now: params.now }),
},
);
} catch {
// Rollback is authoritative; transcript note is best effort.
}
}
return result;
}
export function createAgentPatchedSessionModelRunGuard(params: {
cfg: OpenClawConfig;
agentId: string | undefined;
sessionKey: string | undefined;
storePath: string | undefined;
onError?: (error: unknown) => void;
}) {
let markerTs: number | undefined;
let validatedFallback: AgentPatchedSessionModelFallback | undefined;
if (params.sessionKey) {
try {
const entry = loadSessionEntry({
agentId: params.agentId,
sessionKey: params.sessionKey,
storePath: params.storePath,
});
const marker = entry?.modelFallback;
markerTs = marker?.source === "agent-patch" ? marker.ts : undefined;
if (entry && markerTs !== undefined) {
const current = resolveSessionModelRef(params.cfg, entry, params.agentId);
validatedFallback = createAgentPatchedSessionModelFallback({
model: current.model,
provider: current.provider,
entry,
ts: markerTs,
});
}
} catch {
markerTs = undefined;
}
}
let failure: { error?: unknown; reason?: FailoverReason } = {};
let reconciled = false;
const captureFailure = (error: unknown, reason?: string) => {
const classifiedReason = reason
? (reason as FailoverReason)
: resolveFailoverReasonFromError(error);
const revertReason =
classifiedReason && REVERT_REASONS.has(classifiedReason) ? classifiedReason : undefined;
failure = { error, ...(revertReason ? { reason: revertReason } : {}) };
return revertReason !== undefined;
};
const captureFallbackFailure = (
attempts: readonly { error: string; reason?: string }[],
): boolean | undefined => {
const attempt = attempts[0];
return attempt ? captureFailure(new Error(attempt.error), attempt.reason) : undefined;
};
const reconcile = async (success: boolean) => {
if (reconciled || !params.sessionKey || markerTs === undefined) {
return;
}
reconciled = true;
try {
await reconcileAgentPatchedSessionModel({
cfg: params.cfg,
...(params.agentId ? { agentId: params.agentId } : {}),
sessionKey: params.sessionKey,
...(params.storePath ? { storePath: params.storePath } : {}),
expectedMarkerTs: markerTs,
...(validatedFallback ? { validatedFallback } : {}),
outcome: success ? { success: true } : { success: false, ...failure },
});
} catch (error) {
params.onError?.(error);
}
};
return {
captureFailure,
captureFallbackFailure,
async fail(error: unknown, reason?: string) {
captureFailure(error, reason);
await reconcile(false);
},
async finish(success: boolean) {
await reconcile(success);
},
};
}
+49
View File
@@ -59,6 +59,20 @@ describe("getSubagentDepthFromSessionStore", () => {
expect(depth).toBe(3);
});
it("derives visible dashboard depth from parentSessionKey", () => {
const depth = getSubagentDepthFromSessionStore("agent:main:dashboard:child", {
store: {
"agent:main:main": { sessionId: "root" },
"agent:main:dashboard:child": {
sessionId: "child",
parentSessionKey: "agent:main:main",
},
},
});
expect(depth).toBe(1);
});
it("resolves depth when caller is identified by sessionId", () => {
const key1 = "agent:main:subagent:one";
const key2 = "agent:main:subagent:two";
@@ -100,6 +114,41 @@ describe("getSubagentDepthFromSessionStore", () => {
expect(depth).toBe(2);
});
it("resolves a cross-agent parent outside the supplied child store", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-subagent-depth-cross-agent-"));
try {
const storeTemplate = path.join(tmpDir, "sessions-{agentId}.json");
const parentKey = "agent:main:dashboard:parent";
await replaceSessionEntry(
{
agentId: "main",
storePath: storeTemplate.replaceAll("{agentId}", "main"),
sessionKey: parentKey,
},
{
sessionId: "parent",
updatedAt: Date.now(),
spawnDepth: 2,
},
);
const depth = getSubagentDepthFromSessionStore("agent:work:dashboard:child", {
cfg: { session: { store: storeTemplate } },
store: {
"agent:work:dashboard:child": {
sessionId: "child",
parentSessionKey: parentKey,
},
},
});
expect(depth).toBe(3);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it("falls back to session-key segment counting when metadata is missing", () => {
const key = "agent:main:subagent:flat";
const depth = getSubagentDepthFromSessionStore(key, {
+10 -5
View File
@@ -15,6 +15,7 @@ type SessionDepthEntry = {
sessionId?: unknown;
spawnDepth?: unknown;
spawnedBy?: unknown;
parentSessionKey?: unknown;
};
function normalizeSpawnDepth(value: unknown): number | undefined {
@@ -88,7 +89,10 @@ function resolveEntryForSessionKey(params: {
return entry;
}
}
return findEntryBySessionId(params.store, params.sessionKey);
const entry = findEntryBySessionId(params.store, params.sessionKey);
if (entry || !params.cfg) {
return entry;
}
}
if (!params.cfg) {
@@ -153,17 +157,18 @@ export function getSubagentDepthFromSessionStore(
return storedDepth;
}
const spawnedBy = normalizeOptionalString(entry?.spawnedBy);
if (!spawnedBy) {
const parentKey =
normalizeOptionalString(entry?.spawnedBy) ?? normalizeOptionalString(entry?.parentSessionKey);
if (!parentKey) {
return undefined;
}
const parentDepth = depthFromStore(spawnedBy);
const parentDepth = depthFromStore(parentKey);
if (parentDepth !== undefined) {
return parentDepth + 1;
}
return getSubagentDepth(spawnedBy) + 1;
return getSubagentDepth(parentKey) + 1;
};
return depthFromStore(raw) ?? fallbackDepth;
+2
View File
@@ -37,6 +37,7 @@ describe("tool-catalog", () => {
"x_search",
"memory_search",
"memory_get",
"sessions",
"sessions_list",
"sessions_history",
"sessions_search",
@@ -64,6 +65,7 @@ describe("tool-catalog", () => {
it("includes bundle MCP tools in coding and messaging profile policies", () => {
expect(requirePolicyAllow("coding").at(-1)).toBe("bundle-mcp");
expect(requirePolicyAllow("messaging")).toEqual([
"sessions",
"sessions_list",
"sessions_history",
"sessions_search",
+9 -1
View File
@@ -151,6 +151,14 @@ const CORE_TOOL_DEFINITIONS: CoreToolDefinition[] = [
profiles: ["coding"],
includeInOpenClawGroup: true,
},
{
id: "sessions",
label: "sessions",
description: "Session settings and groups",
sectionId: "sessions",
profiles: ["coding", "messaging"],
includeInOpenClawGroup: true,
},
{
id: "sessions_list",
label: "sessions_list",
@@ -202,7 +210,7 @@ const CORE_TOOL_DEFINITIONS: CoreToolDefinition[] = [
{
id: "subagents",
label: "subagents",
description: "Manage sub-agents",
description: "Background work: subagents, media gen, cron runs. list/cancel.",
sectionId: "sessions",
profiles: ["coding"],
includeInOpenClawGroup: true,
+14
View File
@@ -340,6 +340,20 @@ export const TOOL_DISPLAY_CONFIG: ToolDisplayConfig = {
title: "Session Status",
detailKeys: ["sessionKey", "model"],
},
sessions: {
emoji: "🗂️",
title: "Session Settings",
actions: {
patch: {
label: "update",
detailKeys: ["sessionKey", "label", "pinned", "archived", "model", "thinkingLevel"],
},
group_list: { label: "groups" },
group_set: { label: "set groups", detailKeys: ["names"] },
group_rename: { label: "rename group", detailKeys: ["name", "to"] },
group_delete: { label: "delete group", detailKeys: ["name"] },
},
},
sessions_list: {
emoji: "🗂️",
title: "Sessions",
+33
View File
@@ -0,0 +1,33 @@
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
const MUTATING_TOOL_NAMES = new Set([
"write",
"edit",
"apply_patch",
"exec",
"bash",
"process",
"message",
"sessions",
"sessions_spawn",
"sessions_send",
"cron",
"gateway",
"canvas",
"computer",
"nodes",
"session_status",
"create_goal",
"update_goal",
]);
export function isLikelyMutatingToolName(toolName: string): boolean {
const normalized = normalizeLowercaseStringOrEmpty(toolName);
return Boolean(
normalized &&
(MUTATING_TOOL_NAMES.has(normalized) ||
normalized.endsWith("_actions") ||
normalized.startsWith("message_") ||
normalized.includes("send")),
);
}
+8 -6
View File
@@ -130,13 +130,14 @@ describe("tool mutation helpers", () => {
buildToolMutationState("message", { action: "send", to: "forum:1" }).mutatingAction,
).toBe(true);
expect(buildToolMutationState("browser", { action: "list" }).mutatingAction).toBe(false);
expect(
buildToolMutationState("subagents", { action: "kill", target: "worker-1" }).mutatingAction,
).toBe(true);
expect(
buildToolMutationState("subagents", { action: "steer", target: "worker-1" }).mutatingAction,
).toBe(true);
for (const action of ["cancel", "kill", "steer"]) {
expect(
buildToolMutationState("subagents", { action, target: "worker-1" }).mutatingAction,
).toBe(true);
}
expect(buildToolMutationState("subagents", { action: "list" }).mutatingAction).toBe(false);
expect(buildToolMutationState("sessions", { action: "group_list" }).mutatingAction).toBe(false);
expect(buildToolMutationState("sessions", { action: "patch" }).mutatingAction).toBe(true);
expect(
buildToolMutationState("sessions_spawn", { task: "inspect the failure" }).mutatingAction,
).toBe(true);
@@ -477,6 +478,7 @@ describe("tool mutation helpers", () => {
it("keeps legacy name-only mutating heuristics for payload fallback", () => {
expect(isLikelyMutatingToolName("sessions_spawn")).toBe(true);
expect(isLikelyMutatingToolName("sessions_send")).toBe(true);
expect(isLikelyMutatingToolName("sessions")).toBe(true);
expect(isLikelyMutatingToolName("computer")).toBe(true);
expect(isLikelyMutatingToolName("browser_actions")).toBe(true);
expect(isLikelyMutatingToolName("message_slack")).toBe(true);
+7 -33
View File
@@ -8,26 +8,9 @@ import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "@openclaw/normalization-core/string-coerce";
import { isLikelyMutatingToolName } from "./tool-mutation-names.js";
const MUTATING_TOOL_NAMES = new Set([
"write",
"edit",
"apply_patch",
"exec",
"bash",
"process",
"message",
"sessions_spawn",
"sessions_send",
"cron",
"gateway",
"canvas",
"computer",
"nodes",
"session_status",
"create_goal",
"update_goal",
]);
export { isLikelyMutatingToolName };
// File-mutation tools that operate on the same `path` target identity.
// Recovery is allowed across these even when the tool name differs (e.g.
@@ -348,19 +331,6 @@ function appendFingerprintAlias(
return false;
}
export function isLikelyMutatingToolName(toolName: string): boolean {
const normalized = normalizeLowercaseStringOrEmpty(toolName);
if (!normalized) {
return false;
}
return (
MUTATING_TOOL_NAMES.has(normalized) ||
normalized.endsWith("_actions") ||
normalized.startsWith("message_") ||
normalized.includes("send")
);
}
export function isMutatingToolCall(toolName: string, args: unknown): boolean {
const normalized = normalizeLowercaseStringOrEmpty(toolName);
const record = asRecord(args);
@@ -384,10 +354,12 @@ export function isMutatingToolCall(toolName: string, args: unknown): boolean {
// Message actions are an extensible plugin surface. Only known lookup
// actions are replay-safe; missing and future actions fail closed.
return action == null || !MESSAGE_READ_ONLY_ACTIONS.has(action);
case "sessions":
return action !== "group_list";
case "computer":
return action == null || !COMPUTER_REPLAY_SAFE_ACTIONS.has(action);
case "subagents":
return action === "kill" || action === "steer";
return action === "cancel" || action === "kill" || action === "steer";
case "session_status":
return typeof record?.model === "string" && record.model.trim().length > 0;
case "gateway":
@@ -427,6 +399,8 @@ export function isReplaySafeToolCall(toolName: string, args: unknown): boolean {
return action != null && MESSAGE_READ_ONLY_ACTIONS.has(action);
case "subagents":
return action == null || action === "list";
case "sessions":
return action === "group_list";
case "session_status":
return !isMutatingToolCall(normalized, args);
case "browser":
@@ -16,6 +16,17 @@ type GatewayToolCallerIdentity = {
turnSourceThreadId?: string | number;
};
type GatewayToolCallerSource = {
agentSessionKey?: string;
agentChannel?: string;
currentMessagingTarget?: string;
currentChannelId?: string;
agentTo?: string;
agentAccountId?: string;
currentThreadTs?: string;
agentThreadId?: string | number;
};
const gatewayToolCallerStorage = new AsyncLocalStorage<GatewayToolCallerIdentity>();
export function getGatewayToolCallerIdentity(): GatewayToolCallerIdentity | undefined {
@@ -66,3 +77,21 @@ export function wrapToolWithGatewayCallerIdentity(
copyToolTerminalPresentation(tool, wrapped);
return wrapped;
}
export function createGatewayToolCallerWrapper(
agentId: string | undefined,
source: GatewayToolCallerSource | undefined,
): (tool: AnyAgentTool) => AnyAgentTool {
const identity =
agentId && source?.agentSessionKey?.trim()
? {
agentId,
sessionKey: source.agentSessionKey.trim(),
turnSourceChannel: source.agentChannel,
turnSourceTo: source.currentMessagingTarget ?? source.currentChannelId ?? source.agentTo,
turnSourceAccountId: source.agentAccountId,
turnSourceThreadId: source.currentThreadTs ?? source.agentThreadId,
}
: undefined;
return (tool) => wrapToolWithGatewayCallerIdentity(tool, identity);
}
+30
View File
@@ -0,0 +1,30 @@
/** In-process Gateway calls for built-in agent tools. */
import { resolveLeastPrivilegeOperatorScopesForMethod } from "../../gateway/method-scopes.js";
import {
dispatchGatewayMethodInProcess,
hasInProcessGatewayContext,
} from "../../gateway/server-plugins.js";
import { callGatewayTool } from "./gateway.js";
export type InProcessGatewayCaller = <T = Record<string, unknown>>(
method: string,
params: Record<string, unknown>,
) => Promise<T>;
export function hasInProcessGatewayToolContext(): boolean {
return hasInProcessGatewayContext();
}
export const callInProcessGatewayTool: InProcessGatewayCaller = async <T>(
method: string,
params: Record<string, unknown>,
): Promise<T> => {
const scopes = resolveLeastPrivilegeOperatorScopesForMethod(method, params);
if (hasInProcessGatewayContext()) {
return await dispatchGatewayMethodInProcess<T>(method, params, {
forceSyntheticClient: true,
syntheticScopes: scopes,
});
}
return await callGatewayTool<T>(method, {}, params, { scopes });
};
@@ -1,6 +1,9 @@
// sessions_spawn tool tests cover model-visible schema gating, ACP/subagent
// dispatch, and result details for spawned child sessions.
import path from "node:path";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { upsertSessionEntry } from "../../config/sessions/session-accessor.js";
import { withTempDir } from "../../test-helpers/temp-dir.js";
const hoisted = vi.hoisted(() => {
const spawnSubagentDirectMock = vi.fn();
@@ -248,6 +251,454 @@ describe("sessions_spawn tool", () => {
expect(schema.properties?.timeoutSeconds).toBeUndefined();
});
it("advertises visible sessions with terse UI guidance", () => {
const tool = createSessionsSpawnTool();
const schema = tool.parameters as {
properties?: { visible?: { description?: string }; worktree?: unknown };
};
expect(schema.properties?.visible?.description).toBe(
"visible: user sees session in UI. Use when user asked or talks via web/app.",
);
expect(schema.properties?.worktree).toBeDefined();
});
it("creates visible worktree sessions and registers completion announce", async () => {
await withTempDir({ prefix: "openclaw-visible-spawn-" }, async (dir) => {
const callGateway = vi.fn(async () => ({
key: "agent:main:dashboard:child",
runStarted: true,
runId: "run-visible",
}));
const registerRun = vi.fn();
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
agentChannel: "slack",
agentTo: "channel:C-stale",
agentThreadId: "stale-thread",
currentMessagingTarget: "channel:C-current",
currentChannelId: "C-native",
currentThreadTs: "current-thread",
config: {
session: { store: path.join(dir, "sessions.json") },
agents: {
defaults: {
subagents: { model: "openai/gpt-5.4", runTimeoutSeconds: 120 },
},
list: [{ id: "main" }],
},
},
callGateway: callGateway as never,
registerRun,
countActiveRuns: () => 0,
});
const result = await tool.execute("visible", {
task: "inspect issue",
label: "Issue review",
visible: true,
worktree: true,
worktreeName: "issue-review",
worktreeBaseRef: "main",
cleanup: "delete",
});
expect(result.details).toMatchObject({
status: "accepted",
childSessionKey: "agent:main:dashboard:child",
runId: "run-visible",
cleanup: "keep",
});
expect(callGateway).toHaveBeenCalledWith("sessions.create", {
agentId: "main",
label: "Issue review",
model: "openai/gpt-5.4",
task: "inspect issue",
parentSessionKey: "agent:main:main",
worktree: true,
worktreeName: "issue-review",
worktreeBaseRef: "main",
});
expect(registerRun).toHaveBeenCalledWith(
expect.objectContaining({
runId: "run-visible",
childSessionKey: "agent:main:dashboard:child",
requesterSessionKey: "agent:main:main",
requesterOrigin: {
channel: "slack",
to: "channel:C-current",
threadId: "current-thread",
},
cleanup: "keep",
runTimeoutSeconds: 120,
expectsCompletionMessage: true,
spawnMode: "run",
}),
);
expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled();
});
});
it("requires visible sessions for worktree options", async () => {
const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:main" });
await expect(
tool.execute("hidden-worktree", { task: "inspect", worktree: true }),
).rejects.toThrow("worktree options require visible=true");
expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled();
});
it("uses the target agent model for cross-agent visible sessions", async () => {
const callGateway = vi.fn(async () => ({
key: "agent:reviewer:dashboard:child",
runStarted: true,
runId: "run-reviewer",
}));
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
config: {
agents: {
defaults: { subagents: { allowAgents: ["reviewer"] } },
list: [
{ id: "main" },
{ id: "reviewer", subagents: { model: "anthropic/claude-sonnet-4-6" } },
],
},
},
callGateway: callGateway as never,
registerRun: vi.fn(),
countActiveRuns: () => 0,
});
await tool.execute("visible-reviewer", {
task: "review patch",
agentId: "reviewer",
visible: true,
});
expect(callGateway).toHaveBeenCalledWith(
"sessions.create",
expect.objectContaining({
agentId: "reviewer",
model: "anthropic/claude-sonnet-4-6",
parentSessionKey: "agent:main:main",
}),
);
});
it("denies visible sessions when tool restrictions cannot carry forward", async () => {
const callGateway = vi.fn();
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
config: { agents: { list: [{ id: "main" }] } },
inheritedToolDenylist: ["exec"],
callGateway,
});
const result = await tool.execute("visible-restricted", {
task: "inspect",
visible: true,
});
expect(result.details).toMatchObject({
status: "forbidden",
error: "Visible sessions unavailable with inherited tool restrictions.",
});
expect(callGateway).not.toHaveBeenCalled();
});
it("blocks unsandboxed visible targets for a sandboxed caller runtime", async () => {
const callGateway = vi.fn();
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
sandboxed: true,
config: { agents: { list: [{ id: "main" }] } },
callGateway,
countActiveRuns: () => 0,
});
const result = await tool.execute("visible-sandboxed", { task: "inspect", visible: true });
expect(result.details).toMatchObject({
status: "forbidden",
error: "Sandboxed sessions cannot spawn unsandboxed sessions.",
});
expect(callGateway).not.toHaveBeenCalled();
});
it("reserves visible child capacity before session creation", async () => {
let resolveCreate!: (value: { key: string; runStarted: true; runId: string }) => void;
const pendingCreate = new Promise<{
key: string;
runStarted: true;
runId: string;
}>((resolve) => {
resolveCreate = resolve;
});
const callGateway = vi.fn(async () => await pendingCreate);
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
config: {
agents: {
defaults: { subagents: { maxChildrenPerAgent: 1 } },
list: [{ id: "main" }],
},
},
callGateway: callGateway as never,
registerRun: vi.fn(),
countActiveRuns: () => 0,
});
const first = tool.execute("visible-first", { task: "first", visible: true });
await vi.waitFor(() => expect(callGateway).toHaveBeenCalledTimes(1));
const second = await tool.execute("visible-second", { task: "second", visible: true });
expect(second.details).toMatchObject({
status: "forbidden",
error: expect.stringContaining("max active children"),
});
expect(callGateway).toHaveBeenCalledTimes(1);
resolveCreate({
key: "agent:main:dashboard:first",
runStarted: true,
runId: "run-first",
});
await expect(first).resolves.toEqual(
expect.objectContaining({ details: expect.objectContaining({ status: "accepted" }) }),
);
});
it("deletes a visible session whose initial run did not start", async () => {
const callGateway = vi
.fn()
.mockResolvedValueOnce({
key: "agent:main:dashboard:not-started",
runStarted: false,
runError: "model unavailable",
})
.mockResolvedValueOnce({ deleted: true });
const registerRun = vi.fn();
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
config: { agents: { list: [{ id: "main" }] } },
callGateway,
registerRun,
countActiveRuns: () => 0,
});
const result = await tool.execute("visible-not-started", {
task: "inspect",
visible: true,
});
expect(result.details).toMatchObject({
status: "error",
error: "model unavailable",
childSessionKey: "agent:main:dashboard:not-started",
});
expect(callGateway).toHaveBeenNthCalledWith(2, "sessions.delete", {
key: "agent:main:dashboard:not-started",
deleteTranscript: true,
emitLifecycleHooks: false,
});
expect(registerRun).not.toHaveBeenCalled();
});
it("aborts a partially started visible run before deleting its session", async () => {
const callGateway = vi
.fn()
.mockResolvedValueOnce({
key: "agent:main:dashboard:partial",
runStarted: true,
runError: "missing run id",
})
.mockResolvedValueOnce({ ok: true, abortedRunId: "run-partial" })
.mockResolvedValueOnce({ deleted: true });
const registerRun = vi.fn();
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
config: { agents: { list: [{ id: "main" }] } },
callGateway,
registerRun,
countActiveRuns: () => 0,
});
const result = await tool.execute("visible-partial", { task: "inspect", visible: true });
// A started-but-untrackable run (no run id) is always aborted and deleted,
// never left as a visible orphan the parent cannot cancel.
expect(result.details).toMatchObject({ status: "error" });
expect((result.details as { childSessionKey?: string }).childSessionKey).toBeUndefined();
expect(callGateway).toHaveBeenNthCalledWith(2, "sessions.abort", {
key: "agent:main:dashboard:partial",
agentId: "main",
});
expect(callGateway).toHaveBeenNthCalledWith(3, "sessions.delete", {
key: "agent:main:dashboard:partial",
deleteTranscript: true,
emitLifecycleHooks: false,
});
expect(registerRun).not.toHaveBeenCalled();
});
it("deletes a started run with no run id even when abort reports nothing stopped", async () => {
const callGateway = vi
.fn()
.mockResolvedValueOnce({
key: "agent:main:dashboard:untracked",
runStarted: true,
runError: "missing run id",
})
.mockResolvedValueOnce({ ok: true, abortedRunId: null })
.mockResolvedValueOnce({ deleted: true });
const registerRun = vi.fn();
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
config: { agents: { list: [{ id: "main" }] } },
callGateway,
registerRun,
countActiveRuns: () => 0,
});
const result = await tool.execute("visible-untracked", { task: "inspect", visible: true });
expect(result.details).toMatchObject({ status: "error" });
expect(callGateway).toHaveBeenNthCalledWith(3, "sessions.delete", {
key: "agent:main:dashboard:untracked",
deleteTranscript: true,
emitLifecycleHooks: false,
});
expect(registerRun).not.toHaveBeenCalled();
});
it("rolls back a visible session when announce registration fails", async () => {
const callGateway = vi
.fn()
.mockResolvedValueOnce({
key: "agent:main:dashboard:orphan",
runStarted: true,
runId: "run-orphan",
})
.mockResolvedValueOnce({ ok: true, abortedRunId: "run-orphan" })
.mockResolvedValueOnce({ deleted: true });
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
config: { agents: { list: [{ id: "main" }] } },
callGateway,
registerRun: () => {
throw new Error("registry unavailable");
},
countActiveRuns: () => 0,
});
const result = await tool.execute("visible-orphan", { task: "inspect", visible: true });
expect(result.details).toMatchObject({ status: "error", runId: "run-orphan" });
expect(callGateway).toHaveBeenNthCalledWith(2, "sessions.abort", {
key: "agent:main:dashboard:orphan",
runId: "run-orphan",
agentId: "main",
});
expect(callGateway).toHaveBeenNthCalledWith(3, "sessions.delete", {
key: "agent:main:dashboard:orphan",
deleteTranscript: true,
emitLifecycleHooks: false,
});
});
it("keeps a visible session when rollback cannot abort its run", async () => {
const callGateway = vi
.fn()
.mockResolvedValueOnce({
key: "agent:main:dashboard:live",
runStarted: true,
runId: "run-live",
})
.mockRejectedValueOnce(new Error("abort unavailable"));
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
config: { agents: { list: [{ id: "main" }] } },
callGateway,
registerRun: () => {
throw new Error("registry unavailable");
},
countActiveRuns: () => 0,
});
const result = await tool.execute("visible-live", { task: "inspect", visible: true });
expect(result.details).toMatchObject({
status: "error",
childSessionKey: "agent:main:dashboard:live",
runId: "run-live",
});
expect(callGateway).toHaveBeenCalledTimes(2);
});
it("keeps a visible session when rollback does not confirm its run", async () => {
const callGateway = vi
.fn()
.mockResolvedValueOnce({
key: "agent:main:dashboard:finished",
runStarted: true,
runId: "run-finished",
})
.mockResolvedValueOnce({ ok: true, abortedRunId: null, status: "no-active-run" });
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
config: { agents: { list: [{ id: "main" }] } },
callGateway,
registerRun: () => {
throw new Error("registry unavailable");
},
countActiveRuns: () => 0,
});
const result = await tool.execute("visible-finished", { task: "inspect", visible: true });
expect(result.details).toMatchObject({
status: "error",
error: expect.stringContaining("Run abort unconfirmed. Session kept."),
childSessionKey: "agent:main:dashboard:finished",
runId: "run-finished",
});
expect(callGateway).toHaveBeenCalledTimes(2);
});
it("applies spawn depth limits to visible dashboard descendants", async () => {
await withTempDir({ prefix: "openclaw-visible-depth-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const childKey = "agent:main:dashboard:child";
await upsertSessionEntry(
{ agentId: "main", sessionKey: "agent:main:main", storePath },
{ sessionId: "root", updatedAt: 1 },
);
await upsertSessionEntry(
{ agentId: "main", sessionKey: childKey, storePath },
{ sessionId: "child", updatedAt: 1, parentSessionKey: "agent:main:main" },
);
const callGateway = vi.fn();
const tool = createSessionsSpawnTool({
agentSessionKey: childKey,
config: {
session: { store: storePath },
agents: {
list: [{ id: "main" }],
defaults: { subagents: { maxSpawnDepth: 1 } },
},
},
callGateway,
countActiveRuns: () => 0,
});
const result = await tool.execute("visible-depth", { task: "inspect", visible: true });
expect(result.details).toMatchObject({ status: "forbidden" });
expect(callGateway).not.toHaveBeenCalled();
});
});
it("hides thread-bound spawn fields when current channel disables spawnSessions", () => {
const tool = createSessionsSpawnTool({
agentChannel: "discord",
+30 -43
View File
@@ -11,7 +11,6 @@ import {
} from "../../channels/thread-bindings-policy.js";
import { getRuntimeConfig } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { callGateway } from "../../gateway/call.js";
import { resolveSnakeCaseParamKey } from "../../param-key.js";
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
import { normalizeDeliveryContext } from "../../utils/delivery-context.shared.js";
@@ -45,6 +44,14 @@ import {
readStringParam,
ToolInputError,
} from "./common.js";
import {
cleanupUntrackedAcpSession,
maybeSpawnVisibleSession,
resolveTrackedSpawnMode,
summarizeSessionsSpawnError,
type VisibleSessionsSpawnDeps,
VISIBLE_SESSIONS_SPAWN_SCHEMA,
} from "./sessions-spawn-visible.js";
const SESSIONS_SPAWN_RUNTIMES = ["subagent", "acp"] as const;
const SESSIONS_SPAWN_SANDBOX_MODES = ["inherit", "require"] as const;
@@ -75,16 +82,6 @@ async function loadAcpSpawnModule(): Promise<AcpSpawnModule> {
return await acpSpawnModuleLoader.load();
}
function summarizeError(err: unknown): string {
if (err instanceof Error) {
return err.message;
}
if (typeof err === "string") {
return err;
}
return "error";
}
function addRoleToFailureResult<T extends { status: string }>(
result: T,
role: string | undefined,
@@ -95,36 +92,6 @@ function addRoleToFailureResult<T extends { status: string }>(
return { ...result, role };
}
function resolveTrackedSpawnMode(params: {
requestedMode?: "run" | "session";
threadRequested: boolean;
}): "run" | "session" {
if (params.requestedMode === "run" || params.requestedMode === "session") {
return params.requestedMode;
}
return params.threadRequested ? "session" : "run";
}
async function cleanupUntrackedAcpSession(sessionKey: string): Promise<void> {
const key = sessionKey.trim();
if (!key) {
return;
}
try {
await callGateway({
method: "sessions.delete",
params: {
key,
deleteTranscript: true,
emitLifecycleHooks: false,
},
timeoutMs: 10_000,
});
} catch {
// Best-effort cleanup only.
}
}
type SessionsSpawnThreadAvailability = {
subagent: boolean;
acp: boolean;
@@ -200,6 +167,7 @@ function createSessionsSpawnToolSchema(params: {
description: "Light bootstrap; subagent only.",
}),
),
...VISIBLE_SESSIONS_SPAWN_SCHEMA,
// Inline attachments (snapshot-by-value).
attachments: Type.Optional(
@@ -255,11 +223,15 @@ export function createSessionsSpawnTool(
agentAccountId?: string;
agentTo?: string;
agentThreadId?: string | number;
currentMessagingTarget?: string;
currentChannelId?: string;
currentThreadTs?: string;
sandboxed?: boolean;
config?: OpenClawConfig;
/** Explicit agent ID override for cron/hook sessions where session key parsing may not work. */
requesterAgentIdOverride?: string;
} & SpawnedToolContext,
} & VisibleSessionsSpawnDeps &
SpawnedToolContext,
): AnyAgentTool {
const acpAvailable = isAcpRuntimeSpawnAvailable({
config: opts?.config,
@@ -321,6 +293,21 @@ export function createSessionsSpawnTool(
const streamTo = runtime === "acp" && params.streamTo === "parent" ? "parent" : undefined;
const lightContext = params.lightContext === true;
const roleContext = requestedAgentId ? { role: requestedAgentId } : {};
const visibleResult = await maybeSpawnVisibleSession({
raw: params,
task,
taskName,
label,
runtime,
requestedAgentId,
sandbox,
options: opts,
});
if (visibleResult) {
return jsonResult(
addRoleToFailureResult(visibleResult as { status: string }, requestedAgentId),
);
}
if (runtime === "acp" && !acpAvailable) {
return jsonResult({
status: "error",
@@ -457,7 +444,7 @@ export function createSessionsSpawnTool(
await cleanupUntrackedAcpSession(childSessionKey);
return jsonResult({
status: "error",
error: `Failed to register ACP run: ${summarizeError(err)}. Cleanup was attempted, but the already-started ACP run may still finish in the background.`,
error: `Failed to register ACP run: ${summarizeSessionsSpawnError(err)}. Cleanup was attempted, but the already-started ACP run may still finish in the background.`,
childSessionKey,
runId: childRunId,
...roleContext,
@@ -0,0 +1,35 @@
/** Process-local admission for visible child starts awaiting registry insertion. */
const pendingVisibleChildren = new Map<string, number>();
export type VisibleChildReservation =
| { ok: false; activeChildren: number }
| { ok: true; release: () => void };
export function reserveVisibleChildSlot(params: {
controllerSessionKey: string;
maxChildren: number;
countActiveRuns: (sessionKey: string) => number;
}): VisibleChildReservation {
const pending = pendingVisibleChildren.get(params.controllerSessionKey) ?? 0;
const activeChildren = params.countActiveRuns(params.controllerSessionKey) + pending;
if (activeChildren >= params.maxChildren) {
return { ok: false, activeChildren };
}
pendingVisibleChildren.set(params.controllerSessionKey, pending + 1);
let released = false;
return {
ok: true,
release: () => {
if (released) {
return;
}
released = true;
const next = (pendingVisibleChildren.get(params.controllerSessionKey) ?? 1) - 1;
if (next > 0) {
pendingVisibleChildren.set(params.controllerSessionKey, next);
} else {
pendingVisibleChildren.delete(params.controllerSessionKey);
}
},
};
}
+343
View File
@@ -0,0 +1,343 @@
import { Type } from "typebox";
import {
DEFAULT_SUBAGENT_MAX_CHILDREN_PER_AGENT,
DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH,
} from "../../config/agent-limits.js";
import { getRuntimeConfig } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { callGateway } from "../../gateway/call.js";
import {
isValidAgentId,
normalizeAgentId,
parseAgentSessionKey,
} from "../../routing/session-key.js";
import { normalizeDeliveryContext } from "../../utils/delivery-context.shared.js";
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
import { listAgentIds, resolveAgentConfig } from "../agent-scope.js";
import { resolveSubagentSpawnModelSelection } from "../model-selection.js";
import { resolveSandboxRuntimeStatus } from "../sandbox/runtime-status.js";
import { getSubagentDepthFromSessionStore } from "../subagent-depth.js";
import { countActiveRunsForSession, registerSubagentRun } from "../subagent-registry.js";
import { resolveSubagentSpawnOwnership } from "../subagent-spawn-ownership.js";
import { resolveConfiguredSubagentRunTimeoutSeconds } from "../subagent-spawn-plan.js";
import { resolveSubagentTargetPolicy } from "../subagent-target-policy.js";
import { normalizeToolModelOverride, readStringParam, ToolInputError } from "./common.js";
import { callInProcessGatewayTool, type InProcessGatewayCaller } from "./in-process-gateway.js";
import { reserveVisibleChildSlot } from "./sessions-spawn-visible-admission.js";
export const VISIBLE_SESSIONS_SPAWN_SCHEMA = {
visible: Type.Optional(
Type.Boolean({
description: "visible: user sees session in UI. Use when user asked or talks via web/app.",
}),
),
worktree: Type.Optional(Type.Boolean({ description: "Visible session worktree" })),
worktreeName: Type.Optional(Type.String({ description: "Worktree name" })),
worktreeBaseRef: Type.Optional(Type.String({ description: "Worktree base ref" })),
};
export type VisibleSessionsSpawnDeps = {
callGateway?: InProcessGatewayCaller;
registerRun?: typeof registerSubagentRun;
countActiveRuns?: typeof countActiveRunsForSession;
};
type VisibleSessionsSpawnOptions = VisibleSessionsSpawnDeps & {
agentSessionKey?: string;
completionOwnerKey?: string;
agentChannel?: GatewayMessageChannel;
agentAccountId?: string;
agentTo?: string;
agentThreadId?: string | number;
currentMessagingTarget?: string;
currentChannelId?: string;
currentThreadTs?: string;
sandboxed?: boolean;
config?: OpenClawConfig;
requesterAgentIdOverride?: string;
inheritedToolAllowlist?: string[];
inheritedToolDenylist?: string[];
};
export function summarizeSessionsSpawnError(error: unknown): string {
return error instanceof Error ? error.message : typeof error === "string" ? error : "error";
}
export function resolveTrackedSpawnMode(params: {
requestedMode?: "run" | "session";
threadRequested: boolean;
}): "run" | "session" {
return params.requestedMode ?? (params.threadRequested ? "session" : "run");
}
export async function cleanupUntrackedAcpSession(sessionKey: string): Promise<void> {
const key = sessionKey.trim();
if (!key) {
return;
}
try {
await callGateway({
method: "sessions.delete",
params: { key, deleteTranscript: true, emitLifecycleHooks: false },
timeoutMs: 10_000,
});
} catch {
// Best-effort cleanup only.
}
}
async function deleteVisibleSession(
gatewayCall: InProcessGatewayCaller,
childSessionKey: string,
): Promise<void> {
try {
await gatewayCall("sessions.delete", {
key: childSessionKey,
deleteTranscript: true,
emitLifecycleHooks: false,
});
} catch {
// Best-effort rollback only.
}
}
export async function maybeSpawnVisibleSession(params: {
raw: Record<string, unknown>;
task: string;
taskName?: string;
label: string;
runtime: "subagent" | "acp";
requestedAgentId?: string;
sandbox: "inherit" | "require";
options?: VisibleSessionsSpawnOptions;
}): Promise<Record<string, unknown> | undefined> {
const worktree = params.raw.worktree === true;
const worktreeName = readStringParam(params.raw, "worktreeName");
const worktreeBaseRef = readStringParam(params.raw, "worktreeBaseRef");
if (params.raw.visible !== true) {
if (worktree || worktreeName || worktreeBaseRef) {
throw new ToolInputError("worktree options require visible=true");
}
return undefined;
}
if (params.runtime !== "subagent") {
throw new ToolInputError('visible=true supports runtime="subagent" only');
}
const unsupported = [
["model", normalizeToolModelOverride(readStringParam(params.raw, "model"))],
["thinking", readStringParam(params.raw, "thinking")],
["cwd", readStringParam(params.raw, "cwd")],
["thread", params.raw.thread === true ? true : undefined],
["mode", params.raw.mode],
["context", params.raw.context],
["lightContext", params.raw.lightContext === true ? true : undefined],
["attachments", Array.isArray(params.raw.attachments) ? params.raw.attachments : undefined],
["attachAs", params.raw.attachAs],
] as const;
const unsupportedEntry = unsupported.find(([, value]) => value !== undefined);
if (unsupportedEntry) {
throw new ToolInputError(`${unsupportedEntry[0]} unavailable with visible=true`);
}
const cfg = params.options?.config ?? getRuntimeConfig();
if (
(params.options?.inheritedToolAllowlist?.length ?? 0) > 0 ||
(params.options?.inheritedToolDenylist?.length ?? 0) > 0
) {
return {
status: "forbidden",
error: "Visible sessions unavailable with inherited tool restrictions.",
};
}
const ownership = resolveSubagentSpawnOwnership({
cfg,
agentSessionKey: params.options?.agentSessionKey,
completionOwnerKey: params.options?.completionOwnerKey,
});
const requesterKey = ownership.controllerSessionKey;
const callerDepth = getSubagentDepthFromSessionStore(requesterKey, { cfg });
const maxDepth =
cfg.agents?.defaults?.subagents?.maxSpawnDepth ?? DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH;
if (callerDepth >= maxDepth) {
return {
status: "forbidden",
error: `sessions_spawn is not allowed at this depth (current depth: ${callerDepth}, max: ${maxDepth})`,
};
}
const maxChildren =
cfg.agents?.defaults?.subagents?.maxChildrenPerAgent ?? DEFAULT_SUBAGENT_MAX_CHILDREN_PER_AGENT;
if (params.requestedAgentId && !isValidAgentId(params.requestedAgentId)) {
return {
status: "error",
error: `Invalid agentId "${params.requestedAgentId}". Use agents_list.`,
};
}
const requesterAgentId = normalizeAgentId(
params.options?.requesterAgentIdOverride ?? parseAgentSessionKey(requesterKey)?.agentId,
);
const requireAgentId =
resolveAgentConfig(cfg, requesterAgentId)?.subagents?.requireAgentId ??
cfg.agents?.defaults?.subagents?.requireAgentId ??
false;
if (requireAgentId && !params.requestedAgentId) {
return { status: "forbidden", error: "sessions_spawn requires agentId. Use agents_list." };
}
const targetAgentId = params.requestedAgentId
? normalizeAgentId(params.requestedAgentId)
: requesterAgentId;
const targetPolicy = resolveSubagentTargetPolicy({
requesterAgentId,
targetAgentId,
requestedAgentId: params.requestedAgentId,
allowAgents:
resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ??
cfg.agents?.defaults?.subagents?.allowAgents,
configuredAgentIds: listAgentIds(cfg),
});
if (!targetPolicy.ok) {
return { status: "forbidden", error: targetPolicy.error };
}
const resolvedModel = resolveSubagentSpawnModelSelection({
cfg,
agentId: targetAgentId,
});
const runTimeoutSeconds = resolveConfiguredSubagentRunTimeoutSeconds({ cfg });
const requesterRuntime = resolveSandboxRuntimeStatus({ cfg, sessionKey: requesterKey });
const childRuntime = resolveSandboxRuntimeStatus({
cfg,
sessionKey: `agent:${targetAgentId}:dashboard:pending`,
});
const requesterSandboxed = params.options?.sandboxed === true || requesterRuntime.sandboxed;
if (!childRuntime.sandboxed && (requesterSandboxed || params.sandbox === "require")) {
return {
status: "forbidden",
error: requesterSandboxed
? "Sandboxed sessions cannot spawn unsandboxed sessions."
: 'sessions_spawn sandbox="require" needs sandboxed target.',
};
}
const reservation = reserveVisibleChildSlot({
controllerSessionKey: requesterKey,
maxChildren,
countActiveRuns: params.options?.countActiveRuns ?? countActiveRunsForSession,
});
if (!reservation.ok) {
return {
status: "forbidden",
error: `sessions_spawn has reached max active children for this session (${reservation.activeChildren}/${maxChildren})`,
};
}
try {
const gatewayCall = params.options?.callGateway ?? callInProcessGatewayTool;
const response = await gatewayCall<{
key?: string;
runStarted?: boolean;
runId?: string;
runError?: unknown;
}>("sessions.create", {
agentId: targetAgentId,
...(params.label ? { label: params.label } : {}),
model: resolvedModel,
task: params.task,
parentSessionKey: requesterKey,
...(worktree ? { worktree: true } : {}),
...(worktreeName ? { worktreeName } : {}),
...(worktreeBaseRef ? { worktreeBaseRef } : {}),
});
const childSessionKey = response.key?.trim();
const runId = response.runId?.trim();
const runError = response.runError
? summarizeSessionsSpawnError(response.runError)
: "Visible session run failed";
if (!childSessionKey) {
return {
status: "error",
error: runError,
};
}
if (response.runStarted !== true) {
await deleteVisibleSession(gatewayCall, childSessionKey);
return { status: "error", error: runError, childSessionKey };
}
if (!runId) {
// A started run with no run id is untrackable: it cannot be registered,
// announced, or cancelled, so never leave it as a visible orphan. Abort
// by key to stop whatever is running, then delete the session.
try {
await gatewayCall("sessions.abort", { key: childSessionKey, agentId: targetAgentId });
} catch {
// Best-effort stop before cleanup.
}
await deleteVisibleSession(gatewayCall, childSessionKey);
return { status: "error", error: runError };
}
try {
(params.options?.registerRun ?? registerSubagentRun)({
runId,
childSessionKey,
controllerSessionKey: ownership.controllerSessionKey,
requesterSessionKey: ownership.completionRequesterSessionKey,
requesterOrigin: normalizeDeliveryContext({
channel: params.options?.agentChannel,
accountId: params.options?.agentAccountId,
to:
params.options?.currentMessagingTarget ??
params.options?.currentChannelId ??
params.options?.agentTo,
threadId: params.options?.currentThreadTs ?? params.options?.agentThreadId,
}),
requesterDisplayKey: ownership.completionRequesterDisplayKey,
task: params.task,
taskName: params.taskName,
agentId: targetAgentId,
requesterAgentId: params.options?.requesterAgentIdOverride,
cleanup: "keep",
label: params.label || undefined,
runTimeoutSeconds,
expectsCompletionMessage: params.raw.expectsCompletionMessage !== false,
spawnMode: "run",
});
} catch (error) {
let abortResponse: { abortedRunId?: string | null };
try {
abortResponse = await gatewayCall<{ abortedRunId?: string | null }>("sessions.abort", {
key: childSessionKey,
runId,
agentId: targetAgentId,
});
} catch (abortError) {
return {
status: "error",
error: `Visible run registration failed: ${summarizeSessionsSpawnError(error)}. Run abort failed: ${summarizeSessionsSpawnError(abortError)}. Session kept.`,
childSessionKey,
runId,
};
}
if (abortResponse.abortedRunId !== runId) {
return {
status: "error",
error: `Visible run registration failed: ${summarizeSessionsSpawnError(error)}. Run abort unconfirmed. Session kept.`,
childSessionKey,
runId,
};
}
await deleteVisibleSession(gatewayCall, childSessionKey);
return {
status: "error",
error: `Visible run registration failed: ${summarizeSessionsSpawnError(error)}. Run aborted; cleanup attempted.`,
childSessionKey,
runId,
};
}
return {
status: "accepted",
childSessionKey,
runId,
mode: "run",
cleanup: "keep",
};
} finally {
reservation.release();
}
}
+402
View File
@@ -0,0 +1,402 @@
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
loadSessionEntry,
loadTranscriptEvents,
patchSessionEntry,
upsertSessionEntry,
} from "../../config/sessions/session-accessor.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { isAgentSessionModelPatchOrigin } from "../../gateway/session-model-patch-origin.js";
import { GATEWAY_OWNER_ONLY_CORE_TOOLS } from "../../security/dangerous-tools.js";
import { withTempDir } from "../../test-helpers/temp-dir.js";
import { createAgentPatchedSessionModelRunGuard } from "../session-model-auto-revert.js";
import { testing as sessionsResolutionTesting } from "./sessions-resolution.js";
import { createSessionsTool } from "./sessions-tool.js";
describe("sessions tool", () => {
afterEach(() => {
sessionsResolutionTesting.setDepsForTest();
});
it("uses the core owner gate", () => {
expect(GATEWAY_OWNER_ONLY_CORE_TOOLS).toContain("sessions");
});
it("patches its session, then reverts a failed agent-selected model", async () => {
await withTempDir({ prefix: "openclaw-sessions-tool-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const sessionKey = "agent:main:main";
const cfg: OpenClawConfig = {
session: { store: storePath },
agents: { defaults: { model: { primary: "openai/good" } } },
};
await upsertSessionEntry(
{ agentId: "main", sessionKey, storePath },
{
sessionId: "session-main",
updatedAt: 1,
model: "good",
modelProvider: "openai",
modelOverride: "good",
providerOverride: "openai",
modelOverrideSource: "auto",
modelOverrideFallbackOriginProvider: "openai",
modelOverrideFallbackOriginModel: "primary",
authProfileOverride: "good-profile",
authProfileOverrideSource: "user",
thinkingLevel: "high",
},
);
const callGateway = vi.fn(async (method: string, params: Record<string, unknown>) => {
expect(method).toBe("sessions.patch");
expect(isAgentSessionModelPatchOrigin()).toBe(true);
await patchSessionEntry({ agentId: "main", sessionKey, storePath }, () => ({
label: params.label as string,
model: "bad",
modelProvider: "broken",
modelOverride: "bad",
providerOverride: "broken",
modelOverrideSource: "user",
modelOverrideFallbackOriginProvider: undefined,
modelOverrideFallbackOriginModel: undefined,
authProfileOverride: "bad-profile",
authProfileOverrideSource: "user",
thinkingLevel: "low",
modelFallback: {
prevModel: "good",
prevProvider: "openai",
prevModelOverride: "good",
prevProviderOverride: "openai",
prevModelOverrideSource: "auto",
prevModelOverrideFallbackOriginProvider: "openai",
prevModelOverrideFallbackOriginModel: "primary",
prevAuthProfileOverride: "good-profile",
prevAuthProfileOverrideSource: "user",
prevThinkingLevel: "high",
ts: Date.now(),
source: "agent-patch",
},
}));
return { ok: true };
});
const tool = createSessionsTool({
agentSessionKey: sessionKey,
config: cfg,
callGateway: callGateway as never,
});
const currentRunGuard = createAgentPatchedSessionModelRunGuard({
cfg,
agentId: "main",
sessionKey,
storePath,
});
await tool.execute("patch-model", {
action: "patch",
label: "Research",
model: "broken/bad",
});
expect(callGateway).toHaveBeenCalledWith("sessions.patch", {
key: sessionKey,
label: "Research",
model: "broken/bad",
});
expect(loadSessionEntry({ agentId: "main", sessionKey, storePath })).toMatchObject({
label: "Research",
modelFallback: {
prevModel: "good",
prevProvider: "openai",
prevModelOverrideSource: "auto",
prevModelOverrideFallbackOriginProvider: "openai",
prevModelOverrideFallbackOriginModel: "primary",
prevAuthProfileOverride: "good-profile",
prevThinkingLevel: "high",
source: "agent-patch",
},
});
await currentRunGuard.finish(true);
expect(loadSessionEntry({ agentId: "main", sessionKey, storePath })).toHaveProperty(
"modelFallback",
);
const runGuard = createAgentPatchedSessionModelRunGuard({
cfg,
agentId: "main",
sessionKey,
storePath,
});
await runGuard.fail({ status: 404, message: "No endpoints found for broken/bad." });
expect(loadSessionEntry({ agentId: "main", sessionKey, storePath })).toMatchObject({
model: "good",
modelProvider: "openai",
modelOverrideSource: "auto",
modelOverrideFallbackOriginProvider: "openai",
modelOverrideFallbackOriginModel: "primary",
authProfileOverride: "good-profile",
thinkingLevel: "high",
});
expect(loadSessionEntry({ agentId: "main", sessionKey, storePath })).not.toHaveProperty(
"modelFallback",
);
const events = await loadTranscriptEvents({
agentId: "main",
sessionId: "session-main",
sessionKey,
storePath,
});
expect(events).toContainEqual(
expect.objectContaining({
message: expect.objectContaining({
customType: "openclaw.system-note",
content: "System note: model broken/bad failed; reverted to openai/good.",
}),
}),
);
});
});
it("clears the model fallback marker after a successful run", async () => {
await withTempDir({ prefix: "openclaw-sessions-tool-success-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const sessionKey = "agent:main:main";
await upsertSessionEntry(
{ agentId: "main", sessionKey, storePath },
{
sessionId: "session-main",
updatedAt: 1,
modelFallback: {
prevModel: "good",
prevProvider: "openai",
ts: 1,
source: "agent-patch",
},
},
);
await createAgentPatchedSessionModelRunGuard({
cfg: {},
agentId: "main",
sessionKey,
storePath,
}).finish(true);
expect(loadSessionEntry({ agentId: "main", sessionKey, storePath })).not.toHaveProperty(
"modelFallback",
);
});
});
it("denies model patches without in-process gateway context", async () => {
const callGateway = vi.fn();
const tool = createSessionsTool({
agentSessionKey: "agent:main:main",
config: {},
callGateway,
hasInProcessGatewayContext: () => false,
});
const result = await tool.execute("patch-model", {
action: "patch",
model: "openai/gpt-5.4",
});
expect(result.details).toEqual({
status: "forbidden",
error: "Model patch needs in-process gateway.",
});
expect(callGateway).not.toHaveBeenCalled();
});
it("reverts when the patched model fails but a fallback completes the run", async () => {
await withTempDir({ prefix: "openclaw-sessions-tool-fallback-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const sessionKey = "agent:main:main";
await upsertSessionEntry(
{ agentId: "main", sessionKey, storePath },
{
sessionId: "session-main",
updatedAt: 1,
model: "bad",
modelProvider: "broken",
modelOverride: "bad",
providerOverride: "broken",
modelFallback: {
prevModel: "good",
prevProvider: "openai",
ts: 1,
source: "agent-patch",
},
},
);
const runGuard = createAgentPatchedSessionModelRunGuard({
cfg: {},
agentId: "main",
sessionKey,
storePath,
});
const needsRevert = runGuard.captureFallbackFailure([
{
error: "No endpoints found for broken/bad.",
reason: "model_not_found",
},
{ error: "Fallback context overflow.", reason: "context_overflow" },
]);
await runGuard.finish(!needsRevert);
expect(loadSessionEntry({ agentId: "main", sessionKey, storePath })).toMatchObject({
model: "good",
modelProvider: "openai",
});
expect(loadSessionEntry({ agentId: "main", sessionKey, storePath })).not.toHaveProperty(
"modelFallback",
);
});
});
it("promotes the newest validated model across overlapping patches", async () => {
await withTempDir({ prefix: "openclaw-sessions-tool-overlap-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const sessionKey = "agent:main:main";
const cfg: OpenClawConfig = {
agents: { defaults: { model: { primary: "openai/a" } } },
};
await upsertSessionEntry(
{ agentId: "main", sessionKey, storePath },
{
sessionId: "session-main",
updatedAt: 1,
model: "b",
modelProvider: "openai",
modelOverride: "b",
providerOverride: "openai",
modelFallback: {
prevModel: "a",
prevProvider: "openai",
ts: 10,
source: "agent-patch",
},
},
);
const runB = createAgentPatchedSessionModelRunGuard({
cfg,
agentId: "main",
sessionKey,
storePath,
});
await patchSessionEntry({ agentId: "main", sessionKey, storePath }, () => ({
model: "c",
modelOverride: "c",
modelFallback: {
prevModel: "a",
prevProvider: "openai",
ts: 20,
source: "agent-patch",
},
}));
const runC = createAgentPatchedSessionModelRunGuard({
cfg,
agentId: "main",
sessionKey,
storePath,
});
await patchSessionEntry({ agentId: "main", sessionKey, storePath }, () => ({
model: "d",
modelOverride: "d",
modelFallback: {
prevModel: "a",
prevProvider: "openai",
ts: 30,
source: "agent-patch",
},
}));
const runD = createAgentPatchedSessionModelRunGuard({
cfg,
agentId: "main",
sessionKey,
storePath,
});
await runC.finish(true);
await runB.finish(true);
expect(
loadSessionEntry({ agentId: "main", sessionKey, storePath })?.modelFallback,
).toMatchObject({
prevModel: "c",
prevProvider: "openai",
lastValidatedPatchTs: 20,
ts: 30,
});
await runD.fail({ status: 404, message: "No endpoints found for openai/d." });
expect(loadSessionEntry({ agentId: "main", sessionKey, storePath })).toMatchObject({
model: "c",
modelProvider: "openai",
modelOverride: "c",
providerOverride: "openai",
});
});
});
it("routes group actions to existing gateway methods", async () => {
const callGateway = vi.fn(async (method: string, params: Record<string, unknown>) => ({
method,
params,
}));
const tool = createSessionsTool({
agentSessionKey: "agent:main:main",
callGateway: callGateway as never,
});
await tool.execute("list", { action: "group_list" });
await tool.execute("set", { action: "group_set", names: ["Now", "Later"] });
await tool.execute("rename", { action: "group_rename", name: "Now", to: "Next" });
await tool.execute("delete", { action: "group_delete", name: "Later" });
expect(callGateway.mock.calls).toEqual([
["sessions.groups.list", {}],
["sessions.groups.put", { names: ["Now", "Later"] }],
["sessions.groups.rename", { name: "Now", to: "Next" }],
["sessions.groups.delete", { name: "Later" }],
]);
await expect(tool.execute("set-missing", { action: "group_set" })).rejects.toThrow(
"names required",
);
await expect(
tool.execute("set-invalid", { action: "group_set", names: ["Now", null] }),
).rejects.toThrow("names[1] required");
expect(callGateway).toHaveBeenCalledTimes(4);
});
it("rejects an empty patch", async () => {
const callGateway = vi.fn();
const tool = createSessionsTool({
agentSessionKey: "agent:main:main",
config: {},
callGateway,
});
await expect(tool.execute("patch-empty", { action: "patch" })).rejects.toThrow(
"Patch setting required",
);
expect(callGateway).not.toHaveBeenCalled();
});
it("denies patch targets outside the caller session tree", async () => {
sessionsResolutionTesting.setDepsForTest({
callGateway: vi.fn(async () => ({ sessions: [] })) as never,
});
const callGateway = vi.fn();
const tool = createSessionsTool({ agentSessionKey: "agent:main:main", callGateway });
await expect(
tool.execute("patch-other", {
action: "patch",
sessionKey: "agent:main:other",
archived: true,
}),
).rejects.toThrow("Session status visibility is restricted");
expect(callGateway).not.toHaveBeenCalled();
});
});
+202
View File
@@ -0,0 +1,202 @@
/** Session self-service tool. */
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { Type } from "typebox";
import { getRuntimeConfig } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { withAgentSessionModelPatchOrigin } from "../../gateway/session-model-patch-origin.js";
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import type { AnyAgentTool } from "./common.js";
import { jsonResult, readStringParam, ToolAuthorizationError, ToolInputError } from "./common.js";
import {
callInProcessGatewayTool,
hasInProcessGatewayToolContext,
type InProcessGatewayCaller,
} from "./in-process-gateway.js";
import {
createAgentToAgentPolicy,
createSessionVisibilityGuard,
resolveEffectiveSessionToolsVisibility,
} from "./sessions-access.js";
import { resolveSessionToolContext } from "./sessions-helpers.js";
import { resolveSessionReference } from "./sessions-resolution.js";
const ACTIONS = ["patch", "group_list", "group_set", "group_rename", "group_delete"] as const;
const GROUP_NAME_MAX_LENGTH = 512;
const GROUP_NAMES_MAX_ITEMS = 200;
const SessionsToolSchema = Type.Object(
{
action: Type.Union(
ACTIONS.map((action) => Type.Literal(action)),
{
description: "Action",
},
),
sessionKey: Type.Optional(Type.String({ description: "Target session. Default: current" })),
label: Type.Optional(Type.String({ description: "Session label" })),
pinned: Type.Optional(Type.Boolean({ description: "Pin session" })),
archived: Type.Optional(Type.Boolean({ description: "Archive session" })),
model: Type.Optional(Type.String({ description: "Model override" })),
thinkingLevel: Type.Optional(Type.String({ description: "Thinking override" })),
names: Type.Optional(Type.Array(Type.String(), { description: "Ordered group names" })),
name: Type.Optional(Type.String({ description: "Group name" })),
to: Type.Optional(Type.String({ description: "New group name" })),
},
{ additionalProperties: false },
);
type SessionsToolOptions = {
agentSessionKey?: string;
sandboxed?: boolean;
config?: OpenClawConfig;
callGateway?: InProcessGatewayCaller;
hasInProcessGatewayContext?: () => boolean;
};
function readBoolean(params: Record<string, unknown>, key: string): boolean | undefined {
const value = params[key];
if (value === undefined) {
return undefined;
}
if (typeof value !== "boolean") {
throw new ToolInputError(`${key} must be boolean`);
}
return value;
}
function readGroupName(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new ToolInputError(`${label} required`);
}
const name = value.trim();
if (name.length > GROUP_NAME_MAX_LENGTH) {
throw new ToolInputError(`${label} too long`);
}
return name;
}
function readGroupNames(value: unknown): string[] {
if (!Array.isArray(value)) {
throw new ToolInputError("names required");
}
if (value.length > GROUP_NAMES_MAX_ITEMS) {
throw new ToolInputError("Too many group names");
}
return value.map((name, index) => readGroupName(name, `names[${index}]`));
}
async function resolvePatchTarget(
opts: SessionsToolOptions,
sessionKey: string | undefined,
): Promise<{ cfg: OpenClawConfig; key: string }> {
const context = resolveSessionToolContext(opts);
const rawKey = sessionKey ?? context.effectiveRequesterKey;
const resolved = await resolveSessionReference({
sessionKey: rawKey,
alias: context.alias,
mainKey: context.mainKey,
requesterInternalKey: context.effectiveRequesterKey,
restrictToSpawned: context.restrictToSpawned,
});
if (!resolved.ok) {
throw new ToolInputError(resolved.error);
}
if (resolved.key !== context.effectiveRequesterKey) {
// Session visibility is the configured read/write scope for session tools;
// the action only selects error copy. Owner gating remains separate.
const guard = await createSessionVisibilityGuard({
action: "status",
requesterSessionKey: context.effectiveRequesterKey,
requesterAgentId: resolveAgentIdFromSessionKey(context.effectiveRequesterKey),
visibility: resolveEffectiveSessionToolsVisibility({
cfg: context.cfg,
sandboxed: opts.sandboxed === true,
}),
a2aPolicy: createAgentToAgentPolicy(context.cfg),
});
const access = guard.check(resolved.key);
if (!access.allowed) {
throw new ToolAuthorizationError(access.error);
}
}
return { cfg: context.cfg, key: resolved.key };
}
export function createSessionsTool(opts: SessionsToolOptions = {}): AnyAgentTool {
const gatewayCall = opts.callGateway ?? callInProcessGatewayTool;
return {
label: "Sessions",
name: "sessions",
description:
"Session settings and groups. patch/group_list/group_set/group_rename/group_delete.",
parameters: SessionsToolSchema,
execute: async (_toolCallId, rawArgs) => {
const params = rawArgs as Record<string, unknown>;
const action = readStringParam(params, "action", { required: true });
if (action === "group_list") {
return jsonResult(await gatewayCall("sessions.groups.list", {}));
}
// Group catalog is global by contract. Owner-only tool gating protects mutations.
if (action === "group_set") {
const names = readGroupNames(params.names);
return jsonResult(await gatewayCall("sessions.groups.put", { names }));
}
if (action === "group_rename") {
return jsonResult(
await gatewayCall("sessions.groups.rename", {
name: readGroupName(params.name, "name"),
to: readGroupName(params.to, "to"),
}),
);
}
if (action === "group_delete") {
return jsonResult(
await gatewayCall("sessions.groups.delete", {
name: readGroupName(params.name, "name"),
}),
);
}
if (action !== "patch") {
throw new ToolInputError(`Unknown action: ${action}`);
}
const { key } = await resolvePatchTarget(
{ ...opts, config: opts.config ?? getRuntimeConfig() },
normalizeOptionalString(readStringParam(params, "sessionKey")),
);
const patch = {
key,
...(params.label !== undefined
? { label: readStringParam(params, "label", { required: true }) }
: {}),
...(params.pinned !== undefined ? { pinned: readBoolean(params, "pinned") } : {}),
...(params.archived !== undefined ? { archived: readBoolean(params, "archived") } : {}),
...(params.model !== undefined
? { model: readStringParam(params, "model", { required: true }) }
: {}),
...(params.thinkingLevel !== undefined
? { thinkingLevel: readStringParam(params, "thinkingLevel", { required: true }) }
: {}),
};
if (Object.keys(patch).length === 1) {
throw new ToolInputError("Patch setting required");
}
const inProcessGatewayAvailable =
opts.hasInProcessGatewayContext?.() ??
(opts.callGateway ? true : hasInProcessGatewayToolContext());
if (patch.model !== undefined && !inProcessGatewayAvailable) {
return jsonResult({
status: "forbidden",
error: "Model patch needs in-process gateway.",
});
}
const result =
patch.model === undefined
? await gatewayCall("sessions.patch", patch)
: await withAgentSessionModelPatchOrigin(
async () => await gatewayCall("sessions.patch", patch),
);
return jsonResult(result);
},
};
}
+147 -8
View File
@@ -1,17 +1,156 @@
// Subagents tool tests cover requester-scoped listing guidance and numeric
// status-window validation.
import { describe, expect, it } from "vitest";
// Subagents tool tests cover requester-scoped task listing and cancellation.
import { describe, expect, it, vi } from "vitest";
import type { TaskRecord, TaskRuntime, TaskStatus } from "../../tasks/task-registry.types.js";
import { createSubagentsTool } from "./subagents-tool.js";
function task(params: {
taskId: string;
runtime: TaskRuntime;
status?: TaskStatus;
ownerKey?: string;
requesterSessionKey?: string;
childSessionKey?: string;
label?: string;
progressSummary?: string;
terminalSummary?: string;
}): TaskRecord {
return {
taskId: params.taskId,
runtime: params.runtime,
ownerKey: params.ownerKey ?? "agent:main:main",
requesterSessionKey: params.requesterSessionKey ?? "agent:main:main",
scopeKind: "session",
task: params.taskId,
status: params.status ?? "running",
deliveryStatus: "not_applicable",
notifyPolicy: "done_only",
createdAt: Date.now(),
lastEventAt: Date.now(),
...(params.childSessionKey ? { childSessionKey: params.childSessionKey } : {}),
...(params.label ? { label: params.label } : {}),
...(params.progressSummary ? { progressSummary: params.progressSummary } : {}),
...(params.terminalSummary ? { terminalSummary: params.terminalSummary } : {}),
};
}
describe("subagents tool", () => {
it("does not advertise sessions_yield as unconditionally available", () => {
// sessions_yield is context-dependent; the model-facing description should
// not promise it exists in every runtime.
it("advertises the unified task ledger", () => {
const tool = createSubagentsTool();
expect(tool.description).toBe(
"List requester-session active/recent subagents. If available, wait via sessions_yield; never poll-loop.",
expect(tool.description).toBe("Background work: subagents, media gen, cron runs. list/cancel.");
});
it("lists cross-runtime tasks in the caller session tree", async () => {
const tasks = [
task({
taskId: "subagent-task",
runtime: "subagent",
childSessionKey: "agent:main:dashboard:child",
label: "Research",
progressSummary: "Reading",
}),
task({ taskId: "acp-task", runtime: "acp", status: "succeeded", terminalSummary: "Done" }),
task({ taskId: "cli-task", runtime: "cli" }),
task({ taskId: "cron-task", runtime: "cron" }),
task({
taskId: "outside-owner",
runtime: "cli",
ownerKey: "agent:other:main",
requesterSessionKey: "agent:main:main",
}),
task({
taskId: "child-task",
runtime: "cli",
ownerKey: "agent:main:dashboard:child",
requesterSessionKey: "agent:main:dashboard:child",
}),
task({
taskId: "outside",
runtime: "cron",
ownerKey: "agent:other:main",
requesterSessionKey: "agent:other:main",
}),
];
const tool = createSubagentsTool({
agentSessionKey: "agent:main:main",
config: {},
listTasks: () => tasks,
});
const result = await tool.execute("list", { action: "list" });
expect(result.details).toMatchObject({ status: "ok", taskTotal: 5 });
const rows = (result.details as { tasks: Array<Record<string, unknown>> }).tasks;
expect(rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
taskId: "subagent-task",
runtime: "subagent",
status: "running",
label: "Research",
progressSummary: "Reading",
}),
expect.objectContaining({
taskId: "acp-task",
runtime: "acp",
status: "completed",
terminalSummary: "Done",
}),
expect.objectContaining({ taskId: "cli-task", runtime: "cli" }),
expect.objectContaining({ taskId: "cron-task", runtime: "cron" }),
expect.objectContaining({ taskId: "child-task", runtime: "cli" }),
]),
);
expect(rows).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ taskId: "outside" }),
expect.objectContaining({ taskId: "outside-owner" }),
]),
);
});
it("cancels only tasks in the caller session tree", async () => {
const tasks = [
task({ taskId: "inside", runtime: "cli" }),
task({
taskId: "outside",
runtime: "cron",
ownerKey: "agent:other:main",
requesterSessionKey: "agent:other:main",
}),
task({
taskId: "outside-owner",
runtime: "cli",
ownerKey: "agent:other:main",
requesterSessionKey: "agent:main:main",
}),
];
const cancelTask = vi.fn(async () => ({ found: true, cancelled: true }));
const tool = createSubagentsTool({
agentSessionKey: "agent:main:main",
config: {},
listTasks: () => tasks,
cancelTask: cancelTask as never,
});
await expect(tool.execute("cancel", { action: "cancel", taskId: "inside" })).resolves.toEqual(
expect.objectContaining({ details: expect.objectContaining({ status: "cancelled" }) }),
);
expect(cancelTask).toHaveBeenCalledWith({ cfg: {}, taskId: "inside" });
await expect(
tool.execute("cancel-outside", { action: "cancel", taskId: "outside" }),
).resolves.toEqual(
expect.objectContaining({ details: expect.objectContaining({ status: "forbidden" }) }),
);
expect(cancelTask).toHaveBeenCalledTimes(1);
await expect(
tool.execute("cancel-outside-owner", { action: "cancel", taskId: "outside-owner" }),
).resolves.toEqual(
expect.objectContaining({ details: expect.objectContaining({ status: "forbidden" }) }),
);
expect(cancelTask).toHaveBeenCalledTimes(1);
});
it.each([0, 1.5])("rejects invalid recentMinutes value %s", async (recentMinutes) => {
+108 -6
View File
@@ -1,10 +1,14 @@
/**
* subagents built-in tool.
*
* Lists active and recent subagents controlled by the caller's session tree.
* Lists and cancels background work in the caller's session tree.
*/
import { Type } from "typebox";
import { getRuntimeConfig } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { listTaskRecordsUnsorted } from "../../tasks/runtime-internal.js";
import { cancelDetachedTaskRunById } from "../../tasks/task-executor.js";
import type { TaskRecord, TaskStatus } from "../../tasks/task-registry.types.js";
import { optionalPositiveIntegerSchema, optionalStringEnum } from "../schema/typebox.js";
import {
DEFAULT_RECENT_MINUTES,
@@ -16,26 +20,81 @@ import { buildSubagentList } from "../subagent-list.js";
import type { AnyAgentTool } from "./common.js";
import { jsonResult, readPositiveIntegerParam, readStringParam } from "./common.js";
const SUBAGENT_ACTIONS = ["list"] as const;
const SUBAGENT_ACTIONS = ["list", "cancel"] as const;
type SubagentAction = (typeof SUBAGENT_ACTIONS)[number];
const SubagentsToolSchema = Type.Object({
action: optionalStringEnum(SUBAGENT_ACTIONS),
recentMinutes: optionalPositiveIntegerSchema(),
taskId: Type.Optional(Type.String({ description: "Task id" })),
});
const STATUS_MAP: Record<TaskStatus, string> = {
queued: "queued",
running: "running",
succeeded: "completed",
failed: "failed",
timed_out: "timed_out",
cancelled: "cancelled",
lost: "failed",
};
type SubagentsToolOptions = {
agentSessionKey?: string;
config?: OpenClawConfig;
listTasks?: typeof listTaskRecordsUnsorted;
cancelTask?: typeof cancelDetachedTaskRunById;
};
function taskUpdatedAt(task: TaskRecord): number {
return task.lastEventAt ?? task.endedAt ?? task.startedAt ?? task.createdAt;
}
function listTreeTasks(tasks: TaskRecord[], rootSessionKey: string): TaskRecord[] {
const visibleKeys = new Set([rootSessionKey]);
const visibleTasks = new Set<string>();
let changed = true;
while (changed) {
changed = false;
for (const task of tasks) {
if (task.scopeKind !== "session" || visibleTasks.has(task.taskId)) {
continue;
}
if (!visibleKeys.has(task.ownerKey)) {
continue;
}
visibleTasks.add(task.taskId);
if (task.childSessionKey && !visibleKeys.has(task.childSessionKey)) {
visibleKeys.add(task.childSessionKey);
changed = true;
}
}
}
return tasks.filter((task) => visibleTasks.has(task.taskId));
}
function mapTask(task: TaskRecord) {
return {
taskId: task.taskId,
runtime: task.runtime,
status: STATUS_MAP[task.status],
...(task.label ? { label: task.label } : {}),
...(task.progressSummary ? { progressSummary: task.progressSummary } : {}),
...(task.terminalSummary ? { terminalSummary: task.terminalSummary } : {}),
};
}
/** Creates the subagents list tool scoped to the caller's controlled session tree. */
export function createSubagentsTool(opts?: { agentSessionKey?: string }): AnyAgentTool {
export function createSubagentsTool(opts: SubagentsToolOptions = {}): AnyAgentTool {
return {
label: "Subagents",
name: "subagents",
description:
"List requester-session active/recent subagents. If available, wait via sessions_yield; never poll-loop.",
description: "Background work: subagents, media gen, cron runs. list/cancel.",
parameters: SubagentsToolSchema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
const action = (readStringParam(params, "action") ?? "list") as SubagentAction;
const cfg = getRuntimeConfig();
const cfg = opts.config ?? getRuntimeConfig();
const recentMinutesRaw = readPositiveIntegerParam(params, "recentMinutes");
const recentMinutes =
recentMinutesRaw === undefined
@@ -47,6 +106,10 @@ export function createSubagentsTool(opts?: { agentSessionKey?: string }): AnyAge
});
// The caller only sees subagents controlled by its effective controller session.
const runs = listControlledSubagentRuns(controller.controllerSessionKey);
const treeTasks = listTreeTasks(
(opts.listTasks ?? listTaskRecordsUnsorted)(),
controller.controllerSessionKey,
);
if (action === "list") {
const list = buildSubagentList({
@@ -54,6 +117,16 @@ export function createSubagentsTool(opts?: { agentSessionKey?: string }): AnyAge
runs,
recentMinutes,
});
const cutoff = Date.now() - recentMinutes * 60_000;
const tasks = treeTasks
.filter(
(task) =>
task.status === "queued" ||
task.status === "running" ||
taskUpdatedAt(task) >= cutoff,
)
.toSorted((left, right) => taskUpdatedAt(right) - taskUpdatedAt(left))
.map(mapTask);
return jsonResult({
status: "ok",
action: "list",
@@ -61,12 +134,41 @@ export function createSubagentsTool(opts?: { agentSessionKey?: string }): AnyAge
callerSessionKey: controller.callerSessionKey,
callerIsSubagent: controller.callerIsSubagent,
total: list.total,
taskTotal: tasks.length,
tasks,
active: list.active.map(({ line: _line, ...view }) => view),
recent: list.recent.map(({ line: _line, ...view }) => view),
text: list.text,
});
}
if (action === "cancel") {
const taskId = readStringParam(params, "taskId", { required: true });
const target = treeTasks.find((task) => task.taskId === taskId);
if (!target) {
return jsonResult({ status: "forbidden", error: "Task outside session tree." });
}
// Leaf subagents may cancel only their own tasks, matching the
// control-scope gate every other cross-session subagent mutation enforces.
if (
controller.controlScope !== "children" &&
target.ownerKey !== controller.callerSessionKey
) {
return jsonResult({
status: "forbidden",
error: "Leaf subagents cannot cancel other sessions.",
});
}
const result = await (opts.cancelTask ?? cancelDetachedTaskRunById)({ cfg, taskId });
return jsonResult({
status: result.cancelled ? "cancelled" : "error",
taskId,
found: result.found,
cancelled: result.cancelled,
...(result.reason ? { reason: result.reason } : {}),
});
}
return jsonResult({
status: "error",
error: "Unsupported action.",
+27 -74
View File
@@ -70,6 +70,7 @@ import {
resolveAgentRunErrorLifecycleFields,
} from "../../agents/run-termination.js";
import { buildAgentRuntimeOutcomePlan } from "../../agents/runtime-plan/build.js";
import { createAgentPatchedSessionModelRunGuard } from "../../agents/session-model-auto-revert.js";
import { withLocalSessionPlacementTurnAdmission } from "../../agents/session-placement-admission.js";
import { resolveSessionRuntimeOverrideForProvider } from "../../agents/session-runtime-compat.js";
import { resolveCandidateThinkingLevel } from "../../agents/thinking-runtime.js";
@@ -133,6 +134,7 @@ import {
GENERIC_EXTERNAL_RUN_FAILURE_TEXT,
HEARTBEAT_EXTERNAL_RUN_FAILURE_TEXT,
} from "./agent-runner-failure-copy.js";
import { emitModelFallbackStepLifecycle } from "./agent-runner-model-fallback-lifecycle.js";
import {
buildEmbeddedRunExecutionParams,
resolveQueuedReplyRuntimeConfig,
@@ -160,6 +162,12 @@ import type { FollowupRun } from "./queue.js";
import { createBlockReplyDeliveryHandler } from "./reply-delivery.js";
import type { ReplyMediaContext } from "./reply-media-paths.js";
import { createReplyMediaContext } from "./reply-media-paths.runtime.js";
import {
buildRestartLifecycleReplyText,
isReplyOperationRestartAbort,
isReplyOperationUserAbort,
resolveRestartLifecycleError,
} from "./reply-operation-abort.js";
import type { ReplyOperation } from "./reply-run-registry.js";
import { isReplyProfilerEnabled } from "./reply-timing-tracker.js";
import type { TypingSignaler } from "./typing-mode.js";
@@ -1283,80 +1291,6 @@ export function buildContextOverflowRecoveryText(params: {
return prefix + (heartbeatBleedHint ?? buildContextOverflowResetHint(primaryContextWindow));
}
function buildRestartLifecycleReplyText(): string {
return "⚠️ Gateway is restarting. Please wait a few seconds and try again.";
}
function resolveRestartLifecycleError(
err: unknown,
): GatewayDrainingError | CommandLaneClearedError | undefined {
const pending = [err];
const seen = new Set<unknown>();
let pendingIndex = 0;
while (pendingIndex < pending.length) {
const candidate = pending[pendingIndex++];
if (!candidate || seen.has(candidate)) {
continue;
}
seen.add(candidate);
if (candidate instanceof GatewayDrainingError || candidate instanceof CommandLaneClearedError) {
return candidate;
}
if (isFallbackSummaryError(candidate)) {
for (const attempt of candidate.attempts) {
pending.push(attempt.error);
}
}
if (candidate instanceof Error && "cause" in candidate) {
pending.push(candidate.cause);
}
}
return undefined;
}
function isReplyOperationUserAbort(replyOperation?: ReplyOperation): boolean {
if (
replyOperation?.result?.kind === "aborted" &&
replyOperation.result.code === "aborted_by_user"
) {
return true;
}
const abortSignal = replyOperation?.abortSignal;
return abortSignal?.aborted === true && !isAgentRunRestartAbortReason(abortSignal.reason);
}
function isReplyOperationRestartAbort(replyOperation?: ReplyOperation): boolean {
if (
replyOperation?.result?.kind === "aborted" &&
replyOperation.result.code === "aborted_for_restart"
) {
return true;
}
const abortSignal = replyOperation?.abortSignal;
return abortSignal?.aborted === true && isAgentRunRestartAbortReason(abortSignal.reason);
}
function emitModelFallbackStepLifecycle(params: {
runId: string;
sessionKey?: string;
step: Record<string, unknown>;
}) {
emitAgentEvent({
runId: params.runId,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
stream: "lifecycle",
data: {
phase: "fallback_step",
...params.step,
},
});
}
/** Decides whether to retry after rechecking auto-fallback primary probe state. */
export function resolveRunAfterAutoFallbackPrimaryProbeRecheck(params: {
run: FollowupRun["run"];
@@ -1646,6 +1580,14 @@ async function runAgentTurnWithFallbackInternal(
let fallbackAttempts: RuntimeFallbackAttempt[] = [];
let fallbackExhausted = false;
let terminalRunFailed = false;
const modelPatch = createAgentPatchedSessionModelRunGuard({
cfg: runtimeConfig,
agentId: params.followupRun.run.agentId,
sessionKey: params.sessionKey,
storePath: params.storePath,
onError: (error) =>
logVerbose(`agent model patch reconciliation failed: ${formatErrorMessage(error)}`),
});
let pendingLifecycleTerminal:
| {
provider: string;
@@ -2972,6 +2914,9 @@ async function runAgentTurnWithFallbackInternal(
terminalErrorMessage ?? "All model fallback candidates failed",
);
terminalRunFailed = true;
if (modelPatch.captureFallbackFailure(fallbackAttempts) === undefined) {
modelPatch.captureFailure(embeddedError ?? exhaustionError);
}
emitSettledLifecycleError(exhaustionError, {
...terminalMetadata,
fallbackExhaustedFailure: true,
@@ -2981,6 +2926,7 @@ async function runAgentTurnWithFallbackInternal(
} else if (deferredLifecycleError || embeddedError) {
const terminalError = new Error(terminalErrorMessage ?? "Agent run failed");
terminalRunFailed = true;
modelPatch.captureFailure(embeddedError ?? terminalError);
emitSettledLifecycleError(terminalError, terminalMetadata);
params.replyOperation?.retainFailureUntilComplete();
params.replyOperation?.fail("run_failed", terminalError);
@@ -3011,6 +2957,7 @@ async function runAgentTurnWithFallbackInternal(
"The requested model may be temporarily unavailable. Please try again shortly."
: "⚠️ Model switch could not be completed. The requested model may be temporarily unavailable. Please try again shortly.";
params.replyOperation?.fail("run_failed", err);
await modelPatch.fail(err);
return {
kind: "final",
payload: markAgentRunFailureReplyPayload({
@@ -3158,6 +3105,7 @@ async function runAgentTurnWithFallbackInternal(
if (providerRequestError) {
takePendingLifecycleTerminal()?.emit("error", err);
params.replyOperation?.fail("run_failed", err);
await modelPatch.fail(err);
return {
kind: "final",
payload: markAgentRunFailureReplyPayload({
@@ -3279,6 +3227,7 @@ async function runAgentTurnWithFallbackInternal(
});
}
params.replyOperation?.fail("run_failed", err);
await modelPatch.fail(err);
return {
kind: "final",
payload: markAgentRunFailureReplyPayload({
@@ -3351,6 +3300,10 @@ async function runAgentTurnWithFallbackInternal(
}
}
}
const patchedModelNeedsRevert = terminalRunFailed
? false
: (modelPatch.captureFallbackFailure(fallbackAttempts) ?? false);
await modelPatch.finish(!terminalRunFailed && !patchedModelNeedsRevert);
const terminalFailurePayload = terminalRunFailed
? buildTerminalAgentRunFailureReplyPayload({
isHeartbeat: params.isHeartbeat,
@@ -0,0 +1,14 @@
import { emitAgentEvent } from "../../infra/agent-events.js";
export function emitModelFallbackStepLifecycle(params: {
runId: string;
sessionKey?: string;
step: Record<string, unknown>;
}) {
emitAgentEvent({
runId: params.runId,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
stream: "lifecycle",
data: { phase: "fallback_step", ...params.step },
});
}
@@ -0,0 +1,53 @@
import { isFallbackSummaryError } from "../../agents/model-fallback.js";
import { isAgentRunRestartAbortReason } from "../../agents/run-termination.js";
import { CommandLaneClearedError, GatewayDrainingError } from "../../process/command-queue.js";
import type { ReplyOperation } from "./reply-run-registry.js";
export function buildRestartLifecycleReplyText(): string {
return "⚠️ Gateway is restarting. Please wait a few seconds and try again.";
}
export function isReplyOperationUserAbort(replyOperation?: ReplyOperation): boolean {
if (
replyOperation?.result?.kind === "aborted" &&
replyOperation.result.code === "aborted_by_user"
) {
return true;
}
const abortSignal = replyOperation?.abortSignal;
return abortSignal?.aborted === true && !isAgentRunRestartAbortReason(abortSignal.reason);
}
export function isReplyOperationRestartAbort(replyOperation?: ReplyOperation): boolean {
if (
replyOperation?.result?.kind === "aborted" &&
replyOperation.result.code === "aborted_for_restart"
) {
return true;
}
const abortSignal = replyOperation?.abortSignal;
return abortSignal?.aborted === true && isAgentRunRestartAbortReason(abortSignal.reason);
}
export function resolveRestartLifecycleError(
error: unknown,
): GatewayDrainingError | CommandLaneClearedError | undefined {
const pending = [error];
const seen = new Set<unknown>();
for (const candidate of pending) {
if (!candidate || seen.has(candidate)) {
continue;
}
seen.add(candidate);
if (candidate instanceof GatewayDrainingError || candidate instanceof CommandLaneClearedError) {
return candidate;
}
if (isFallbackSummaryError(candidate)) {
pending.push(...candidate.attempts.map((attempt) => attempt.error));
}
if (candidate instanceof Error && "cause" in candidate) {
pending.push(candidate.cause);
}
}
return undefined;
}
@@ -0,0 +1,58 @@
export type AgentPatchedSessionModelFallback = {
prevModel: string;
prevProvider: string;
prevModelOverride?: string;
prevProviderOverride?: string;
prevModelOverrideSource?: "auto" | "user";
prevModelOverrideFallbackOriginProvider?: string;
prevModelOverrideFallbackOriginModel?: string;
prevAuthProfileOverride?: string;
prevAuthProfileOverrideSource?: "auto" | "user";
prevAuthProfileOverrideCompactionCount?: number;
prevThinkingLevel?: string;
lastValidatedPatchTs?: number;
ts: number;
source: "agent-patch";
};
export function createAgentPatchedSessionModelFallback(params: {
model: string;
provider: string;
entry: {
modelOverride?: string;
providerOverride?: string;
modelOverrideSource?: "auto" | "user";
modelOverrideFallbackOriginProvider?: string;
modelOverrideFallbackOriginModel?: string;
authProfileOverride?: string;
authProfileOverrideSource?: "auto" | "user";
authProfileOverrideCompactionCount?: number;
thinkingLevel?: string;
};
ts: number;
}): AgentPatchedSessionModelFallback {
const { entry } = params;
return {
prevModel: params.model,
prevProvider: params.provider,
...(entry.modelOverride ? { prevModelOverride: entry.modelOverride } : {}),
...(entry.providerOverride ? { prevProviderOverride: entry.providerOverride } : {}),
...(entry.modelOverrideSource ? { prevModelOverrideSource: entry.modelOverrideSource } : {}),
...(entry.modelOverrideFallbackOriginProvider
? { prevModelOverrideFallbackOriginProvider: entry.modelOverrideFallbackOriginProvider }
: {}),
...(entry.modelOverrideFallbackOriginModel
? { prevModelOverrideFallbackOriginModel: entry.modelOverrideFallbackOriginModel }
: {}),
...(entry.authProfileOverride ? { prevAuthProfileOverride: entry.authProfileOverride } : {}),
...(entry.authProfileOverrideSource
? { prevAuthProfileOverrideSource: entry.authProfileOverrideSource }
: {}),
...(entry.authProfileOverrideCompactionCount !== undefined
? { prevAuthProfileOverrideCompactionCount: entry.authProfileOverrideCompactionCount }
: {}),
...(entry.thinkingLevel ? { prevThinkingLevel: entry.thinkingLevel } : {}),
ts: params.ts,
source: "agent-patch",
};
}
+4 -4
View File
@@ -15,11 +15,9 @@ import type { TtsAutoMode } from "../types.tts.js";
import type { SessionRestartRecoveryState } from "./restart-recovery-types.js";
import type { SessionEntryProvenance } from "./session-entry-provenance.js";
import { rewriteSessionFileForNewSessionId } from "./session-file-rotation.js";
import type { AgentPatchedSessionModelFallback } from "./session-model-fallback.js";
export type SessionScope = "per-sender" | "global";
type SessionChannelId = ChannelId;
export type SessionChatType = ChatType;
export type SessionOrigin = {
@@ -388,6 +386,8 @@ export type SessionEntry = SessionRestartRecoveryState &
/** Selected model that produced the current auto fallback override. */
modelOverrideFallbackOriginProvider?: string;
modelOverrideFallbackOriginModel?: string;
/** One-run rollback guard for a model selected by the agent sessions tool. */
modelFallback?: AgentPatchedSessionModelFallback;
authProfileOverride?: string;
authProfileOverrideSource?: "auto" | "user";
authProfileOverrideCompactionCount?: number;
@@ -480,7 +480,7 @@ export type SessionEntry = SessionRestartRecoveryState &
deliveryContext?: DeliveryContext;
/** Last ambient room message durably appended to this transcript, keyed by channel scope. */
ambientTranscriptWatermarks?: Record<string, AmbientTranscriptWatermark>;
lastChannel?: SessionChannelId;
lastChannel?: ChannelId;
lastTo?: string;
lastAccountId?: string;
lastThreadId?: string | number;
+61
View File
@@ -0,0 +1,61 @@
import { AsyncLocalStorage } from "node:async_hooks";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "@openclaw/normalization-core/string-coerce";
import { resolveProviderIdForAuth } from "../agents/provider-auth-aliases.js";
import { resolveSessionModelRef } from "../agents/session-model-ref.js";
import type { SessionEntry } from "../config/sessions.js";
import { createAgentPatchedSessionModelFallback } from "../config/sessions/session-model-fallback.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
const agentSessionModelPatch = new AsyncLocalStorage<boolean>();
export function withAgentSessionModelPatchOrigin<T>(run: () => T): T {
return agentSessionModelPatch.run(true, run);
}
export function isAgentSessionModelPatchOrigin(): boolean {
return agentSessionModelPatch.getStore() === true;
}
export function shouldPreserveSessionAuthProfileOverride(params: {
cfg: OpenClawConfig;
entry: SessionEntry;
currentProvider: string;
provider: string;
}): boolean {
const profileOverride = normalizeOptionalString(params.entry.authProfileOverride);
const provider = normalizeOptionalLowercaseString(params.provider);
if (!profileOverride || !provider) {
return false;
}
const resolvesToTargetProvider = (rawProvider: string | undefined): boolean => {
const candidate = normalizeOptionalLowercaseString(rawProvider);
return Boolean(
candidate &&
resolveProviderIdForAuth(candidate, { config: params.cfg }) ===
resolveProviderIdForAuth(provider, { config: params.cfg }),
);
};
const delimiterIndex = profileOverride.indexOf(":");
if (delimiterIndex < 0) {
return resolvesToTargetProvider(params.currentProvider);
}
return resolvesToTargetProvider(profileOverride.slice(0, delimiterIndex));
}
export function snapshotAgentModelFallback(
cfg: OpenClawConfig,
entry: SessionEntry,
agentId: string,
now: number,
): NonNullable<SessionEntry["modelFallback"]> {
const prior = resolveSessionModelRef(cfg, entry, agentId);
return createAgentPatchedSessionModelFallback({
model: prior.model,
provider: prior.provider,
entry,
ts: now,
});
}
+144
View File
@@ -8,6 +8,7 @@ import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js";
import { AGENT_HARNESS_SESSION_KEY_RESERVED_MESSAGE } from "../sessions/agent-harness-session-key.js";
import { MODEL_SELECTION_LOCKED_MESSAGE } from "../sessions/model-overrides.js";
import { withAgentSessionModelPatchOrigin } from "./session-model-patch-origin.js";
import { applySessionsPatchToStore } from "./sessions-patch.js";
const acpSessionMetaMocks = vi.hoisted(() => ({
@@ -23,6 +24,7 @@ const KIMI_SUBAGENT_KEY = "agent:kimi:subagent:child";
const MAIN_SESSION_KEY = "agent:main:main";
const ANTHROPIC_SONNET_MODEL = "anthropic/claude-sonnet-4-6";
const ANTHROPIC_SONNET_ID = "claude-sonnet-4-6";
const ANTHROPIC_OPUS_MODEL = "anthropic/claude-opus-4-6";
const ANTHROPIC_OPUS_ID = "claude-opus-4-6";
const OPENAI_GPT_MODEL = "openai/gpt-5.4";
const OPENAI_GPT_ID = "gpt-5.4";
@@ -708,6 +710,148 @@ describe("gateway sessions patch", () => {
expect(entry.liveModelSwitchPending).toBe(true);
});
test("clears an agent model rollback marker on explicit model patches", async () => {
const store = mainStoreEntry({
sessionId: "sess-agent-model-patch",
modelFallback: {
prevModel: OPENAI_GPT_ID,
prevProvider: "openai",
ts: 1,
source: "agent-patch",
},
});
const entry = await applyMainModelPatch({
store,
cfg: createAllowlistedAnthropicModelCfg(),
model: ANTHROPIC_SONNET_MODEL,
catalogRefs: [OPENAI_GPT_MODEL, ANTHROPIC_SONNET_MODEL],
});
expect(entry.modelFallback).toBeUndefined();
});
test("atomically snapshots prior selection for agent model patches", async () => {
const store = mainStoreEntry({
providerOverride: "openai",
modelOverride: OPENAI_GPT_ID,
modelOverrideSource: "auto",
modelOverrideFallbackOriginProvider: "openai",
modelOverrideFallbackOriginModel: "gpt-primary",
authProfileOverride: "openai:good",
authProfileOverrideSource: "user",
thinkingLevel: "high",
});
const entry = await withAgentSessionModelPatchOrigin(
async () =>
await applyMainModelPatch({
store,
cfg: createAllowlistedAnthropicModelCfg(),
model: ANTHROPIC_SONNET_MODEL,
catalogRefs: [OPENAI_GPT_MODEL, ANTHROPIC_SONNET_MODEL],
}),
);
expect(entry.modelFallback).toMatchObject({
prevModel: OPENAI_GPT_ID,
prevProvider: "openai",
prevModelOverrideSource: "auto",
prevModelOverrideFallbackOriginProvider: "openai",
prevModelOverrideFallbackOriginModel: "gpt-primary",
prevAuthProfileOverride: "openai:good",
prevThinkingLevel: "high",
source: "agent-patch",
});
});
test("keeps the last validated model across consecutive agent patches", async () => {
const cfg = createAllowlistedAnthropicModelCfg();
cfg.agents!.defaults!.models![ANTHROPIC_OPUS_MODEL] = { alias: "opus" };
const first = await withAgentSessionModelPatchOrigin(
async () =>
await applyMainModelPatch({
store: mainStoreEntry({
providerOverride: "openai",
modelOverride: OPENAI_GPT_ID,
}),
cfg,
model: ANTHROPIC_SONNET_MODEL,
catalogRefs: [OPENAI_GPT_MODEL, ANTHROPIC_SONNET_MODEL],
}),
);
const firstMarker = first.modelFallback;
expect(firstMarker).toMatchObject({
prevModel: OPENAI_GPT_ID,
prevProvider: "openai",
});
const second = await withAgentSessionModelPatchOrigin(
async () =>
await applyMainModelPatch({
store: { [MAIN_SESSION_KEY]: first },
cfg,
model: ANTHROPIC_OPUS_MODEL,
catalogRefs: [OPENAI_GPT_MODEL, ANTHROPIC_SONNET_MODEL, ANTHROPIC_OPUS_MODEL],
}),
);
expect(second.modelFallback).toMatchObject({
prevModel: OPENAI_GPT_ID,
prevProvider: "openai",
});
expect(second.modelFallback?.ts).toBeGreaterThan(firstMarker?.ts ?? 0);
});
test("realigns the model-revert marker with an independent thinkingLevel change", async () => {
const store: Record<string, SessionEntry> = {
[MAIN_SESSION_KEY]: {
thinkingLevel: "high",
modelOverride: ANTHROPIC_SONNET_ID,
providerOverride: "anthropic",
modelFallback: {
prevModel: OPENAI_GPT_ID,
prevProvider: "openai",
prevThinkingLevel: "high",
ts: 1,
source: "agent-patch",
},
} as SessionEntry,
};
const entry = expectPatchOk(
await runPatch({ store, patch: { key: MAIN_SESSION_KEY, thinkingLevel: "low" } }),
);
// The model-revert target still points at the pre-switch model, but its
// thinkingLevel restore now honors the user's newer choice instead of "high".
expect(entry.thinkingLevel).toBe("low");
expect(entry.modelFallback).toMatchObject({
prevModel: OPENAI_GPT_ID,
prevProvider: "openai",
prevThinkingLevel: "low",
ts: 1,
source: "agent-patch",
});
});
test("clears the marker thinkingLevel restore when the user clears thinkingLevel", async () => {
const store: Record<string, SessionEntry> = {
[MAIN_SESSION_KEY]: {
thinkingLevel: "high",
modelFallback: {
prevModel: OPENAI_GPT_ID,
prevProvider: "openai",
prevThinkingLevel: "high",
ts: 1,
source: "agent-patch",
},
} as SessionEntry,
};
const entry = expectPatchOk(
await runPatch({ store, patch: { key: MAIN_SESSION_KEY, thinkingLevel: null } }),
);
expect(entry.thinkingLevel).toBeUndefined();
expect(entry.modelFallback?.prevThinkingLevel).toBeUndefined();
expect(entry.modelFallback?.prevModel).toBe(OPENAI_GPT_ID);
});
test("clears pending live model switches for model reset patches", async () => {
const store = mainStoreEntry({
sessionId: "sess-live-reset",
+27 -39
View File
@@ -23,7 +23,6 @@ import {
resolveDefaultModelForAgent,
resolveSubagentConfiguredModelSelection,
} from "../agents/model-selection.js";
import { resolveProviderIdForAuth } from "../agents/provider-auth-aliases.js";
import { resolveEffectiveAgentRuntime } from "../agents/thinking-runtime.js";
import { normalizeGroupActivation } from "../auto-reply/group-activation.js";
import {
@@ -62,6 +61,11 @@ import {
} from "../sessions/model-overrides.js";
import { normalizeSendPolicy } from "../sessions/send-policy.js";
import { parseSessionLabel, SESSION_LABEL_MAX_LENGTH } from "../sessions/session-label.js";
import {
isAgentSessionModelPatchOrigin,
shouldPreserveSessionAuthProfileOverride,
snapshotAgentModelFallback,
} from "./session-model-patch-origin.js";
function invalid(message: string): { ok: false; error: ErrorShape } {
return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, message) };
@@ -83,43 +87,6 @@ function normalizeExecAsk(raw: string): "off" | "on-miss" | "always" | undefined
return undefined;
}
function shouldPreserveSessionAuthProfileOverride(params: {
cfg: OpenClawConfig;
entry: SessionEntry;
currentProvider: string;
provider: string;
}): boolean {
const profileOverride = normalizeOptionalString(params.entry.authProfileOverride);
if (!profileOverride) {
return false;
}
const provider = normalizeOptionalLowercaseString(params.provider);
if (!provider) {
return false;
}
const resolvesToTargetProvider = (rawProvider: string | undefined): boolean => {
const candidate = normalizeOptionalLowercaseString(rawProvider);
if (!candidate) {
return false;
}
return (
resolveProviderIdForAuth(candidate, { config: params.cfg }) ===
resolveProviderIdForAuth(provider, { config: params.cfg })
);
};
const delimiterIndex = profileOverride.indexOf(":");
if (delimiterIndex < 0) {
return resolvesToTargetProvider(params.currentProvider);
}
const profileProvider = normalizeOptionalLowercaseString(
profileOverride.slice(0, delimiterIndex),
);
if (!profileProvider) {
return false;
}
return resolvesToTargetProvider(profileProvider);
}
function supportsSpawnLineage(storeKey: string): boolean {
return isSubagentSessionKey(storeKey) || isAcpSessionKey(storeKey);
}
@@ -611,8 +578,13 @@ export async function projectSessionsPatchEntry(params: {
next.execNode = trimmed;
}
}
if ("model" in patch) {
const agentModelFallback = isAgentSessionModelPatchOrigin()
? next.modelFallback?.source === "agent-patch"
? { ...next.modelFallback, ts: Math.max(now, next.modelFallback.ts + 1) }
: snapshotAgentModelFallback(cfg, next, sessionAgentId, now)
: undefined;
delete next.modelFallback;
const raw = patch.model;
if (raw === null) {
applyModelOverrideToSessionEntry({
@@ -680,6 +652,9 @@ export async function projectSessionsPatchEntry(params: {
markLiveSwitchPending: true,
});
}
if (agentModelFallback) {
next.modelFallback = agentModelFallback;
}
}
if (next.thinkingLevel && ("thinkingLevel" in patch || "model" in patch)) {
@@ -716,6 +691,19 @@ export async function projectSessionsPatchEntry(params: {
}
}
// A thinkingLevel change made on its own (no model switch) never touches the
// agent-patch revert marker, so realign its restore target with the user's
// newer choice; otherwise a later model-failure revert clobbers it.
if (
"thinkingLevel" in patch &&
!("model" in patch) &&
next.modelFallback?.source === "agent-patch"
) {
next.modelFallback = next.thinkingLevel
? { ...next.modelFallback, prevThinkingLevel: next.thinkingLevel }
: { ...next.modelFallback, prevThinkingLevel: undefined };
}
if ("sendPolicy" in patch) {
const raw = patch.sendPolicy;
if (raw === null) {
+8 -2
View File
@@ -150,8 +150,14 @@ describe("resolveGatewayScopedTools excludeToolNames", () => {
]);
expect(nonOwnerResult.tools.map((tool) => tool.name)).toEqual(["read", "sessions_spawn"]);
const args = readCreateToolsArgs(1);
expect(args.pluginToolDenylist).toEqual(["cron", "gateway", "nodes", "computer"]);
expect(args.inheritedToolDenylist).toEqual(["cron", "gateway", "nodes", "computer"]);
expect(args.pluginToolDenylist).toEqual(["cron", "gateway", "sessions", "nodes", "computer"]);
expect(args.inheritedToolDenylist).toEqual([
"cron",
"gateway",
"sessions",
"nodes",
"computer",
]);
});
it("keeps real gateway deny policy inheritable while excluding native dedup tools", () => {
+1
View File
@@ -76,6 +76,7 @@ const SESSION_ENTRY_RESERVED_SLOT_KEY_LIST = [
"modelOverrideSource",
"modelOverrideFallbackOriginProvider",
"modelOverrideFallbackOriginModel",
"modelFallback",
"authProfileOverride",
"authProfileOverrideSource",
"authProfileOverrideCompactionCount",
+1
View File
@@ -48,6 +48,7 @@ export const GATEWAY_CONTROL_PLANE_TOOLS = ["cron", "gateway"] as const;
*/
export const GATEWAY_OWNER_ONLY_CORE_TOOLS = [
...GATEWAY_CONTROL_PLANE_TOOLS,
"sessions",
"nodes",
"computer",
] as const;
+103
View File
@@ -0,0 +1,103 @@
import os from "node:os";
import type { SessionEntry } from "../config/sessions.js";
import { resolveSessionFilePath, resolveSessionFilePathOptions } from "../config/sessions/paths.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { formatDurationCompact } from "../infra/format-time/format-duration.ts";
import {
loadSessionCostSummariesFromCache,
resolveExistingUsageSessionFile,
} from "../infra/session-cost-usage.js";
import { formatTokenCount, formatUsd } from "../utils/usage-format.js";
export function buildStatusUptimeLine(): string {
const format = (ms: number) => formatDurationCompact(ms, { spaced: true }) ?? "0s";
const gatewayMs = Math.max(0, Math.round(process.uptime() * 1000));
const systemMs = Math.max(0, Math.round(os.uptime() * 1000));
return `⏱️ Uptime: gateway ${format(gatewayMs)} · system ${format(systemMs)}`;
}
export async function resolveSessionCostLine(
params: {
cfg: OpenClawConfig;
agentId: string;
sessionEntry?: SessionEntry;
storePath?: string;
},
deps: {
load?: typeof loadSessionCostSummariesFromCache;
now?: () => number;
timeoutMs?: number;
} = {},
): Promise<string | undefined> {
const sessionId = params.sessionEntry?.sessionId?.trim();
if (!sessionId) {
return undefined;
}
let sessionFile: string | undefined;
try {
const pathOpts = resolveSessionFilePathOptions({
storePath: params.storePath,
agentId: params.agentId,
});
sessionFile = resolveExistingUsageSessionFile({
sessionId,
sessionEntry: params.sessionEntry,
sessionFile: resolveSessionFilePath(sessionId, params.sessionEntry, pathOpts),
agentId: params.agentId,
});
} catch {
return undefined;
}
if (!sessionFile) {
return undefined;
}
const now = deps.now?.() ?? Date.now();
const date = new Date(now);
const startMs = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
const timeoutMs = deps.timeoutMs ?? 3_500;
let timeout: NodeJS.Timeout | undefined;
try {
const loaded = await Promise.race([
(deps.load ?? loadSessionCostSummariesFromCache)({
sessions: [{ sessionId, sessionFile }],
config: params.cfg,
agentId: params.agentId,
startMs,
endMs: now,
dayBucket: { mode: "utc-offset", utcOffsetMinutes: -date.getTimezoneOffset() },
requestRefresh: false,
}),
new Promise<never>((_, reject) => {
timeout = setTimeout(() => reject(new Error("session cost timeout")), timeoutMs);
}),
]).finally(() => {
if (timeout) {
clearTimeout(timeout);
}
});
const summary = loaded.cacheStatus.status === "fresh" ? loaded.summaries[0] : null;
if (!summary) {
return undefined;
}
const cost = summary.missingCostEntries > 0 ? "cost partial" : formatUsd(summary.totalCost);
return `💵 ${cost ? `${cost} · ` : ""}${formatTokenCount(summary.totalTokens)} tok (today)`;
} catch {
return undefined;
}
}
export async function appendSessionCostLine(
usageLine: string | null,
cfg: OpenClawConfig,
agentId: string,
sessionEntry?: SessionEntry,
storePath?: string,
): Promise<string | null> {
const line = await resolveSessionCostLine({
cfg,
agentId,
...(sessionEntry ? { sessionEntry } : {}),
...(storePath ? { storePath } : {}),
});
return line ? [usageLine, line].filter(Boolean).join("\n") : usageLine;
}
+132
View File
@@ -1,4 +1,6 @@
import { describe, expect, it } from "vitest";
import { formatSqliteSessionFileMarker } from "../config/sessions/sqlite-marker.js";
import { resolveSessionCostLine } from "./status-runtime-lines.js";
import { buildStatusText } from "./status-text.js";
type StatusTextParams = Parameters<typeof buildStatusText>[0];
@@ -94,3 +96,133 @@ describe("buildStatusText channel features", () => {
expect(text).toContain("enable richMessages for this Telegram account");
});
});
describe("session status cost line", () => {
const sessionEntry = {
sessionId: "cost-session",
updatedAt: 0,
sessionFile: formatSqliteSessionFileMarker({
agentId: "main",
sessionId: "cost-session",
storePath: "/tmp/openclaw-status-cost/sessions.json",
}),
};
it("shows cached current-session cost and tokens", async () => {
const load = async () => ({
cacheStatus: {
status: "fresh" as const,
cachedFiles: 1,
pendingFiles: 0,
staleFiles: 0,
},
summaries: [
{
input: 400_000,
output: 56_000,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 456_000,
totalCost: 1.23,
inputCost: 1,
outputCost: 0.23,
cacheReadCost: 0,
cacheWriteCost: 0,
missingCostEntries: 0,
},
],
});
await expect(
resolveSessionCostLine(
{ cfg: {}, agentId: "main", sessionEntry },
{ load, now: () => new Date(2026, 6, 14, 12).getTime() },
),
).resolves.toBe("💵 $1.23 · 456k tok (today)");
});
it("omits a cold cost cache", async () => {
await expect(
resolveSessionCostLine(
{ cfg: {}, agentId: "main", sessionEntry },
{
load: async () => ({
cacheStatus: {
status: "partial",
cachedFiles: 0,
pendingFiles: 1,
staleFiles: 0,
},
summaries: [null],
}),
},
),
).resolves.toBeUndefined();
});
it("omits a stale cached summary", async () => {
await expect(
resolveSessionCostLine(
{ cfg: {}, agentId: "main", sessionEntry },
{
load: async () => ({
cacheStatus: {
status: "stale",
cachedFiles: 0,
pendingFiles: 1,
staleFiles: 1,
},
summaries: [
{
input: 1,
output: 1,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 2,
totalCost: 1,
inputCost: 1,
outputCost: 0,
cacheReadCost: 0,
cacheWriteCost: 0,
missingCostEntries: 0,
},
],
}),
},
),
).resolves.toBeUndefined();
});
it("marks incomplete pricing", async () => {
await expect(
resolveSessionCostLine(
{ cfg: {}, agentId: "main", sessionEntry },
{
load: async () => ({
cacheStatus: {
status: "fresh",
cachedFiles: 1,
pendingFiles: 0,
staleFiles: 0,
},
summaries: [
{
input: 400_000,
output: 56_000,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 456_000,
totalCost: 1.23,
inputCost: 1,
outputCost: 0.23,
cacheReadCost: 0,
cacheWriteCost: 0,
missingCostEntries: 1,
},
],
}),
},
),
).resolves.toBe("💵 cost partial · 456k tok (today)");
});
});
+2 -12
View File
@@ -1,4 +1,3 @@
import os from "node:os";
import path from "node:path";
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
import {
@@ -32,7 +31,6 @@ import { toAgentModelListLike } from "../config/model-input.js";
import type { SessionEntry } from "../config/sessions.js";
import { hasSessionAutoModelFallbackProvenance } from "../config/sessions/model-override-provenance.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { formatDurationCompact } from "../infra/format-time/format-duration.ts";
import {
formatUsageWindowSummary,
loadProviderUsageSummary,
@@ -58,6 +56,7 @@ import {
} from "./codex-synthetic-usage.js";
import { resolveActiveFallbackState } from "./fallback-notice-state.js";
import { formatCompactPluginHealthLine } from "./status-plugin-health.js";
import { appendSessionCostLine, buildStatusUptimeLine } from "./status-runtime-lines.js";
import type { BuildStatusTextParams } from "./status-text.types.js";
// Status text assembly gathers runtime/model/session/task facts, then delegates
@@ -270,16 +269,6 @@ function formatAgentTaskCountsLine(agentId: string): string | undefined {
return `📌 Tasks: ${snapshot.activeCount} active · ${snapshot.totalCount} total · agent-local`;
}
function formatStatusUptimeDuration(ms: number): string {
return formatDurationCompact(ms, { spaced: true }) ?? "0s";
}
function buildStatusUptimeLine(): string {
const gatewayUptimeMs = Math.max(0, Math.round(process.uptime() * 1000));
const systemUptimeMs = Math.max(0, Math.round(os.uptime() * 1000));
return `⏱️ Uptime: gateway ${formatStatusUptimeDuration(gatewayUptimeMs)} · system ${formatStatusUptimeDuration(systemUptimeMs)}`;
}
async function resolveRuntimePluginHealthLine(): Promise<string | undefined> {
try {
const { collectRuntimePluginHealthSnapshot } = await loadStatusPluginHealthRuntime();
@@ -499,6 +488,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
usageLine = null;
}
}
usageLine = await appendSessionCostLine(usageLine, cfg, statusAgentId, sessionEntry, storePath);
const { getFollowupQueueDepth, resolveQueueSettings } = await loadStatusQueueRuntime();
const queueSettings = resolveQueueSettings({
cfg,
@@ -220,6 +220,22 @@
"thread": {
"description": "Bind new chat thread when supported; true defaults mode=\"session\".",
"type": "boolean"
},
"visible": {
"description": "visible: user sees session in UI. Use when user asked or talks via web/app.",
"type": "boolean"
},
"worktree": {
"description": "Visible session worktree",
"type": "boolean"
},
"worktreeBaseRef": {
"description": "Worktree base ref",
"type": "string"
},
"worktreeName": {
"description": "Worktree name",
"type": "string"
}
},
"required": ["task"],
@@ -1343,16 +1359,20 @@
},
{
"deferLoading": true,
"description": "List requester-session active/recent subagents. If available, wait via sessions_yield; never poll-loop.",
"description": "Background work: subagents, media gen, cron runs. list/cancel.",
"inputSchema": {
"properties": {
"action": {
"enum": ["list"],
"enum": ["list", "cancel"],
"type": "string"
},
"recentMinutes": {
"minimum": 1,
"type": "integer"
},
"taskId": {
"description": "Task id",
"type": "string"
}
},
"type": "object"
@@ -216,6 +216,22 @@
},
"thinking": {
"type": "string"
},
"visible": {
"description": "visible: user sees session in UI. Use when user asked or talks via web/app.",
"type": "boolean"
},
"worktree": {
"description": "Visible session worktree",
"type": "boolean"
},
"worktreeBaseRef": {
"description": "Worktree base ref",
"type": "string"
},
"worktreeName": {
"description": "Worktree name",
"type": "string"
}
},
"required": ["task"],
@@ -1375,16 +1391,20 @@
},
{
"deferLoading": true,
"description": "List requester-session active/recent subagents. If available, wait via sessions_yield; never poll-loop.",
"description": "Background work: subagents, media gen, cron runs. list/cancel.",
"inputSchema": {
"properties": {
"action": {
"enum": ["list"],
"enum": ["list", "cancel"],
"type": "string"
},
"recentMinutes": {
"minimum": 1,
"type": "integer"
},
"taskId": {
"description": "Task id",
"type": "string"
}
},
"type": "object"
@@ -216,6 +216,22 @@
},
"thinking": {
"type": "string"
},
"visible": {
"description": "visible: user sees session in UI. Use when user asked or talks via web/app.",
"type": "boolean"
},
"worktree": {
"description": "Visible session worktree",
"type": "boolean"
},
"worktreeBaseRef": {
"description": "Worktree base ref",
"type": "string"
},
"worktreeName": {
"description": "Worktree name",
"type": "string"
}
},
"required": ["task"],
@@ -1339,16 +1355,20 @@
},
{
"deferLoading": true,
"description": "List requester-session active/recent subagents. If available, wait via sessions_yield; never poll-loop.",
"description": "Background work: subagents, media gen, cron runs. list/cancel.",
"inputSchema": {
"properties": {
"action": {
"enum": ["list"],
"enum": ["list", "cancel"],
"type": "string"
},
"recentMinutes": {
"minimum": 1,
"type": "integer"
},
"taskId": {
"description": "Task id",
"type": "string"
}
},
"type": "object"
@@ -208,8 +208,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 53240,
"roughTokens": 13310
"chars": 53832,
"roughTokens": 13458
},
"openClawDeveloperInstructions": {
"chars": 3431,
@@ -220,8 +220,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6989
},
"totalWithDynamicToolsJson": {
"chars": 81198,
"roughTokens": 20300
"chars": 81790,
"roughTokens": 20448
},
"userInputText": {
"chars": 1442,
@@ -208,8 +208,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 52967,
"roughTokens": 13242
"chars": 53559,
"roughTokens": 13390
},
"openClawDeveloperInstructions": {
"chars": 2322,
@@ -220,8 +220,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6610
},
"totalWithDynamicToolsJson": {
"chars": 79407,
"roughTokens": 19852
"chars": 79999,
"roughTokens": 20000
},
"userInputText": {
"chars": 1033,
@@ -209,8 +209,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 54257,
"roughTokens": 13565
"chars": 54849,
"roughTokens": 13713
},
"openClawDeveloperInstructions": {
"chars": 2341,
@@ -221,8 +221,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6745
},
"totalWithDynamicToolsJson": {
"chars": 81236,
"roughTokens": 20309
"chars": 81828,
"roughTokens": 20457
},
"userInputText": {
"chars": 1271,