mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
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:
@@ -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 l’automatisation n’est pas valide."</string>
|
||||
<string name="native_4ec30e9d85725d41" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Liste d’autorisation"</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 n’a é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 d’activation"</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">"L’approbation du Gateway est en attente. Exécutez ceci sur l’hô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",
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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, {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")),
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -76,6 +76,7 @@ const SESSION_ENTRY_RESERVED_SLOT_KEY_LIST = [
|
||||
"modelOverrideSource",
|
||||
"modelOverrideFallbackOriginProvider",
|
||||
"modelOverrideFallbackOriginModel",
|
||||
"modelFallback",
|
||||
"authProfileOverride",
|
||||
"authProfileOverrideSource",
|
||||
"authProfileOverrideCompactionCount",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
+22
-2
@@ -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"
|
||||
|
||||
+22
-2
@@ -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"
|
||||
|
||||
+22
-2
@@ -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"
|
||||
|
||||
Vendored
+4
-4
@@ -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,
|
||||
|
||||
test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md
Vendored
+4
-4
@@ -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,
|
||||
|
||||
Vendored
+4
-4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user