feat: reset bot session context while retaining monitoring history

This commit is contained in:
Hyu
2026-10-10 22:35:27 +08:00
parent b8a7649ac2
commit 1c70218383
20 changed files with 408 additions and 5 deletions
+4
View File
@@ -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.
@@ -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)
@@ -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
@@ -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:
@@ -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/<session_id>/analysis',
methods=['GET'],
@@ -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,
+6
View File
@@ -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.'
)
@@ -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
@@ -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'
)
@@ -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
@@ -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<string | null>(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<SessionInfo[]>([]);
const [sessionTotal, setSessionTotal] = useState(0);
const [sessionPage, setSessionPage] = useState(0);
@@ -686,7 +707,7 @@ const BotSessionMonitor = forwardRef<
{/* Left Panel: Session List */}
<div className="max-h-48 md:max-h-none md:w-60 flex-shrink-0 border-b md:border-b-0 md:border-r flex flex-col min-h-0">
{/* Admin header */}
<div className="px-2 py-1.5 border-b shrink-0 flex items-center justify-between">
<div className="px-2 py-1.5 border-b shrink-0 flex items-center justify-between gap-4">
<button
type="button"
className="inline-flex items-center gap-1.5 text-sm font-medium hover:text-foreground transition-colors"
@@ -860,7 +881,7 @@ const BotSessionMonitor = forwardRef<
selectedSession?.user_id ||
selectedSessionId.slice(0, 20)}
</div>
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mt-0.5">
<div className="flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground mt-0.5">
{parseSessionType(selectedSessionId) && (
<span>{parseSessionType(selectedSessionId)}</span>
)}
@@ -923,6 +944,13 @@ const BotSessionMonitor = forwardRef<
)}
</div>
</div>
{canReset && (
<Button variant="outline" size="sm" className="h-7 shrink-0 gap-1.5"
disabled={resetting} onClick={() => setResetTarget(selectedSessionId)}>
<RotateCcw className="size-3.5" />
{t('bots.sessionMonitor.resetContext')}
</Button>
)}
</div>
{/* Messages Area */}
@@ -1194,6 +1222,21 @@ const BotSessionMonitor = forwardRef<
</div>
</div>
<Dialog open={resetTarget !== null} onOpenChange={(open) => { if (!open && !resetting) setResetTarget(null); }}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('bots.sessionMonitor.resetContext')}</DialogTitle>
<DialogDescription>{t('bots.sessionMonitor.resetDescription')}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" disabled={resetting} onClick={() => setResetTarget(null)}>{t('common.cancel')}</Button>
<Button disabled={resetting} onClick={resetContext}>
<RotateCcw className={cn('size-4', resetting && 'animate-spin motion-reduce:animate-none')} />
{t('bots.sessionMonitor.resetContext')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<BotAdminsDialog
botId={botId}
open={adminsDialogOpen}
+6
View File
@@ -822,6 +822,12 @@ export class BackendClient extends BaseHttpClient {
return this.get(`/api/v1/monitoring/sessions?${queryParams.toString()}`);
}
public resetSessionContext(botId: string, sessionId: string): Promise<{ reset: boolean }> {
return this.post('/api/v1/monitoring/sessions/reset-context', {
bot_id: botId, session_id: sessionId,
});
}
public getSessionAnalysis<T>(
sessionId: string,
botId: string,
+5
View File
@@ -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',
+5
View File
@@ -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',
+5
View File
@@ -1182,6 +1182,11 @@ const jaJP = {
imagesAttached: '枚の画像が添付されています',
noLogs: 'ログはまだありません',
sessionMonitor: {
resetContext: "コンテキストをリセット",
resetDescription: "新しいメッセージは空のコンテキストから開始します。監視履歴、ファイル、長期記憶は保持されます。実行中のタスクが完了してからリセットしてください。",
resetSuccess: "コンテキストをリセットしました。",
resetError: "リセットできませんでした。実行中のタスクが完了してから再試行してください。",
title: 'セッション監視',
sessions: 'セッション一覧',
noSessions: 'セッションが見つかりません',
+5
View File
@@ -1192,6 +1192,11 @@ const ruRU = {
imagesAttached: 'изображений прикреплено',
noLogs: 'Журналов пока нет',
sessionMonitor: {
resetContext: "Сбросить контекст",
resetDescription: "Новые сообщения начнутся с пустого контекста. История мониторинга, файлы и долговременная память сохранятся. Дождитесь завершения активных задач.",
resetSuccess: "Контекст сброшен.",
resetError: "Не удалось сбросить контекст. Дождитесь завершения активных задач и повторите попытку.",
title: 'Сессии',
sessions: 'Сессии',
noSessions: 'Сессии не найдены',
+5
View File
@@ -1152,6 +1152,11 @@ const thTH = {
imagesAttached: 'รูปภาพที่แนบ',
noLogs: 'ยังไม่มีบันทึก',
sessionMonitor: {
resetContext: "รีเซ็ตบริบท",
resetDescription: "ข้อความใหม่จะเริ่มด้วยบริบทว่าง ประวัติการตรวจสอบ ไฟล์ และความจำระยะยาวจะยังคงอยู่ โปรดรอให้งานที่กำลังทำอยู่เสร็จก่อน",
resetSuccess: "รีเซ็ตบริบทแล้ว",
resetError: "รีเซ็ตไม่สำเร็จ โปรดรอให้งานที่กำลังทำอยู่เสร็จแล้วลองอีกครั้ง",
title: 'เซสชัน',
sessions: 'เซสชัน',
noSessions: 'ไม่พบเซสชัน',
+5
View File
@@ -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',
+5
View File
@@ -1108,6 +1108,11 @@ const zhHans = {
imagesAttached: '张图片',
noLogs: '暂无日志',
sessionMonitor: {
resetContext: "重置上下文",
resetDescription: "重置后,新消息将从空上下文开始。监控历史会保留,文件和长期记忆不受影响。请等待当前任务结束后再重置。",
resetSuccess: "上下文已重置,新消息将开启新的对话。",
resetError: "重置失败,请等待当前任务结束后重试。",
title: '会话监控',
sessions: '会话列表',
noSessions: '暂无会话',
+5
View File
@@ -1107,6 +1107,11 @@ const zhHant = {
imagesAttached: '張圖片已附加',
noLogs: '暫無日誌',
sessionMonitor: {
resetContext: "重設上下文",
resetDescription: "重設後,新訊息將從空上下文開始。監控歷史會保留,檔案和長期記憶不受影響。請等待目前任務結束後再重設。",
resetSuccess: "上下文已重設,新訊息將開始新的對話。",
resetError: "重設失敗,請等待目前任務結束後重試。",
title: '會話監控',
sessions: '會話列表',
noSessions: '暫無會話',