diff --git a/skills/skills/langbot-mcp-ops/SKILL.md b/skills/skills/langbot-mcp-ops/SKILL.md index ceec4d2ec..591767c7f 100644 --- a/skills/skills/langbot-mcp-ops/SKILL.md +++ b/skills/skills/langbot-mcp-ops/SKILL.md @@ -209,3 +209,7 @@ recommendation only when LangBot Models is enabled and the Workspace owner is bound to a LangBot Account. It returns a nullable `uuid`. New processor LLM selector defaults use this preference; existing configurations are not rewritten. Embedding and rerank models are not eligible. + +### Reset a bot session context + +Use `reset_session_context(bot_id, session_id)` only when a user asks to start a session afresh. It requires `resource.manage`, preserves monitoring records, and refuses sessions with active tasks. It clears conversation runner state and excludes earlier transcript entries from subsequent model context. Files and long-term memory are not removed. diff --git a/src/langbot/pkg/agent/runner/context_reset.py b/src/langbot/pkg/agent/runner/context_reset.py new file mode 100644 index 000000000..73d6b3fd4 --- /dev/null +++ b/src/langbot/pkg/agent/runner/context_reset.py @@ -0,0 +1,46 @@ +"""Scoped context reset markers, independent of retained monitoring history.""" + +import hashlib +import json + +import sqlalchemy as sa + +from ...entity.persistence.runner_state import RunnerState + +HOST_RESET_RUNNER = '__langbot_context_reset__' + + +def reset_scope(workspace, bot, conversation): + return 'host-reset:' + hashlib.sha256(json.dumps([workspace, bot, conversation]).encode()).hexdigest() + + +async def get_reset_generation(engine, workspace, bot, conversation): + if not all((workspace, bot, conversation)): + return None + async with engine.connect() as connection: + value = ( + await connection.execute( + sa.select(RunnerState.value_json).where( + RunnerState.scope_key == reset_scope(workspace, bot, conversation), + RunnerState.state_key == 'generation', + RunnerState.runner_id == HOST_RESET_RUNNER, + ) + ) + ).scalar_one_or_none() + return json.loads(value) if value else None + + +def active_history_condition(transcript): + cutoff = ( + sa.select(sa.func.max(RunnerState.created_at)) + .where( + RunnerState.runner_id == HOST_RESET_RUNNER, + RunnerState.state_key == 'generation', + RunnerState.workspace_id == transcript.workspace_id, + RunnerState.bot_id == transcript.bot_id, + RunnerState.conversation_id == transcript.conversation_id, + ) + .correlate(transcript) + .scalar_subquery() + ) + return sa.or_(cutoff.is_(None), transcript.created_at > cutoff) diff --git a/src/langbot/pkg/agent/runner/persistent_state_store.py b/src/langbot/pkg/agent/runner/persistent_state_store.py index 7477838b8..e0a58af1a 100644 --- a/src/langbot/pkg/agent/runner/persistent_state_store.py +++ b/src/langbot/pkg/agent/runner/persistent_state_store.py @@ -191,7 +191,9 @@ class PersistentStateStore: # Seed external.conversation_id from event.conversation_id if not set if self._check_scope_enabled('conversation', binding) and event.conversation_id: if 'external.conversation_id' not in snapshot['conversation']: - snapshot['conversation']['external.conversation_id'] = event.conversation_id + from .context_reset import get_reset_generation + generation = await get_reset_generation(self._db_engine, event.workspace_id, event.bot_id, event.conversation_id) + snapshot['conversation']['external.conversation_id'] = generation or event.conversation_id return snapshot diff --git a/src/langbot/pkg/agent/runner/transcript_store.py b/src/langbot/pkg/agent/runner/transcript_store.py index aec02502b..6f1a7757d 100644 --- a/src/langbot/pkg/agent/runner/transcript_store.py +++ b/src/langbot/pkg/agent/runner/transcript_store.py @@ -335,6 +335,8 @@ class TranscriptStore: thread_id: str | None, strict_thread: bool, ) -> typing.Any: + from .context_reset import active_history_condition + query = query.where(active_history_condition(Transcript)) if bot_id is not None: query = query.where(Transcript.bot_id == bot_id) if workspace_id is not None: diff --git a/src/langbot/pkg/api/http/controller/groups/monitoring.py b/src/langbot/pkg/api/http/controller/groups/monitoring.py index 3f9dae245..929c061e1 100644 --- a/src/langbot/pkg/api/http/controller/groups/monitoring.py +++ b/src/langbot/pkg/api/http/controller/groups/monitoring.py @@ -545,6 +545,24 @@ class MonitoringRouterGroup(group.RouterGroup): } ) + @self.route('/sessions/reset-context', methods=['POST'], + auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, permission=Permission.RESOURCE_MANAGE) + async def reset_session_context(request_context: RequestContext): + body = await quart.request.get_json() + if not isinstance(body, dict): + return self.http_status(400, 'invalid_request', 'Expected a JSON object') + try: + result = await self.ap.monitoring_service.reset_session_context( + request_context, body.get('bot_id'), body.get('session_id'), + ) + except ValueError as exc: + return self.http_status(400, 'invalid_request', str(exc)) + except LookupError as exc: + return self.http_status(404, 'resource_not_found', str(exc)) + except RuntimeError as exc: + return self.http_status(409, 'session_busy', str(exc)) + return self.success(data=result) + @self.route( '/sessions//analysis', methods=['GET'], diff --git a/src/langbot/pkg/api/http/service/monitoring.py b/src/langbot/pkg/api/http/service/monitoring.py index fcd98d2aa..fc3e30813 100644 --- a/src/langbot/pkg/api/http/service/monitoring.py +++ b/src/langbot/pkg/api/http/service/monitoring.py @@ -2193,6 +2193,60 @@ class MonitoringService(ExecutionDetailsMixin): total, ) + @_workspace_transaction + async def reset_session_context(self, context: TenantContext, bot_id: str, session_id: str) -> dict: + """Reset model context without deleting monitoring history or other sessions.""" + from ....agent.runner.context_reset import HOST_RESET_RUNNER, reset_scope + from ....entity.persistence.runner_state import RunnerState + + if not isinstance(bot_id, str) or not bot_id or not isinstance(session_id, str) or not session_id: + raise ValueError('bot_id and session_id are required') + workspace = require_workspace_uuid(context) + execute = self.ap.persistence_mgr.execute_async + source = self._session_message_source(workspace, [bot_id]) + exists = (await execute(sqlalchemy.select(source.c.session_id).where( + source.c.session_id == session_id, + ).limit(1))).first() + if exists is None: + session = persistence_monitoring.MonitoringSession + exists = (await execute(sqlalchemy.select(session.session_id).where( + session.workspace_uuid == workspace, session.bot_id == bot_id, session.session_id == session_id, + ).limit(1))).first() + if exists is None: + raise LookupError('Session not found') + # Legacy sessions use uppercase PERSON/GROUP; envelope IDs use lowercase. + conversation = re.sub(r'^(PERSON|GROUP)_', lambda match: match[1].lower() + '_', session_id) + run = persistence_agent_run.AgentRun + busy = (await execute(sqlalchemy.select(run.run_id).where( + run.workspace_id == workspace, run.bot_id == bot_id, run.conversation_id == conversation, + run.status.in_(['created', 'queued', 'claimed', 'running']), + ).limit(1))).first() + message = persistence_monitoring.MonitoringMessage + pending = (await execute(sqlalchemy.select(message.id).where( + message.workspace_uuid == workspace, message.bot_id == bot_id, + message.session_id.in_([session_id, conversation]), message.status == 'pending', + ).limit(1))).first() + interaction = persistence_agent_interaction.AgentInteraction + waiting = (await execute(sqlalchemy.select(interaction.id).where( + interaction.workspace_id == workspace, interaction.bot_id == bot_id, + interaction.conversation_id == conversation, interaction.status.in_(['pending', 'submitted']), + sqlalchemy.or_(interaction.expires_at.is_(None), interaction.expires_at > datetime.datetime.utcnow()), + ).limit(1))).first() + if busy or pending or waiting: + raise RuntimeError('Session is processing a task; wait until it finishes') + await execute(sqlalchemy.delete(RunnerState).where( + RunnerState.workspace_id == workspace, RunnerState.bot_id == bot_id, + RunnerState.conversation_id == conversation, RunnerState.scope == 'conversation', + )) + generation = str(uuid.uuid4()) + await execute(sqlalchemy.insert(RunnerState).values( + runner_id=HOST_RESET_RUNNER, binding_identity=HOST_RESET_RUNNER, scope='conversation', + scope_key=reset_scope(workspace, bot_id, conversation), state_key='generation', + value_json=json.dumps(generation), workspace_id=workspace, bot_id=bot_id, + conversation_id=conversation, created_at=datetime.datetime.utcnow(), + )) + return {'reset': True, 'session_id': session_id} + async def get_sessions( self, context: TenantContext, diff --git a/src/langbot/pkg/api/mcp/server.py b/src/langbot/pkg/api/mcp/server.py index 030c89f4e..1a6f73fd9 100644 --- a/src/langbot/pkg/api/mcp/server.py +++ b/src/langbot/pkg/api/mcp/server.py @@ -211,6 +211,12 @@ class LangBotMCPServer: async def get_inflight_executions() -> str: return _dump(await ap.monitoring_service.get_inflight_snapshot(_authorized(Permission.RESOURCE_VIEW))) + @mcp.tool(description='Reset a bot session model context, preserving monitoring history. Refuses busy sessions.') + async def reset_session_context(bot_id: str, session_id: str) -> str: + return _dump(await ap.monitoring_service.reset_session_context( + _authorized(Permission.RESOURCE_MANAGE), bot_id, session_id, + )) + @mcp.tool( description='List Workspace executions, including events that did not start a processor. The legacy pipeline_ids filter accepts Agent, Pipeline and event processor IDs. Returns bounded rows and summary metrics.' ) diff --git a/src/langbot/pkg/provider/session/sessionmgr.py b/src/langbot/pkg/provider/session/sessionmgr.py index 74b9a8c40..a43f3c271 100644 --- a/src/langbot/pkg/provider/session/sessionmgr.py +++ b/src/langbot/pkg/provider/session/sessionmgr.py @@ -271,8 +271,19 @@ class SessionManager: async def get_session(self, query: pipeline_query.Query) -> provider_session.Session: """获取会话""" session_key, execution_context = _query_session_key(query) + from ...agent.runner.context_reset import get_reset_generation + generation = None + persistence = getattr(self.ap, 'persistence_mgr', None) + if persistence is not None: + generation = await get_reset_generation( + persistence.get_db_engine(), execution_context.workspace_uuid, query.bot_uuid, + f'{str(query.launcher_type.value).lower()}_{query.launcher_id}', + ) now = time.monotonic() session = self._session_index.get(session_key) + if session is not None and getattr(session, '_context_generation', None) != generation: + self._remove_session(session) + session = None if session is not None: self._touch_session(session, session_key, now) return session @@ -319,6 +330,7 @@ class SessionManager: object.__setattr__(session, 'bot_uuid', query.bot_uuid) object.__setattr__(session, '_execution_context', session_context) object.__setattr__(session, '_langbot_session_key', session_key) + object.__setattr__(session, '_context_generation', generation) object.__setattr__(session, '_langbot_session_concurrency', session_concurrency) session._semaphore = asyncio.Semaphore(session_concurrency) self._session_index[session_key] = session diff --git a/tests/unit_tests/api/service/test_session_context_reset.py b/tests/unit_tests/api/service/test_session_context_reset.py new file mode 100644 index 000000000..ece96c7bd --- /dev/null +++ b/tests/unit_tests/api/service/test_session_context_reset.py @@ -0,0 +1,148 @@ +import contextlib +import datetime +import json +from types import SimpleNamespace + +import pytest +import sqlalchemy as sa +from sqlalchemy.ext.asyncio import create_async_engine + +from langbot.pkg.api.http.service.monitoring import MonitoringService +from langbot.pkg.agent.runner.context_reset import get_reset_generation +from langbot.pkg.agent.runner.transcript_store import TranscriptStore +from langbot.pkg.entity.persistence.base import Base +from langbot.pkg.entity.persistence.monitoring import MonitoringSession +from langbot.pkg.entity.persistence.runner_state import RunnerState +from langbot.pkg.entity.persistence.agent_run import AgentRun +from langbot.pkg.entity.persistence.transcript import Transcript + + +class Persistence: + def __init__(self, engine): + self.engine = engine + + def get_db_engine(self): + return self.engine + + @contextlib.asynccontextmanager + async def tenant_uow(self, workspace): + async with self.engine.begin() as conn: + self.conn = conn + yield + + async def execute_async(self, statement): + return await self.conn.execute(statement) + + +@pytest.fixture +async def context_service(tmp_path): + engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "reset.db"}') + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + await conn.execute( + sa.insert(MonitoringSession).values( + workspace_uuid='w', + bot_id='b', + session_id='person_u', + bot_name='Bot', + pipeline_id='p', + pipeline_name='Pipeline', + start_time=datetime.datetime.utcnow(), + last_activity=datetime.datetime.utcnow(), + ) + ) + for workspace, bot, conversation, scope in [ + ('w', 'b', 'person_u', 'conversation'), + ('w', 'b2', 'person_u', 'conversation'), + ('w2', 'b', 'person_u', 'conversation'), + ('w', 'b', 'person_other', 'conversation'), + ('w', 'b', 'person_u', 'actor'), + ]: + await conn.execute( + sa.insert(RunnerState).values( + workspace_id=workspace, + bot_id=bot, + conversation_id=conversation, + scope=scope, + scope_key=f'{workspace}:{bot}:{conversation}:{scope}', + state_key='checkpoint', + runner_id='runner', + binding_identity='binding', + value_json=json.dumps('old'), + ) + ) + yield MonitoringService(SimpleNamespace(persistence_mgr=Persistence(engine))), engine + await engine.dispose() + + +@pytest.mark.asyncio +async def test_reset_preserves_logs_and_isolates_context(context_service): + service, engine = context_service + store = TranscriptStore(engine) + for bot in ['b', 'b2']: + await store.append_transcript(None, 'event', 'person_u', 'user', bot_id=bot, workspace_id='w', content='old') + await service.reset_session_context('w', 'b', 'person_u') + assert await get_reset_generation(engine, 'w', 'b', 'person_u') + async with engine.connect() as conn: + assert (await conn.execute(sa.select(sa.func.count()).select_from(Transcript))).scalar() == 2 + assert (await conn.execute(sa.select(sa.func.count()).select_from(MonitoringSession))).scalar() == 1 + assert ( + await conn.execute( + sa.select(sa.func.count()).select_from(RunnerState).where(RunnerState.runner_id == 'runner') + ) + ).scalar() == 4 + page = await store.page_transcript('person_u', bot_id='b', workspace_id='w') + assert not page[0] + assert (await store.page_transcript('person_u', bot_id='b2', workspace_id='w'))[0] + await store.append_transcript(None, 'new', 'person_u', 'user', bot_id='b', workspace_id='w', content='fresh') + assert len((await store.page_transcript('person_u', bot_id='b', workspace_id='w'))[0]) == 1 + await service.reset_session_context('w', 'b', 'person_u') + assert not (await store.page_transcript('person_u', bot_id='b', workspace_id='w'))[0] + + +@pytest.mark.asyncio +async def test_reset_refuses_wrong_scope_and_running_tasks(context_service): + service, engine = context_service + for workspace, bot in [('other', 'b'), ('w', 'other')]: + with pytest.raises(LookupError): + await service.reset_session_context(workspace, bot, 'person_u') + async with engine.begin() as conn: + await conn.execute( + sa.insert(AgentRun).values( + run_id='run', + event_id='evt', + agent_id='agent', + binding_id='binding', + runner_id='runner', + workspace_id='w', + bot_id='b', + conversation_id='person_u', + status='running', + ) + ) + with pytest.raises(RuntimeError): + await service.reset_session_context('w', 'b', 'person_u') + assert await get_reset_generation(engine, 'w', 'b', 'person_u') is None + + +@pytest.mark.asyncio +async def test_reset_starts_new_external_runner_conversation(context_service): + from unit_tests.agent.test_state_store import FakeBinding, FakeEventEnvelope, make_descriptor + from langbot.pkg.agent.runner.persistent_state_store import PersistentStateStore + + service, engine = context_service + store = PersistentStateStore(engine) + event = FakeEventEnvelope(workspace_id='w', bot_id='b', conversation_id='person_u') + binding = FakeBinding() + descriptor = make_descriptor() + await store.apply_update_from_event( + event, binding, descriptor, 'conversation', 'external.conversation_id', 'remote-old', None + ) + assert (await store.build_snapshot_from_event(event, binding, descriptor))['conversation'][ + 'external.conversation_id' + ] == 'remote-old' + await service.reset_session_context('w', 'b', 'person_u') + snapshot = await store.build_snapshot_from_event(event, binding, descriptor) + assert snapshot['conversation']['external.conversation_id'] == await get_reset_generation( + engine, 'w', 'b', 'person_u' + ) diff --git a/tests/unit_tests/provider/test_session_manager.py b/tests/unit_tests/provider/test_session_manager.py index 469743c5b..456a3a9c9 100644 --- a/tests/unit_tests/provider/test_session_manager.py +++ b/tests/unit_tests/provider/test_session_manager.py @@ -11,7 +11,7 @@ from __future__ import annotations import pytest import asyncio from types import SimpleNamespace -from unittest.mock import Mock +from unittest.mock import Mock, AsyncMock from importlib import import_module import langbot_plugin.api.entities.builtin.provider.session as provider_session @@ -26,6 +26,13 @@ from langbot.pkg.pipeline.pool import ( ) +@pytest.fixture(autouse=True) +def reset_generation(monkeypatch): + lookup = AsyncMock(return_value=None) + monkeypatch.setattr('langbot.pkg.agent.runner.context_reset.get_reset_generation', lookup) + return lookup + + TEST_CONTEXT = ExecutionContext( instance_uuid='instance-test', workspace_uuid='workspace-test', @@ -447,7 +454,7 @@ class TestSessionManagerWorkspaceIsolation: sessions.append(await manager.get_session(query)) assert len(manager.session_list) == 2 - assert sessions[0] not in manager.session_list + assert all(session is not sessions[0] for session in manager.session_list) assert sessions[1:] == manager.session_list assert await manager.get_session(queries[-1]) is manager.session_list[-1] @@ -556,3 +563,18 @@ class TestSessionManagerWorkspaceIsolation: content = conversation.messages[0].content assert content[1].image_base64 is None assert content[2].file_base64 is None + + +@pytest.mark.asyncio +async def test_reset_generation_replaces_cached_session(reset_generation): + app = Mock() + app.instance_config.data = {'concurrency': {'session': 5}} + manager = get_session_module().SessionManager(app) + query = scoped_query() + previous = await manager.get_session(query) + assert await manager.get_session(query) is previous + reset_generation.return_value = 'new-context' + fresh = await manager.get_session(query) + assert fresh is not previous + assert not fresh.conversations + assert await manager.get_session(query) is fresh diff --git a/web/src/app/home/bots/components/bot-session/BotSessionMonitor.tsx b/web/src/app/home/bots/components/bot-session/BotSessionMonitor.tsx index ddfa72710..86cf40de5 100644 --- a/web/src/app/home/bots/components/bot-session/BotSessionMonitor.tsx +++ b/web/src/app/home/bots/components/bot-session/BotSessionMonitor.tsx @@ -1,3 +1,6 @@ +import { useCurrentWorkspace } from '@/app/infra/http'; +import { Button } from '@/components/ui/button'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import LoadErrorState from '@/components/LoadErrorState'; import React, { useState, @@ -13,6 +16,7 @@ import { httpClient } from '@/app/infra/http/HttpClient'; import { ScrollArea } from '@/components/ui/scroll-area'; import { cn } from '@/lib/utils'; import { + RotateCcw, Ban, Bot, Copy, @@ -143,6 +147,23 @@ const BotSessionMonitor = forwardRef< BotSessionMonitorProps >(function BotSessionMonitor({ botId }, ref) { const { t } = useTranslation(); + const workspace = useCurrentWorkspace(); + const canReset = workspace?.permissions.includes('resource.manage') ?? false; + const [resetTarget, setResetTarget] = useState(null); + const [resetting, setResetting] = useState(false); + const resetContext = async () => { + if (!resetTarget || resetting) return; + setResetting(true); + try { + await httpClient.resetSessionContext(botId, resetTarget); + toast.success(t('bots.sessionMonitor.resetSuccess')); + setResetTarget(null); + } catch { + toast.error(t('bots.sessionMonitor.resetError')); + } finally { + setResetting(false); + } + }; const [sessions, setSessions] = useState([]); const [sessionTotal, setSessionTotal] = useState(0); const [sessionPage, setSessionPage] = useState(0); @@ -686,7 +707,7 @@ const BotSessionMonitor = forwardRef< {/* Left Panel: Session List */}
{/* Admin header */} -
+
+ )}
{/* Messages Area */} @@ -1194,6 +1222,21 @@ const BotSessionMonitor = forwardRef<
+ { if (!open && !resetting) setResetTarget(null); }}> + + + {t('bots.sessionMonitor.resetContext')} + {t('bots.sessionMonitor.resetDescription')} + + + + + + + { + return this.post('/api/v1/monitoring/sessions/reset-context', { + bot_id: botId, session_id: sessionId, + }); + } + public getSessionAnalysis( sessionId: string, botId: string, diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index c3f06a22a..9a8c859a3 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -1172,6 +1172,11 @@ const enUS = { imagesAttached: 'image(s) attached', noLogs: 'No logs yet', sessionMonitor: { + resetContext: "Reset context", + resetDescription: "New messages will start with an empty context. Monitoring history is kept; files and long-term memory are unchanged. Wait for active tasks to finish before resetting.", + resetSuccess: "Context reset. New messages will start a fresh conversation.", + resetError: "Could not reset context. Wait for active tasks to finish and try again.", + title: 'Sessions', sessions: 'Sessions', noSessions: 'No sessions found', diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts index 7f08df1cd..b45a140de 100644 --- a/web/src/i18n/locales/es-ES.ts +++ b/web/src/i18n/locales/es-ES.ts @@ -1194,6 +1194,11 @@ const esES = { imagesAttached: 'imagen(es) adjunta(s)', noLogs: 'Aún no hay registros', sessionMonitor: { + resetContext: "Restablecer contexto", + resetDescription: "Los mensajes nuevos comenzarán sin contexto previo. Se conservan el historial de monitoreo, los archivos y la memoria a largo plazo. Espere a que terminen las tareas activas.", + resetSuccess: "Contexto restablecido.", + resetError: "No se pudo restablecer. Espere a que terminen las tareas activas y vuelva a intentarlo.", + title: 'Sesiones', sessions: 'Sesiones', noSessions: 'No se encontraron sesiones', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 1e928b045..5ece813ee 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -1182,6 +1182,11 @@ const jaJP = { imagesAttached: '枚の画像が添付されています', noLogs: 'ログはまだありません', sessionMonitor: { + resetContext: "コンテキストをリセット", + resetDescription: "新しいメッセージは空のコンテキストから開始します。監視履歴、ファイル、長期記憶は保持されます。実行中のタスクが完了してからリセットしてください。", + resetSuccess: "コンテキストをリセットしました。", + resetError: "リセットできませんでした。実行中のタスクが完了してから再試行してください。", + title: 'セッション監視', sessions: 'セッション一覧', noSessions: 'セッションが見つかりません', diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts index 4d7365660..0b49d05ba 100644 --- a/web/src/i18n/locales/ru-RU.ts +++ b/web/src/i18n/locales/ru-RU.ts @@ -1192,6 +1192,11 @@ const ruRU = { imagesAttached: 'изображений прикреплено', noLogs: 'Журналов пока нет', sessionMonitor: { + resetContext: "Сбросить контекст", + resetDescription: "Новые сообщения начнутся с пустого контекста. История мониторинга, файлы и долговременная память сохранятся. Дождитесь завершения активных задач.", + resetSuccess: "Контекст сброшен.", + resetError: "Не удалось сбросить контекст. Дождитесь завершения активных задач и повторите попытку.", + title: 'Сессии', sessions: 'Сессии', noSessions: 'Сессии не найдены', diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts index 56825b302..3b9c6c9a0 100644 --- a/web/src/i18n/locales/th-TH.ts +++ b/web/src/i18n/locales/th-TH.ts @@ -1152,6 +1152,11 @@ const thTH = { imagesAttached: 'รูปภาพที่แนบ', noLogs: 'ยังไม่มีบันทึก', sessionMonitor: { + resetContext: "รีเซ็ตบริบท", + resetDescription: "ข้อความใหม่จะเริ่มด้วยบริบทว่าง ประวัติการตรวจสอบ ไฟล์ และความจำระยะยาวจะยังคงอยู่ โปรดรอให้งานที่กำลังทำอยู่เสร็จก่อน", + resetSuccess: "รีเซ็ตบริบทแล้ว", + resetError: "รีเซ็ตไม่สำเร็จ โปรดรอให้งานที่กำลังทำอยู่เสร็จแล้วลองอีกครั้ง", + title: 'เซสชัน', sessions: 'เซสชัน', noSessions: 'ไม่พบเซสชัน', diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts index c4bdb6e7b..2c19bf139 100644 --- a/web/src/i18n/locales/vi-VN.ts +++ b/web/src/i18n/locales/vi-VN.ts @@ -1170,6 +1170,11 @@ const viVN = { imagesAttached: 'hình ảnh đính kèm', noLogs: 'Chưa có nhật ký', sessionMonitor: { + resetContext: "Đặt lại ngữ cảnh", + resetDescription: "Tin nhắn mới sẽ bắt đầu với ngữ cảnh trống. Lịch sử giám sát, tệp và bộ nhớ dài hạn được giữ lại. Hãy đợi các tác vụ đang chạy hoàn tất.", + resetSuccess: "Đã đặt lại ngữ cảnh.", + resetError: "Không thể đặt lại. Hãy đợi các tác vụ đang chạy hoàn tất rồi thử lại.", + title: 'Phiên', sessions: 'Phiên', noSessions: 'Không tìm thấy phiên nào', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 80b641b06..00018fb4f 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -1108,6 +1108,11 @@ const zhHans = { imagesAttached: '张图片', noLogs: '暂无日志', sessionMonitor: { + resetContext: "重置上下文", + resetDescription: "重置后,新消息将从空上下文开始。监控历史会保留,文件和长期记忆不受影响。请等待当前任务结束后再重置。", + resetSuccess: "上下文已重置,新消息将开启新的对话。", + resetError: "重置失败,请等待当前任务结束后重试。", + title: '会话监控', sessions: '会话列表', noSessions: '暂无会话', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 7534359f6..a01659ceb 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -1107,6 +1107,11 @@ const zhHant = { imagesAttached: '張圖片已附加', noLogs: '暫無日誌', sessionMonitor: { + resetContext: "重設上下文", + resetDescription: "重設後,新訊息將從空上下文開始。監控歷史會保留,檔案和長期記憶不受影響。請等待目前任務結束後再重設。", + resetSuccess: "上下文已重設,新訊息將開始新的對話。", + resetError: "重設失敗,請等待目前任務結束後重試。", + title: '會話監控', sessions: '會話列表', noSessions: '暫無會話',