From 5738df9deb0ce261b234252a06faf8eb350cd306 Mon Sep 17 00:00:00 2001 From: alectimison-maker Date: Fri, 21 Aug 2026 15:47:06 +0800 Subject: [PATCH 1/6] feat(trace): opt-in lossless recording tier sharing the event pipeline Adds a second, opt-in recording tier for the existing trace pipeline. The default tier is byte-identical to today (content-free provenance, 20 KB tool-result cap); the lossless tier persists full LLM request messages/tool schemas (clamped at 500 KB) and raises the tool-result cap to 200 KB, enabling deep debugging and request reconstruction without weakening the default privacy contract. Tier wiring: startRun reads the opt-in 'losslessTrace' storage key once per run (explicit meta.lossless override wins), stamps the run record, and restores the decision after SW eviction from the durable record via peekRunFlags. recordLLMRequest branches on the tier; recordToolCall picks the cap from the run state. Settings UI gains a 'Record full request details (debug tier)' toggle gated on tracing being enabled, with disclosure copy in all 23 locales (en fallback never needed since keys are present everywhere). The Markdown exporter renders bounded, credential-masked message previews for lossless runs only; default-tier exports are unchanged. One pinning test updated for the forced-flag recovery implementation change (peekRunFlags replaces the in-memory lookahead; behavior identical). Mirrored to Firefox. Closes #2876 --- src/chrome/src/agent/trace-export.js | 45 +++++++++++- src/chrome/src/trace/recorder.js | 73 ++++++++++++++++--- src/chrome/src/trace/runtime-config.js | 1 + src/chrome/src/ui/locales/ar.js | 2 + src/chrome/src/ui/locales/bn.js | 2 + src/chrome/src/ui/locales/de.js | 2 + src/chrome/src/ui/locales/en.js | 2 + src/chrome/src/ui/locales/es.js | 2 + src/chrome/src/ui/locales/fa.js | 2 + src/chrome/src/ui/locales/fr.js | 2 + src/chrome/src/ui/locales/he.js | 2 + src/chrome/src/ui/locales/hi.js | 2 + src/chrome/src/ui/locales/id.js | 2 + src/chrome/src/ui/locales/ja.js | 2 + src/chrome/src/ui/locales/ko.js | 2 + src/chrome/src/ui/locales/ms.js | 2 + src/chrome/src/ui/locales/nl.js | 2 + src/chrome/src/ui/locales/pl.js | 2 + src/chrome/src/ui/locales/pt.js | 2 + src/chrome/src/ui/locales/ru.js | 2 + src/chrome/src/ui/locales/th.js | 2 + src/chrome/src/ui/locales/tl.js | 2 + src/chrome/src/ui/locales/tr.js | 2 + src/chrome/src/ui/locales/uk.js | 2 + src/chrome/src/ui/locales/vi.js | 2 + src/chrome/src/ui/locales/zh.js | 2 + src/chrome/src/ui/settings.html | 10 +++ src/chrome/src/ui/settings.js | 16 ++++- src/firefox/src/agent/trace-export.js | 45 +++++++++++- src/firefox/src/trace/recorder.js | 74 ++++++++++++++++--- src/firefox/src/trace/runtime-config.js | 1 + src/firefox/src/ui/locales/ar.js | 2 + src/firefox/src/ui/locales/bn.js | 2 + src/firefox/src/ui/locales/de.js | 2 + src/firefox/src/ui/locales/en.js | 2 + src/firefox/src/ui/locales/es.js | 2 + src/firefox/src/ui/locales/fa.js | 2 + src/firefox/src/ui/locales/fr.js | 2 + src/firefox/src/ui/locales/he.js | 2 + src/firefox/src/ui/locales/hi.js | 2 + src/firefox/src/ui/locales/id.js | 2 + src/firefox/src/ui/locales/ja.js | 2 + src/firefox/src/ui/locales/ko.js | 2 + src/firefox/src/ui/locales/ms.js | 2 + src/firefox/src/ui/locales/nl.js | 2 + src/firefox/src/ui/locales/pl.js | 2 + src/firefox/src/ui/locales/pt.js | 2 + src/firefox/src/ui/locales/ru.js | 2 + src/firefox/src/ui/locales/th.js | 2 + src/firefox/src/ui/locales/tl.js | 2 + src/firefox/src/ui/locales/tr.js | 2 + src/firefox/src/ui/locales/uk.js | 2 + src/firefox/src/ui/locales/vi.js | 2 + src/firefox/src/ui/locales/zh.js | 2 + src/firefox/src/ui/settings.html | 10 +++ src/firefox/src/ui/settings.js | 20 +++++- test/run.js | 96 ++++++++++++++++++++++++- 57 files changed, 459 insertions(+), 24 deletions(-) diff --git a/src/chrome/src/agent/trace-export.js b/src/chrome/src/agent/trace-export.js index 0716016db..6494271fd 100644 --- a/src/chrome/src/agent/trace-export.js +++ b/src/chrome/src/agent/trace-export.js @@ -22,9 +22,52 @@ import { isKnownKind } from '../trace/event-model.js'; const ARGS_LIMIT = 300; const RESULT_LIMIT = 600; +const LOSSILESS_MESSAGE_PREVIEW_LIMIT = 2000; const FOOTER = '_Screenshot pixels and vision descriptions are omitted here — see the Traces page for the complete record._'; const UNKNOWN_EVENTS_NOTE = (n) => `_Note: ${n} unknown event(s) skipped._`; +// Credential masking for the opt-in lossless tier. Exports of lossless runs +// contain real request content, so obvious secret shapes are masked before +// they reach a Markdown file — the same spirit as the strict-redaction path +// used elsewhere, kept pure and browser-neutral here. +const SECRET_PATTERNS = [ + /\bsk-[A-Za-z0-9_-]{8,}/g, + /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, + /\b(?:api[_-]?key|apikey|token|password|passwd|secret)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{8,}/gi, +]; + +function maskSecrets(text) { + let out = String(text ?? ''); + for (const pattern of SECRET_PATTERNS) out = out.replace(pattern, '[redacted]'); + return out; +} + +// Lossless requests carry the full message/tool shape. Render a bounded, +// masked preview per message so the export stays readable without dumping +// every token of a 500 KB request. +function renderLosslessRequest(messages, tools) { + const list = Array.isArray(messages) ? messages : []; + if (!list.length) return ' (empty request log)'; + const lines = []; + for (const message of list.slice(0, 12)) { + const role = oneLine(message?.role || '?'); + const content = maskSecrets( + typeof message?.content === 'string' + ? message.content + : (Array.isArray(message.content) + ? message.content.map(block => block?.text || block?.image_url?.url || '').join(' ') + : ''), + ); + const body = content ? truncate(oneLine(content), LOSSILESS_MESSAGE_PREVIEW_LIMIT) : '(no text)'; + lines.push(`**${role}:** ${body}`); + } + if (list.length > 12) lines.push(`… +${list.length - 12} more message(s) omitted`); + if (Array.isArray(tools) && tools.length) { + lines.push(`tools: ${tools.map(tool => oneLine(tool?.function?.name || '?')).join(', ')}`); + } + return `\n${lines.join('\n')}`; +} + function oneLine(t) { return String(t ?? '').replace(/\s+/g, ' ').trim(); } function humanSize(n) { return n >= 1024 ? `${(n / 1024).toFixed(1)}kb` : `${n}b`; } @@ -202,7 +245,7 @@ export function tracesToMarkdown(runsWithEvents, { Number.isFinite(d.imageBlockCount) ? `${d.imageBlockCount} image block${d.imageBlockCount === 1 ? '' : 's'}` : '', Number.isFinite(d.documentBlockCount) ? `${d.documentBlockCount} document block${d.documentBlockCount === 1 ? '' : 's'}` : '', ].filter(Boolean).join(' · '); - md += `- 🧠 Model request: ${Number(d.messageCount) || 0} messages · ${Number(d.toolsCount) || 0} tools${media ? ` · ${media}` : ''}${renderLocalWikipediaRag(d.localWikipediaRag)}${renderPromptProvenance(d.promptProvenance)}\n`; + md += `- 🧠 Model request: ${Number(d.messageCount) || 0} messages · ${Number(d.toolsCount) || 0} tools${media ? ` · ${media}` : ''}${renderLocalWikipediaRag(d.localWikipediaRag)}${renderPromptProvenance(d.promptProvenance)}${d.lossless === true ? renderLosslessRequest(d.messages, d.tools) : ''}\n`; } else if (ev.kind === 'llm_response') { const content = String(d.content || '').trim(); if (!content) continue; diff --git a/src/chrome/src/trace/recorder.js b/src/chrome/src/trace/recorder.js index 2f1cdd0f2..214525c8d 100644 --- a/src/chrome/src/trace/recorder.js +++ b/src/chrome/src/trace/recorder.js @@ -20,6 +20,12 @@ import { normalizeRunHeader, effectiveDelegationDepth } from './run-header.js'; const DB_NAME = 'webbrain_traces'; const DB_VERSION = 2; +// Lossless tier bounds: tool results up to 200 KB verbatim, request payloads +// up to 500 KB before the head is clamped with a truncation marker. Keeps an +// opt-in debugging tier from exhausting IndexedDB on a single long run. +const LOSSILESS_RESULT_CAP = 200_000; +const LOSSILESS_MESSAGES_CAP = 500_000; + let _dbPromise = null; function openDB() { if (_dbPromise) return _dbPromise; @@ -82,6 +88,16 @@ async function tracingEnabled() { } catch { return false; } } +// Opt-in lossless tier: same event pipeline, full request payloads instead of +// content-free provenance. Read once per run at startRun; never per event. +async function losslessTraceEnabled() { + try { + if (typeof indexedDB === 'undefined') return false; + const { losslessTrace } = await chrome.storage.local.get(['losslessTrace']); + return losslessTrace === true; + } catch { return false; } +} + async function isForcedTraceRun(runId) { if (!runId) return false; if (_runState.get(runId)?.forced === true) return true; @@ -94,6 +110,17 @@ async function isForcedTraceRun(runId) { } catch { return false; } } +// Restore per-run flags after SW eviction from the durable run record so the +// lossless tier decision survives a worker restart mid-run. +async function peekRunFlags(db, runId) { + try { + const record = await promisifyReq( + tx(db, ['runs'], 'readonly').objectStore('runs').get(runId), + ); + return { forced: record?.forced === true, lossless: record?.lossless === true }; + } catch { return { forced: false, lossless: false }; } +} + async function tracingEnabledForRun(runId) { return (await tracingEnabled()) || (await isForcedTraceRun(runId)); } @@ -145,6 +172,9 @@ export async function startRun(meta = {}) { const db = await openDB(); const runId = meta.runId || `run_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const lineage = normalizeRunHeader(meta) || {}; + // Tier is decided once per run: explicit caller override wins, otherwise + // the opt-in setting. Never forced for local runs by default. + const lossless = meta.lossless === true || await losslessTraceEnabled(); const record = { runId, // Stable per-conversation id so the Traces UI can group sibling runs @@ -172,6 +202,7 @@ export async function startRun(meta = {}) { tabTitle: meta.tabTitle || '', mode: meta.mode || 'act', attachments: normalizeTraceAttachments(meta.attachments), + lossless, forced, stepCount: 0, totalInputTokens: 0, @@ -179,7 +210,7 @@ export async function startRun(meta = {}) { finalContent: null, }; await promisifyReq(tx(db, ['runs']).objectStore('runs').put(record)); - _runState.set(runId, { seq: 0, model: record.model, providerId: record.providerId, forced }); + _runState.set(runId, { seq: 0, model: record.model, providerId: record.providerId, forced, lossless }); return runId; } catch (e) { console.warn('[trace] startRun failed:', e); @@ -195,7 +226,8 @@ async function _appendEvent(runId, kind, data) { if (!_runState.has(runId)) { // Recover from SW eviction. const seq = await _peekSeq(db, runId); - _runState.set(runId, { seq, forced: await isForcedTraceRun(runId) }); + const flags = await peekRunFlags(db, runId); + _runState.set(runId, { seq, forced: flags.forced, lossless: flags.lossless }); } const seq = _newSeq(runId); const ev = makeEvent(runId, seq, kind, data); @@ -213,8 +245,29 @@ async function _appendEvent(runId, kind, data) { } export function recordLLMRequest(runId, step, payload, provenanceInput = null) { - // Never persist full prompts, message text, tool schemas, or tool names here. - // The optional fourth argument is reduced to content-free provenance only. + // Lossless tier (opt-in): persist the request's full message/tool shape for + // deep debugging and request reconstruction. Clamped so one oversized + // request cannot exhaust IndexedDB; the marker mirrors the tool-result + // truncation convention ({ _truncated, length, head }). + if (_runState.get(runId)?.lossless === true && provenanceInput) { + let messages = provenanceInput.messages || null; + if (messages !== null) { + const serialized = JSON.stringify(messages); + if (serialized && serialized.length > LOSSILESS_MESSAGES_CAP) { + messages = { _truncated: true, length: serialized.length, head: serialized.slice(0, LOSSILESS_MESSAGES_CAP) }; + } + } + return _appendEvent(runId, 'llm_request', { + step, + ...payload, + lossless: true, + messages, + tools: provenanceInput.tools || null, + }); + } + // Default tier: never persist full prompts, message text, tool schemas, or + // tool names here. The optional fourth argument is reduced to content-free + // provenance only. let promptProvenance = null; if (provenanceInput) { try { @@ -254,13 +307,14 @@ export function recordLLMResponse(runId, step, { content, toolCalls, usage, late export function recordToolCall(runId, step, { name, args, result, latencyMs }) { // Truncate very large tool results (a11y trees can be huge). Keep the first - // 20KB verbatim and note the truncation — plenty for debugging flow, and - // the model response still has the full thing in context anyway. + // 20KB verbatim by default — plenty for debugging flow — and 200KB in the + // opt-in lossless tier; note the truncation either way. + const cap = _runState.get(runId)?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; let shortResult = result; try { const s = typeof result === 'string' ? result : JSON.stringify(result); - if (s && s.length > 20_000) { - shortResult = { _truncated: true, length: s.length, head: s.slice(0, 20_000) }; + if (s && s.length > cap) { + shortResult = { _truncated: true, length: s.length, head: s.slice(0, cap) }; } } catch {} return _appendEvent(runId, 'tool', { @@ -280,7 +334,8 @@ export async function recordScreenshot(runId, step, dataUrl, caption = '') { const db = await openDB(); if (!_runState.has(runId)) { const seq = await _peekSeq(db, runId); - _runState.set(runId, { seq, forced: await isForcedTraceRun(runId) }); + const flags = await peekRunFlags(db, runId); + _runState.set(runId, { seq, forced: flags.forced, lossless: flags.lossless }); } const seq = _newSeq(runId); // Decode data URL to a Blob so IDB stores raw bytes (no base64 overhead). diff --git a/src/chrome/src/trace/runtime-config.js b/src/chrome/src/trace/runtime-config.js index 660fd6773..659b90e39 100644 --- a/src/chrome/src/trace/runtime-config.js +++ b/src/chrome/src/trace/runtime-config.js @@ -17,6 +17,7 @@ const BOOLEAN_FIELDS = Object.freeze([ 'selection_grounded', 'standalone_chat_profile', 'standalone_webgpu_profile', + 'lossless_trace', ]); // Bounds keep the payload sane, not to re-validate settings: each range is a diff --git a/src/chrome/src/ui/locales/ar.js b/src/chrome/src/ui/locales/ar.js index 8085d44e1..d255c05b9 100644 --- a/src/chrome/src/ui/locales/ar.js +++ b/src/chrome/src/ui/locales/ar.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': 'كل خطوة', 'st.display.tracing.label': 'تسجيل التتبّعات (لمقارنة النماذج)', 'st.display.tracing.desc_html': 'حفظ كل تشغيل (طلبات LLM، الاستجابات، استدعاءات الأدوات، لقطات الشاشة) في IndexedDB المحلية حتى تتمكّن من فحص النماذج ومقارنتها جنبًا إلى جنب. يُفتح في علامة تبويب «التتبّعات» منفصلة. مُعطَّل افتراضيًا لأنه يضيف عمليات كتابة على القرص في كل خطوة. افتح صفحة التتبّعات →', + 'st.display.lossless_tracing.label': 'تسجيل تفاصيل الطلب الكاملة (طبقة تصحيح الأخطاء)', + 'st.display.lossless_tracing.desc_html': 'يحفظ أيضًا المطالبات والرسائل ومخططات الأدوات الكاملة لكل طلب نموذج لغوي بدلاً من الملخصات الخالية من المحتوى، ويحتفظ بما يصل إلى 200 كيلوبايت من كل نتيجة أداة. يتيح تصحيح الأخطاء العميق وإعادة بناء الطلبات. معطّل افتراضيًا لأنه يكتب محتوى محادثتك محليًا ويستهلك مساحة أكبر بكثير. يتطلب تفعيل تسجيل التتبعات.', 'st.display.allow_local_network.label': 'السماح للوكيل بالوصول إلى الشبكة المحلية', 'st.display.allow_local_network.desc': 'يسمح لأداة fetch_url للوكيل بالوصول إلى عناوين loopback (127.0.0.1، localhost) وعناوين RFC1918 الخاصة (10.x، 192.168.x، 172.16–31.x). مفيد لخوادم التطوير المحلية، وأدوات الإنترانت، وواجهات API ذاتية الاستضافة. مُعطَّل افتراضيًا — عند تعطيله لا تستطيع صفحات حقن الـ prompt دفع الوكيل لفحص شبكتك المحلية. تظل عناوين بيانات السحابة (169.254.169.254) وأسماء *.internal/*.local محظورة حتى عند التفعيل.', 'st.display.notify_sound.label': 'صوت الإكمال', diff --git a/src/chrome/src/ui/locales/bn.js b/src/chrome/src/ui/locales/bn.js index 3c90af87b..5ee6e84d5 100644 --- a/src/chrome/src/ui/locales/bn.js +++ b/src/chrome/src/ui/locales/bn.js @@ -603,6 +603,8 @@ export default { 'st.display.auto_screenshot.every_step': "প্রতিটি পদক্ষেপ", 'st.display.tracing.label': "রেকর্ড ট্রেস (মডেল তুলনা জন্য)", 'st.display.tracing.desc_html': "স্থানীয় IndexedDB-তে প্রতিটি রান (LLM অনুরোধ, প্রতিক্রিয়া, টুল কল, স্ক্রিনশট) চালিয়ে যান যাতে আপনি পাশাপাশি মডেলগুলি পরিদর্শন এবং তুলনা করতে পারেন। একটি পৃথক ট্রেস ট্যাবে খোলে। ডিফল্টরূপে বন্ধ কারণ এটি প্রতি ধাপে ডিস্ক রাইট যোগ করে। ট্রেস পৃষ্ঠা খুলুন →", + 'st.display.lossless_tracing.label': 'সম্পূর্ণ অনুরোধের বিবরণ রেকর্ড করুন (ডিবাগ স্তর)', + 'st.display.lossless_tracing.desc_html': 'প্রতিটি LLM অনুরোধের সম্পূর্ণ প্রম্পট, বার্তা এবং টুল স্কিমা বিষয়বস্তুহীন সারাংশের পরিবর্তে সংরক্ষণ করে এবং প্রতিটি টুল ফলাফলের ২০০ KB পর্যন্ত রাখে। গভীর ডিবাগিং এবং অনুরোধ পুনর্নির্মাণ সক্ষম করে। ডিফল্টভাবে বন্ধ কারণ এটি আপনার কথোপকথনের বিষয়বস্তু স্থানীয়ভাবে লেখে এবং অনেক বেশি ডিস্ক ব্যবহার করে। ট্রেস রেকর্ডিং চালু থাকা প্রয়োজন।', 'st.display.cost_session_limit.label': "মেঘ খরচ অধিবেশন ভাতা", 'st.display.cost_session_limit.desc': "স্টপ ক্লাউড এবং OpenRouter এই সেশনটি রিপোর্ট করা বা আনুমানিক খরচ ভাতা পৌঁছানোর পরে অন্য একটি প্রদত্ত মডেল কলের আগে চলে। ডিফল্ট $10।", 'st.display.cost_total_limit.label': "ক্লাউড খরচ মোট ভাতা", diff --git a/src/chrome/src/ui/locales/de.js b/src/chrome/src/ui/locales/de.js index 07e936798..5231cd9eb 100644 --- a/src/chrome/src/ui/locales/de.js +++ b/src/chrome/src/ui/locales/de.js @@ -598,6 +598,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Bei jedem Schritt', 'st.display.tracing.label': 'Traces aufzeichnen (zum Modellvergleich)', 'st.display.tracing.desc_html': 'Speichert jeden Durchlauf in der lokalen IndexedDB, damit Sie Modelle seitenweise inspizieren und vergleichen können. Traces öffnen →', + 'st.display.lossless_tracing.label': 'Vollständige Anforderungsdetails aufzeichnen (Debug-Stufe)', + 'st.display.lossless_tracing.desc_html': 'Speichert zusätzlich die vollständigen Prompts, Nachrichten und Tool-Schemata jeder LLM-Anfrage statt inhaltsloser Zusammenfassungen und behält bis zu 200 KB jedes Tool-Ergebnisses. Ermöglicht tiefes Debugging und die Rekonstruktion von Anfragen. Standardmäßig aus, weil es Ihren Gesprächsinhalt lokal schreibt und deutlich mehr Speicherplatz benötigt. Erfordert aktivierte Trace-Aufzeichnung.', 'st.display.cost_session_limit.label': 'Cloud-Kosten-Sitzungskontingent', 'st.display.cost_session_limit.desc': 'Stoppt Cloud- und OpenRouter-Durchläufe vor einem weiteren kostenpflichtigen Modellaufruf, wenn diese Sitzung das Kostenkontingent erreicht.', 'st.display.cost_total_limit.label': 'Cloud-Kosten-Gesamtkontingent', diff --git a/src/chrome/src/ui/locales/en.js b/src/chrome/src/ui/locales/en.js index 2637fab55..515b0b708 100644 --- a/src/chrome/src/ui/locales/en.js +++ b/src/chrome/src/ui/locales/en.js @@ -604,6 +604,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Every step', 'st.display.tracing.label': 'Record traces (for model comparison)', 'st.display.tracing.desc_html': 'Persist every run (LLM requests, responses, tool calls, screenshots) into local IndexedDB so you can inspect and compare models side-by-side. Opens in a separate Traces tab. Off by default because it adds disk writes per step. Open Traces page →', + 'st.display.lossless_tracing.label': 'Record full request details (debug tier)', + 'st.display.lossless_tracing.desc_html': 'Also persist the full prompts, messages, and tool schemas of each LLM request instead of content-free summaries, and keep up to 200 KB of each tool result. Enables deep debugging and request reconstruction. Off by default because it writes your conversation content locally and uses much more disk. Requires Record traces to be on.', 'st.display.cost_session_limit.label': 'Cloud cost session allowance', 'st.display.cost_session_limit.desc': 'Stops eligible cloud and router runs before another paid model call once this session reaches the reported or estimated cost allowance. Default $10.', 'st.display.cost_total_limit.label': 'Cloud cost total allowance', diff --git a/src/chrome/src/ui/locales/es.js b/src/chrome/src/ui/locales/es.js index dd709ffab..d87cc6dd2 100644 --- a/src/chrome/src/ui/locales/es.js +++ b/src/chrome/src/ui/locales/es.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': 'En cada paso', 'st.display.tracing.label': 'Registrar trazas (para comparar modelos)', 'st.display.tracing.desc_html': 'Guarda cada ejecución (peticiones al LLM, respuestas, llamadas a herramientas, capturas) en IndexedDB local para que puedas inspeccionar y comparar modelos lado a lado. Se abre en una pestaña de Trazas aparte. Desactivado por defecto porque añade escrituras en disco por paso. Abrir página de Trazas →', + 'st.display.lossless_tracing.label': 'Grabar detalles completos de la solicitud (nivel de depuración)', + 'st.display.lossless_tracing.desc_html': 'También guarda los prompts, mensajes y esquemas de herramientas completos de cada solicitud LLM en lugar de resúmenes sin contenido, y conserva hasta 200 KB de cada resultado de herramienta. Permite depuración profunda y reconstrucción de solicitudes. Desactivado por defecto porque escribe el contenido de tu conversación localmente y usa mucho más disco. Requiere que Grabar trazas esté activado.', 'st.display.allow_local_network.label': 'Permitir al agente acceder a la red local', 'st.display.allow_local_network.desc': 'Permite que la herramienta fetch_url del agente alcance direcciones loopback (127.0.0.1, localhost) y privadas/RFC1918 (10.x, 192.168.x, 172.16–31.x). Útil para servidores de desarrollo locales, herramientas de intranet o APIs auto-alojadas. Desactivado por defecto — cuando está desactivado, páginas con inyección de prompt no pueden dirigir al agente a sondear tu red local. Las direcciones de metadatos en la nube (169.254.169.254) y los nombres de host *.internal/*.local siguen bloqueados incluso cuando está activado.', 'st.display.notify_sound.label': 'Sonido al terminar', diff --git a/src/chrome/src/ui/locales/fa.js b/src/chrome/src/ui/locales/fa.js index e16de6d75..cc3bbed36 100644 --- a/src/chrome/src/ui/locales/fa.js +++ b/src/chrome/src/ui/locales/fa.js @@ -603,6 +603,8 @@ export default { 'st.display.auto_screenshot.every_step': "هر قدم", 'st.display.tracing.label': "ثبت ردیابی (برای مقایسه مدل)", 'st.display.tracing.desc_html': "هر اجرا (درخواست‌های LLM، پاسخ‌ها، تماس‌های ابزار، تصاویر صفحه) را در IndexedDB محلی ادامه دهید تا بتوانید مدل‌ها را در کنار یکدیگر بررسی و مقایسه کنید. در یک برگه Traces جداگانه باز می شود. به‌طور پیش‌فرض خاموش است زیرا نوشته‌های دیسک را در هر مرحله اضافه می‌کند. باز کردن صفحه Traces →", + 'st.display.lossless_tracing.label': 'ثبت جزئیات کامل درخواست (لایه اشکالزدایی)', + 'st.display.lossless_tracing.desc_html': 'همچنین پرامپتها، پیامها و ساختار ابزارهای کامل هر درخواست LLM را بهجای خلاصههای بدون محتوا ذخیره میکند و تا ۲۰۰ کیلوبایت از هر نتیجه ابزار را نگه میدارد. اشکالزدایی عمیق و بازسازی درخواست را ممکن میسازد. بهصورت پیشفرض خاموش است زیرا محتوای گفتوگوی شما را محلی مینویسد و فضای دیسک بسیار بیشتری مصرف میکند. نیازمند فعال بودن ضبط ردگیری است.', 'st.display.cost_session_limit.label': "کمک هزینه جلسه هزینه ابر", 'st.display.cost_session_limit.desc': "هنگامی که این جلسه به میزان هزینه گزارش شده یا تخمین زده شده رسید، ابر را متوقف می کند و OpenRouter قبل از تماس مدل پولی دیگر اجرا می شود. پیش فرض 10 دلار.", 'st.display.cost_total_limit.label': "هزینه کل کمک هزینه", diff --git a/src/chrome/src/ui/locales/fr.js b/src/chrome/src/ui/locales/fr.js index 1f509b56b..8295167d8 100644 --- a/src/chrome/src/ui/locales/fr.js +++ b/src/chrome/src/ui/locales/fr.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': 'À chaque étape', 'st.display.tracing.label': 'Enregistrer les traces (pour comparer des modèles)', 'st.display.tracing.desc_html': 'Conserve chaque exécution (requêtes LLM, réponses, appels d\'outils, captures) dans IndexedDB local pour pouvoir inspecter et comparer les modèles côte à côte. S\'ouvre dans un onglet Traces distinct. Désactivé par défaut car cela ajoute des écritures disque à chaque étape. Ouvrir la page Traces →', + 'st.display.lossless_tracing.label': 'Enregistrer les détails complets des requêtes (niveau débogage)', + 'st.display.lossless_tracing.desc_html': 'Conserve aussi les prompts, messages et schémas d\'outils complets de chaque requête LLM au lieu de résumés sans contenu, et garde jusqu\'à 200 Ko de chaque résultat d\'outil. Permet un débogage approfondi et la reconstruction des requêtes. Désactivé par défaut car cela écrit localement le contenu de votre conversation et utilise beaucoup plus d\'espace disque. Nécessite que l\'enregistrement des traces soit activé.', 'st.display.allow_local_network.label': 'Autoriser l\'agent à accéder au réseau local', 'st.display.allow_local_network.desc': 'Permet à l\'outil fetch_url de l\'agent d\'atteindre les adresses loopback (127.0.0.1, localhost) et privées/RFC1918 (10.x, 192.168.x, 172.16–31.x). Utile pour les serveurs de dev locaux, outils intranet ou API auto-hébergées. Désactivé par défaut — quand désactivé, des pages avec injection de prompt ne peuvent pas pousser l\'agent à sonder votre réseau local. Les adresses de méta-données cloud (169.254.169.254) et les noms d\'hôte *.internal/*.local restent bloqués même quand activé.', 'st.display.notify_sound.label': 'Son de fin', diff --git a/src/chrome/src/ui/locales/he.js b/src/chrome/src/ui/locales/he.js index 8e1fa03fd..76e4329e6 100644 --- a/src/chrome/src/ui/locales/he.js +++ b/src/chrome/src/ui/locales/he.js @@ -573,6 +573,8 @@ export default { "st.display.auto_screenshot.every_step": "כל צעד", "st.display.tracing.label": "הקלטת עקבות (להשוואת מודלים)", "st.display.tracing.desc_html": "תתמיד בכל ריצה (LLM בקשות, תגובות, קריאות לכלים, צילומי מסך) לתוך IndexedDB המקומי כדי שתוכל לבדוק ולהשוות מודלים זה לצד זה. נפתח בלשונית Traces נפרדת. כבוי כברירת מחדל מכיוון שהוא מוסיף כתיבה בדיסק בכל שלב. פתח את דף העקבות →", + "st.display.lossless_tracing.label": "רישום פרטי בקשה מלאים (שכבת דיבאג)", + "st.display.lossless_tracing.desc_html": "שומר גם את ההנחיות, ההודעות והמבנים המלאים של כל בקשת LLM במקום סיכומים חסרי תוכן, ומחזיק עד 200KB מכל תוצאת כלי. מאפשר ניפוי שגיאות מעמיק ושחזור בקשות. כבוי כברירת מחדל משום שהוא כותב את תוכן השיחה שלך מקומית ומשתמש בהרבה יותר מקום. דורש שהקלטת מעקב תהיה פעילה.", "st.display.cost_session_limit.label": "קצבת הפגישה בעלות ענן", "st.display.cost_session_limit.desc": "מפסיק ענן ו OpenRouter פועל לפני שיחת מודל בתשלום נוספת ברגע שההפעלה הזו מגיעה לקצבת העלות המדווחת או המשוערת. ברירת מחדל 10 $.", "st.display.cost_total_limit.label": "הקצבה כוללת של עלות ענן", diff --git a/src/chrome/src/ui/locales/hi.js b/src/chrome/src/ui/locales/hi.js index c9f292b6a..62cdb168e 100644 --- a/src/chrome/src/ui/locales/hi.js +++ b/src/chrome/src/ui/locales/hi.js @@ -603,6 +603,8 @@ export default { 'st.display.auto_screenshot.every_step': "हर कदम", 'st.display.tracing.label': "रिकॉर्ड निशान (मॉडल तुलना के लिए)", 'st.display.tracing.desc_html': "प्रत्येक रन (एलएलएम अनुरोध, प्रतिक्रिया, टूल कॉल, स्क्रीनशॉट) को स्थानीय IndexedDB में जारी रखें ताकि आप साथ-साथ मॉडलों का निरीक्षण और तुलना कर सकें। एक अलग ट्रेसेस टैब में खुलता है। डिफ़ॉल्ट रूप से बंद है क्योंकि यह प्रति चरण डिस्क राइट जोड़ता है। ट्रेस पेज खोलें →", + 'st.display.lossless_tracing.label': 'पूर्ण अनुरोध विवरण रिकॉर्ड करें (डीबग स्तर)', + 'st.display.lossless_tracing.desc_html': 'प्रत्येक LLM अनुरोध के पूर्ण प्रॉम्प्ट, संदेश और टूल स्कीमा को बिना सामग्री वाले सारांशों के बजाय सहेजता है, और प्रत्येक टूल परिणाम का 200 KB तक रखता है। गहन डीबगिंग और अनुरोध पुनर्निर्माण सक्षम करता है। डिफ़ॉल्ट रूप से बंद क्योंकि यह आपकी बातचीत की सामग्री को स्थानीय रूप से लिखता है और बहुत अधिक डिस्क उपयोग करता है। ट्रेस रिकॉर्डिंग चालू होना आवश्यक है।', 'st.display.cost_session_limit.label': "क्लाउड लागत सत्र भत्ता", 'st.display.cost_session_limit.desc': "एक बार जब यह सत्र रिपोर्ट की गई या अनुमानित लागत भत्ता तक पहुंच जाता है, तो क्लाउड रुक जाता है और OpenRouter किसी अन्य भुगतान मॉडल कॉल से पहले चलता है। डिफ़ॉल्ट $10.", 'st.display.cost_total_limit.label': "क्लाउड लागत कुल भत्ता", diff --git a/src/chrome/src/ui/locales/id.js b/src/chrome/src/ui/locales/id.js index 9b33c516d..7e05ce427 100644 --- a/src/chrome/src/ui/locales/id.js +++ b/src/chrome/src/ui/locales/id.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Setiap langkah', 'st.display.tracing.label': 'Rekam jejak (untuk perbandingan model)', 'st.display.tracing.desc_html': 'Simpan setiap proses (permintaan LLM, respons, pemanggilan alat, tangkapan layar) ke IndexedDB lokal sehingga Anda dapat memeriksa dan membandingkan model berdampingan. Membuka tab Jejak terpisah. Nonaktif secara bawaan karena menambah penulisan disk per langkah. Buka halaman Jejak →', + 'st.display.lossless_tracing.label': 'Rekam detail lengkap permintaan (tingkat debug)', + 'st.display.lossless_tracing.desc_html': 'Juga menyimpan prompt, pesan, dan skema alat lengkap dari setiap permintaan LLM alih-alih ringkasan tanpa konten, dan menyimpan hingga 200 KB setiap hasil alat. Memungkinkan debug mendalam dan rekonstruksi permintaan. Nonaktif secara default karena menulis konten percakapan Anda secara lokal dan memakai disk jauh lebih banyak. Memerlukan perekaman trace aktif.', 'st.display.allow_local_network.label': 'Izinkan agen mengakses jaringan lokal', 'st.display.allow_local_network.desc': 'Memungkinkan alat fetch_url agen menjangkau loopback (127.0.0.1, localhost) dan alamat privat/RFC1918 (10.x, 192.168.x, 172.16–31.x). Berguna untuk server pengembangan lokal, alat intranet, atau API yang di-host sendiri. Nonaktif secara bawaan — saat nonaktif, halaman yang disisipi prompt-injection tidak dapat menyuruh agen memindai jaringan lokal Anda. Alamat metadata cloud (169.254.169.254) dan nama host *.internal/*.local tetap diblokir bahkan saat diaktifkan.', 'st.display.notify_sound.label': 'Suara penyelesaian', diff --git a/src/chrome/src/ui/locales/ja.js b/src/chrome/src/ui/locales/ja.js index fade1fc42..77cf1089f 100644 --- a/src/chrome/src/ui/locales/ja.js +++ b/src/chrome/src/ui/locales/ja.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': '毎ステップ', 'st.display.tracing.label': 'トレースを記録(モデル比較用)', 'st.display.tracing.desc_html': 'すべての実行(LLM リクエスト、レスポンス、ツール呼び出し、スクリーンショット)をローカルの IndexedDB に保存し、モデルを並べて検査・比較できるようにします。別の「トレース」タブで開きます。毎ステップで書き込みが増えるためデフォルトはオフです。トレースページを開く →', + 'st.display.lossless_tracing.label': 'リクエストの完全な詳細を記録(デバッグ層)', + 'st.display.lossless_tracing.desc_html': '各LLMリクエストの完全なプロンプト・メッセージ・ツールスキーマを、内容を含まない要約の代わりに保存し、各ツール結果を最大200KB保持します。深いデバッグとリクエスト再構築が可能になります。会話内容をローカルに書き込み、ディスク消費が大幅に増えるため、既定ではオフです。トレース記録の有効化が必要です。', 'st.display.allow_local_network.label': 'エージェントにローカルネットワークへのアクセスを許可', 'st.display.allow_local_network.desc': 'エージェントの fetch_url ツールがループバック(127.0.0.1、localhost)やプライベート/RFC1918 アドレス(10.x、192.168.x、172.16–31.x)に届くようになります。ローカル開発サーバー、社内ツール、セルフホスト API に便利です。デフォルトはオフ — オフのときは prompt 注入されたページがエージェントにローカルネットワークの探索をさせることはできません。クラウドメタデータアドレス(169.254.169.254)と *.internal/*.local ホスト名はオンでもブロックされたままです。', 'st.display.notify_sound.label': '完了サウンド', diff --git a/src/chrome/src/ui/locales/ko.js b/src/chrome/src/ui/locales/ko.js index e22cc0431..cfaeb6dd5 100644 --- a/src/chrome/src/ui/locales/ko.js +++ b/src/chrome/src/ui/locales/ko.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': '매 단계', 'st.display.tracing.label': '트레이스 기록 (모델 비교용)', 'st.display.tracing.desc_html': '모든 실행(LLM 요청, 응답, 도구 호출, 스크린샷)을 로컬 IndexedDB에 보관하여 모델을 나란히 검사하고 비교할 수 있게 합니다. 별도의 트레이스 탭에서 열립니다. 매 단계마다 디스크 쓰기가 늘어나므로 기본값은 꺼짐입니다. 트레이스 페이지 열기 →', + 'st.display.lossless_tracing.label': '요청 전체 세부정보 기록(디버그 계층)', + 'st.display.lossless_tracing.desc_html': '각 LLM 요청의 전체 프롬프트·메시지·도구 스키마를 내용 없는 요약 대신 저장하고, 각 도구 결과를 최대 200KB까지 보관합니다. 심층 디버깅과 요청 재구성이 가능합니다. 대화 내용을 로컬에 기록하고 디스크를 훨씬 더 사용하므로 기본적으로 꺼져 있습니다. 트레이스 기록이 켜져 있어야 합니다.', 'st.display.allow_local_network.label': '에이전트의 로컬 네트워크 접근 허용', 'st.display.allow_local_network.desc': '에이전트의 fetch_url 도구가 루프백(127.0.0.1, localhost)과 사설/RFC1918 주소(10.x, 192.168.x, 172.16–31.x)에 접근할 수 있게 합니다. 로컬 개발 서버, 인트라넷 도구, 자체 호스팅 API에 유용합니다. 기본값은 끔 — 꺼져 있으면 prompt 주입된 페이지가 에이전트로 하여금 로컬 네트워크를 탐색하게 만들 수 없습니다. 클라우드 메타데이터 주소(169.254.169.254)와 *.internal/*.local 호스트명은 켜진 상태에서도 차단됩니다.', 'st.display.notify_sound.label': '완료 알림음', diff --git a/src/chrome/src/ui/locales/ms.js b/src/chrome/src/ui/locales/ms.js index f35a12ff6..2b1ad861d 100644 --- a/src/chrome/src/ui/locales/ms.js +++ b/src/chrome/src/ui/locales/ms.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Setiap langkah', 'st.display.tracing.label': 'Rakam jejak (untuk perbandingan model)', 'st.display.tracing.desc_html': 'Simpan setiap larian (permintaan LLM, respons, panggilan alat, tangkapan skrin) ke dalam IndexedDB tempatan supaya anda boleh memeriksa dan membandingkan model secara bersebelahan. Dibuka dalam tab Jejak yang berasingan. Dimatikan secara lalai kerana ia menambah penulisan cakera pada setiap langkah. Buka halaman Jejak →', + 'st.display.lossless_tracing.label': 'Rekod butiran penuh permintaan (peringkat debug)', + 'st.display.lossless_tracing.desc_html': 'Juga menyimpan prompt, mesej, dan skema alatan penuh bagi setiap permintaan LLM dan bukannya ringkasan tanpa kandungan, serta mengekalkan sehingga 200 KB bagi setiap hasil alatan. Membolehkan penyahpepijatan mendalam dan pembinaan semula permintaan. Dimatikan secara lalai kerana menulis kandungan perbualan anda secara setempat dan menggunakan lebih banyak cakera. Memerlukan rakaman jejak dihidupkan.', 'st.display.allow_local_network.label': 'Benarkan ejen mengakses rangkaian tempatan', 'st.display.allow_local_network.desc': 'Membenarkan alat fetch_url ejen mencapai loopback (127.0.0.1, localhost) dan alamat peribadi/RFC1918 (10.x, 192.168.x, 172.16–31.x). Berguna untuk pelayan pembangunan tempatan, alat intranet, atau API yang dihos sendiri. Dimatikan secara lalai — apabila dimatikan, halaman yang disuntik prompt-injection tidak boleh mendorong ejen untuk meneliti rangkaian tempatan anda. Alamat metadata awan (169.254.169.254) dan nama hos *.internal/*.local kekal disekat walaupun dihidupkan.', 'st.display.notify_sound.label': 'Bunyi selesai', diff --git a/src/chrome/src/ui/locales/nl.js b/src/chrome/src/ui/locales/nl.js index 6dae32f5a..3fc8c5ba3 100644 --- a/src/chrome/src/ui/locales/nl.js +++ b/src/chrome/src/ui/locales/nl.js @@ -580,6 +580,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Elke stap', 'st.display.tracing.label': 'Traces opslaan (voor modelvergelijking)', 'st.display.tracing.desc_html': 'Sla elke uitvoering op in lokale IndexedDB... Traces openen →', + 'st.display.lossless_tracing.label': 'Volledige verzoekdetails opnemen (debuglaag)', + 'st.display.lossless_tracing.desc_html': 'Bewaart ook de volledige prompts, berichten en tool-schema\'s van elk LLM-verzoek in plaats van inhoudsloze samenvattingen, en houdt tot 200 KB van elk toolresultaat. Maakt diepgaand debuggen en reconstructie van verzoeken mogelijk. Standaard uit omdat het de inhoud van uw gesprek lokaal opslaat en veel meer schijfruimte gebruikt. Vereist dat tracerecordering aan staat.', 'st.display.cost_session_limit.label': 'Cloudkostensessietegoed', 'st.display.cost_session_limit.desc': 'Stopt cloud- en OpenRouter-uitvoeringen voordat nog een betaald modelaanroep plaatsvindt...', 'st.display.cost_total_limit.label': 'Cloudkostentotaaltegoed', diff --git a/src/chrome/src/ui/locales/pl.js b/src/chrome/src/ui/locales/pl.js index c2ef1ab6d..d3e681122 100644 --- a/src/chrome/src/ui/locales/pl.js +++ b/src/chrome/src/ui/locales/pl.js @@ -419,6 +419,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Przy każdym kroku', 'st.display.tracing.label': 'Rejestruj ślady (do porównywania modeli)', 'st.display.tracing.desc_html': 'Zapisuj każde wykonanie (żądania LLM, odpowiedzi, wywołania narzędzi, zrzuty ekranu) w lokalnej bazie IndexedDB, aby móc je analizować i porównywać modele obok siebie. Otwiera się w osobnej karcie Ślady. Domyślnie wyłączone, ponieważ dodaje zapisy na dysk przy każdym kroku. Otwórz stronę Ślady →', + 'st.display.lossless_tracing.label': 'Zapisuj pełne szczegóły żądań (poziom debugowania)', + 'st.display.lossless_tracing.desc_html': 'Zapisuje również pełne prompty, wiadomości i schematy narzędzi każdego żądania LLM zamiast podsumowań bez treści oraz przechowuje do 200 KB każdego wyniku narzędzia. Umożliwia głębokie debugowanie i rekonstrukcję żądań. Wyłączone domyślnie, ponieważ zapisuje treść rozmowy lokalnie i zużywa znacznie więcej miejsca. Wymaga włączonego zapisu śladów.', 'st.display.cost_session_limit.label': 'Limit kosztów chmury na sesję', 'st.display.cost_session_limit.desc': 'Zatrzymuje wykonania w chmurze i OpenRouter przed kolejnym płatnym wywołaniem modelu, gdy ta sesja osiągnie zgłoszony lub szacowany limit kosztów. Domyślnie 10 USD.', 'st.display.cost_total_limit.label': 'Całkowity limit kosztów chmury', diff --git a/src/chrome/src/ui/locales/pt.js b/src/chrome/src/ui/locales/pt.js index b96b206d7..4fc242f52 100644 --- a/src/chrome/src/ui/locales/pt.js +++ b/src/chrome/src/ui/locales/pt.js @@ -603,6 +603,8 @@ export default { 'st.display.auto_screenshot.every_step': "Cada passo", 'st.display.tracing.label': "Registrar rastreamentos (para comparação de modelos)", 'st.display.tracing.desc_html': "Persista cada execução (solicitações LLM, respostas, chamadas de ferramentas, capturas de tela) no IndexedDB local para que você possa inspecionar e comparar modelos lado a lado. Abre em uma guia Traces separada. Desativado por padrão porque adiciona gravações de disco por etapa. Abra a página Traces →", + 'st.display.lossless_tracing.label': 'Registrar detalhes completos da solicitação (nível de depuração)', + 'st.display.lossless_tracing.desc_html': 'Também persiste os prompts, mensagens e esquemas de ferramentas completos de cada solicitação LLM em vez de resumos sem conteúdo, e mantém até 200 KB de cada resultado de ferramenta. Permite depuração profunda e reconstrução de solicitações. Desativado por padrão porque grava o conteúdo da sua conversa localmente e usa muito mais disco. Exige que o registro de rastros esteja ativado.', 'st.display.cost_session_limit.label': "Subsídio de sessão de custo na nuvem", 'st.display.cost_session_limit.desc': "Interrompe a nuvem e OpenRouter é executado antes de outra chamada de modelo pago quando esta sessão atinge o limite de custo relatado ou estimado. Padrão $ 10.", 'st.display.cost_total_limit.label': "Permissão total de custos de nuvem", diff --git a/src/chrome/src/ui/locales/ru.js b/src/chrome/src/ui/locales/ru.js index f46355c82..7467eb98e 100644 --- a/src/chrome/src/ui/locales/ru.js +++ b/src/chrome/src/ui/locales/ru.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Каждый шаг', 'st.display.tracing.label': 'Записывать трассы (для сравнения моделей)', 'st.display.tracing.desc_html': 'Сохранять каждый запуск (LLM-запросы, ответы, вызовы инструментов, скриншоты) в локальную IndexedDB, чтобы можно было разбирать и сравнивать модели бок о бок. Открывается в отдельной вкладке «Трассы». По умолчанию выключено, потому что добавляет запись на диск на каждом шаге. Открыть страницу трасс →', + 'st.display.lossless_tracing.label': 'Записывать полные детали запросов (отладочный уровень)', + 'st.display.lossless_tracing.desc_html': 'Также сохраняет полные промпты, сообщения и схемы инструментов каждого LLM-запроса вместо кратких сводок без содержимого, и хранит до 200 КБ каждого результата инструмента. Позволяет глубокую отладку и восстановление запросов. Выключено по умолчанию, так как записывает содержимое вашего диалога локально и занимает заметно больше места. Требует включённой записи трасс.', 'st.display.allow_local_network.label': 'Разрешить агенту доступ к локальной сети', 'st.display.allow_local_network.desc': 'Разрешает инструменту fetch_url агента обращаться к loopback (127.0.0.1, localhost) и частным адресам RFC1918 (10.x, 192.168.x, 172.16–31.x). Полезно для локальных серверов разработки, интранет-инструментов и self-hosted API. По умолчанию выключено — когда выключено, страницы с prompt-инъекцией не смогут заставить агента сканировать вашу локальную сеть. Адреса метаданных облака (169.254.169.254) и имена *.internal/*.local остаются заблокированы даже когда включено.', 'st.display.notify_sound.label': 'Звук завершения', diff --git a/src/chrome/src/ui/locales/th.js b/src/chrome/src/ui/locales/th.js index 3d64315ce..8754d6878 100644 --- a/src/chrome/src/ui/locales/th.js +++ b/src/chrome/src/ui/locales/th.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': 'ทุกขั้น', 'st.display.tracing.label': 'บันทึกการติดตาม (เพื่อเปรียบเทียบโมเดล)', 'st.display.tracing.desc_html': 'จัดเก็บทุกการทำงาน (คำขอ LLM, การตอบกลับ, การเรียกใช้เครื่องมือ, ภาพหน้าจอ) ลงใน IndexedDB ในเครื่อง เพื่อให้คุณตรวจสอบและเปรียบเทียบโมเดลแบบเคียงข้างกันได้ จะเปิดในแท็บ Traces แยกต่างหาก ปิดเป็นค่าเริ่มต้นเพราะเพิ่มการเขียนดิสก์ในทุกขั้น เปิดหน้า Traces →', + 'st.display.lossless_tracing.label': 'บันทึกรายละเอียดคำขอทั้งหมด (ระดับดีบัก)', + 'st.display.lossless_tracing.desc_html': 'จัดเก็บพรอมต์ ข้อความ และโครงสร้างเครื่องมือแบบเต็มของแต่ละคำขอ LLM แทนสรุปที่ไม่มีเนื้อหา และเก็บผลลัพธ์เครื่องมือแต่ละรายการสูงสุด 200 KB เปิดใช้งานการดีบักเชิงลึกและการสร้างคำขอใหม่ ปิดโดยค่าเริ่มต้นเพราะเขียนเนื้อหาสนทนาของคุณในเครื่องและใช้พื้นที่ดิสก์มากกว่ามาก ต้องเปิดการบันทึกเทรซ', 'st.display.allow_local_network.label': 'อนุญาตให้เอเจนต์เข้าถึงเครือข่ายภายใน', 'st.display.allow_local_network.desc': 'ให้เครื่องมือ fetch_url ของเอเจนต์เข้าถึง loopback (127.0.0.1, localhost) และที่อยู่ส่วนตัว/RFC1918 (10.x, 192.168.x, 172.16–31.x) มีประโยชน์สำหรับเซิร์ฟเวอร์พัฒนาภายใน เครื่องมืออินทราเน็ต หรือ API ที่โฮสต์เอง ปิดเป็นค่าเริ่มต้น — เมื่อปิด หน้าเว็บที่ถูกแทรก prompt-injection ไม่สามารถสั่งเอเจนต์ให้สำรวจเครือข่ายภายในของคุณได้ ส่วนที่อยู่เมตาดาตาคลาวด์ (169.254.169.254) และโฮสต์ *.internal/*.local จะยังถูกบล็อกแม้จะเปิดไว้', 'st.display.notify_sound.label': 'เสียงเมื่อทำเสร็จ', diff --git a/src/chrome/src/ui/locales/tl.js b/src/chrome/src/ui/locales/tl.js index d0f5cf24b..b4fad7fe8 100644 --- a/src/chrome/src/ui/locales/tl.js +++ b/src/chrome/src/ui/locales/tl.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Bawat hakbang', 'st.display.tracing.label': 'I-record ang mga trace (para sa paghahambing ng modelo)', 'st.display.tracing.desc_html': 'I-save ang bawat run (mga LLM request, response, tool call, screenshot) sa lokal na IndexedDB para maaari mong suriin at ihambing ang mga modelo nang magkatabi. Bumubukas sa hiwalay na Traces tab. Naka-off bilang default dahil nagdadagdag ito ng disk writes kada hakbang. Buksan ang Traces page →', + 'st.display.lossless_tracing.label': 'Itala ang buong detalye ng kahilingan (antas ng debug)', + 'st.display.lossless_tracing.desc_html': 'Iniimbak din ang buong prompt, mensahe, at schema ng tool ng bawat LLM request sa halip na buod na walang nilalaman, at nagpapanatili ng hanggang 200 KB ng bawat resulta ng tool. Nagbibigay-daan sa malalim na pag-debug at muling pagbuo ng request. Naka-off bilang default dahil isinusulat nito ang nilalaman ng iyong usapan nang lokal at gumagamit ng mas malaking disk. Nangangailangan ng naka-on na pag-record ng trace.', 'st.display.allow_local_network.label': 'Pahintulutan ang ahente na ma-access ang lokal na network', 'st.display.allow_local_network.desc': 'Hinahayaan ang fetch_url tool ng ahente na maabot ang loopback (127.0.0.1, localhost) at mga private/RFC1918 address (10.x, 192.168.x, 172.16–31.x). Mainam para sa mga lokal na dev server, intranet tool, o self-hosted API. Naka-off bilang default — kapag naka-off, hindi mapipilit ng mga pahina na may prompt-injection ang ahente na sumiyasat sa iyong lokal na network. Mananatiling naka-block ang mga cloud-metadata address (169.254.169.254) at mga *.internal/*.local hostname kahit naka-on ito.', 'st.display.notify_sound.label': 'Tunog kapag tapos na', diff --git a/src/chrome/src/ui/locales/tr.js b/src/chrome/src/ui/locales/tr.js index bf3114b83..05a345c60 100644 --- a/src/chrome/src/ui/locales/tr.js +++ b/src/chrome/src/ui/locales/tr.js @@ -303,6 +303,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Her adımda', 'st.display.tracing.label': 'İzleri kaydet (model karşılaştırması için)', 'st.display.tracing.desc_html': 'Her çalışmayı (LLM istekleri, yanıtlar, araç çağrıları, ekran görüntüleri) yerel IndexedDB\'de saklar, böylece modelleri yan yana inceleyip karşılaştırabilirsin. Ayrı bir İzler sekmesinde açılır. Her adımda diske yazdığı için varsayılan olarak kapalıdır. İzler sayfasını aç →', + 'st.display.lossless_tracing.label': 'Tüm istek ayrıntılarını kaydet (hata ayıklama katmanı)', + 'st.display.lossless_tracing.desc_html': 'Her LLM isteğinin tam prompt, mesaj ve araç şemalarını içeriksiz özetler yerine de kalıcı olarak kaydeder ve her araç sonucundan en fazla 200 KB tutar. Derin hata ayıklama ve istek yeniden yapılandırmasını sağlar. Varsayılan olarak kapalıdır çünkü konuşma içeriğinizi yerel olarak yazar ve çok daha fazla disk kullanır. İz kaydının açık olmasını gerektirir.', 'st.display.allow_local_network.label': 'Aracının yerel ağa erişmesine izin ver', 'st.display.allow_local_network.desc': 'Aracının fetch_url aracının loopback (127.0.0.1, localhost) ve özel/RFC1918 adreslerine (10.x, 192.168.x, 172.16–31.x) ulaşmasına izin verir. Yerel geliştirme sunucuları, intranet araçları veya kendi sunucundaki API\'lar için kullanışlıdır. Varsayılan olarak kapalıdır — kapalıyken, prompt-injection içeren sayfalar aracıyı yerel ağını taramaya yönlendiremez. Bulut meta-veri adresleri (169.254.169.254) ve *.internal/*.local ana makine adları açıkken bile engellenir.', 'st.display.notify_sound.label': 'Tamamlama sesi', diff --git a/src/chrome/src/ui/locales/uk.js b/src/chrome/src/ui/locales/uk.js index 245427618..e202c528b 100644 --- a/src/chrome/src/ui/locales/uk.js +++ b/src/chrome/src/ui/locales/uk.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Кожен крок', 'st.display.tracing.label': 'Записувати траси (для порівняння моделей)', 'st.display.tracing.desc_html': 'Зберігати кожен запуск (LLM-запити, відповіді, виклики інструментів, скриншоти) у локальну IndexedDB, щоб переглядати й порівнювати моделі поряд. Відкривається в окремій вкладці «Траси». За замовчуванням вимкнено, бо додає запис на диск на кожному кроці. Відкрити сторінку трас →', + 'st.display.lossless_tracing.label': 'Записувати повні деталі запитів (рівень налагодження)', + 'st.display.lossless_tracing.desc_html': 'Також зберігає повні промпти, повідомлення та схеми інструментів кожного LLM-запиту замість стислих зведень без вмісту, та тримає до 200 КБ кожного результату інструмента. Дає змогу глибокого налагодження та відтворення запитів. Вимкнено за замовчуванням, бо записує вміст вашої розмови локально та займає значно більше диска. Потребує увімкненого запису трас.', 'st.display.allow_local_network.label': 'Дозволити агенту доступ до локальної мережі', 'st.display.allow_local_network.desc': 'Дозволяє інструменту fetch_url агента звертатися до loopback (127.0.0.1, localhost) та приватних адрес RFC1918 (10.x, 192.168.x, 172.16–31.x). Корисно для локальних серверів розробки, інтранет-інструментів і self-hosted API. За замовчуванням вимкнено — коли вимкнено, сторінки з prompt-ін\'єкцією не зможуть змусити агента сканувати вашу локальну мережу. Адреси метаданих хмари (169.254.169.254) та імена *.internal/*.local залишаються заблокованими навіть коли ввімкнено.', 'st.display.notify_sound.label': 'Звук завершення', diff --git a/src/chrome/src/ui/locales/vi.js b/src/chrome/src/ui/locales/vi.js index 849073f4f..533e78af2 100644 --- a/src/chrome/src/ui/locales/vi.js +++ b/src/chrome/src/ui/locales/vi.js @@ -603,6 +603,8 @@ export default { 'st.display.auto_screenshot.every_step': "Mỗi bước", 'st.display.tracing.label': "Ghi lại dấu vết (để so sánh mô hình)", 'st.display.tracing.desc_html': "Duy trì mọi lần chạy (yêu cầu LLM, phản hồi, lệnh gọi công cụ, ảnh chụp màn hình) vào IndexedDB cục bộ để bạn có thể kiểm tra và so sánh các mô hình song song. Mở trong tab Dấu vết riêng biệt. Tắt theo mặc định vì nó thêm ghi vào đĩa mỗi bước. Mở trang Dấu vết →", + 'st.display.lossless_tracing.label': 'Ghi lại chi tiết đầy đủ của yêu cầu (lớp gỡ lỗi)', + 'st.display.lossless_tracing.desc_html': 'Cũng lưu trữ đầy đủ prompt, tin nhắn và cấu trúc công cụ của mỗi yêu cầu LLM thay vì bản tóm tắt không nội dung, đồng thời giữ tối đa 200 KB mỗi kết quả công cụ. Cho phép gỡ lỗi sâu và dựng lại yêu cầu. Tắt theo mặc định vì ghi nội dung hội thoại cục bộ và tốn nhiều dung lượng hơn. Yêu cầu bật tính năng ghi trace.', 'st.display.cost_session_limit.label': "Phụ cấp phiên chi phí đám mây", 'st.display.cost_session_limit.desc': "Dừng đám mây và OpenRouter chạy trước một lệnh gọi mô hình trả phí khác sau khi phiên này đạt đến mức chi phí được báo cáo hoặc ước tính. Mặc định là 10$.", 'st.display.cost_total_limit.label': "Tổng phụ cấp chi phí đám mây", diff --git a/src/chrome/src/ui/locales/zh.js b/src/chrome/src/ui/locales/zh.js index fdc0f90bb..21f1a6995 100644 --- a/src/chrome/src/ui/locales/zh.js +++ b/src/chrome/src/ui/locales/zh.js @@ -267,6 +267,8 @@ export default { 'st.display.auto_screenshot.every_step': '每一步', 'st.display.tracing.label': '记录轨迹(用于模型对比)', 'st.display.tracing.desc_html': '将每次运行(LLM 请求、响应、工具调用、截图)持久化到本地 IndexedDB,以便并排查看和对比模型。在独立的轨迹页打开。因为每步都会写盘,默认关闭。打开轨迹页 →', + 'st.display.lossless_tracing.label': '记录完整请求详情(调试档)', + 'st.display.lossless_tracing.desc_html': '同时持久化每次 LLM 请求的完整提示词、消息与工具结构(而非无内容摘要),并保留每个工具结果最多 200 KB。支持深度调试与请求重建。默认关闭,因为会在本地写入对话内容并占用更多磁盘。需要先开启记录轨迹。', 'st.display.allow_local_network.label': '允许代理访问本地网络', 'st.display.allow_local_network.desc': '允许代理的 fetch_url 工具访问回环(127.0.0.1、localhost)和私有/RFC1918 地址(10.x、192.168.x、172.16–31.x)。用于本地开发服务器、内网工具或自托管 API。默认关闭 — 关闭时,含 prompt 注入的页面无法引导代理探测你的本地网络。即使开启,云元数据地址(169.254.169.254)和 *.internal/*.local 主机名仍然会被阻止。', 'st.display.notify_sound.label': '完成提示音', diff --git a/src/chrome/src/ui/settings.html b/src/chrome/src/ui/settings.html index 548cad883..5449ac1db 100644 --- a/src/chrome/src/ui/settings.html +++ b/src/chrome/src/ui/settings.html @@ -1499,6 +1499,16 @@

+
+
+
+
+
+ +
diff --git a/src/chrome/src/ui/settings.js b/src/chrome/src/ui/settings.js index f95636112..1c36ee631 100644 --- a/src/chrome/src/ui/settings.js +++ b/src/chrome/src/ui/settings.js @@ -108,6 +108,7 @@ const planReviewConfidenceRow = document.getElementById('row-plan-review-confide const notifySoundToggle = document.getElementById('toggle-notify-sound'); const completionConfettiToggle = document.getElementById('toggle-completion-confetti'); const tracingToggle = document.getElementById('toggle-tracing'); +const losslessTracingToggle = document.getElementById('toggle-lossless-tracing'); const strictSecretToggle = document.getElementById('toggle-strict-secret'); const allowLocalNetworkToggle = document.getElementById('toggle-allow-local-network'); const cloudBridgeToggle = document.getElementById('toggle-cloud-bridge'); @@ -698,7 +699,7 @@ async function init() { chrome.storage.local.remove(['authToken', 'authEmail', 'authDefaultModel']).catch(() => {}); // Load display settings - const stored = await chrome.storage.local.get(['verboseMode', 'selectionShortcutEnabled', AUTO_GROUP_TABS_KEY, 'helpImproveWebBrain', 'screenshotFallback', 'maxAgentSteps', 'autoScreenshot', 'useSiteAdapters', 'researchEscalationEnabled', 'researchEscalationEngine', 'voiceInputEnabled', 'alwaysAllowApiMutations', 'apiMutationObserverEnabled', 'webMcpEnabled', 'openaiAskStreamingEnabled', 'planBeforeActMode', 'planBeforeAct', 'planReviewMode', 'planReviewConfidenceThreshold', DOWNLOAD_DIRECTORY_STORAGE_KEY, 'notifySound', 'completionConfetti', 'tracingEnabled', 'strictSecretMode', 'agentAllowLocalNetwork', CLOUD_BRIDGE_ENABLED_KEY, CLOUD_BRIDGE_URL_KEY, 'scheduledTasksEnabled', 'scheduledRequireConsequentialConfirmation', 'providerFilter', 'requestTimeoutMs', 'clarifyTimeoutSec', 'clarifyTimeoutSemanticsV2', 'costAllowanceSessionUsd', 'costAllowanceTotalUsd', 'meteredProviderCostSpentUsd', 'screenshotRedaction', 'imageDetail', 'maxScreenshotsPerTurn', 'maxImageDimension']); + const stored = await chrome.storage.local.get(['verboseMode', 'selectionShortcutEnabled', AUTO_GROUP_TABS_KEY, 'helpImproveWebBrain', 'screenshotFallback', 'maxAgentSteps', 'autoScreenshot', 'useSiteAdapters', 'researchEscalationEnabled', 'researchEscalationEngine', 'voiceInputEnabled', 'alwaysAllowApiMutations', 'apiMutationObserverEnabled', 'webMcpEnabled', 'openaiAskStreamingEnabled', 'planBeforeActMode', 'planBeforeAct', 'planReviewMode', 'planReviewConfidenceThreshold', DOWNLOAD_DIRECTORY_STORAGE_KEY, 'notifySound', 'completionConfetti', 'tracingEnabled', 'losslessTrace', 'strictSecretMode', 'agentAllowLocalNetwork', CLOUD_BRIDGE_ENABLED_KEY, CLOUD_BRIDGE_URL_KEY, 'scheduledTasksEnabled', 'scheduledRequireConsequentialConfirmation', 'providerFilter', 'requestTimeoutMs', 'clarifyTimeoutSec', 'clarifyTimeoutSemanticsV2', 'costAllowanceSessionUsd', 'costAllowanceTotalUsd', 'meteredProviderCostSpentUsd', 'screenshotRedaction', 'imageDetail', 'maxScreenshotsPerTurn', 'maxImageDimension']); if (typeof stored.providerFilter === 'string' && ['all','active','local','cloud','router'].includes(stored.providerFilter)) { providerFilter = stored.providerFilter; } @@ -770,6 +771,10 @@ async function init() { notifySoundToggle.checked = stored.notifySound ?? true; // on by default completionConfettiToggle.checked = stored.completionConfetti ?? true; // on by default tracingToggle.checked = stored.tracingEnabled === true; + losslessTracingToggle.checked = stored.losslessTrace === true; + // Lossless recording only means something when tracing is on; mirror the + // disabled state so the disclosure reads honestly. + losslessTracingToggle.disabled = tracingToggle.checked !== true; const sessionLimit = normalizeCostAmount(stored.costAllowanceSessionUsd); const totalLimit = normalizeCostAmount(stored.costAllowanceTotalUsd); const totalSpent = normalizeCostAmount(stored.meteredProviderCostSpentUsd, 0); @@ -1459,6 +1464,15 @@ completionConfettiToggle.addEventListener('change', async () => { tracingToggle.addEventListener('change', async () => { await chrome.storage.local.set({ tracingEnabled: tracingToggle.checked }).catch(() => {}); + losslessTracingToggle.disabled = tracingToggle.checked !== true; + if (!tracingToggle.checked) { + losslessTracingToggle.checked = false; + await chrome.storage.local.set({ losslessTrace: false }).catch(() => {}); + } +}); + +losslessTracingToggle.addEventListener('change', async () => { + await chrome.storage.local.set({ losslessTrace: losslessTracingToggle.checked }).catch(() => {}); }); costSessionLimitInput?.addEventListener('change', async () => { diff --git a/src/firefox/src/agent/trace-export.js b/src/firefox/src/agent/trace-export.js index 0716016db..6494271fd 100644 --- a/src/firefox/src/agent/trace-export.js +++ b/src/firefox/src/agent/trace-export.js @@ -22,9 +22,52 @@ import { isKnownKind } from '../trace/event-model.js'; const ARGS_LIMIT = 300; const RESULT_LIMIT = 600; +const LOSSILESS_MESSAGE_PREVIEW_LIMIT = 2000; const FOOTER = '_Screenshot pixels and vision descriptions are omitted here — see the Traces page for the complete record._'; const UNKNOWN_EVENTS_NOTE = (n) => `_Note: ${n} unknown event(s) skipped._`; +// Credential masking for the opt-in lossless tier. Exports of lossless runs +// contain real request content, so obvious secret shapes are masked before +// they reach a Markdown file — the same spirit as the strict-redaction path +// used elsewhere, kept pure and browser-neutral here. +const SECRET_PATTERNS = [ + /\bsk-[A-Za-z0-9_-]{8,}/g, + /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, + /\b(?:api[_-]?key|apikey|token|password|passwd|secret)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{8,}/gi, +]; + +function maskSecrets(text) { + let out = String(text ?? ''); + for (const pattern of SECRET_PATTERNS) out = out.replace(pattern, '[redacted]'); + return out; +} + +// Lossless requests carry the full message/tool shape. Render a bounded, +// masked preview per message so the export stays readable without dumping +// every token of a 500 KB request. +function renderLosslessRequest(messages, tools) { + const list = Array.isArray(messages) ? messages : []; + if (!list.length) return ' (empty request log)'; + const lines = []; + for (const message of list.slice(0, 12)) { + const role = oneLine(message?.role || '?'); + const content = maskSecrets( + typeof message?.content === 'string' + ? message.content + : (Array.isArray(message.content) + ? message.content.map(block => block?.text || block?.image_url?.url || '').join(' ') + : ''), + ); + const body = content ? truncate(oneLine(content), LOSSILESS_MESSAGE_PREVIEW_LIMIT) : '(no text)'; + lines.push(`**${role}:** ${body}`); + } + if (list.length > 12) lines.push(`… +${list.length - 12} more message(s) omitted`); + if (Array.isArray(tools) && tools.length) { + lines.push(`tools: ${tools.map(tool => oneLine(tool?.function?.name || '?')).join(', ')}`); + } + return `\n${lines.join('\n')}`; +} + function oneLine(t) { return String(t ?? '').replace(/\s+/g, ' ').trim(); } function humanSize(n) { return n >= 1024 ? `${(n / 1024).toFixed(1)}kb` : `${n}b`; } @@ -202,7 +245,7 @@ export function tracesToMarkdown(runsWithEvents, { Number.isFinite(d.imageBlockCount) ? `${d.imageBlockCount} image block${d.imageBlockCount === 1 ? '' : 's'}` : '', Number.isFinite(d.documentBlockCount) ? `${d.documentBlockCount} document block${d.documentBlockCount === 1 ? '' : 's'}` : '', ].filter(Boolean).join(' · '); - md += `- 🧠 Model request: ${Number(d.messageCount) || 0} messages · ${Number(d.toolsCount) || 0} tools${media ? ` · ${media}` : ''}${renderLocalWikipediaRag(d.localWikipediaRag)}${renderPromptProvenance(d.promptProvenance)}\n`; + md += `- 🧠 Model request: ${Number(d.messageCount) || 0} messages · ${Number(d.toolsCount) || 0} tools${media ? ` · ${media}` : ''}${renderLocalWikipediaRag(d.localWikipediaRag)}${renderPromptProvenance(d.promptProvenance)}${d.lossless === true ? renderLosslessRequest(d.messages, d.tools) : ''}\n`; } else if (ev.kind === 'llm_response') { const content = String(d.content || '').trim(); if (!content) continue; diff --git a/src/firefox/src/trace/recorder.js b/src/firefox/src/trace/recorder.js index d642f6551..f39a241ed 100644 --- a/src/firefox/src/trace/recorder.js +++ b/src/firefox/src/trace/recorder.js @@ -20,6 +20,12 @@ import { normalizeRunHeader, effectiveDelegationDepth } from './run-header.js'; const DB_NAME = 'webbrain_traces'; const DB_VERSION = 2; +// Lossless tier bounds: tool results up to 200 KB verbatim, request payloads +// up to 500 KB before the head is clamped with a truncation marker. Keeps an +// opt-in debugging tier from exhausting IndexedDB on a single long run. +const LOSSILESS_RESULT_CAP = 200_000; +const LOSSILESS_MESSAGES_CAP = 500_000; + let _dbPromise = null; function openDB() { if (_dbPromise) return _dbPromise; @@ -83,6 +89,17 @@ async function tracingEnabled() { } catch { return false; } } +// Opt-in lossless tier: same event pipeline, full request payloads instead of +// content-free provenance. Read once per run at startRun; never per event. +async function losslessTraceEnabled() { + try { + if (typeof indexedDB === 'undefined') return false; + const storageApi = (typeof browser !== 'undefined' ? browser : chrome).storage.local; + const { losslessTrace } = await storageApi.get(['losslessTrace']); + return losslessTrace === true; + } catch { return false; } +} + // ----- Per-run state (held in memory on the service worker) ------------------ // // A run lives only as long as its processMessage() call. If the SW gets @@ -129,6 +146,9 @@ export async function startRun(meta) { const db = await openDB(); const runId = meta.runId || `run_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const lineage = normalizeRunHeader(meta) || {}; + // Tier is decided once per run: explicit caller override wins, otherwise + // the opt-in setting. Never forced for local runs by default. + const lossless = meta.lossless === true || await losslessTraceEnabled(); const record = { runId, // Stable per-conversation id so the Traces UI can group sibling runs @@ -156,13 +176,14 @@ export async function startRun(meta) { tabTitle: meta.tabTitle || '', mode: meta.mode || 'act', attachments: normalizeTraceAttachments(meta.attachments), + lossless, stepCount: 0, totalInputTokens: 0, totalOutputTokens: 0, finalContent: null, }; await promisifyReq(tx(db, ['runs']).objectStore('runs').put(record)); - _runState.set(runId, { seq: 0, model: record.model, providerId: record.providerId }); + _runState.set(runId, { seq: 0, model: record.model, providerId: record.providerId, lossless }); return runId; } catch (e) { console.warn('[trace] startRun failed:', e); @@ -176,9 +197,15 @@ async function _appendEvent(runId, kind, data) { try { const db = await openDB(); if (!_runState.has(runId)) { - // Recover from SW eviction. + // Recover from SW eviction; restore the lossless tier decision from the + // durable run record so it survives a worker restart mid-run. const seq = await _peekSeq(db, runId); - _runState.set(runId, { seq }); + let lossless = false; + try { + const record = await promisifyReq(tx(db, ['runs'], 'readonly').objectStore('runs').get(runId)); + lossless = record?.lossless === true; + } catch {} + _runState.set(runId, { seq, lossless }); } const seq = _newSeq(runId); const ev = makeEvent(runId, seq, kind, data); @@ -196,8 +223,29 @@ async function _appendEvent(runId, kind, data) { } export function recordLLMRequest(runId, step, payload, provenanceInput = null) { - // Never persist full prompts, message text, tool schemas, or tool names here. - // The optional fourth argument is reduced to content-free provenance only. + // Lossless tier (opt-in): persist the request's full message/tool shape for + // deep debugging and request reconstruction. Clamped so one oversized + // request cannot exhaust IndexedDB; the marker mirrors the tool-result + // truncation convention ({ _truncated, length, head }). + if (_runState.get(runId)?.lossless === true && provenanceInput) { + let messages = provenanceInput.messages || null; + if (messages !== null) { + const serialized = JSON.stringify(messages); + if (serialized && serialized.length > LOSSILESS_MESSAGES_CAP) { + messages = { _truncated: true, length: serialized.length, head: serialized.slice(0, LOSSILESS_MESSAGES_CAP) }; + } + } + return _appendEvent(runId, 'llm_request', { + step, + ...payload, + lossless: true, + messages, + tools: provenanceInput.tools || null, + }); + } + // Default tier: never persist full prompts, message text, tool schemas, or + // tool names here. The optional fourth argument is reduced to content-free + // provenance only. let promptProvenance = null; if (provenanceInput) { try { @@ -237,13 +285,14 @@ export function recordLLMResponse(runId, step, { content, toolCalls, usage, late export function recordToolCall(runId, step, { name, args, result, latencyMs }) { // Truncate very large tool results (a11y trees can be huge). Keep the first - // 20KB verbatim and note the truncation — plenty for debugging flow, and - // the model response still has the full thing in context anyway. + // 20KB verbatim by default — plenty for debugging flow — and 200KB in the + // opt-in lossless tier; note the truncation either way. + const cap = _runState.get(runId)?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; let shortResult = result; try { const s = typeof result === 'string' ? result : JSON.stringify(result); - if (s && s.length > 20_000) { - shortResult = { _truncated: true, length: s.length, head: s.slice(0, 20_000) }; + if (s && s.length > cap) { + shortResult = { _truncated: true, length: s.length, head: s.slice(0, cap) }; } } catch {} return _appendEvent(runId, 'tool', { @@ -263,7 +312,12 @@ export async function recordScreenshot(runId, step, dataUrl, caption = '') { const db = await openDB(); if (!_runState.has(runId)) { const seq = await _peekSeq(db, runId); - _runState.set(runId, { seq }); + let lossless = false; + try { + const record = await promisifyReq(tx(db, ['runs'], 'readonly').objectStore('runs').get(runId)); + lossless = record?.lossless === true; + } catch {} + _runState.set(runId, { seq, lossless }); } const seq = _newSeq(runId); // Decode data URL to a Blob so IDB stores raw bytes (no base64 overhead). diff --git a/src/firefox/src/trace/runtime-config.js b/src/firefox/src/trace/runtime-config.js index 660fd6773..659b90e39 100644 --- a/src/firefox/src/trace/runtime-config.js +++ b/src/firefox/src/trace/runtime-config.js @@ -17,6 +17,7 @@ const BOOLEAN_FIELDS = Object.freeze([ 'selection_grounded', 'standalone_chat_profile', 'standalone_webgpu_profile', + 'lossless_trace', ]); // Bounds keep the payload sane, not to re-validate settings: each range is a diff --git a/src/firefox/src/ui/locales/ar.js b/src/firefox/src/ui/locales/ar.js index a53297f31..a322d833d 100644 --- a/src/firefox/src/ui/locales/ar.js +++ b/src/firefox/src/ui/locales/ar.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': 'كل خطوة', 'st.display.tracing.label': 'تسجيل التتبّعات (لمقارنة النماذج)', 'st.display.tracing.desc_html': 'حفظ كل تشغيل (طلبات LLM، الاستجابات، استدعاءات الأدوات، لقطات الشاشة) في IndexedDB المحلية حتى تتمكّن من فحص النماذج ومقارنتها جنبًا إلى جنب. يُفتح في علامة تبويب «التتبّعات» منفصلة. مُعطَّل افتراضيًا لأنه يضيف عمليات كتابة على القرص في كل خطوة. افتح صفحة التتبّعات →', + 'st.display.lossless_tracing.label': 'تسجيل تفاصيل الطلب الكاملة (طبقة تصحيح الأخطاء)', + 'st.display.lossless_tracing.desc_html': 'يحفظ أيضًا المطالبات والرسائل ومخططات الأدوات الكاملة لكل طلب نموذج لغوي بدلاً من الملخصات الخالية من المحتوى، ويحتفظ بما يصل إلى 200 كيلوبايت من كل نتيجة أداة. يتيح تصحيح الأخطاء العميق وإعادة بناء الطلبات. معطّل افتراضيًا لأنه يكتب محتوى محادثتك محليًا ويستهلك مساحة أكبر بكثير. يتطلب تفعيل تسجيل التتبعات.', 'st.display.allow_local_network.label': 'السماح للوكيل بالوصول إلى الشبكة المحلية', 'st.display.allow_local_network.desc': 'يسمح لأداة fetch_url للوكيل بالوصول إلى عناوين loopback (127.0.0.1، localhost) وعناوين RFC1918 الخاصة (10.x، 192.168.x، 172.16–31.x). مفيد لخوادم التطوير المحلية، وأدوات الإنترانت، وواجهات API ذاتية الاستضافة. مُعطَّل افتراضيًا — عند تعطيله لا تستطيع صفحات حقن الـ prompt دفع الوكيل لفحص شبكتك المحلية. تظل عناوين بيانات السحابة (169.254.169.254) وأسماء *.internal/*.local محظورة حتى عند التفعيل.', 'st.display.notify_sound.label': 'صوت الإكمال', diff --git a/src/firefox/src/ui/locales/bn.js b/src/firefox/src/ui/locales/bn.js index 4c64ff4fd..c80dace6d 100644 --- a/src/firefox/src/ui/locales/bn.js +++ b/src/firefox/src/ui/locales/bn.js @@ -589,6 +589,8 @@ export default { 'st.display.auto_screenshot.every_step': "প্রতিটি পদক্ষেপ", 'st.display.tracing.label': "রেকর্ড ট্রেস (মডেল তুলনা জন্য)", 'st.display.tracing.desc_html': "স্থানীয় IndexedDB-তে প্রতিটি রান (LLM অনুরোধ, প্রতিক্রিয়া, টুল কল, স্ক্রিনশট) চালিয়ে যান যাতে আপনি পাশাপাশি মডেলগুলি পরিদর্শন এবং তুলনা করতে পারেন। একটি পৃথক ট্রেস ট্যাবে খোলে। ডিফল্টরূপে বন্ধ কারণ এটি প্রতি ধাপে ডিস্ক রাইট যোগ করে। ট্রেস পৃষ্ঠা খুলুন →", + 'st.display.lossless_tracing.label': 'সম্পূর্ণ অনুরোধের বিবরণ রেকর্ড করুন (ডিবাগ স্তর)', + 'st.display.lossless_tracing.desc_html': 'প্রতিটি LLM অনুরোধের সম্পূর্ণ প্রম্পট, বার্তা এবং টুল স্কিমা বিষয়বস্তুহীন সারাংশের পরিবর্তে সংরক্ষণ করে এবং প্রতিটি টুল ফলাফলের ২০০ KB পর্যন্ত রাখে। গভীর ডিবাগিং এবং অনুরোধ পুনর্নির্মাণ সক্ষম করে। ডিফল্টভাবে বন্ধ কারণ এটি আপনার কথোপকথনের বিষয়বস্তু স্থানীয়ভাবে লেখে এবং অনেক বেশি ডিস্ক ব্যবহার করে। ট্রেস রেকর্ডিং চালু থাকা প্রয়োজন।', 'st.display.cost_session_limit.label': "মেঘ খরচ অধিবেশন ভাতা", 'st.display.cost_session_limit.desc': "স্টপ ক্লাউড এবং OpenRouter এই সেশনটি রিপোর্ট করা বা আনুমানিক খরচ ভাতা পৌঁছানোর পরে অন্য একটি প্রদত্ত মডেল কলের আগে চলে। ডিফল্ট $10।", 'st.display.cost_total_limit.label': "ক্লাউড খরচ মোট ভাতা", diff --git a/src/firefox/src/ui/locales/de.js b/src/firefox/src/ui/locales/de.js index cd9921dd9..66afd9898 100644 --- a/src/firefox/src/ui/locales/de.js +++ b/src/firefox/src/ui/locales/de.js @@ -584,6 +584,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Bei jedem Schritt', 'st.display.tracing.label': 'Traces aufzeichnen (zum Modellvergleich)', 'st.display.tracing.desc_html': 'Speichert jeden Durchlauf in der lokalen IndexedDB, damit Sie Modelle seitenweise inspizieren und vergleichen können. Traces öffnen →', + 'st.display.lossless_tracing.label': 'Vollständige Anforderungsdetails aufzeichnen (Debug-Stufe)', + 'st.display.lossless_tracing.desc_html': 'Speichert zusätzlich die vollständigen Prompts, Nachrichten und Tool-Schemata jeder LLM-Anfrage statt inhaltsloser Zusammenfassungen und behält bis zu 200 KB jedes Tool-Ergebnisses. Ermöglicht tiefes Debugging und die Rekonstruktion von Anfragen. Standardmäßig aus, weil es Ihren Gesprächsinhalt lokal schreibt und deutlich mehr Speicherplatz benötigt. Erfordert aktivierte Trace-Aufzeichnung.', 'st.display.cost_session_limit.label': 'Cloud-Kosten-Sitzungskontingent', 'st.display.cost_session_limit.desc': 'Stoppt Cloud- und OpenRouter-Durchläufe vor einem weiteren kostenpflichtigen Modellaufruf, wenn diese Sitzung das Kostenkontingent erreicht.', 'st.display.cost_total_limit.label': 'Cloud-Kosten-Gesamtkontingent', diff --git a/src/firefox/src/ui/locales/en.js b/src/firefox/src/ui/locales/en.js index 79ce7eef7..683f03316 100644 --- a/src/firefox/src/ui/locales/en.js +++ b/src/firefox/src/ui/locales/en.js @@ -590,6 +590,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Every step', 'st.display.tracing.label': 'Record traces (for model comparison)', 'st.display.tracing.desc_html': 'Persist every run (LLM requests, responses, tool calls, screenshots) into local IndexedDB so you can inspect and compare models side-by-side. Opens in a separate Traces tab. Off by default because it adds disk writes per step. Open Traces page →', + 'st.display.lossless_tracing.label': 'Record full request details (debug tier)', + 'st.display.lossless_tracing.desc_html': 'Also persist the full prompts, messages, and tool schemas of each LLM request instead of content-free summaries, and keep up to 200 KB of each tool result. Enables deep debugging and request reconstruction. Off by default because it writes your conversation content locally and uses much more disk. Requires Record traces to be on.', 'st.display.cost_session_limit.label': 'Cloud cost session allowance', 'st.display.cost_session_limit.desc': 'Stops eligible cloud and router runs before another paid model call once this session reaches the reported or estimated cost allowance. Default $10.', 'st.display.cost_total_limit.label': 'Cloud cost total allowance', diff --git a/src/firefox/src/ui/locales/es.js b/src/firefox/src/ui/locales/es.js index c9d93dd7d..cb281e39a 100644 --- a/src/firefox/src/ui/locales/es.js +++ b/src/firefox/src/ui/locales/es.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': 'En cada paso', 'st.display.tracing.label': 'Registrar trazas (para comparar modelos)', 'st.display.tracing.desc_html': 'Guarda cada ejecución (peticiones al LLM, respuestas, llamadas a herramientas, capturas) en IndexedDB local para que puedas inspeccionar y comparar modelos lado a lado. Se abre en una pestaña de Trazas aparte. Desactivado por defecto porque añade escrituras en disco por paso. Abrir página de Trazas →', + 'st.display.lossless_tracing.label': 'Grabar detalles completos de la solicitud (nivel de depuración)', + 'st.display.lossless_tracing.desc_html': 'También guarda los prompts, mensajes y esquemas de herramientas completos de cada solicitud LLM en lugar de resúmenes sin contenido, y conserva hasta 200 KB de cada resultado de herramienta. Permite depuración profunda y reconstrucción de solicitudes. Desactivado por defecto porque escribe el contenido de tu conversación localmente y usa mucho más disco. Requiere que Grabar trazas esté activado.', 'st.display.allow_local_network.label': 'Permitir al agente acceder a la red local', 'st.display.allow_local_network.desc': 'Permite que la herramienta fetch_url del agente alcance direcciones loopback (127.0.0.1, localhost) y privadas/RFC1918 (10.x, 192.168.x, 172.16–31.x). Útil para servidores de desarrollo locales, herramientas de intranet o APIs auto-alojadas. Desactivado por defecto — cuando está desactivado, páginas con inyección de prompt no pueden dirigir al agente a sondear tu red local. Las direcciones de metadatos en la nube (169.254.169.254) y los nombres de host *.internal/*.local siguen bloqueados incluso cuando está activado.', 'st.display.notify_sound.label': 'Sonido al terminar', diff --git a/src/firefox/src/ui/locales/fa.js b/src/firefox/src/ui/locales/fa.js index 9e0b011e9..3497f6c98 100644 --- a/src/firefox/src/ui/locales/fa.js +++ b/src/firefox/src/ui/locales/fa.js @@ -589,6 +589,8 @@ export default { 'st.display.auto_screenshot.every_step': "هر قدم", 'st.display.tracing.label': "ثبت ردیابی (برای مقایسه مدل)", 'st.display.tracing.desc_html': "هر اجرا (درخواست‌های LLM، پاسخ‌ها، تماس‌های ابزار، تصاویر صفحه) را در IndexedDB محلی ادامه دهید تا بتوانید مدل‌ها را در کنار یکدیگر بررسی و مقایسه کنید. در یک برگه Traces جداگانه باز می شود. به‌طور پیش‌فرض خاموش است زیرا نوشته‌های دیسک را در هر مرحله اضافه می‌کند. باز کردن صفحه Traces →", + 'st.display.lossless_tracing.label': 'ثبت جزئیات کامل درخواست (لایه اشکالزدایی)', + 'st.display.lossless_tracing.desc_html': 'همچنین پرامپتها، پیامها و ساختار ابزارهای کامل هر درخواست LLM را بهجای خلاصههای بدون محتوا ذخیره میکند و تا ۲۰۰ کیلوبایت از هر نتیجه ابزار را نگه میدارد. اشکالزدایی عمیق و بازسازی درخواست را ممکن میسازد. بهصورت پیشفرض خاموش است زیرا محتوای گفتوگوی شما را محلی مینویسد و فضای دیسک بسیار بیشتری مصرف میکند. نیازمند فعال بودن ضبط ردگیری است.', 'st.display.cost_session_limit.label': "کمک هزینه جلسه هزینه ابر", 'st.display.cost_session_limit.desc': "هنگامی که این جلسه به میزان هزینه گزارش شده یا تخمین زده شده رسید، ابر را متوقف می کند و OpenRouter قبل از تماس مدل پولی دیگر اجرا می شود. پیش فرض 10 دلار.", 'st.display.cost_total_limit.label': "هزینه کل کمک هزینه", diff --git a/src/firefox/src/ui/locales/fr.js b/src/firefox/src/ui/locales/fr.js index 5b73c4115..7c9138ad9 100644 --- a/src/firefox/src/ui/locales/fr.js +++ b/src/firefox/src/ui/locales/fr.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': 'À chaque étape', 'st.display.tracing.label': 'Enregistrer les traces (pour comparer des modèles)', 'st.display.tracing.desc_html': 'Conserve chaque exécution (requêtes LLM, réponses, appels d\'outils, captures) dans IndexedDB local pour pouvoir inspecter et comparer les modèles côte à côte. S\'ouvre dans un onglet Traces distinct. Désactivé par défaut car cela ajoute des écritures disque à chaque étape. Ouvrir la page Traces →', + 'st.display.lossless_tracing.label': 'Enregistrer les détails complets des requêtes (niveau débogage)', + 'st.display.lossless_tracing.desc_html': 'Conserve aussi les prompts, messages et schémas d\'outils complets de chaque requête LLM au lieu de résumés sans contenu, et garde jusqu\'à 200 Ko de chaque résultat d\'outil. Permet un débogage approfondi et la reconstruction des requêtes. Désactivé par défaut car cela écrit localement le contenu de votre conversation et utilise beaucoup plus d\'espace disque. Nécessite que l\'enregistrement des traces soit activé.', 'st.display.allow_local_network.label': 'Autoriser l\'agent à accéder au réseau local', 'st.display.allow_local_network.desc': 'Permet à l\'outil fetch_url de l\'agent d\'atteindre les adresses loopback (127.0.0.1, localhost) et privées/RFC1918 (10.x, 192.168.x, 172.16–31.x). Utile pour les serveurs de dev locaux, outils intranet ou API auto-hébergées. Désactivé par défaut — quand désactivé, des pages avec injection de prompt ne peuvent pas pousser l\'agent à sonder votre réseau local. Les adresses de méta-données cloud (169.254.169.254) et les noms d\'hôte *.internal/*.local restent bloqués même quand activé.', 'st.display.notify_sound.label': 'Son de fin', diff --git a/src/firefox/src/ui/locales/he.js b/src/firefox/src/ui/locales/he.js index 9fe0a30d9..f43403863 100644 --- a/src/firefox/src/ui/locales/he.js +++ b/src/firefox/src/ui/locales/he.js @@ -552,6 +552,8 @@ export default { "st.display.auto_screenshot.every_step": "כל צעד", "st.display.tracing.label": "הקלטת עקבות (להשוואת מודלים)", "st.display.tracing.desc_html": "תתמיד בכל ריצה (LLM בקשות, תגובות, קריאות לכלים, צילומי מסך) לתוך IndexedDB המקומי כדי שתוכל לבדוק ולהשוות מודלים זה לצד זה. נפתח בלשונית Traces נפרדת. כבוי כברירת מחדל מכיוון שהוא מוסיף כתיבה בדיסק בכל שלב. פתח את דף העקבות →", + "st.display.lossless_tracing.label": "רישום פרטי בקשה מלאים (שכבת דיבאג)", + "st.display.lossless_tracing.desc_html": "שומר גם את ההנחיות, ההודעות והמבנים המלאים של כל בקשת LLM במקום סיכומים חסרי תוכן, ומחזיק עד 200KB מכל תוצאת כלי. מאפשר ניפוי שגיאות מעמיק ושחזור בקשות. כבוי כברירת מחדל משום שהוא כותב את תוכן השיחה שלך מקומית ומשתמש בהרבה יותר מקום. דורש שהקלטת מעקב תהיה פעילה.", "st.display.cost_session_limit.label": "קצבת הפגישה בעלות ענן", "st.display.cost_session_limit.desc": "מפסיק ענן ו OpenRouter פועל לפני שיחת מודל בתשלום נוספת ברגע שההפעלה הזו מגיעה לקצבת העלות המדווחת או המשוערת. ברירת מחדל 10 $.", "st.display.cost_total_limit.label": "הקצבה כוללת של עלות ענן", diff --git a/src/firefox/src/ui/locales/hi.js b/src/firefox/src/ui/locales/hi.js index 41393336d..44d51dfee 100644 --- a/src/firefox/src/ui/locales/hi.js +++ b/src/firefox/src/ui/locales/hi.js @@ -589,6 +589,8 @@ export default { 'st.display.auto_screenshot.every_step': "हर कदम", 'st.display.tracing.label': "रिकॉर्ड निशान (मॉडल तुलना के लिए)", 'st.display.tracing.desc_html': "प्रत्येक रन (एलएलएम अनुरोध, प्रतिक्रिया, टूल कॉल, स्क्रीनशॉट) को स्थानीय IndexedDB में जारी रखें ताकि आप साथ-साथ मॉडलों का निरीक्षण और तुलना कर सकें। एक अलग ट्रेसेस टैब में खुलता है। डिफ़ॉल्ट रूप से बंद है क्योंकि यह प्रति चरण डिस्क राइट जोड़ता है। ट्रेस पेज खोलें →", + 'st.display.lossless_tracing.label': 'पूर्ण अनुरोध विवरण रिकॉर्ड करें (डीबग स्तर)', + 'st.display.lossless_tracing.desc_html': 'प्रत्येक LLM अनुरोध के पूर्ण प्रॉम्प्ट, संदेश और टूल स्कीमा को बिना सामग्री वाले सारांशों के बजाय सहेजता है, और प्रत्येक टूल परिणाम का 200 KB तक रखता है। गहन डीबगिंग और अनुरोध पुनर्निर्माण सक्षम करता है। डिफ़ॉल्ट रूप से बंद क्योंकि यह आपकी बातचीत की सामग्री को स्थानीय रूप से लिखता है और बहुत अधिक डिस्क उपयोग करता है। ट्रेस रिकॉर्डिंग चालू होना आवश्यक है।', 'st.display.cost_session_limit.label': "क्लाउड लागत सत्र भत्ता", 'st.display.cost_session_limit.desc': "एक बार जब यह सत्र रिपोर्ट की गई या अनुमानित लागत भत्ता तक पहुंच जाता है, तो क्लाउड रुक जाता है और OpenRouter किसी अन्य भुगतान मॉडल कॉल से पहले चलता है। डिफ़ॉल्ट $10.", 'st.display.cost_total_limit.label': "क्लाउड लागत कुल भत्ता", diff --git a/src/firefox/src/ui/locales/id.js b/src/firefox/src/ui/locales/id.js index b3964a5e1..eb8cd735d 100644 --- a/src/firefox/src/ui/locales/id.js +++ b/src/firefox/src/ui/locales/id.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Setiap langkah', 'st.display.tracing.label': 'Rekam jejak (untuk perbandingan model)', 'st.display.tracing.desc_html': 'Simpan setiap proses (permintaan LLM, respons, pemanggilan alat, tangkapan layar) ke IndexedDB lokal sehingga Anda dapat memeriksa dan membandingkan model berdampingan. Membuka tab Jejak terpisah. Nonaktif secara bawaan karena menambah penulisan disk per langkah. Buka halaman Jejak →', + 'st.display.lossless_tracing.label': 'Rekam detail lengkap permintaan (tingkat debug)', + 'st.display.lossless_tracing.desc_html': 'Juga menyimpan prompt, pesan, dan skema alat lengkap dari setiap permintaan LLM alih-alih ringkasan tanpa konten, dan menyimpan hingga 200 KB setiap hasil alat. Memungkinkan debug mendalam dan rekonstruksi permintaan. Nonaktif secara default karena menulis konten percakapan Anda secara lokal dan memakai disk jauh lebih banyak. Memerlukan perekaman trace aktif.', 'st.display.allow_local_network.label': 'Izinkan agen mengakses jaringan lokal', 'st.display.allow_local_network.desc': 'Memungkinkan alat fetch_url agen menjangkau loopback (127.0.0.1, localhost) dan alamat privat/RFC1918 (10.x, 192.168.x, 172.16–31.x). Berguna untuk server pengembangan lokal, alat intranet, atau API yang di-host sendiri. Nonaktif secara bawaan — saat nonaktif, halaman yang disisipi prompt-injection tidak dapat menyuruh agen memindai jaringan lokal Anda. Alamat metadata cloud (169.254.169.254) dan nama host *.internal/*.local tetap diblokir bahkan saat diaktifkan.', 'st.display.notify_sound.label': 'Suara penyelesaian', diff --git a/src/firefox/src/ui/locales/ja.js b/src/firefox/src/ui/locales/ja.js index 9874799b1..07541dd40 100644 --- a/src/firefox/src/ui/locales/ja.js +++ b/src/firefox/src/ui/locales/ja.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': '毎ステップ', 'st.display.tracing.label': 'トレースを記録(モデル比較用)', 'st.display.tracing.desc_html': 'すべての実行(LLM リクエスト、レスポンス、ツール呼び出し、スクリーンショット)をローカルの IndexedDB に保存し、モデルを並べて検査・比較できるようにします。別の「トレース」タブで開きます。毎ステップで書き込みが増えるためデフォルトはオフです。トレースページを開く →', + 'st.display.lossless_tracing.label': 'リクエストの完全な詳細を記録(デバッグ層)', + 'st.display.lossless_tracing.desc_html': '各LLMリクエストの完全なプロンプト・メッセージ・ツールスキーマを、内容を含まない要約の代わりに保存し、各ツール結果を最大200KB保持します。深いデバッグとリクエスト再構築が可能になります。会話内容をローカルに書き込み、ディスク消費が大幅に増えるため、既定ではオフです。トレース記録の有効化が必要です。', 'st.display.allow_local_network.label': 'エージェントにローカルネットワークへのアクセスを許可', 'st.display.allow_local_network.desc': 'エージェントの fetch_url ツールがループバック(127.0.0.1、localhost)やプライベート/RFC1918 アドレス(10.x、192.168.x、172.16–31.x)に届くようになります。ローカル開発サーバー、社内ツール、セルフホスト API に便利です。デフォルトはオフ — オフのときは prompt 注入されたページがエージェントにローカルネットワークの探索をさせることはできません。クラウドメタデータアドレス(169.254.169.254)と *.internal/*.local ホスト名はオンでもブロックされたままです。', 'st.display.notify_sound.label': '完了サウンド', diff --git a/src/firefox/src/ui/locales/ko.js b/src/firefox/src/ui/locales/ko.js index 649fb200c..be2399c8a 100644 --- a/src/firefox/src/ui/locales/ko.js +++ b/src/firefox/src/ui/locales/ko.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': '매 단계', 'st.display.tracing.label': '트레이스 기록 (모델 비교용)', 'st.display.tracing.desc_html': '모든 실행(LLM 요청, 응답, 도구 호출, 스크린샷)을 로컬 IndexedDB에 보관하여 모델을 나란히 검사하고 비교할 수 있게 합니다. 별도의 트레이스 탭에서 열립니다. 매 단계마다 디스크 쓰기가 늘어나므로 기본값은 꺼짐입니다. 트레이스 페이지 열기 →', + 'st.display.lossless_tracing.label': '요청 전체 세부정보 기록(디버그 계층)', + 'st.display.lossless_tracing.desc_html': '각 LLM 요청의 전체 프롬프트·메시지·도구 스키마를 내용 없는 요약 대신 저장하고, 각 도구 결과를 최대 200KB까지 보관합니다. 심층 디버깅과 요청 재구성이 가능합니다. 대화 내용을 로컬에 기록하고 디스크를 훨씬 더 사용하므로 기본적으로 꺼져 있습니다. 트레이스 기록이 켜져 있어야 합니다.', 'st.display.allow_local_network.label': '에이전트의 로컬 네트워크 접근 허용', 'st.display.allow_local_network.desc': '에이전트의 fetch_url 도구가 루프백(127.0.0.1, localhost)과 사설/RFC1918 주소(10.x, 192.168.x, 172.16–31.x)에 접근할 수 있게 합니다. 로컬 개발 서버, 인트라넷 도구, 자체 호스팅 API에 유용합니다. 기본값은 끔 — 꺼져 있으면 prompt 주입된 페이지가 에이전트로 하여금 로컬 네트워크를 탐색하게 만들 수 없습니다. 클라우드 메타데이터 주소(169.254.169.254)와 *.internal/*.local 호스트명은 켜진 상태에서도 차단됩니다.', 'st.display.notify_sound.label': '완료 알림음', diff --git a/src/firefox/src/ui/locales/ms.js b/src/firefox/src/ui/locales/ms.js index a6fe1d01c..7e921271d 100644 --- a/src/firefox/src/ui/locales/ms.js +++ b/src/firefox/src/ui/locales/ms.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Setiap langkah', 'st.display.tracing.label': 'Rakam jejak (untuk perbandingan model)', 'st.display.tracing.desc_html': 'Simpan setiap larian (permintaan LLM, respons, panggilan alat, tangkapan skrin) ke dalam IndexedDB tempatan supaya anda boleh memeriksa dan membandingkan model secara bersebelahan. Dibuka dalam tab Jejak yang berasingan. Dimatikan secara lalai kerana ia menambah penulisan cakera pada setiap langkah. Buka halaman Jejak →', + 'st.display.lossless_tracing.label': 'Rekod butiran penuh permintaan (peringkat debug)', + 'st.display.lossless_tracing.desc_html': 'Juga menyimpan prompt, mesej, dan skema alatan penuh bagi setiap permintaan LLM dan bukannya ringkasan tanpa kandungan, serta mengekalkan sehingga 200 KB bagi setiap hasil alatan. Membolehkan penyahpepijatan mendalam dan pembinaan semula permintaan. Dimatikan secara lalai kerana menulis kandungan perbualan anda secara setempat dan menggunakan lebih banyak cakera. Memerlukan rakaman jejak dihidupkan.', 'st.display.allow_local_network.label': 'Benarkan ejen mengakses rangkaian tempatan', 'st.display.allow_local_network.desc': 'Membenarkan alat fetch_url ejen mencapai loopback (127.0.0.1, localhost) dan alamat peribadi/RFC1918 (10.x, 192.168.x, 172.16–31.x). Berguna untuk pelayan pembangunan tempatan, alat intranet, atau API yang dihos sendiri. Dimatikan secara lalai — apabila dimatikan, halaman yang disuntik prompt-injection tidak boleh mendorong ejen untuk meneliti rangkaian tempatan anda. Alamat metadata awan (169.254.169.254) dan nama hos *.internal/*.local kekal disekat walaupun dihidupkan.', 'st.display.notify_sound.label': 'Bunyi selesai', diff --git a/src/firefox/src/ui/locales/nl.js b/src/firefox/src/ui/locales/nl.js index bb625241f..87507362e 100644 --- a/src/firefox/src/ui/locales/nl.js +++ b/src/firefox/src/ui/locales/nl.js @@ -566,6 +566,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Elke stap', 'st.display.tracing.label': 'Traces opslaan (voor modelvergelijking)', 'st.display.tracing.desc_html': 'Sla elke uitvoering op in lokale IndexedDB... Traces openen →', + 'st.display.lossless_tracing.label': 'Volledige verzoekdetails opnemen (debuglaag)', + 'st.display.lossless_tracing.desc_html': 'Bewaart ook de volledige prompts, berichten en tool-schema\'s van elk LLM-verzoek in plaats van inhoudsloze samenvattingen, en houdt tot 200 KB van elk toolresultaat. Maakt diepgaand debuggen en reconstructie van verzoeken mogelijk. Standaard uit omdat het de inhoud van uw gesprek lokaal opslaat en veel meer schijfruimte gebruikt. Vereist dat tracerecordering aan staat.', 'st.display.cost_session_limit.label': 'Cloudkostensessietegoed', 'st.display.cost_session_limit.desc': 'Stopt cloud- en OpenRouter-uitvoeringen voordat nog een betaald modelaanroep plaatsvindt...', 'st.display.cost_total_limit.label': 'Cloudkostentotaaltegoed', diff --git a/src/firefox/src/ui/locales/pl.js b/src/firefox/src/ui/locales/pl.js index b95c3cfd7..6893e749b 100644 --- a/src/firefox/src/ui/locales/pl.js +++ b/src/firefox/src/ui/locales/pl.js @@ -410,6 +410,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Przy każdym kroku', 'st.display.tracing.label': 'Rejestruj ślady (do porównywania modeli)', 'st.display.tracing.desc_html': 'Zapisuj każde wykonanie (żądania LLM, odpowiedzi, wywołania narzędzi, zrzuty ekranu) w lokalnej bazie IndexedDB, aby móc je analizować i porównywać modele obok siebie. Otwiera się w osobnej karcie Ślady. Domyślnie wyłączone, ponieważ dodaje zapisy na dysk przy każdym kroku. Otwórz stronę Ślady →', + 'st.display.lossless_tracing.label': 'Zapisuj pełne szczegóły żądań (poziom debugowania)', + 'st.display.lossless_tracing.desc_html': 'Zapisuje również pełne prompty, wiadomości i schematy narzędzi każdego żądania LLM zamiast podsumowań bez treści oraz przechowuje do 200 KB każdego wyniku narzędzia. Umożliwia głębokie debugowanie i rekonstrukcję żądań. Wyłączone domyślnie, ponieważ zapisuje treść rozmowy lokalnie i zużywa znacznie więcej miejsca. Wymaga włączonego zapisu śladów.', 'st.display.cost_session_limit.label': 'Limit kosztów chmury na sesję', 'st.display.cost_session_limit.desc': 'Zatrzymuje wykonania w chmurze i OpenRouter przed kolejnym płatnym wywołaniem modelu, gdy ta sesja osiągnie zgłoszony lub szacowany limit kosztów. Domyślnie 10 USD.', 'st.display.cost_total_limit.label': 'Całkowity limit kosztów chmury', diff --git a/src/firefox/src/ui/locales/pt.js b/src/firefox/src/ui/locales/pt.js index c509832a8..039121351 100644 --- a/src/firefox/src/ui/locales/pt.js +++ b/src/firefox/src/ui/locales/pt.js @@ -589,6 +589,8 @@ export default { 'st.display.auto_screenshot.every_step': "Cada passo", 'st.display.tracing.label': "Registrar rastreamentos (para comparação de modelos)", 'st.display.tracing.desc_html': "Persista cada execução (solicitações LLM, respostas, chamadas de ferramentas, capturas de tela) no IndexedDB local para que você possa inspecionar e comparar modelos lado a lado. Abre em uma guia Traces separada. Desativado por padrão porque adiciona gravações de disco por etapa. Abra a página Traces →", + 'st.display.lossless_tracing.label': 'Registrar detalhes completos da solicitação (nível de depuração)', + 'st.display.lossless_tracing.desc_html': 'Também persiste os prompts, mensagens e esquemas de ferramentas completos de cada solicitação LLM em vez de resumos sem conteúdo, e mantém até 200 KB de cada resultado de ferramenta. Permite depuração profunda e reconstrução de solicitações. Desativado por padrão porque grava o conteúdo da sua conversa localmente e usa muito mais disco. Exige que o registro de rastros esteja ativado.', 'st.display.cost_session_limit.label': "Subsídio de sessão de custo na nuvem", 'st.display.cost_session_limit.desc': "Interrompe a nuvem e OpenRouter é executado antes de outra chamada de modelo pago quando esta sessão atinge o limite de custo relatado ou estimado. Padrão $ 10.", 'st.display.cost_total_limit.label': "Permissão total de custos de nuvem", diff --git a/src/firefox/src/ui/locales/ru.js b/src/firefox/src/ui/locales/ru.js index d03080b06..a539cbfa5 100644 --- a/src/firefox/src/ui/locales/ru.js +++ b/src/firefox/src/ui/locales/ru.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Каждый шаг', 'st.display.tracing.label': 'Записывать трассы (для сравнения моделей)', 'st.display.tracing.desc_html': 'Сохранять каждый запуск (LLM-запросы, ответы, вызовы инструментов, скриншоты) в локальную IndexedDB, чтобы можно было разбирать и сравнивать модели бок о бок. Открывается в отдельной вкладке «Трассы». По умолчанию выключено, потому что добавляет запись на диск на каждом шаге. Открыть страницу трасс →', + 'st.display.lossless_tracing.label': 'Записывать полные детали запросов (отладочный уровень)', + 'st.display.lossless_tracing.desc_html': 'Также сохраняет полные промпты, сообщения и схемы инструментов каждого LLM-запроса вместо кратких сводок без содержимого, и хранит до 200 КБ каждого результата инструмента. Позволяет глубокую отладку и восстановление запросов. Выключено по умолчанию, так как записывает содержимое вашего диалога локально и занимает заметно больше места. Требует включённой записи трасс.', 'st.display.allow_local_network.label': 'Разрешить агенту доступ к локальной сети', 'st.display.allow_local_network.desc': 'Разрешает инструменту fetch_url агента обращаться к loopback (127.0.0.1, localhost) и частным адресам RFC1918 (10.x, 192.168.x, 172.16–31.x). Полезно для локальных серверов разработки, интранет-инструментов и self-hosted API. По умолчанию выключено — когда выключено, страницы с prompt-инъекцией не смогут заставить агента сканировать вашу локальную сеть. Адреса метаданных облака (169.254.169.254) и имена *.internal/*.local остаются заблокированы даже когда включено.', 'st.display.notify_sound.label': 'Звук завершения', diff --git a/src/firefox/src/ui/locales/th.js b/src/firefox/src/ui/locales/th.js index 78cc80afe..f2175b852 100644 --- a/src/firefox/src/ui/locales/th.js +++ b/src/firefox/src/ui/locales/th.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': 'ทุกขั้น', 'st.display.tracing.label': 'บันทึกการติดตาม (เพื่อเปรียบเทียบโมเดล)', 'st.display.tracing.desc_html': 'จัดเก็บทุกการทำงาน (คำขอ LLM, การตอบกลับ, การเรียกใช้เครื่องมือ, ภาพหน้าจอ) ลงใน IndexedDB ในเครื่อง เพื่อให้คุณตรวจสอบและเปรียบเทียบโมเดลแบบเคียงข้างกันได้ จะเปิดในแท็บ Traces แยกต่างหาก ปิดเป็นค่าเริ่มต้นเพราะเพิ่มการเขียนดิสก์ในทุกขั้น เปิดหน้า Traces →', + 'st.display.lossless_tracing.label': 'บันทึกรายละเอียดคำขอทั้งหมด (ระดับดีบัก)', + 'st.display.lossless_tracing.desc_html': 'จัดเก็บพรอมต์ ข้อความ และโครงสร้างเครื่องมือแบบเต็มของแต่ละคำขอ LLM แทนสรุปที่ไม่มีเนื้อหา และเก็บผลลัพธ์เครื่องมือแต่ละรายการสูงสุด 200 KB เปิดใช้งานการดีบักเชิงลึกและการสร้างคำขอใหม่ ปิดโดยค่าเริ่มต้นเพราะเขียนเนื้อหาสนทนาของคุณในเครื่องและใช้พื้นที่ดิสก์มากกว่ามาก ต้องเปิดการบันทึกเทรซ', 'st.display.allow_local_network.label': 'อนุญาตให้เอเจนต์เข้าถึงเครือข่ายภายใน', 'st.display.allow_local_network.desc': 'ให้เครื่องมือ fetch_url ของเอเจนต์เข้าถึง loopback (127.0.0.1, localhost) และที่อยู่ส่วนตัว/RFC1918 (10.x, 192.168.x, 172.16–31.x) มีประโยชน์สำหรับเซิร์ฟเวอร์พัฒนาภายใน เครื่องมืออินทราเน็ต หรือ API ที่โฮสต์เอง ปิดเป็นค่าเริ่มต้น — เมื่อปิด หน้าเว็บที่ถูกแทรก prompt-injection ไม่สามารถสั่งเอเจนต์ให้สำรวจเครือข่ายภายในของคุณได้ ส่วนที่อยู่เมตาดาตาคลาวด์ (169.254.169.254) และโฮสต์ *.internal/*.local จะยังถูกบล็อกแม้จะเปิดไว้', 'st.display.notify_sound.label': 'เสียงเมื่อทำเสร็จ', diff --git a/src/firefox/src/ui/locales/tl.js b/src/firefox/src/ui/locales/tl.js index bf21dd525..268ec4ec1 100644 --- a/src/firefox/src/ui/locales/tl.js +++ b/src/firefox/src/ui/locales/tl.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Bawat hakbang', 'st.display.tracing.label': 'I-record ang mga trace (para sa paghahambing ng modelo)', 'st.display.tracing.desc_html': 'I-save ang bawat run (mga LLM request, response, tool call, screenshot) sa lokal na IndexedDB para maaari mong suriin at ihambing ang mga modelo nang magkatabi. Bumubukas sa hiwalay na Traces tab. Naka-off bilang default dahil nagdadagdag ito ng disk writes kada hakbang. Buksan ang Traces page →', + 'st.display.lossless_tracing.label': 'Itala ang buong detalye ng kahilingan (antas ng debug)', + 'st.display.lossless_tracing.desc_html': 'Iniimbak din ang buong prompt, mensahe, at schema ng tool ng bawat LLM request sa halip na buod na walang nilalaman, at nagpapanatili ng hanggang 200 KB ng bawat resulta ng tool. Nagbibigay-daan sa malalim na pag-debug at muling pagbuo ng request. Naka-off bilang default dahil isinusulat nito ang nilalaman ng iyong usapan nang lokal at gumagamit ng mas malaking disk. Nangangailangan ng naka-on na pag-record ng trace.', 'st.display.allow_local_network.label': 'Pahintulutan ang ahente na ma-access ang lokal na network', 'st.display.allow_local_network.desc': 'Hinahayaan ang fetch_url tool ng ahente na maabot ang loopback (127.0.0.1, localhost) at mga private/RFC1918 address (10.x, 192.168.x, 172.16–31.x). Mainam para sa mga lokal na dev server, intranet tool, o self-hosted API. Naka-off bilang default — kapag naka-off, hindi mapipilit ng mga pahina na may prompt-injection ang ahente na sumiyasat sa iyong lokal na network. Mananatiling naka-block ang mga cloud-metadata address (169.254.169.254) at mga *.internal/*.local hostname kahit naka-on ito.', 'st.display.notify_sound.label': 'Tunog kapag tapos na', diff --git a/src/firefox/src/ui/locales/tr.js b/src/firefox/src/ui/locales/tr.js index 147eb2a25..1a0d62176 100644 --- a/src/firefox/src/ui/locales/tr.js +++ b/src/firefox/src/ui/locales/tr.js @@ -295,6 +295,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Her adımda', 'st.display.tracing.label': 'İzleri kaydet (model karşılaştırması için)', 'st.display.tracing.desc_html': 'Her çalışmayı (LLM istekleri, yanıtlar, araç çağrıları, ekran görüntüleri) yerel IndexedDB\'de saklar, böylece modelleri yan yana inceleyip karşılaştırabilirsin. Ayrı bir İzler sekmesinde açılır. Her adımda diske yazdığı için varsayılan olarak kapalıdır. İzler sayfasını aç →', + 'st.display.lossless_tracing.label': 'Tüm istek ayrıntılarını kaydet (hata ayıklama katmanı)', + 'st.display.lossless_tracing.desc_html': 'Her LLM isteğinin tam prompt, mesaj ve araç şemalarını içeriksiz özetler yerine de kalıcı olarak kaydeder ve her araç sonucundan en fazla 200 KB tutar. Derin hata ayıklama ve istek yeniden yapılandırmasını sağlar. Varsayılan olarak kapalıdır çünkü konuşma içeriğinizi yerel olarak yazar ve çok daha fazla disk kullanır. İz kaydının açık olmasını gerektirir.', 'st.display.allow_local_network.label': 'Aracının yerel ağa erişmesine izin ver', 'st.display.allow_local_network.desc': 'Aracının fetch_url aracının loopback (127.0.0.1, localhost) ve özel/RFC1918 adreslerine (10.x, 192.168.x, 172.16–31.x) ulaşmasına izin verir. Yerel geliştirme sunucuları, intranet araçları veya kendi sunucundaki API\'lar için kullanışlıdır. Varsayılan olarak kapalıdır — kapalıyken, prompt-injection içeren sayfalar aracıyı yerel ağını taramaya yönlendiremez. Bulut meta-veri adresleri (169.254.169.254) ve *.internal/*.local ana makine adları açıkken bile engellenir.', 'st.display.notify_sound.label': 'Tamamlama sesi', diff --git a/src/firefox/src/ui/locales/uk.js b/src/firefox/src/ui/locales/uk.js index 0cb6dba84..37018b7ff 100644 --- a/src/firefox/src/ui/locales/uk.js +++ b/src/firefox/src/ui/locales/uk.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': 'Кожен крок', 'st.display.tracing.label': 'Записувати траси (для порівняння моделей)', 'st.display.tracing.desc_html': 'Зберігати кожен запуск (LLM-запити, відповіді, виклики інструментів, скриншоти) у локальну IndexedDB, щоб переглядати й порівнювати моделі поряд. Відкривається в окремій вкладці «Траси». За замовчуванням вимкнено, бо додає запис на диск на кожному кроці. Відкрити сторінку трас →', + 'st.display.lossless_tracing.label': 'Записувати повні деталі запитів (рівень налагодження)', + 'st.display.lossless_tracing.desc_html': 'Також зберігає повні промпти, повідомлення та схеми інструментів кожного LLM-запиту замість стислих зведень без вмісту, та тримає до 200 КБ кожного результату інструмента. Дає змогу глибокого налагодження та відтворення запитів. Вимкнено за замовчуванням, бо записує вміст вашої розмови локально та займає значно більше диска. Потребує увімкненого запису трас.', 'st.display.allow_local_network.label': 'Дозволити агенту доступ до локальної мережі', 'st.display.allow_local_network.desc': 'Дозволяє інструменту fetch_url агента звертатися до loopback (127.0.0.1, localhost) та приватних адрес RFC1918 (10.x, 192.168.x, 172.16–31.x). Корисно для локальних серверів розробки, інтранет-інструментів і self-hosted API. За замовчуванням вимкнено — коли вимкнено, сторінки з prompt-ін\'єкцією не зможуть змусити агента сканувати вашу локальну мережу. Адреси метаданих хмари (169.254.169.254) та імена *.internal/*.local залишаються заблокованими навіть коли ввімкнено.', 'st.display.notify_sound.label': 'Звук завершення', diff --git a/src/firefox/src/ui/locales/vi.js b/src/firefox/src/ui/locales/vi.js index d048b043e..d5bde6b50 100644 --- a/src/firefox/src/ui/locales/vi.js +++ b/src/firefox/src/ui/locales/vi.js @@ -589,6 +589,8 @@ export default { 'st.display.auto_screenshot.every_step': "Mỗi bước", 'st.display.tracing.label': "Ghi lại dấu vết (để so sánh mô hình)", 'st.display.tracing.desc_html': "Duy trì mọi lần chạy (yêu cầu LLM, phản hồi, lệnh gọi công cụ, ảnh chụp màn hình) vào IndexedDB cục bộ để bạn có thể kiểm tra và so sánh các mô hình song song. Mở trong tab Dấu vết riêng biệt. Tắt theo mặc định vì nó thêm ghi vào đĩa mỗi bước. Mở trang Dấu vết →", + 'st.display.lossless_tracing.label': 'Ghi lại chi tiết đầy đủ của yêu cầu (lớp gỡ lỗi)', + 'st.display.lossless_tracing.desc_html': 'Cũng lưu trữ đầy đủ prompt, tin nhắn và cấu trúc công cụ của mỗi yêu cầu LLM thay vì bản tóm tắt không nội dung, đồng thời giữ tối đa 200 KB mỗi kết quả công cụ. Cho phép gỡ lỗi sâu và dựng lại yêu cầu. Tắt theo mặc định vì ghi nội dung hội thoại cục bộ và tốn nhiều dung lượng hơn. Yêu cầu bật tính năng ghi trace.', 'st.display.cost_session_limit.label': "Phụ cấp phiên chi phí đám mây", 'st.display.cost_session_limit.desc': "Dừng đám mây và OpenRouter chạy trước một lệnh gọi mô hình trả phí khác sau khi phiên này đạt đến mức chi phí được báo cáo hoặc ước tính. Mặc định là 10$.", 'st.display.cost_total_limit.label': "Tổng phụ cấp chi phí đám mây", diff --git a/src/firefox/src/ui/locales/zh.js b/src/firefox/src/ui/locales/zh.js index a78b073cc..03c3e0d6d 100644 --- a/src/firefox/src/ui/locales/zh.js +++ b/src/firefox/src/ui/locales/zh.js @@ -259,6 +259,8 @@ export default { 'st.display.auto_screenshot.every_step': '每一步', 'st.display.tracing.label': '记录轨迹(用于模型对比)', 'st.display.tracing.desc_html': '将每次运行(LLM 请求、响应、工具调用、截图)持久化到本地 IndexedDB,以便并排查看和对比模型。在独立的轨迹页打开。因为每步都会写盘,默认关闭。打开轨迹页 →', + 'st.display.lossless_tracing.label': '记录完整请求详情(调试档)', + 'st.display.lossless_tracing.desc_html': '同时持久化每次 LLM 请求的完整提示词、消息与工具结构(而非无内容摘要),并保留每个工具结果最多 200 KB。支持深度调试与请求重建。默认关闭,因为会在本地写入对话内容并占用更多磁盘。需要先开启记录轨迹。', 'st.display.allow_local_network.label': '允许代理访问本地网络', 'st.display.allow_local_network.desc': '允许代理的 fetch_url 工具访问回环(127.0.0.1、localhost)和私有/RFC1918 地址(10.x、192.168.x、172.16–31.x)。用于本地开发服务器、内网工具或自托管 API。默认关闭 — 关闭时,含 prompt 注入的页面无法引导代理探测你的本地网络。即使开启,云元数据地址(169.254.169.254)和 *.internal/*.local 主机名仍然会被阻止。', 'st.display.notify_sound.label': '完成提示音', diff --git a/src/firefox/src/ui/settings.html b/src/firefox/src/ui/settings.html index 7317fbbe5..89ae05530 100644 --- a/src/firefox/src/ui/settings.html +++ b/src/firefox/src/ui/settings.html @@ -1342,6 +1342,16 @@

+
+
+
+
+
+ +
diff --git a/src/firefox/src/ui/settings.js b/src/firefox/src/ui/settings.js index 531738d9e..6a87b7b20 100644 --- a/src/firefox/src/ui/settings.js +++ b/src/firefox/src/ui/settings.js @@ -101,6 +101,7 @@ const planReviewConfidenceRow = document.getElementById('row-plan-review-confide const notifySoundToggle = document.getElementById('toggle-notify-sound'); const completionConfettiToggle = document.getElementById('toggle-completion-confetti'); const tracingToggle = document.getElementById('toggle-tracing'); +const losslessTracingToggle = document.getElementById('toggle-lossless-tracing'); const strictSecretToggle = document.getElementById('toggle-strict-secret'); const allowLocalNetworkToggle = document.getElementById('toggle-allow-local-network'); const scheduledTasksToggle = document.getElementById('toggle-scheduled-tasks'); @@ -469,7 +470,7 @@ async function init() { browser.storage.local.remove(['authToken', 'authEmail', 'authDefaultModel']).catch(() => {}); // Load display settings - const stored = await browser.storage.local.get(['verboseMode', 'selectionShortcutEnabled', AUTO_GROUP_TABS_KEY, 'helpImproveWebBrain', 'screenshotFallback', 'maxAgentSteps', 'autoScreenshot', 'useSiteAdapters', 'researchEscalationEnabled', 'researchEscalationEngine', 'voiceInputEnabled', 'alwaysAllowApiMutations', 'apiMutationObserverEnabled', 'openaiAskStreamingEnabled', 'planBeforeActMode', 'planBeforeAct', 'planReviewMode', 'planReviewConfidenceThreshold', DOWNLOAD_DIRECTORY_STORAGE_KEY, 'notifySound', 'completionConfetti', 'tracingEnabled', 'strictSecretMode', 'agentAllowLocalNetwork', 'scheduledTasksEnabled', 'scheduledRequireConsequentialConfirmation', 'providerFilter', 'requestTimeoutMs', 'clarifyTimeoutSec', 'clarifyTimeoutSemanticsV2', 'costAllowanceSessionUsd', 'costAllowanceTotalUsd', 'meteredProviderCostSpentUsd', 'screenshotRedaction', 'imageDetail', 'maxScreenshotsPerTurn', 'maxImageDimension']); + const stored = await browser.storage.local.get(['verboseMode', 'selectionShortcutEnabled', AUTO_GROUP_TABS_KEY, 'helpImproveWebBrain', 'screenshotFallback', 'maxAgentSteps', 'autoScreenshot', 'useSiteAdapters', 'researchEscalationEnabled', 'researchEscalationEngine', 'voiceInputEnabled', 'alwaysAllowApiMutations', 'apiMutationObserverEnabled', 'openaiAskStreamingEnabled', 'planBeforeActMode', 'planBeforeAct', 'planReviewMode', 'planReviewConfidenceThreshold', DOWNLOAD_DIRECTORY_STORAGE_KEY, 'notifySound', 'completionConfetti', 'tracingEnabled', 'losslessTrace', 'strictSecretMode', 'agentAllowLocalNetwork', 'scheduledTasksEnabled', 'scheduledRequireConsequentialConfirmation', 'providerFilter', 'requestTimeoutMs', 'clarifyTimeoutSec', 'clarifyTimeoutSemanticsV2', 'costAllowanceSessionUsd', 'costAllowanceTotalUsd', 'meteredProviderCostSpentUsd', 'screenshotRedaction', 'imageDetail', 'maxScreenshotsPerTurn', 'maxImageDimension']); if (typeof stored.providerFilter === 'string' && ['all','active','local','cloud','router'].includes(stored.providerFilter)) { providerFilter = stored.providerFilter; } @@ -535,6 +536,12 @@ async function init() { if (notifySoundToggle) notifySoundToggle.checked = stored.notifySound ?? true; if (completionConfettiToggle) completionConfettiToggle.checked = stored.completionConfetti ?? true; if (tracingToggle) tracingToggle.checked = stored.tracingEnabled === true; + if (losslessTracingToggle) { + losslessTracingToggle.checked = stored.losslessTrace === true; + // Lossless recording only means something when tracing is on; mirror the + // disabled state so the disclosure reads honestly. + losslessTracingToggle.disabled = tracingToggle?.checked !== true; + } const sessionLimit = normalizeCostAmount(stored.costAllowanceSessionUsd); const totalLimit = normalizeCostAmount(stored.costAllowanceTotalUsd); const totalSpent = normalizeCostAmount(stored.meteredProviderCostSpentUsd, 0); @@ -1206,6 +1213,17 @@ completionConfettiToggle?.addEventListener('change', async () => { tracingToggle?.addEventListener('change', async () => { await browser.storage.local.set({ tracingEnabled: tracingToggle.checked }).catch(() => {}); + if (losslessTracingToggle) { + losslessTracingToggle.disabled = tracingToggle.checked !== true; + if (!tracingToggle.checked) { + losslessTracingToggle.checked = false; + await browser.storage.local.set({ losslessTrace: false }).catch(() => {}); + } + } +}); + +losslessTracingToggle?.addEventListener('change', async () => { + await browser.storage.local.set({ losslessTrace: losslessTracingToggle.checked }).catch(() => {}); }); costSessionLimitInput?.addEventListener('change', async () => { diff --git a/test/run.js b/test/run.js index aeaa7e352..2a05e13d0 100644 --- a/test/run.js +++ b/test/run.js @@ -8861,6 +8861,98 @@ test('trace run header: mirrors are identical and browser-neutral', () => { assert.doesNotMatch(chromeSource, /indexedDB|storage\./, 'run header must not touch storage'); }); +// trace lossless tier — opt-in full-request recording +// ──────────────────────────────────────────────────────────────────────── + +console.log('\ntrace lossless tier'); + +const RUNTIME_CONFIG_CH = await import('file://' + path.join(ROOT, 'src/chrome/src/trace/runtime-config.js').replace(/\\/g, '/')); + +test('trace lossless tier: runtime-config accepts the boolean and defaults stay off', () => { + const normalized = RUNTIME_CONFIG_CH.normalizeRuntimeTraceConfig({ lossless_trace: true }); + assert.equal(normalized.lossless_trace, true, 'lossless_trace boolean not allowlisted'); + const off = RUNTIME_CONFIG_CH.normalizeRuntimeTraceConfig({ lossless_trace: false }); + assert.equal(off.lossless_trace, false, 'lossless_trace false should survive'); + const junk = RUNTIME_CONFIG_CH.normalizeRuntimeTraceConfig({ lossless_trace: 'yes' }); + assert.equal(junk.lossless_trace, undefined, 'non-boolean lossless_trace must be rejected'); +}); + +test('trace lossless tier: recorder branches on the tier and clamps payloads', () => { + for (const browser of ['chrome', 'firefox']) { + const recorderSource = fs.readFileSync(path.join(ROOT, `src/${browser}/src/trace/recorder.js`), 'utf8'); + assert.match(recorderSource, /async function losslessTraceEnabled\(\)/, `${browser}: losslessTraceEnabled missing`); + assert.match(recorderSource, /const lossless = meta\.lossless === true \|\| await losslessTraceEnabled\(\);/, `${browser}: tier decision missing in startRun`); + assert.match(recorderSource, /lossless,\n/, `${browser}: run record does not stamp the tier`); + assert.match(recorderSource, /const LOSSILESS_RESULT_CAP = 200_000;/, `${browser}: lossless result cap missing`); + assert.match(recorderSource, /const LOSSILESS_MESSAGES_CAP = 500_000;/, `${browser}: lossless messages cap missing`); + assert.match(recorderSource, /_runState\.get\(runId\)\?\.lossless === true && provenanceInput/, `${browser}: request lossless branch missing`); + assert.match(recorderSource, /_runState\.get\(runId\)\?\.lossless === true \? LOSSILESS_RESULT_CAP : 20_000/, `${browser}: tool-result cap does not branch on tier`); + assert.match(recorderSource, /peekRunFlags|lossless: record\?\.lossless === true|lossless = record\?\.lossless === true/, `${browser}: SW-eviction recovery does not restore the tier`); + // Default tier must keep the content-free provenance path. + assert.match(recorderSource, /buildPromptTraceProvenance\(/, `${browser}: default tier lost its provenance reduction`); + } +}); + +test('trace lossless tier: settings UI wires the toggle and the disclosure', () => { + for (const browser of ['chrome', 'firefox']) { + const settingsHtml = fs.readFileSync(path.join(ROOT, `src/${browser}/src/ui/settings.html`), 'utf8'); + const settingsJs = fs.readFileSync(path.join(ROOT, `src/${browser}/src/ui/settings.js`), 'utf8'); + const enLocale = fs.readFileSync(path.join(ROOT, `src/${browser}/src/ui/locales/en.js`), 'utf8'); + assert.match(settingsHtml, /id="toggle-lossless-tracing"/, `${browser}: settings HTML toggle missing`); + assert.match(settingsHtml, /st\.display\.lossless_tracing\.label/, `${browser}: settings HTML label key missing`); + assert.match(settingsJs, /losslessTracingToggle\.checked = stored\.losslessTrace === true/, `${browser}: settings JS does not load the key`); + assert.match(settingsJs, /storage\.local\.set\(\{ losslessTrace: losslessTracingToggle\.checked \}\)/, `${browser}: settings JS does not persist the key`); + assert.match(settingsJs, /losslessTracingToggle\.disabled = tracingToggle\.checked !== true/, `${browser}: lossless toggle is not gated on tracing`); + assert.match(enLocale, /st\.display\.lossless_tracing\.label/, `${browser}: en locale label missing`); + assert.match(enLocale, /writes your conversation content locally/, `${browser}: en locale disclosure missing`); + } +}); + +test('trace lossless tier: export renders masked request previews only for lossless runs', () => { + const losslessRun = { + runId: 'r_lossless', userMessage: 'hello', status: 'done', webbrainVersion: '', + }; + const losslessEvents = [ + { + runId: 'r_lossless', seq: 1, ts: 1, kind: 'llm_request', + data: { + messageCount: 2, toolsCount: 1, lossless: true, + messages: [ + { role: 'system', content: 'You are WebBrain.' }, + { role: 'user', content: 'api_key=sk-secret1234567890 and Bearer tok1234567890' }, + ], + tools: [{ function: { name: 'click' } }], + }, + }, + ]; + for (const [label, serialize] of [['chrome', tracesToMarkdown], ['firefox', tracesToMarkdownFx]]) { + const { markdown } = serialize([{ run: losslessRun, events: losslessEvents }]); + assert.match(markdown, /\*\*user:\*\*/, `${label}: lossless message preview missing`); + assert.match(markdown, /tools: click/, `${label}: lossless tool list missing`); + assert.doesNotMatch(markdown, /sk-secret1234567890|tok1234567890/, `${label}: lossless export leaked secrets`); + assert.match(markdown, /\[redacted\]/, `${label}: lossless export has no redaction marker`); + } + // Default tier: no message preview even when a request payload exists. + const defaultRun = { + runId: 'r_default', userMessage: 'hello', status: 'done', webbrainVersion: '', + }; + const defaultEvents = [ + { + runId: 'r_default', seq: 1, ts: 1, kind: 'llm_request', + data: { messageCount: 2, toolsCount: 0, promptProvenance: { systemPromptVariant: 'ask' } }, + }, + ]; + const { markdown } = tracesToMarkdown([{ run: defaultRun, events: defaultEvents }]); + assert.doesNotMatch(markdown, /\*\*system:\*\*|\*\*user:\*\*/, 'default tier export must not render messages'); +}); + +test('trace lossless tier: default-tier privacy guard survives in both builds', () => { + const chromeRecorder = fs.readFileSync(path.join(ROOT, 'src/chrome/src/trace/recorder.js'), 'utf8'); + const firefoxRecorder = fs.readFileSync(path.join(ROOT, 'src/firefox/src/trace/recorder.js'), 'utf8'); + assert.match(chromeRecorder, /Default tier: never persist full prompts, message text, tool schemas, or[\s\S]*?tool names here\./, 'chrome default tier lost its privacy comment guard'); + assert.match(firefoxRecorder, /Default tier: never persist full prompts, message text, tool schemas, or[\s\S]*?tool names here\./, 'firefox default tier lost its privacy comment guard'); +}); + test('trace recorder: DB v2 adds lineage indexes and startRun stores lineage fields', () => { for (const browser of ['chrome', 'firefox']) { const recorderSource = fs.readFileSync(path.join(ROOT, `src/${browser}/src/trace/recorder.js`), 'utf8'); @@ -18874,7 +18966,9 @@ test('cloud runs force trace capture without changing the interactive opt-in def assert.match(agentSource, /force:\s*runOptions\?\.cloudRun === true/); assert.match(recorderSource, /if \(!forced && !\(await tracingEnabled\(\)\)\) return null/); assert.match(recorderSource, /tracingEnabledForRun\(runId\)/); - assert.match(recorderSource, /forced:\s*await isForcedTraceRun\(runId\)/); + // The forced flag is restored from the durable run record after SW eviction + // (peekRunFlags), keeping forced-capture semantics without an in-memory map. + assert.match(recorderSource, /forced: flags\.forced, lossless: flags\.lossless/); }); test('cloud trace keeps CAPTCHA frame/vendor diagnostics after the rolling update window drops the event', async () => { From e85ba919480afcdd42a87e47a7ff1c38e332fa44 Mon Sep 17 00:00:00 2001 From: alectimison-maker Date: Fri, 21 Aug 2026 17:41:29 +0800 Subject: [PATCH 2/6] fix(trace): address lossless tier review findings --- src/chrome/src/agent/trace-export.js | 15 +++- src/chrome/src/trace/recorder.js | 91 ++++++++++++++++------- src/chrome/src/ui/locales/ar.js | 2 + src/chrome/src/ui/locales/bn.js | 2 + src/chrome/src/ui/locales/de.js | 2 + src/chrome/src/ui/locales/en.js | 2 + src/chrome/src/ui/locales/es.js | 2 + src/chrome/src/ui/locales/fa.js | 2 + src/chrome/src/ui/locales/fr.js | 2 + src/chrome/src/ui/locales/he.js | 2 + src/chrome/src/ui/locales/hi.js | 2 + src/chrome/src/ui/locales/id.js | 2 + src/chrome/src/ui/locales/ja.js | 2 + src/chrome/src/ui/locales/ko.js | 2 + src/chrome/src/ui/locales/ms.js | 2 + src/chrome/src/ui/locales/nl.js | 2 + src/chrome/src/ui/locales/pl.js | 2 + src/chrome/src/ui/locales/pt.js | 2 + src/chrome/src/ui/locales/ru.js | 2 + src/chrome/src/ui/locales/th.js | 2 + src/chrome/src/ui/locales/tl.js | 2 + src/chrome/src/ui/locales/tr.js | 2 + src/chrome/src/ui/locales/uk.js | 2 + src/chrome/src/ui/locales/vi.js | 2 + src/chrome/src/ui/locales/zh.js | 2 + src/chrome/src/ui/traces.html | 20 ++++++ src/chrome/src/ui/traces.js | 6 +- src/firefox/src/agent/trace-export.js | 15 +++- src/firefox/src/trace/recorder.js | 100 +++++++++++++++++--------- src/firefox/src/ui/locales/ar.js | 2 + src/firefox/src/ui/locales/bn.js | 2 + src/firefox/src/ui/locales/de.js | 2 + src/firefox/src/ui/locales/en.js | 2 + src/firefox/src/ui/locales/es.js | 2 + src/firefox/src/ui/locales/fa.js | 2 + src/firefox/src/ui/locales/fr.js | 2 + src/firefox/src/ui/locales/he.js | 2 + src/firefox/src/ui/locales/hi.js | 2 + src/firefox/src/ui/locales/id.js | 2 + src/firefox/src/ui/locales/ja.js | 2 + src/firefox/src/ui/locales/ko.js | 2 + src/firefox/src/ui/locales/ms.js | 2 + src/firefox/src/ui/locales/nl.js | 2 + src/firefox/src/ui/locales/pl.js | 2 + src/firefox/src/ui/locales/pt.js | 2 + src/firefox/src/ui/locales/ru.js | 2 + src/firefox/src/ui/locales/th.js | 2 + src/firefox/src/ui/locales/tl.js | 2 + src/firefox/src/ui/locales/tr.js | 2 + src/firefox/src/ui/locales/uk.js | 2 + src/firefox/src/ui/locales/vi.js | 2 + src/firefox/src/ui/locales/zh.js | 2 + src/firefox/src/ui/traces.html | 20 ++++++ src/firefox/src/ui/traces.js | 6 +- test/run.js | 40 +++++++++-- 55 files changed, 337 insertions(+), 68 deletions(-) diff --git a/src/chrome/src/agent/trace-export.js b/src/chrome/src/agent/trace-export.js index 6494271fd..1e5cf7264 100644 --- a/src/chrome/src/agent/trace-export.js +++ b/src/chrome/src/agent/trace-export.js @@ -33,12 +33,15 @@ const UNKNOWN_EVENTS_NOTE = (n) => `_Note: ${n} unknown event(s) skipped._`; const SECRET_PATTERNS = [ /\bsk-[A-Za-z0-9_-]{8,}/g, /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, - /\b(?:api[_-]?key|apikey|token|password|passwd|secret)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{8,}/gi, ]; function maskSecrets(text) { let out = String(text ?? ''); for (const pattern of SECRET_PATTERNS) out = out.replace(pattern, '[redacted]'); + out = out + .replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [redacted]') + .replace(/((?:^|[^a-zA-Z0-9_])["']?(?:api[_ -]?key|access[_ -]?token|token|secret|password|passwd)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;}\]]+)/gi, '$1[redacted]') + .replace(/([?&](?:api[_-]?key|access[_-]?token|token|key)=)[^&\s]+/gi, '$1[redacted]'); return out; } @@ -46,6 +49,14 @@ function maskSecrets(text) { // masked preview per message so the export stays readable without dumping // every token of a 500 KB request. function renderLosslessRequest(messages, tools) { + if (messages?._truncated === true) { + const total = Number(messages.length) || 0; + const head = truncate(oneLine(maskSecrets(messages.head || '')), LOSSILESS_MESSAGE_PREVIEW_LIMIT); + const toolNames = Array.isArray(messages.toolNames) ? messages.toolNames : []; + const lines = [`request truncated (${humanSize(total)} total): ${head || '(head unavailable)'}`]; + if (toolNames.length) lines.push(`tools: ${toolNames.join(', ')}`); + return `\n${lines.join('\n')}`; + } const list = Array.isArray(messages) ? messages : []; if (!list.length) return ' (empty request log)'; const lines = []; @@ -64,6 +75,8 @@ function renderLosslessRequest(messages, tools) { if (list.length > 12) lines.push(`… +${list.length - 12} more message(s) omitted`); if (Array.isArray(tools) && tools.length) { lines.push(`tools: ${tools.map(tool => oneLine(tool?.function?.name || '?')).join(', ')}`); + } else if (tools?._truncated === true && Array.isArray(tools.toolNames) && tools.toolNames.length) { + lines.push(`tools: ${tools.toolNames.join(', ')}`); } return `\n${lines.join('\n')}`; } diff --git a/src/chrome/src/trace/recorder.js b/src/chrome/src/trace/recorder.js index 214525c8d..db49fd32a 100644 --- a/src/chrome/src/trace/recorder.js +++ b/src/chrome/src/trace/recorder.js @@ -24,7 +24,7 @@ const DB_VERSION = 2; // up to 500 KB before the head is clamped with a truncation marker. Keeps an // opt-in debugging tier from exhausting IndexedDB on a single long run. const LOSSILESS_RESULT_CAP = 200_000; -const LOSSILESS_MESSAGES_CAP = 500_000; +const LOSSILESS_REQUEST_CAP = 500_000; let _dbPromise = null; function openDB() { @@ -146,6 +146,60 @@ async function _peekSeq(db, runId) { return result; } +const _runStateLoads = new Map(); + +async function _ensureRunState(runId, db = null) { + if (!runId) return null; + const existing = _runState.get(runId); + if (existing) return existing; + const pending = _runStateLoads.get(runId); + if (pending) return pending; + const load = (async () => { + try { + const resolvedDb = db || await openDB(); + const seq = await _peekSeq(resolvedDb, runId); + const flags = await peekRunFlags(resolvedDb, runId); + const state = { seq, forced: flags.forced, lossless: flags.lossless }; + _runState.set(runId, state); + return state; + } catch { return null; } + })(); + _runStateLoads.set(runId, load); + try { return await load; } + finally { if (_runStateLoads.get(runId) === load) _runStateLoads.delete(runId); } +} + +function boundedToolNames(tools) { + return Array.isArray(tools) + ? tools.slice(0, 100).map(tool => String(tool?.function?.name || '?').slice(0, 120)) + : []; +} + +function clampLosslessRequest(messages, tools) { + const request = { messages: messages ?? null, tools: tools ?? null }; + let serialized; + try { serialized = JSON.stringify(request); } catch { serialized = null; } + if (!serialized) { + return { + messages: { _truncated: true, length: null, head: '(unserializable request)', toolNames: boundedToolNames(tools) }, + tools: null, + }; + } + if (serialized.length <= LOSSILESS_REQUEST_CAP) return { messages, tools }; + // Keep one bounded head of the combined request. Tool names remain visible + // even when the head ends before the dynamic tool catalog. + const headCap = Math.max(1, LOSSILESS_REQUEST_CAP - 2048); + return { + messages: { + _truncated: true, + length: serialized.length, + head: serialized.slice(0, headCap), + toolNames: boundedToolNames(tools), + }, + tools: null, + }; +} + function _newSeq(runId) { const st = _runState.get(runId); if (!st) return 0; @@ -202,7 +256,7 @@ export async function startRun(meta = {}) { tabTitle: meta.tabTitle || '', mode: meta.mode || 'act', attachments: normalizeTraceAttachments(meta.attachments), - lossless, + ...(lossless ? { lossless: true } : {}), forced, stepCount: 0, totalInputTokens: 0, @@ -223,12 +277,7 @@ async function _appendEvent(runId, kind, data) { if (!(await tracingEnabledForRun(runId))) return; try { const db = await openDB(); - if (!_runState.has(runId)) { - // Recover from SW eviction. - const seq = await _peekSeq(db, runId); - const flags = await peekRunFlags(db, runId); - _runState.set(runId, { seq, forced: flags.forced, lossless: flags.lossless }); - } + await _ensureRunState(runId, db); const seq = _newSeq(runId); const ev = makeEvent(runId, seq, kind, data); if (!ev) { @@ -244,25 +293,20 @@ async function _appendEvent(runId, kind, data) { } } -export function recordLLMRequest(runId, step, payload, provenanceInput = null) { +export async function recordLLMRequest(runId, step, payload, provenanceInput = null) { // Lossless tier (opt-in): persist the request's full message/tool shape for // deep debugging and request reconstruction. Clamped so one oversized // request cannot exhaust IndexedDB; the marker mirrors the tool-result // truncation convention ({ _truncated, length, head }). - if (_runState.get(runId)?.lossless === true && provenanceInput) { - let messages = provenanceInput.messages || null; - if (messages !== null) { - const serialized = JSON.stringify(messages); - if (serialized && serialized.length > LOSSILESS_MESSAGES_CAP) { - messages = { _truncated: true, length: serialized.length, head: serialized.slice(0, LOSSILESS_MESSAGES_CAP) }; - } - } + const state = await _ensureRunState(runId); + if (state?.lossless === true && provenanceInput) { + const { messages, tools } = clampLosslessRequest(provenanceInput.messages || null, provenanceInput.tools || null); return _appendEvent(runId, 'llm_request', { step, ...payload, lossless: true, messages, - tools: provenanceInput.tools || null, + tools, }); } // Default tier: never persist full prompts, message text, tool schemas, or @@ -305,11 +349,12 @@ export function recordLLMResponse(runId, step, { content, toolCalls, usage, late }); } -export function recordToolCall(runId, step, { name, args, result, latencyMs }) { +export async function recordToolCall(runId, step, { name, args, result, latencyMs }) { // Truncate very large tool results (a11y trees can be huge). Keep the first // 20KB verbatim by default — plenty for debugging flow — and 200KB in the // opt-in lossless tier; note the truncation either way. - const cap = _runState.get(runId)?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; + const state = await _ensureRunState(runId); + const cap = state?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; let shortResult = result; try { const s = typeof result === 'string' ? result : JSON.stringify(result); @@ -332,11 +377,7 @@ export async function recordScreenshot(runId, step, dataUrl, caption = '') { if (!dataUrl) return; try { const db = await openDB(); - if (!_runState.has(runId)) { - const seq = await _peekSeq(db, runId); - const flags = await peekRunFlags(db, runId); - _runState.set(runId, { seq, forced: flags.forced, lossless: flags.lossless }); - } + await _ensureRunState(runId, db); const seq = _newSeq(runId); // Decode data URL to a Blob so IDB stores raw bytes (no base64 overhead). let blob = null; diff --git a/src/chrome/src/ui/locales/ar.js b/src/chrome/src/ui/locales/ar.js index d255c05b9..d4b10bf24 100644 --- a/src/chrome/src/ui/locales/ar.js +++ b/src/chrome/src/ui/locales/ar.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': 'الأمان — اقرأ هذا:
  • النص الذي تُدخله هنا يُخزَّن بصيغة نصية واضحة في التخزين المحلي للمتصفح. لا يُرسَل إلى مشروع WebBrain — لكنه يُرسَل إلى مزوّد LLM الذي ضبطته في كل دور، كجزء من prompt النظام.
  • لا تضع كلمات مرور حسابات مهمة هنا (Google، Apple، iCloud، البنوك، SSO العمل، البريد الرئيسي). هذه الحسابات يجب أن تستخدم المصادقة الثنائية، ولا يجدر بك تسليمها لوكيل أصلًا.
  • الاستخدام المقصود هو كلمة مرور للاستعمال لمرة واحدة تعيد استخدامها لتسجيلات منخفضة الأهمية (النشرات البريدية، التجارب المجانية، حسابات المنتديات).
  • إذا اختُرق هذا الملف الشخصي للمتصفح يومًا، فإن مهاجمًا يملك الوصول إلى القرص قادر على قراءة هذا النص. أبقِه في حدّه الأدنى.
', 'tr.title': 'تتبّعات WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} عملية تشغيل', 'tr.run': '{n} عملية تشغيل', 'tr.btn.refresh': '↻ تحديث', diff --git a/src/chrome/src/ui/locales/bn.js b/src/chrome/src/ui/locales/bn.js index 5ee6e84d5..c9de0f2f4 100644 --- a/src/chrome/src/ui/locales/bn.js +++ b/src/chrome/src/ui/locales/bn.js @@ -937,6 +937,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': "WebBrain ট্রেস", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': "{n} চলে", 'tr.run': "{n} রান", 'tr.btn.refresh': "↻ রিফ্রেশ করুন", diff --git a/src/chrome/src/ui/locales/de.js b/src/chrome/src/ui/locales/de.js index 5231cd9eb..be3234b6f 100644 --- a/src/chrome/src/ui/locales/de.js +++ b/src/chrome/src/ui/locales/de.js @@ -935,6 +935,8 @@ export default { 'hist.confirm_clear_all': 'ALLE gespeicherten Gespräche löschen? Dies kann nicht rückgängig gemacht werden.', // --- Traces --- 'tr.title': 'WebBrain Traces', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} Durchläufe', 'tr.run': '{n} Durchlauf', 'tr.btn.refresh': '↻ Aktualisieren', diff --git a/src/chrome/src/ui/locales/en.js b/src/chrome/src/ui/locales/en.js index 515b0b708..743475f20 100644 --- a/src/chrome/src/ui/locales/en.js +++ b/src/chrome/src/ui/locales/en.js @@ -941,6 +941,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': 'WebBrain Traces', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} runs', 'tr.run': '{n} run', 'tr.btn.refresh': '↻ Refresh', diff --git a/src/chrome/src/ui/locales/es.js b/src/chrome/src/ui/locales/es.js index d87cc6dd2..6a72cc899 100644 --- a/src/chrome/src/ui/locales/es.js +++ b/src/chrome/src/ui/locales/es.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': 'Seguridad — léelo:
  • El texto que introduces aquí se guarda en texto plano en el almacenamiento local del navegador. No se envía al proyecto WebBrain — pero se envía al proveedor de LLM que hayas configurado en cada turno, como parte del prompt del sistema.
  • No pongas aquí contraseñas de cuentas importantes (Google, Apple, iCloud, banca, SSO de trabajo, correo principal). Esas cuentas deberían usar 2FA y no deberías entregárselas a un agente.
  • Una contraseña desechable que reutilizas para registros poco importantes (boletines, pruebas gratuitas, cuentas de foro) es el uso previsto.
  • Si este perfil del navegador alguna vez se ve comprometido, un atacante con acceso al disco podrá leer este texto. Mantenlo al mínimo.
', 'tr.title': 'Trazas de WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} ejecuciones', 'tr.run': '{n} ejecución', 'tr.btn.refresh': '↻ Actualizar', diff --git a/src/chrome/src/ui/locales/fa.js b/src/chrome/src/ui/locales/fa.js index cc3bbed36..71745128b 100644 --- a/src/chrome/src/ui/locales/fa.js +++ b/src/chrome/src/ui/locales/fa.js @@ -937,6 +937,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': "WebBrain Traces", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': "{n} اجرا می شود", 'tr.run': "اجرای {n}", 'tr.btn.refresh': "↻ تازه کردن", diff --git a/src/chrome/src/ui/locales/fr.js b/src/chrome/src/ui/locales/fr.js index 8295167d8..e0e3427d8 100644 --- a/src/chrome/src/ui/locales/fr.js +++ b/src/chrome/src/ui/locales/fr.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': 'Sécurité — à lire :
  • Le texte que vous saisissez ici est stocké en clair dans le stockage local de votre navigateur. Il n\'est pas transmis au projet WebBrain — mais il est envoyé au fournisseur de LLM que vous avez configuré à chaque tour, dans le cadre du prompt système.
  • Ne mettez pas ici les mots de passe de comptes importants (Google, Apple, iCloud, banque, SSO professionnel, e-mail principal). Ces comptes devraient utiliser la 2FA et vous ne devriez pas avoir à les confier à un agent.
  • Un mot de passe jetable que vous réutilisez pour des inscriptions peu sensibles (newsletters, essais gratuits, comptes de forum) est l\'usage prévu.
  • Si ce profil de navigateur est un jour compromis, un attaquant ayant accès au disque pourra lire ce texte. Gardez-le au minimum.
', 'tr.title': 'Traces WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} exécutions', 'tr.run': '{n} exécution', 'tr.btn.refresh': '↻ Actualiser', diff --git a/src/chrome/src/ui/locales/he.js b/src/chrome/src/ui/locales/he.js index 76e4329e6..b0b6fcaf1 100644 --- a/src/chrome/src/ui/locales/he.js +++ b/src/chrome/src/ui/locales/he.js @@ -819,6 +819,8 @@ export default { "hist.confirm_delete": "למחוק את השיחה השמורה הזו?", "hist.confirm_clear_all": "למחוק את כל השיחות השמורות? לא ניתן לבטל פעולה זו.", "tr.title": "WebBrain עקבות", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', "tr.runs": "{n} פועל", "tr.run": "{n} לָרוּץ", "tr.btn.refresh": "↻ רענן", diff --git a/src/chrome/src/ui/locales/hi.js b/src/chrome/src/ui/locales/hi.js index 62cdb168e..853970814 100644 --- a/src/chrome/src/ui/locales/hi.js +++ b/src/chrome/src/ui/locales/hi.js @@ -937,6 +937,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': "WebBrain निशान", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': "{n} चलता है", 'tr.run': "{n} चलाएँ", 'tr.btn.refresh': "↻ ताज़ा करें", diff --git a/src/chrome/src/ui/locales/id.js b/src/chrome/src/ui/locales/id.js index 7e05ce427..303dda0d4 100644 --- a/src/chrome/src/ui/locales/id.js +++ b/src/chrome/src/ui/locales/id.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': 'Keamanan — baca ini:
  • Teks yang Anda masukkan di sini disimpan dalam bentuk teks biasa di penyimpanan lokal peramban Anda. Teks ini tidak dikirim ke proyek WebBrain — tetapi dikirim ke penyedia LLM yang Anda konfigurasikan pada setiap giliran, sebagai bagian dari prompt sistem.
  • Jangan masukkan kata sandi untuk akun penting di sini (Google, Apple, iCloud, perbankan, SSO kantor, email utama). Akun-akun itu seharusnya memakai 2FA dan tidak perlu Anda serahkan ke agen.
  • Kasus penggunaan yang dimaksud adalah kata sandi sekali pakai yang Anda gunakan kembali untuk pendaftaran situs berisiko rendah (newsletter, uji coba gratis, akun forum).
  • Jika profil peramban ini suatu saat disusupi, penyerang yang memiliki akses disk dapat membaca teks ini. Buat seminimal mungkin.
', 'tr.title': 'Jejak WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} proses', 'tr.run': '{n} proses', 'tr.btn.refresh': '↻ Segarkan', diff --git a/src/chrome/src/ui/locales/ja.js b/src/chrome/src/ui/locales/ja.js index 77cf1089f..aa7f5ff62 100644 --- a/src/chrome/src/ui/locales/ja.js +++ b/src/chrome/src/ui/locales/ja.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': 'セキュリティ — 必ずお読みください:
  • ここに入力したテキストはブラウザのローカルストレージに平文で保存されます。WebBrain プロジェクトへは送信されませんが、設定済みの LLM プロバイダーへはシステム prompt の一部として毎ターン送信されます
  • 重要アカウント(Google、Apple、iCloud、銀行、社内 SSO、メインのメール)のパスワードは絶対にここに入れないでください。それらは 2FA を有効にすべきで、そもそもエージェントに渡すべきではありません。
  • 意図された使い方は、低リスクの登録(メルマガ、無料体験、フォーラム)で使い回す使い捨てパスワードです。
  • このブラウザプロファイルが侵害された場合、ディスクにアクセスできる攻撃者はこのテキストを読み取れます。最小限にとどめてください。
', 'tr.title': 'WebBrain トレース', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} 件の実行', 'tr.run': '{n} 件の実行', 'tr.btn.refresh': '↻ 更新', diff --git a/src/chrome/src/ui/locales/ko.js b/src/chrome/src/ui/locales/ko.js index cfaeb6dd5..97f1cab42 100644 --- a/src/chrome/src/ui/locales/ko.js +++ b/src/chrome/src/ui/locales/ko.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': '보안 — 반드시 읽어주세요:
  • 여기에 입력한 텍스트는 브라우저 로컬 저장소에 평문으로 저장됩니다. WebBrain 프로젝트로는 전송되지 않지만, 설정한 LLM 제공자에게는 시스템 prompt의 일부로 매 턴 전송됩니다.
  • 중요한 계정(Google, Apple, iCloud, 은행, 회사 SSO, 주 이메일)의 비밀번호는 여기에 넣지 마세요. 이런 계정은 2FA를 사용해야 하며 애초에 에이전트에게 넘길 일이 없어야 합니다.
  • 의도된 사용 사례는 저위험 사이트 가입(뉴스레터, 무료 체험, 포럼 계정)에 재사용하는 일회용 비밀번호입니다.
  • 이 브라우저 프로필이 침해되면 디스크에 접근할 수 있는 공격자가 이 텍스트를 읽을 수 있습니다. 최소한으로 유지하세요.
', 'tr.title': 'WebBrain 트레이스', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '실행 {n}건', 'tr.run': '실행 {n}건', 'tr.btn.refresh': '↻ 새로고침', diff --git a/src/chrome/src/ui/locales/ms.js b/src/chrome/src/ui/locales/ms.js index 2b1ad861d..672580296 100644 --- a/src/chrome/src/ui/locales/ms.js +++ b/src/chrome/src/ui/locales/ms.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': 'Keselamatan — sila baca:
  • Teks yang anda masukkan di sini disimpan sebagai teks biasa dalam storan tempatan pelayar anda. Ia tidak dihantar kepada projek WebBrain — tetapi dihantar kepada pembekal LLM yang anda konfigurasikan pada setiap giliran, sebagai sebahagian daripada system prompt.
  • Jangan letakkan kata laluan untuk akaun penting di sini (Google, Apple, iCloud, perbankan, SSO kerja, e-mel utama). Akaun-akaun itu sepatutnya menggunakan 2FA dan anda sememangnya tidak perlu menyerahkannya kepada ejen.
  • Kes penggunaan yang dimaksudkan ialah kata laluan pakai buang yang anda guna semula untuk pendaftaran laman berisiko rendah (surat berita, percubaan percuma, akaun forum).
  • Jika profil pelayar ini diceroboh, penyerang yang mempunyai akses cakera boleh membaca teks ini. Kekalkan ia seminimum mungkin.
', 'tr.title': 'Jejak WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} larian', 'tr.run': '{n} larian', 'tr.btn.refresh': '↻ Muat semula', diff --git a/src/chrome/src/ui/locales/nl.js b/src/chrome/src/ui/locales/nl.js index 3fc8c5ba3..d4b13eb18 100644 --- a/src/chrome/src/ui/locales/nl.js +++ b/src/chrome/src/ui/locales/nl.js @@ -837,6 +837,8 @@ export default { 'hist.confirm_delete': 'Dit opgeslagen gesprek verwijderen?', 'hist.confirm_clear_all': 'Alle opgeslagen gesprekken verwijderen? Dit kan niet ongedaan worden gemaakt.', 'tr.title': 'WebBrain Traces', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} uitvoeringen', 'tr.run': '{n} uitvoering', 'tr.btn.refresh': '↻ Vernieuwen', diff --git a/src/chrome/src/ui/locales/pl.js b/src/chrome/src/ui/locales/pl.js index d3e681122..867b42d86 100644 --- a/src/chrome/src/ui/locales/pl.js +++ b/src/chrome/src/ui/locales/pl.js @@ -588,6 +588,8 @@ export default { 'st.captcha.balance_fail': 'Niepowodzenie: {error}', 'st.captcha.security_html': 'Uwaga: klucz API jest przechowywany w postaci jawnej w lokalnej pamięci przeglądarki. CapSolver obciąża Twoje konto za każde rozwiązanie; agent wywoła go tylko, gdy CAPTCHA faktycznie zablokuje krok (maksymalnie raz na napotkanie — nie ponawia po niepowodzeniu). Regulaminy niektórych witryn zabraniają automatycznego rozwiązywania CAPTCHA; kieruj się własnym osądem.', 'tr.title': 'Ślady WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} wykonań', 'tr.run': '{n} wykonanie', 'tr.btn.refresh': '↻ Odśwież', diff --git a/src/chrome/src/ui/locales/pt.js b/src/chrome/src/ui/locales/pt.js index 4fc242f52..ac655167e 100644 --- a/src/chrome/src/ui/locales/pt.js +++ b/src/chrome/src/ui/locales/pt.js @@ -937,6 +937,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': "WebBrain Rastreamentos", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': "{n} é executado", 'tr.run': "{n} executado", 'tr.btn.refresh': "↻ Atualizar", diff --git a/src/chrome/src/ui/locales/ru.js b/src/chrome/src/ui/locales/ru.js index 7467eb98e..966ddc615 100644 --- a/src/chrome/src/ui/locales/ru.js +++ b/src/chrome/src/ui/locales/ru.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': 'Безопасность — прочтите:
  • Введённый здесь текст хранится в открытом виде в локальном хранилище браузера. Он не отправляется в проект WebBrain — но отправляется на каждом ходу как часть системного prompt тому LLM-провайдеру, которого вы настроили.
  • Не размещайте здесь пароли от важных аккаунтов (Google, Apple, iCloud, банк, рабочий SSO, основная почта). Эти аккаунты должны быть с 2FA, и их в любом случае не стоит передавать агенту.
  • Целевой сценарий — одноразовый пароль, который вы повторно используете для регистраций с низкими ставками (рассылки, бесплатные пробные периоды, аккаунты на форумах).
  • Если этот профиль браузера будет скомпрометирован, атакующий с доступом к диску сможет прочитать этот текст. Держите его минимальным.
', 'tr.title': 'Трассы WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': 'запусков: {n}', 'tr.run': 'запуск: {n}', 'tr.btn.refresh': '↻ Обновить', diff --git a/src/chrome/src/ui/locales/th.js b/src/chrome/src/ui/locales/th.js index 8754d6878..84808b18c 100644 --- a/src/chrome/src/ui/locales/th.js +++ b/src/chrome/src/ui/locales/th.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': 'ความปลอดภัย — โปรดอ่าน:
  • ข้อความที่คุณกรอกที่นี่ถูกเก็บ เป็นข้อความธรรมดา ใน local storage ของเบราว์เซอร์ มัน จะไม่ ถูกส่งไปยังโปรเจกต์ WebBrain — แต่ จะถูกส่ง ไปยังผู้ให้บริการ LLM ที่คุณตั้งค่าไว้ในทุกเทิร์น ในฐานะส่วนหนึ่งของ system prompt
  • อย่าใส่รหัสผ่านของบัญชีสำคัญ ที่นี่ (Google, Apple, iCloud, ธนาคาร, SSO ที่ทำงาน, อีเมลหลัก) บัญชีเหล่านั้นควรใช้ 2FA และไม่ควรมอบให้เอเจนต์อยู่แล้ว
  • กรณีใช้งานที่ตั้งใจคือ รหัสผ่านใช้แล้วทิ้ง ที่คุณใช้ซ้ำสำหรับการสมัครเว็บไซต์เสี่ยงต่ำ (จดหมายข่าว ทดลองใช้ฟรี บัญชีฟอรัม)
  • หากโปรไฟล์เบราว์เซอร์นี้ถูกบุกรุก ผู้โจมตีที่เข้าถึงดิสก์ได้จะอ่านข้อความนี้ได้ ควรเก็บให้น้อยที่สุด
', 'tr.title': 'Traces ของ WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} ครั้ง', 'tr.run': '{n} ครั้ง', 'tr.btn.refresh': '↻ รีเฟรช', diff --git a/src/chrome/src/ui/locales/tl.js b/src/chrome/src/ui/locales/tl.js index b4fad7fe8..19e65b444 100644 --- a/src/chrome/src/ui/locales/tl.js +++ b/src/chrome/src/ui/locales/tl.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': 'Seguridad — basahin ito:
  • Ang teksto na inilagay mo dito ay iniimbak bilang plain text sa local storage ng iyong browser. Hindi ito ipinapadala sa proyektong WebBrain — ngunit ipinapadala ito sa kung anong LLM provider ang iyong na-configure sa bawat turn, bilang bahagi ng system prompt.
  • Huwag maglagay ng mga password para sa mahahalagang account dito (Google, Apple, iCloud, banking, work SSO, pangunahing email). Dapat gumamit ng 2FA ang mga account na iyon at hindi mo naman dapat ipinapasa sa isang ahente.
  • Ang inilaang gamit ay isahang-gamit na password na ginagamit mo sa mga low-stakes na site signup (newsletter, libreng trial, forum account).
  • Kung sakaling ma-compromise ang browser profile na ito, ang isang attacker na may disk access ay makakabasa ng tekstong ito. Panatilihin itong minimal.
', 'tr.title': 'Mga Trace ng WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} run', 'tr.run': '{n} run', 'tr.btn.refresh': '↻ I-refresh', diff --git a/src/chrome/src/ui/locales/tr.js b/src/chrome/src/ui/locales/tr.js index 05a345c60..8a4b396e9 100644 --- a/src/chrome/src/ui/locales/tr.js +++ b/src/chrome/src/ui/locales/tr.js @@ -387,6 +387,8 @@ export default { 'st.profile.security_html': 'Güvenlik — lütfen oku:
  • Buraya yazdığın metin tarayıcının yerel depolamasında düz metin olarak saklanır. WebBrain projesine iletilmez — fakat yapılandırdığın LLM sağlayıcısına her turda sistem isteminin bir parçası olarak gönderilir.
  • Önemli hesapların parolalarını buraya koyma (Google, Apple, iCloud, bankacılık, iş SSO\'su, birincil e-posta). Bu hesaplar 2FA kullanmalı ve zaten bir aracıya teslim edilmemeli.
  • Düşük riskli site kayıtları (bültenler, ücretsiz denemeler, forum hesapları) için kullandığın tek kullanımlık bir parola hedeflenen kullanım durumudur.
  • Bu tarayıcı profili ele geçirilirse, diske erişebilen bir saldırgan bu metni okuyabilir. Asgaride tut.
', 'tr.title': 'WebBrain İzleri', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} çalışma', 'tr.run': '{n} çalışma', 'tr.btn.refresh': '↻ Yenile', diff --git a/src/chrome/src/ui/locales/uk.js b/src/chrome/src/ui/locales/uk.js index e202c528b..427ba54cd 100644 --- a/src/chrome/src/ui/locales/uk.js +++ b/src/chrome/src/ui/locales/uk.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': 'Безпека — прочитайте:
  • Введений тут текст зберігається у відкритому вигляді в локальному сховищі браузера. Він не надсилається проєкту WebBrain — але надсилається на кожному ходу як частина системного prompt тому LLM-провайдеру, якого ви налаштували.
  • Не вводьте сюди паролі від важливих акаунтів (Google, Apple, iCloud, банк, робочий SSO, основна пошта). Ці акаунти мають мати 2FA, і їх у будь-якому разі не варто передавати агенту.
  • Цільовий сценарій — одноразовий пароль, який ви повторно використовуєте для реєстрацій із низькими ставками (розсилки, безкоштовні пробні версії, акаунти на форумах).
  • Якщо цей профіль браузера колись буде скомпрометовано, зловмисник із доступом до диска зможе прочитати цей текст. Тримайте його мінімальним.
', 'tr.title': 'Траси WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': 'запусків: {n}', 'tr.run': 'запуск: {n}', 'tr.btn.refresh': '↻ Оновити', diff --git a/src/chrome/src/ui/locales/vi.js b/src/chrome/src/ui/locales/vi.js index 533e78af2..3eeb7f28b 100644 --- a/src/chrome/src/ui/locales/vi.js +++ b/src/chrome/src/ui/locales/vi.js @@ -937,6 +937,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': "Dấu vết WebBrain", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': "{n} chạy", 'tr.run': "Chạy {n}", 'tr.btn.refresh': "↻ Làm mới", diff --git a/src/chrome/src/ui/locales/zh.js b/src/chrome/src/ui/locales/zh.js index 21f1a6995..5437cf450 100644 --- a/src/chrome/src/ui/locales/zh.js +++ b/src/chrome/src/ui/locales/zh.js @@ -348,6 +348,8 @@ export default { 'st.profile.security_html': '安全提示 — 请阅读:
  • 你在这里输入的文本以明文保存在浏览器的本地存储中。它不会被发送到 WebBrain 项目 — 但在每一轮作为系统提示的一部分发送给你所配置的 LLM 提供商。
  • 不要在这里放重要账户的密码(Google、Apple、iCloud、银行、工作 SSO、主邮箱)。这些账户应启用 2FA,也本就不该交给代理。
  • 适合此处的是你用于低风险注册(时事通讯、免费试用、论坛账户)的一次性密码
  • 如果此浏览器配置文件被攻破,能访问磁盘的攻击者就能读到此文本。请尽量精简。
', 'tr.title': 'WebBrain 轨迹', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} 次运行', 'tr.run': '{n} 次运行', 'tr.btn.refresh': '↻ 刷新', diff --git a/src/chrome/src/ui/traces.html b/src/chrome/src/ui/traces.html index 157ad8c2e..7254f9a1f 100644 --- a/src/chrome/src/ui/traces.html +++ b/src/chrome/src/ui/traces.html @@ -118,6 +118,17 @@ text-overflow: ellipsis; margin-bottom: 3px; } + .lossless-badge { + display: inline-block; + margin-left: 6px; + padding: 1px 4px; + border: 1px solid rgba(255,152,0,0.55); + border-radius: 4px; + color: var(--warning); + font-size: 9px; + font-weight: 600; + vertical-align: 1px; + } .run-item .run-meta { font-size: 10px; color: var(--text2); @@ -188,6 +199,15 @@ margin-bottom: 14px; flex-wrap: wrap; } + .lossless-warning { + margin: 0 0 14px; + padding: 9px 12px; + border-left: 3px solid var(--warning); + background: rgba(255,152,0,0.12); + color: var(--warning); + border-radius: 0 6px 6px 0; + font-size: 12px; + } /* Costly-failure highlight — runs ≥ $0.50 with empty finalContent. */ .cost-warn { color: #ff9d4d; diff --git a/src/chrome/src/ui/traces.js b/src/chrome/src/ui/traces.js index 8da742441..6ac7328be 100644 --- a/src/chrome/src/ui/traces.js +++ b/src/chrome/src/ui/traces.js @@ -119,6 +119,9 @@ function renderList() { compareIds.includes(r.runId) ? 'compare' : '', ].filter(Boolean).join(' '); const title = r.userMessage || t('tr.no_task'); + const losslessBadge = r.lossless === true + ? `${escapeHtml(t('tr.lossless.badge'))}` + : ''; const siblings = siblingsOf(r); const convChip = siblings.length > 1 ? `🧵 ${siblings.length}` @@ -129,7 +132,7 @@ function renderList() { const costClass = isCostlyFailure ? 'cost-warn' : ''; return `
-
${escapeHtml(title.slice(0, 120))}${convChip}
+
${escapeHtml(title.slice(0, 120))}${losslessBadge}${convChip}
${escapeHtml(r.model || '?')} ${escapeHtml(r.providerId || '')} @@ -269,6 +272,7 @@ async function buildRunView(run, events, compact, objectUrls = new Set()) { ${escapeHtml(t('tr.outtokens.label'))} ${(run.totalOutputTokens || 0).toLocaleString()} ${formatCost(run.totalCost) ? `${escapeHtml(t('tr.cost.label'))} ${escapeHtml(formatCost(run.totalCost))}` : ''}
+ ${run.lossless === true ? `` : ''} ${renderConversationPanel(run, compact)}
${escapeHtml(run.userMessage || '')}
${run.finalContent ? `
${escapeHtml(t('tr.final_label'))} ${escapeHtml(run.finalContent)}
` : ''} diff --git a/src/firefox/src/agent/trace-export.js b/src/firefox/src/agent/trace-export.js index 6494271fd..1e5cf7264 100644 --- a/src/firefox/src/agent/trace-export.js +++ b/src/firefox/src/agent/trace-export.js @@ -33,12 +33,15 @@ const UNKNOWN_EVENTS_NOTE = (n) => `_Note: ${n} unknown event(s) skipped._`; const SECRET_PATTERNS = [ /\bsk-[A-Za-z0-9_-]{8,}/g, /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, - /\b(?:api[_-]?key|apikey|token|password|passwd|secret)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{8,}/gi, ]; function maskSecrets(text) { let out = String(text ?? ''); for (const pattern of SECRET_PATTERNS) out = out.replace(pattern, '[redacted]'); + out = out + .replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [redacted]') + .replace(/((?:^|[^a-zA-Z0-9_])["']?(?:api[_ -]?key|access[_ -]?token|token|secret|password|passwd)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;}\]]+)/gi, '$1[redacted]') + .replace(/([?&](?:api[_-]?key|access[_-]?token|token|key)=)[^&\s]+/gi, '$1[redacted]'); return out; } @@ -46,6 +49,14 @@ function maskSecrets(text) { // masked preview per message so the export stays readable without dumping // every token of a 500 KB request. function renderLosslessRequest(messages, tools) { + if (messages?._truncated === true) { + const total = Number(messages.length) || 0; + const head = truncate(oneLine(maskSecrets(messages.head || '')), LOSSILESS_MESSAGE_PREVIEW_LIMIT); + const toolNames = Array.isArray(messages.toolNames) ? messages.toolNames : []; + const lines = [`request truncated (${humanSize(total)} total): ${head || '(head unavailable)'}`]; + if (toolNames.length) lines.push(`tools: ${toolNames.join(', ')}`); + return `\n${lines.join('\n')}`; + } const list = Array.isArray(messages) ? messages : []; if (!list.length) return ' (empty request log)'; const lines = []; @@ -64,6 +75,8 @@ function renderLosslessRequest(messages, tools) { if (list.length > 12) lines.push(`… +${list.length - 12} more message(s) omitted`); if (Array.isArray(tools) && tools.length) { lines.push(`tools: ${tools.map(tool => oneLine(tool?.function?.name || '?')).join(', ')}`); + } else if (tools?._truncated === true && Array.isArray(tools.toolNames) && tools.toolNames.length) { + lines.push(`tools: ${tools.toolNames.join(', ')}`); } return `\n${lines.join('\n')}`; } diff --git a/src/firefox/src/trace/recorder.js b/src/firefox/src/trace/recorder.js index f39a241ed..057924de1 100644 --- a/src/firefox/src/trace/recorder.js +++ b/src/firefox/src/trace/recorder.js @@ -24,7 +24,7 @@ const DB_VERSION = 2; // up to 500 KB before the head is clamped with a truncation marker. Keeps an // opt-in debugging tier from exhausting IndexedDB on a single long run. const LOSSILESS_RESULT_CAP = 200_000; -const LOSSILESS_MESSAGES_CAP = 500_000; +const LOSSILESS_REQUEST_CAP = 500_000; let _dbPromise = null; function openDB() { @@ -121,6 +121,60 @@ async function _peekSeq(db, runId) { return result; } +const _runStateLoads = new Map(); + +async function _ensureRunState(runId, db = null) { + if (!runId) return null; + const existing = _runState.get(runId); + if (existing) return existing; + const pending = _runStateLoads.get(runId); + if (pending) return pending; + const load = (async () => { + try { + const resolvedDb = db || await openDB(); + const seq = await _peekSeq(resolvedDb, runId); + const flags = await peekRunFlags(resolvedDb, runId); + const state = { seq, forced: flags.forced, lossless: flags.lossless }; + _runState.set(runId, state); + return state; + } catch { return null; } + })(); + _runStateLoads.set(runId, load); + try { return await load; } + finally { if (_runStateLoads.get(runId) === load) _runStateLoads.delete(runId); } +} + +function boundedToolNames(tools) { + return Array.isArray(tools) + ? tools.slice(0, 100).map(tool => String(tool?.function?.name || '?').slice(0, 120)) + : []; +} + +function clampLosslessRequest(messages, tools) { + const request = { messages: messages ?? null, tools: tools ?? null }; + let serialized; + try { serialized = JSON.stringify(request); } catch { serialized = null; } + if (!serialized) { + return { + messages: { _truncated: true, length: null, head: '(unserializable request)', toolNames: boundedToolNames(tools) }, + tools: null, + }; + } + if (serialized.length <= LOSSILESS_REQUEST_CAP) return { messages, tools }; + // Keep one bounded head of the combined request. Tool names remain visible + // even when the head ends before the dynamic tool catalog. + const headCap = Math.max(1, LOSSILESS_REQUEST_CAP - 2048); + return { + messages: { + _truncated: true, + length: serialized.length, + head: serialized.slice(0, headCap), + toolNames: boundedToolNames(tools), + }, + tools: null, + }; +} + function _newSeq(runId) { const st = _runState.get(runId); if (!st) return 0; @@ -176,7 +230,7 @@ export async function startRun(meta) { tabTitle: meta.tabTitle || '', mode: meta.mode || 'act', attachments: normalizeTraceAttachments(meta.attachments), - lossless, + ...(lossless ? { lossless: true } : {}), stepCount: 0, totalInputTokens: 0, totalOutputTokens: 0, @@ -196,17 +250,7 @@ async function _appendEvent(runId, kind, data) { if (!(await tracingEnabled())) return; try { const db = await openDB(); - if (!_runState.has(runId)) { - // Recover from SW eviction; restore the lossless tier decision from the - // durable run record so it survives a worker restart mid-run. - const seq = await _peekSeq(db, runId); - let lossless = false; - try { - const record = await promisifyReq(tx(db, ['runs'], 'readonly').objectStore('runs').get(runId)); - lossless = record?.lossless === true; - } catch {} - _runState.set(runId, { seq, lossless }); - } + await _ensureRunState(runId, db); const seq = _newSeq(runId); const ev = makeEvent(runId, seq, kind, data); if (!ev) { @@ -222,25 +266,20 @@ async function _appendEvent(runId, kind, data) { } } -export function recordLLMRequest(runId, step, payload, provenanceInput = null) { +export async function recordLLMRequest(runId, step, payload, provenanceInput = null) { // Lossless tier (opt-in): persist the request's full message/tool shape for // deep debugging and request reconstruction. Clamped so one oversized // request cannot exhaust IndexedDB; the marker mirrors the tool-result // truncation convention ({ _truncated, length, head }). - if (_runState.get(runId)?.lossless === true && provenanceInput) { - let messages = provenanceInput.messages || null; - if (messages !== null) { - const serialized = JSON.stringify(messages); - if (serialized && serialized.length > LOSSILESS_MESSAGES_CAP) { - messages = { _truncated: true, length: serialized.length, head: serialized.slice(0, LOSSILESS_MESSAGES_CAP) }; - } - } + const state = await _ensureRunState(runId); + if (state?.lossless === true && provenanceInput) { + const { messages, tools } = clampLosslessRequest(provenanceInput.messages || null, provenanceInput.tools || null); return _appendEvent(runId, 'llm_request', { step, ...payload, lossless: true, messages, - tools: provenanceInput.tools || null, + tools, }); } // Default tier: never persist full prompts, message text, tool schemas, or @@ -283,11 +322,12 @@ export function recordLLMResponse(runId, step, { content, toolCalls, usage, late }); } -export function recordToolCall(runId, step, { name, args, result, latencyMs }) { +export async function recordToolCall(runId, step, { name, args, result, latencyMs }) { // Truncate very large tool results (a11y trees can be huge). Keep the first // 20KB verbatim by default — plenty for debugging flow — and 200KB in the // opt-in lossless tier; note the truncation either way. - const cap = _runState.get(runId)?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; + const state = await _ensureRunState(runId); + const cap = state?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; let shortResult = result; try { const s = typeof result === 'string' ? result : JSON.stringify(result); @@ -310,15 +350,7 @@ export async function recordScreenshot(runId, step, dataUrl, caption = '') { if (!dataUrl) return; try { const db = await openDB(); - if (!_runState.has(runId)) { - const seq = await _peekSeq(db, runId); - let lossless = false; - try { - const record = await promisifyReq(tx(db, ['runs'], 'readonly').objectStore('runs').get(runId)); - lossless = record?.lossless === true; - } catch {} - _runState.set(runId, { seq, lossless }); - } + await _ensureRunState(runId, db); const seq = _newSeq(runId); // Decode data URL to a Blob so IDB stores raw bytes (no base64 overhead). let blob = null; diff --git a/src/firefox/src/ui/locales/ar.js b/src/firefox/src/ui/locales/ar.js index a322d833d..8aae01c0e 100644 --- a/src/firefox/src/ui/locales/ar.js +++ b/src/firefox/src/ui/locales/ar.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': 'الأمان — اقرأ هذا:
  • النص الذي تُدخله هنا يُخزَّن بصيغة نصية واضحة في التخزين المحلي للمتصفح. لا يُرسَل إلى مشروع WebBrain — لكنه يُرسَل إلى مزوّد LLM الذي ضبطته في كل دور، كجزء من prompt النظام.
  • لا تضع كلمات مرور حسابات مهمة هنا (Google، Apple، iCloud، البنوك، SSO العمل، البريد الرئيسي). هذه الحسابات يجب أن تستخدم المصادقة الثنائية، ولا يجدر بك تسليمها لوكيل أصلًا.
  • الاستخدام المقصود هو كلمة مرور للاستعمال لمرة واحدة تعيد استخدامها لتسجيلات منخفضة الأهمية (النشرات البريدية، التجارب المجانية، حسابات المنتديات).
  • إذا اختُرق هذا الملف الشخصي للمتصفح يومًا، فإن مهاجمًا يملك الوصول إلى القرص قادر على قراءة هذا النص. أبقِه في حدّه الأدنى.
', 'tr.title': 'تتبّعات WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} عملية تشغيل', 'tr.run': '{n} عملية تشغيل', 'tr.btn.refresh': '↻ تحديث', diff --git a/src/firefox/src/ui/locales/bn.js b/src/firefox/src/ui/locales/bn.js index c80dace6d..7f7c4326e 100644 --- a/src/firefox/src/ui/locales/bn.js +++ b/src/firefox/src/ui/locales/bn.js @@ -916,6 +916,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': "WebBrain ট্রেস", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': "{n} চলে", 'tr.run': "{n} রান", 'tr.btn.refresh': "↻ রিফ্রেশ করুন", diff --git a/src/firefox/src/ui/locales/de.js b/src/firefox/src/ui/locales/de.js index 66afd9898..3869750f8 100644 --- a/src/firefox/src/ui/locales/de.js +++ b/src/firefox/src/ui/locales/de.js @@ -914,6 +914,8 @@ export default { 'hist.confirm_clear_all': 'ALLE gespeicherten Gespräche löschen? Dies kann nicht rückgängig gemacht werden.', // --- Traces --- 'tr.title': 'WebBrain Traces', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} Durchläufe', 'tr.run': '{n} Durchlauf', 'tr.btn.refresh': '↻ Aktualisieren', diff --git a/src/firefox/src/ui/locales/en.js b/src/firefox/src/ui/locales/en.js index 683f03316..cb76e4751 100644 --- a/src/firefox/src/ui/locales/en.js +++ b/src/firefox/src/ui/locales/en.js @@ -918,6 +918,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': 'WebBrain Traces', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} runs', 'tr.run': '{n} run', 'tr.btn.refresh': '↻ Refresh', diff --git a/src/firefox/src/ui/locales/es.js b/src/firefox/src/ui/locales/es.js index cb281e39a..696e8e87b 100644 --- a/src/firefox/src/ui/locales/es.js +++ b/src/firefox/src/ui/locales/es.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': 'Seguridad — léelo:
  • El texto que introduces aquí se guarda en texto plano en el almacenamiento local del navegador. No se envía al proyecto WebBrain — pero se envía al proveedor de LLM que hayas configurado en cada turno, como parte del prompt del sistema.
  • No pongas aquí contraseñas de cuentas importantes (Google, Apple, iCloud, banca, SSO de trabajo, correo principal). Esas cuentas deberían usar 2FA y no deberías entregárselas a un agente.
  • Una contraseña desechable que reutilizas para registros poco importantes (boletines, pruebas gratuitas, cuentas de foro) es el uso previsto.
  • Si este perfil del navegador alguna vez se ve comprometido, un atacante con acceso al disco podrá leer este texto. Mantenlo al mínimo.
', 'tr.title': 'Trazas de WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} ejecuciones', 'tr.run': '{n} ejecución', 'tr.btn.refresh': '↻ Actualizar', diff --git a/src/firefox/src/ui/locales/fa.js b/src/firefox/src/ui/locales/fa.js index 3497f6c98..b3eaab2d5 100644 --- a/src/firefox/src/ui/locales/fa.js +++ b/src/firefox/src/ui/locales/fa.js @@ -916,6 +916,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': "WebBrain Traces", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': "{n} اجرا می شود", 'tr.run': "اجرای {n}", 'tr.btn.refresh': "↻ تازه کردن", diff --git a/src/firefox/src/ui/locales/fr.js b/src/firefox/src/ui/locales/fr.js index 7c9138ad9..459bca649 100644 --- a/src/firefox/src/ui/locales/fr.js +++ b/src/firefox/src/ui/locales/fr.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': 'Sécurité — à lire :
  • Le texte que vous saisissez ici est stocké en clair dans le stockage local de votre navigateur. Il n\'est pas transmis au projet WebBrain — mais il est envoyé au fournisseur de LLM que vous avez configuré à chaque tour, dans le cadre du prompt système.
  • Ne mettez pas ici les mots de passe de comptes importants (Google, Apple, iCloud, banque, SSO professionnel, e-mail principal). Ces comptes devraient utiliser la 2FA et vous ne devriez pas avoir à les confier à un agent.
  • Un mot de passe jetable que vous réutilisez pour des inscriptions peu sensibles (newsletters, essais gratuits, comptes de forum) est l\'usage prévu.
  • Si ce profil de navigateur est un jour compromis, un attaquant ayant accès au disque pourra lire ce texte. Gardez-le au minimum.
', 'tr.title': 'Traces WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} exécutions', 'tr.run': '{n} exécution', 'tr.btn.refresh': '↻ Actualiser', diff --git a/src/firefox/src/ui/locales/he.js b/src/firefox/src/ui/locales/he.js index f43403863..288ae62d4 100644 --- a/src/firefox/src/ui/locales/he.js +++ b/src/firefox/src/ui/locales/he.js @@ -791,6 +791,8 @@ export default { "hist.confirm_delete": "למחוק את השיחה השמורה הזו?", "hist.confirm_clear_all": "למחוק את כל השיחות השמורות? לא ניתן לבטל פעולה זו.", "tr.title": "WebBrain עקבות", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', "tr.runs": "{n} פועל", "tr.run": "{n} לָרוּץ", "tr.btn.refresh": "↻ רענן", diff --git a/src/firefox/src/ui/locales/hi.js b/src/firefox/src/ui/locales/hi.js index 44d51dfee..96074bdbc 100644 --- a/src/firefox/src/ui/locales/hi.js +++ b/src/firefox/src/ui/locales/hi.js @@ -916,6 +916,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': "WebBrain निशान", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': "{n} चलता है", 'tr.run': "{n} चलाएँ", 'tr.btn.refresh': "↻ ताज़ा करें", diff --git a/src/firefox/src/ui/locales/id.js b/src/firefox/src/ui/locales/id.js index eb8cd735d..f386c8e17 100644 --- a/src/firefox/src/ui/locales/id.js +++ b/src/firefox/src/ui/locales/id.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': 'Keamanan — baca ini:
  • Teks yang Anda masukkan di sini disimpan dalam bentuk teks biasa di penyimpanan lokal peramban Anda. Teks ini tidak dikirim ke proyek WebBrain — tetapi dikirim ke penyedia LLM yang Anda konfigurasikan pada setiap giliran, sebagai bagian dari prompt sistem.
  • Jangan masukkan kata sandi untuk akun penting di sini (Google, Apple, iCloud, perbankan, SSO kantor, email utama). Akun-akun itu seharusnya memakai 2FA dan tidak perlu Anda serahkan ke agen.
  • Kasus penggunaan yang dimaksud adalah kata sandi sekali pakai yang Anda gunakan kembali untuk pendaftaran situs berisiko rendah (newsletter, uji coba gratis, akun forum).
  • Jika profil peramban ini suatu saat disusupi, penyerang yang memiliki akses disk dapat membaca teks ini. Buat seminimal mungkin.
', 'tr.title': 'Jejak WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} proses', 'tr.run': '{n} proses', 'tr.btn.refresh': '↻ Segarkan', diff --git a/src/firefox/src/ui/locales/ja.js b/src/firefox/src/ui/locales/ja.js index 07541dd40..79e963dbf 100644 --- a/src/firefox/src/ui/locales/ja.js +++ b/src/firefox/src/ui/locales/ja.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': 'セキュリティ — 必ずお読みください:
  • ここに入力したテキストはブラウザのローカルストレージに平文で保存されます。WebBrain プロジェクトへは送信されませんが、設定済みの LLM プロバイダーへはシステム prompt の一部として毎ターン送信されます
  • 重要アカウント(Google、Apple、iCloud、銀行、社内 SSO、メインのメール)のパスワードは絶対にここに入れないでください。それらは 2FA を有効にすべきで、そもそもエージェントに渡すべきではありません。
  • 意図された使い方は、低リスクの登録(メルマガ、無料体験、フォーラム)で使い回す使い捨てパスワードです。
  • このブラウザプロファイルが侵害された場合、ディスクにアクセスできる攻撃者はこのテキストを読み取れます。最小限にとどめてください。
', 'tr.title': 'WebBrain トレース', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} 件の実行', 'tr.run': '{n} 件の実行', 'tr.btn.refresh': '↻ 更新', diff --git a/src/firefox/src/ui/locales/ko.js b/src/firefox/src/ui/locales/ko.js index be2399c8a..9e1c109a7 100644 --- a/src/firefox/src/ui/locales/ko.js +++ b/src/firefox/src/ui/locales/ko.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': '보안 — 반드시 읽어주세요:
  • 여기에 입력한 텍스트는 브라우저 로컬 저장소에 평문으로 저장됩니다. WebBrain 프로젝트로는 전송되지 않지만, 설정한 LLM 제공자에게는 시스템 prompt의 일부로 매 턴 전송됩니다.
  • 중요한 계정(Google, Apple, iCloud, 은행, 회사 SSO, 주 이메일)의 비밀번호는 여기에 넣지 마세요. 이런 계정은 2FA를 사용해야 하며 애초에 에이전트에게 넘길 일이 없어야 합니다.
  • 의도된 사용 사례는 저위험 사이트 가입(뉴스레터, 무료 체험, 포럼 계정)에 재사용하는 일회용 비밀번호입니다.
  • 이 브라우저 프로필이 침해되면 디스크에 접근할 수 있는 공격자가 이 텍스트를 읽을 수 있습니다. 최소한으로 유지하세요.
', 'tr.title': 'WebBrain 트레이스', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '실행 {n}건', 'tr.run': '실행 {n}건', 'tr.btn.refresh': '↻ 새로고침', diff --git a/src/firefox/src/ui/locales/ms.js b/src/firefox/src/ui/locales/ms.js index 7e921271d..b9f8495c5 100644 --- a/src/firefox/src/ui/locales/ms.js +++ b/src/firefox/src/ui/locales/ms.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': 'Keselamatan — sila baca:
  • Teks yang anda masukkan di sini disimpan sebagai teks biasa dalam storan tempatan pelayar anda. Ia tidak dihantar kepada projek WebBrain — tetapi dihantar kepada pembekal LLM yang anda konfigurasikan pada setiap giliran, sebagai sebahagian daripada system prompt.
  • Jangan letakkan kata laluan untuk akaun penting di sini (Google, Apple, iCloud, perbankan, SSO kerja, e-mel utama). Akaun-akaun itu sepatutnya menggunakan 2FA dan anda sememangnya tidak perlu menyerahkannya kepada ejen.
  • Kes penggunaan yang dimaksudkan ialah kata laluan pakai buang yang anda guna semula untuk pendaftaran laman berisiko rendah (surat berita, percubaan percuma, akaun forum).
  • Jika profil pelayar ini diceroboh, penyerang yang mempunyai akses cakera boleh membaca teks ini. Kekalkan ia seminimum mungkin.
', 'tr.title': 'Jejak WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} larian', 'tr.run': '{n} larian', 'tr.btn.refresh': '↻ Muat semula', diff --git a/src/firefox/src/ui/locales/nl.js b/src/firefox/src/ui/locales/nl.js index 87507362e..9aabf530b 100644 --- a/src/firefox/src/ui/locales/nl.js +++ b/src/firefox/src/ui/locales/nl.js @@ -816,6 +816,8 @@ export default { 'hist.confirm_delete': 'Dit opgeslagen gesprek verwijderen?', 'hist.confirm_clear_all': 'Alle opgeslagen gesprekken verwijderen? Dit kan niet ongedaan worden gemaakt.', 'tr.title': 'WebBrain Traces', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} uitvoeringen', 'tr.run': '{n} uitvoering', 'tr.btn.refresh': '↻ Vernieuwen', diff --git a/src/firefox/src/ui/locales/pl.js b/src/firefox/src/ui/locales/pl.js index 6893e749b..4978e4258 100644 --- a/src/firefox/src/ui/locales/pl.js +++ b/src/firefox/src/ui/locales/pl.js @@ -572,6 +572,8 @@ export default { 'st.captcha.balance_fail': 'Niepowodzenie: {error}', 'st.captcha.security_html': 'Uwaga: klucz API jest przechowywany w postaci jawnej w lokalnej pamięci przeglądarki. CapSolver obciąża Twoje konto za każde rozwiązanie; agent wywoła go tylko, gdy CAPTCHA faktycznie zablokuje krok (maksymalnie raz na napotkanie — nie ponawia po niepowodzeniu). Regulaminy niektórych witryn zabraniają automatycznego rozwiązywania CAPTCHA; kieruj się własnym osądem.', 'tr.title': 'Ślady WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} wykonań', 'tr.run': '{n} wykonanie', 'tr.btn.refresh': '↻ Odśwież', diff --git a/src/firefox/src/ui/locales/pt.js b/src/firefox/src/ui/locales/pt.js index 039121351..88f97aff7 100644 --- a/src/firefox/src/ui/locales/pt.js +++ b/src/firefox/src/ui/locales/pt.js @@ -916,6 +916,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': "WebBrain Rastreamentos", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': "{n} é executado", 'tr.run': "{n} executado", 'tr.btn.refresh': "↻ Atualizar", diff --git a/src/firefox/src/ui/locales/ru.js b/src/firefox/src/ui/locales/ru.js index a539cbfa5..a2c558263 100644 --- a/src/firefox/src/ui/locales/ru.js +++ b/src/firefox/src/ui/locales/ru.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': 'Безопасность — прочтите:
  • Введённый здесь текст хранится в открытом виде в локальном хранилище браузера. Он не отправляется в проект WebBrain — но отправляется на каждом ходу как часть системного prompt тому LLM-провайдеру, которого вы настроили.
  • Не размещайте здесь пароли от важных аккаунтов (Google, Apple, iCloud, банк, рабочий SSO, основная почта). Эти аккаунты должны быть с 2FA, и их в любом случае не стоит передавать агенту.
  • Целевой сценарий — одноразовый пароль, который вы повторно используете для регистраций с низкими ставками (рассылки, бесплатные пробные периоды, аккаунты на форумах).
  • Если этот профиль браузера будет скомпрометирован, атакующий с доступом к диску сможет прочитать этот текст. Держите его минимальным.
', 'tr.title': 'Трассы WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': 'запусков: {n}', 'tr.run': 'запуск: {n}', 'tr.btn.refresh': '↻ Обновить', diff --git a/src/firefox/src/ui/locales/th.js b/src/firefox/src/ui/locales/th.js index f2175b852..f8e11b0de 100644 --- a/src/firefox/src/ui/locales/th.js +++ b/src/firefox/src/ui/locales/th.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': 'ความปลอดภัย — โปรดอ่าน:
  • ข้อความที่คุณกรอกที่นี่ถูกเก็บ เป็นข้อความธรรมดา ใน local storage ของเบราว์เซอร์ มัน จะไม่ ถูกส่งไปยังโปรเจกต์ WebBrain — แต่ จะถูกส่ง ไปยังผู้ให้บริการ LLM ที่คุณตั้งค่าไว้ในทุกเทิร์น ในฐานะส่วนหนึ่งของ system prompt
  • อย่าใส่รหัสผ่านของบัญชีสำคัญ ที่นี่ (Google, Apple, iCloud, ธนาคาร, SSO ที่ทำงาน, อีเมลหลัก) บัญชีเหล่านั้นควรใช้ 2FA และไม่ควรมอบให้เอเจนต์อยู่แล้ว
  • กรณีใช้งานที่ตั้งใจคือ รหัสผ่านใช้แล้วทิ้ง ที่คุณใช้ซ้ำสำหรับการสมัครเว็บไซต์เสี่ยงต่ำ (จดหมายข่าว ทดลองใช้ฟรี บัญชีฟอรัม)
  • หากโปรไฟล์เบราว์เซอร์นี้ถูกบุกรุก ผู้โจมตีที่เข้าถึงดิสก์ได้จะอ่านข้อความนี้ได้ ควรเก็บให้น้อยที่สุด
', 'tr.title': 'Traces ของ WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} ครั้ง', 'tr.run': '{n} ครั้ง', 'tr.btn.refresh': '↻ รีเฟรช', diff --git a/src/firefox/src/ui/locales/tl.js b/src/firefox/src/ui/locales/tl.js index 268ec4ec1..1af16e68e 100644 --- a/src/firefox/src/ui/locales/tl.js +++ b/src/firefox/src/ui/locales/tl.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': 'Seguridad — basahin ito:
  • Ang teksto na inilagay mo dito ay iniimbak bilang plain text sa local storage ng iyong browser. Hindi ito ipinapadala sa proyektong WebBrain — ngunit ipinapadala ito sa kung anong LLM provider ang iyong na-configure sa bawat turn, bilang bahagi ng system prompt.
  • Huwag maglagay ng mga password para sa mahahalagang account dito (Google, Apple, iCloud, banking, work SSO, pangunahing email). Dapat gumamit ng 2FA ang mga account na iyon at hindi mo naman dapat ipinapasa sa isang ahente.
  • Ang inilaang gamit ay isahang-gamit na password na ginagamit mo sa mga low-stakes na site signup (newsletter, libreng trial, forum account).
  • Kung sakaling ma-compromise ang browser profile na ito, ang isang attacker na may disk access ay makakabasa ng tekstong ito. Panatilihin itong minimal.
', 'tr.title': 'Mga Trace ng WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} run', 'tr.run': '{n} run', 'tr.btn.refresh': '↻ I-refresh', diff --git a/src/firefox/src/ui/locales/tr.js b/src/firefox/src/ui/locales/tr.js index 1a0d62176..40f24934e 100644 --- a/src/firefox/src/ui/locales/tr.js +++ b/src/firefox/src/ui/locales/tr.js @@ -372,6 +372,8 @@ export default { 'st.profile.security_html': 'Güvenlik — lütfen oku:
  • Buraya yazdığın metin tarayıcının yerel depolamasında düz metin olarak saklanır. WebBrain projesine iletilmez — fakat yapılandırdığın LLM sağlayıcısına her turda sistem isteminin bir parçası olarak gönderilir.
  • Önemli hesapların parolalarını buraya koyma (Google, Apple, iCloud, bankacılık, iş SSO\'su, birincil e-posta). Bu hesaplar 2FA kullanmalı ve zaten bir aracıya teslim edilmemeli.
  • Düşük riskli site kayıtları (bültenler, ücretsiz denemeler, forum hesapları) için kullandığın tek kullanımlık bir parola hedeflenen kullanım durumudur.
  • Bu tarayıcı profili ele geçirilirse, diske erişebilen bir saldırgan bu metni okuyabilir. Asgaride tut.
', 'tr.title': 'WebBrain İzleri', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} çalışma', 'tr.run': '{n} çalışma', 'tr.btn.refresh': '↻ Yenile', diff --git a/src/firefox/src/ui/locales/uk.js b/src/firefox/src/ui/locales/uk.js index 37018b7ff..662cf659b 100644 --- a/src/firefox/src/ui/locales/uk.js +++ b/src/firefox/src/ui/locales/uk.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': 'Безпека — прочитайте:
  • Введений тут текст зберігається у відкритому вигляді в локальному сховищі браузера. Він не надсилається проєкту WebBrain — але надсилається на кожному ходу як частина системного prompt тому LLM-провайдеру, якого ви налаштували.
  • Не вводьте сюди паролі від важливих акаунтів (Google, Apple, iCloud, банк, робочий SSO, основна пошта). Ці акаунти мають мати 2FA, і їх у будь-якому разі не варто передавати агенту.
  • Цільовий сценарій — одноразовий пароль, який ви повторно використовуєте для реєстрацій із низькими ставками (розсилки, безкоштовні пробні версії, акаунти на форумах).
  • Якщо цей профіль браузера колись буде скомпрометовано, зловмисник із доступом до диска зможе прочитати цей текст. Тримайте його мінімальним.
', 'tr.title': 'Траси WebBrain', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': 'запусків: {n}', 'tr.run': 'запуск: {n}', 'tr.btn.refresh': '↻ Оновити', diff --git a/src/firefox/src/ui/locales/vi.js b/src/firefox/src/ui/locales/vi.js index d5bde6b50..4d4709ca3 100644 --- a/src/firefox/src/ui/locales/vi.js +++ b/src/firefox/src/ui/locales/vi.js @@ -916,6 +916,8 @@ export default { // --- Traces page ------------------------------------------------------- 'tr.title': "Dấu vết WebBrain", + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': "{n} chạy", 'tr.run': "Chạy {n}", 'tr.btn.refresh': "↻ Làm mới", diff --git a/src/firefox/src/ui/locales/zh.js b/src/firefox/src/ui/locales/zh.js index 03c3e0d6d..81e525e12 100644 --- a/src/firefox/src/ui/locales/zh.js +++ b/src/firefox/src/ui/locales/zh.js @@ -333,6 +333,8 @@ export default { 'st.profile.security_html': '安全提示 — 请阅读:
  • 你在这里输入的文本以明文保存在浏览器的本地存储中。它不会被发送到 WebBrain 项目 — 但在每一轮作为系统提示的一部分发送给你所配置的 LLM 提供商。
  • 不要在这里放重要账户的密码(Google、Apple、iCloud、银行、工作 SSO、主邮箱)。这些账户应启用 2FA,也本就不该交给代理。
  • 适合此处的是你用于低风险注册(时事通讯、免费试用、论坛账户)的一次性密码
  • 如果此浏览器配置文件被攻破,能访问磁盘的攻击者就能读到此文本。请尽量精简。
', 'tr.title': 'WebBrain 轨迹', + 'tr.lossless.badge': 'Sensitive debug tier', + 'tr.lossless.warning': 'This run contains full request content. Review it for secrets before exporting or sharing.', 'tr.runs': '{n} 次运行', 'tr.run': '{n} 次运行', 'tr.btn.refresh': '↻ 刷新', diff --git a/src/firefox/src/ui/traces.html b/src/firefox/src/ui/traces.html index 157ad8c2e..7254f9a1f 100644 --- a/src/firefox/src/ui/traces.html +++ b/src/firefox/src/ui/traces.html @@ -118,6 +118,17 @@ text-overflow: ellipsis; margin-bottom: 3px; } + .lossless-badge { + display: inline-block; + margin-left: 6px; + padding: 1px 4px; + border: 1px solid rgba(255,152,0,0.55); + border-radius: 4px; + color: var(--warning); + font-size: 9px; + font-weight: 600; + vertical-align: 1px; + } .run-item .run-meta { font-size: 10px; color: var(--text2); @@ -188,6 +199,15 @@ margin-bottom: 14px; flex-wrap: wrap; } + .lossless-warning { + margin: 0 0 14px; + padding: 9px 12px; + border-left: 3px solid var(--warning); + background: rgba(255,152,0,0.12); + color: var(--warning); + border-radius: 0 6px 6px 0; + font-size: 12px; + } /* Costly-failure highlight — runs ≥ $0.50 with empty finalContent. */ .cost-warn { color: #ff9d4d; diff --git a/src/firefox/src/ui/traces.js b/src/firefox/src/ui/traces.js index 7ef31e781..99a7c2b25 100644 --- a/src/firefox/src/ui/traces.js +++ b/src/firefox/src/ui/traces.js @@ -119,6 +119,9 @@ function renderList() { compareIds.includes(r.runId) ? 'compare' : '', ].filter(Boolean).join(' '); const title = r.userMessage || t('tr.no_task'); + const losslessBadge = r.lossless === true + ? `${escapeHtml(t('tr.lossless.badge'))}` + : ''; const siblings = siblingsOf(r); const convChip = siblings.length > 1 ? `🧵 ${siblings.length}` @@ -129,7 +132,7 @@ function renderList() { const costClass = isCostlyFailure ? 'cost-warn' : ''; return `
-
${escapeHtml(title.slice(0, 120))}${convChip}
+
${escapeHtml(title.slice(0, 120))}${losslessBadge}${convChip}
${escapeHtml(r.model || '?')} ${escapeHtml(r.providerId || '')} @@ -269,6 +272,7 @@ async function buildRunView(run, events, compact, objectUrls = new Set()) { ${escapeHtml(t('tr.outtokens.label'))} ${(run.totalOutputTokens || 0).toLocaleString()} ${formatCost(run.totalCost) ? `${escapeHtml(t('tr.cost.label'))} ${escapeHtml(formatCost(run.totalCost))}` : ''}
+ ${run.lossless === true ? `` : ''} ${renderConversationPanel(run, compact)}
${escapeHtml(run.userMessage || '')}
${run.finalContent ? `
${escapeHtml(t('tr.final_label'))} ${escapeHtml(run.finalContent)}
` : ''} diff --git a/test/run.js b/test/run.js index 2a05e13d0..e147c030b 100644 --- a/test/run.js +++ b/test/run.js @@ -8882,12 +8882,17 @@ test('trace lossless tier: recorder branches on the tier and clamps payloads', ( const recorderSource = fs.readFileSync(path.join(ROOT, `src/${browser}/src/trace/recorder.js`), 'utf8'); assert.match(recorderSource, /async function losslessTraceEnabled\(\)/, `${browser}: losslessTraceEnabled missing`); assert.match(recorderSource, /const lossless = meta\.lossless === true \|\| await losslessTraceEnabled\(\);/, `${browser}: tier decision missing in startRun`); - assert.match(recorderSource, /lossless,\n/, `${browser}: run record does not stamp the tier`); + assert.match(recorderSource, /\.\.\.\(lossless \? \{ lossless: true \} : \{\}\)/, `${browser}: run record does not stamp the tier`); assert.match(recorderSource, /const LOSSILESS_RESULT_CAP = 200_000;/, `${browser}: lossless result cap missing`); - assert.match(recorderSource, /const LOSSILESS_MESSAGES_CAP = 500_000;/, `${browser}: lossless messages cap missing`); - assert.match(recorderSource, /_runState\.get\(runId\)\?\.lossless === true && provenanceInput/, `${browser}: request lossless branch missing`); - assert.match(recorderSource, /_runState\.get\(runId\)\?\.lossless === true \? LOSSILESS_RESULT_CAP : 20_000/, `${browser}: tool-result cap does not branch on tier`); + assert.match(recorderSource, /const LOSSILESS_REQUEST_CAP = 500_000;/, `${browser}: lossless request cap missing`); + assert.match(recorderSource, /await _ensureRunState\(runId\)[\s\S]*?state\?\.lossless === true && provenanceInput/, `${browser}: request lossless branch missing`); + assert.match(recorderSource, /await _ensureRunState\(runId\)[\s\S]*?state\?\.lossless === true \? LOSSILESS_RESULT_CAP : 20_000/, `${browser}: tool-result cap does not branch on tier`); assert.match(recorderSource, /peekRunFlags|lossless: record\?\.lossless === true|lossless = record\?\.lossless === true/, `${browser}: SW-eviction recovery does not restore the tier`); + assert.match(recorderSource, /async function _ensureRunState\(runId(?:, db = null)?\)/, `${browser}: recorder has no shared SW-recovery state loader`); + assert.match(recorderSource, /async function recordLLMRequest[\s\S]*?await _ensureRunState\(runId\)[\s\S]*?state\?\.lossless === true/, `${browser}: request tier is checked before SW recovery`); + assert.match(recorderSource, /async function recordToolCall[\s\S]*?await _ensureRunState\(runId\)[\s\S]*?state\?\.lossless === true/, `${browser}: tool tier is checked before SW recovery`); + assert.match(recorderSource, /\.\.\.\(lossless \? \{ lossless: true \} : \{\}\)/, `${browser}: default run records still serialize a false lossless field`); + assert.match(recorderSource, /LOSSILESS_TOOLS_CAP|clampLosslessRequest|tools: \{ _truncated/, `${browser}: lossless tool schemas are not independently bounded`); // Default tier must keep the content-free provenance path. assert.match(recorderSource, /buildPromptTraceProvenance\(/, `${browser}: default tier lost its provenance reduction`); } @@ -8919,7 +8924,7 @@ test('trace lossless tier: export renders masked request previews only for lossl messageCount: 2, toolsCount: 1, lossless: true, messages: [ { role: 'system', content: 'You are WebBrain.' }, - { role: 'user', content: 'api_key=sk-secret1234567890 and Bearer tok1234567890' }, + { role: 'user', content: '{"password":"hunter2abc", "api_key":"value12345"} api_key=sk-secret1234567890 and Bearer tok1234567890' }, ], tools: [{ function: { name: 'click' } }], }, @@ -8931,6 +8936,18 @@ test('trace lossless tier: export renders masked request previews only for lossl assert.match(markdown, /tools: click/, `${label}: lossless tool list missing`); assert.doesNotMatch(markdown, /sk-secret1234567890|tok1234567890/, `${label}: lossless export leaked secrets`); assert.match(markdown, /\[redacted\]/, `${label}: lossless export has no redaction marker`); + + const truncated = serialize([{ run: losslessRun, events: [{ + kind: 'llm_request', + data: { + messageCount: 1, toolsCount: 2, lossless: true, + messages: { _truncated: true, length: 750000, head: '{"messages":[{"role":"user","content":"partial"}' , toolNames: ['click', 'type_text'] }, + tools: null, + }, + }] }]).markdown; + assert.match(truncated, /truncated/i, `${label}: truncated request marker was rendered as empty`); + assert.match(truncated, /partial/, `${label}: truncated request head was omitted`); + assert.match(truncated, /tools: click, type_text/, `${label}: truncated request tool names were omitted`); } // Default tier: no message preview even when a request payload exists. const defaultRun = { @@ -8953,6 +8970,19 @@ test('trace lossless tier: default-tier privacy guard survives in both builds', assert.match(firefoxRecorder, /Default tier: never persist full prompts, message text, tool schemas, or[\s\S]*?tool names here\./, 'firefox default tier lost its privacy comment guard'); }); +test('trace lossless tier: Traces UI identifies sensitive runs before export', () => { + for (const browser of ['chrome', 'firefox']) { + const traces = fs.readFileSync(path.join(ROOT, `src/${browser}/src/ui/traces.js`), 'utf8'); + const html = fs.readFileSync(path.join(ROOT, `src/${browser}/src/ui/traces.html`), 'utf8'); + const locale = fs.readFileSync(path.join(ROOT, `src/${browser}/src/ui/locales/en.js`), 'utf8'); + assert.match(traces, /run\.lossless === true/, `${browser}: run list does not surface lossless traces`); + assert.match(traces, /tr\.lossless\.warning/, `${browser}: selected lossless run has no export warning`); + assert.match(html, /lossless-badge|lossless-warning/, `${browser}: no lossless trace styling exists`); + assert.match(locale, /tr\.lossless\.badge/, `${browser}: lossless badge locale missing`); + assert.match(locale, /tr\.lossless\.warning/, `${browser}: lossless warning locale missing`); + } +}); + test('trace recorder: DB v2 adds lineage indexes and startRun stores lineage fields', () => { for (const browser of ['chrome', 'firefox']) { const recorderSource = fs.readFileSync(path.join(ROOT, `src/${browser}/src/trace/recorder.js`), 'utf8'); From 9bc5c0484f1501edc3157f524e36749e42526fb4 Mon Sep 17 00:00:00 2001 From: alectimison-maker Date: Sat, 22 Aug 2026 08:29:27 +0800 Subject: [PATCH 3/6] fix(trace): redact lossless JSON exports and serialize recovery --- docs/privacy-and-data-flow.md | 4 +- src/chrome/src/agent/trace-export.js | 15 ++++- src/chrome/src/cloud-runs.js | 2 +- src/chrome/src/trace/recorder.js | 92 +++++++++++++-------------- src/chrome/src/ui/traces.js | 3 +- src/firefox/src/agent/trace-export.js | 21 +++++- src/firefox/src/trace/recorder.js | 92 +++++++++++++-------------- src/firefox/src/ui/traces.js | 3 +- test/run.js | 39 ++++++++++-- 9 files changed, 164 insertions(+), 107 deletions(-) diff --git a/docs/privacy-and-data-flow.md b/docs/privacy-and-data-flow.md index 2bb6a8cd0..11176b6fa 100644 --- a/docs/privacy-and-data-flow.md +++ b/docs/privacy-and-data-flow.md @@ -173,10 +173,10 @@ the stored copies are not separately synced to WebBrain. When enabled (Settings → Display → "Record traces"), every agent run is written to an IndexedDB database (`webbrain_traces`): - **`runs` store**: model, provider, token totals, timestamps, user message, final content -- **`events` store**: per-step LLM request provenance, model responses, and tool calls with args and results. Request provenance contains counts, controlled prompt/mode labels, and declared prompt/tool policy revisions; it neither duplicates nor fingerprints raw system prompts, message text, tool schemas, or tool names. +- **`events` store**: per-step LLM request provenance, model responses, and tool calls with args and results. By default, request provenance contains counts, controlled prompt/mode labels, and declared prompt/tool policy revisions; it neither duplicates nor fingerprints raw system prompts, message text, tool schemas, or tool names. Users can explicitly enable the lossless debug tier in Settings → Display; those runs are visibly marked and retain bounded request messages and tool schemas for debugging. Both Markdown and JSON exports mask credential-shaped values for lossless runs before writing a file. - **`shots` store**: screenshot blobs -The Traces page (`ui/traces.html`) reads from local IndexedDB only. Export produces a JSON blob saved to the user's Downloads folder. **No trace data ever leaves the browser.** +The Traces page (`ui/traces.html`) reads from local IndexedDB only. Export produces a JSON blob saved to the user's Downloads folder. **No trace data ever leaves the browser.** Lossless recording is off by default and uses per-request/result bounds; treat a lossless trace as sensitive even though exported credentials are masked. Each run also records an allowlisted effective runtime snapshot (including mode and prompt tier). Trace Markdown surfaces that snapshot and the privacy-safe diff --git a/src/chrome/src/agent/trace-export.js b/src/chrome/src/agent/trace-export.js index 1e5cf7264..452b85a94 100644 --- a/src/chrome/src/agent/trace-export.js +++ b/src/chrome/src/agent/trace-export.js @@ -19,6 +19,7 @@ */ import { isKnownKind } from '../trace/event-model.js'; +import { isSensitiveCloudKey } from '../cloud-runs.js'; const ARGS_LIMIT = 300; const RESULT_LIMIT = 600; @@ -40,11 +41,23 @@ function maskSecrets(text) { for (const pattern of SECRET_PATTERNS) out = out.replace(pattern, '[redacted]'); out = out .replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [redacted]') - .replace(/((?:^|[^a-zA-Z0-9_])["']?(?:api[_ -]?key|access[_ -]?token|token|secret|password|passwd)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;}\]]+)/gi, '$1[redacted]') + .replace(/((?:^|[^a-zA-Z0-9_])["']?(?:authorization|cookie|password|passwd|passphrase|passcode|pincode|(?:verification|confirmation|security|auth|email|twofactor|2fa|mfa|onetime|recovery)[_ -]?code|secret|credential|private[_ -]?key|api[_ -]?key|(?:access|refresh)[_ -]?token|client[_ -]?secret|token|access[_ -]?key[_ -]?id|secret[_ -]?access[_ -]?key|otp)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;}\]]+)/gi, '$1[redacted]') .replace(/([?&](?:api[_-]?key|access[_-]?token|token|key)=)[^&\s]+/gi, '$1[redacted]'); return out; } +function redactExportValue(value, key = '') { + if (isSensitiveCloudKey(key)) return '[redacted]'; + if (typeof value === 'string') return maskSecrets(value); + if (Array.isArray(value)) return value.map(item => redactExportValue(item)); + if (!value || typeof value !== 'object') return value; + return Object.fromEntries(Object.entries(value).map(([childKey, item]) => [childKey, redactExportValue(item, childKey)])); +} + +export function sanitizeTraceExport(payload) { + return payload?.run?.lossless === true ? redactExportValue(payload) : payload; +} + // Lossless requests carry the full message/tool shape. Render a bounded, // masked preview per message so the export stays readable without dumping // every token of a 500 KB request. diff --git a/src/chrome/src/cloud-runs.js b/src/chrome/src/cloud-runs.js index 43874d753..1a9a027de 100644 --- a/src/chrome/src/cloud-runs.js +++ b/src/chrome/src/cloud-runs.js @@ -81,7 +81,7 @@ export function normalizeCloudBridgeUrl(value = DEFAULT_CLOUD_BRIDGE_URL) { return url.href; } -function isSensitiveCloudKey(key) { +export function isSensitiveCloudKey(key) { const normalizedKey = normalizedCloudKey(key); if (!normalizedKey) return false; return SENSITIVE_CLOUD_KEY.test(normalizedKey) || SENSITIVE_CLOUD_KEY_EXACT.has(normalizedKey); diff --git a/src/chrome/src/trace/recorder.js b/src/chrome/src/trace/recorder.js index c90d31937..c822520b6 100644 --- a/src/chrome/src/trace/recorder.js +++ b/src/chrome/src/trace/recorder.js @@ -26,6 +26,7 @@ const DB_VERSION = 2; // opt-in debugging tier from exhausting IndexedDB on a single long run. const LOSSILESS_RESULT_CAP = 200_000; const LOSSILESS_REQUEST_CAP = 500_000; +const LOSSILESS_RUN_CAP = 5_000_000; let _dbPromise = null; function openDB() { @@ -122,8 +123,8 @@ async function peekRunFlags(db, runId) { const record = await promisifyReq( tx(db, ['runs'], 'readonly').objectStore('runs').get(runId), ); - return { forced: record?.forced === true, lossless: record?.lossless === true }; - } catch { return { forced: false, lossless: false }; } + return { forced: record?.forced === true, lossless: record?.lossless === true, losslessBytes: record?.losslessBytes || 0 }; + } catch { return { forced: false, lossless: false, losslessBytes: 0 }; } } // ----- Per-run state (held in memory on the service worker) ------------------ @@ -177,7 +178,7 @@ async function _ensureRunState(runId, db = null) { const resolvedDb = db || await openDB(); const seq = await _peekSeq(resolvedDb, runId); const flags = await peekRunFlags(resolvedDb, runId); - const state = { seq, forced: flags.forced, lossless: flags.lossless }; + const state = { seq, forced: flags.forced, lossless: flags.lossless, losslessBytes: Number(flags.losslessBytes) || 0 }; _runState.set(runId, state); return state; } catch { return null; } @@ -274,7 +275,7 @@ export async function startRun(meta = {}) { tabTitle: meta.tabTitle || '', mode: meta.mode || 'act', attachments: normalizeTraceAttachments(meta.attachments), - ...(lossless ? { lossless: true } : {}), + ...(lossless ? { lossless: true, losslessBytes: 0 } : {}), forced, stepCount: 0, totalInputTokens: 0, @@ -282,7 +283,7 @@ export async function startRun(meta = {}) { finalContent: null, }; await promisifyReq(tx(db, ['runs']).objectStore('runs').put(record)); - _runState.set(runId, { seq: 0, model: record.model, providerId: record.providerId, forced, lossless }); + _runState.set(runId, { seq: 0, model: record.model, providerId: record.providerId, forced, lossless, losslessBytes: 0 }); return runId; } catch (e) { console.warn('[trace] startRun failed:', e); @@ -294,9 +295,10 @@ async function _appendEventNow(runId, kind, data) { if (!(await tracingEnabledForRun(runId))) return; try { const db = await openDB(); - await _ensureRunState(runId, db); + const state = await _ensureRunState(runId, db); + const resolvedData = typeof data === 'function' ? data(state) : data; const seq = _newSeq(runId); - const ev = makeEvent(runId, seq, kind, data); + const ev = makeEvent(runId, seq, kind, resolvedData); if (!ev) { // Unknown kind or unserializable data: skip the write and surface the // bug at recording time instead of storing a ghost event. @@ -304,6 +306,16 @@ async function _appendEventNow(runId, kind, data) { return null; } await promisifyReq(tx(db, ['events']).objectStore('events').put(ev)); + if (state?.lossless === true && (kind === 'llm_request' || kind === 'tool')) { + let bytes = 0; + try { bytes = JSON.stringify(resolvedData).length; } catch {} + state.losslessBytes = (state.losslessBytes || 0) + bytes; + const run = await promisifyReq(tx(db, ['runs']).objectStore('runs').get(runId)); + if (run?.lossless === true) { + run.losslessBytes = state.losslessBytes; + await promisifyReq(tx(db, ['runs']).objectStore('runs').put(run)); + } + } return seq; } catch (e) { console.warn('[trace] appendEvent failed:', e); @@ -314,39 +326,27 @@ function _appendEvent(runId, kind, data) { return _queueRunWrite(runId, () => _appendEventNow(runId, kind, data)); } -export async function recordLLMRequest(runId, step, payload, provenanceInput = null) { +export function recordLLMRequest(runId, step, payload, provenanceInput = null) { // Lossless tier (opt-in): persist the request's full message/tool shape for // deep debugging and request reconstruction. Clamped so one oversized // request cannot exhaust IndexedDB; the marker mirrors tool-result // truncation ({ _truncated, length, head }). - const state = await _ensureRunState(runId); - if (state?.lossless === true && provenanceInput) { - const { messages, tools } = clampLosslessRequest(provenanceInput.messages || null, provenanceInput.tools || null); - return _appendEvent(runId, 'llm_request', { - step, - ...payload, - lossless: true, - messages, - tools, - }); - } + return _appendEvent(runId, 'llm_request', (state) => { + if (state?.lossless === true && provenanceInput) { + if ((state.losslessBytes || 0) >= LOSSILESS_RUN_CAP) { + return { step, ...payload, lossless: true, messages: { _truncated: true, length: 0, head: '(per-run lossless budget reached)', toolNames: [] }, tools: null }; + } + const { messages, tools } = clampLosslessRequest(provenanceInput.messages || null, provenanceInput.tools || null); + return { step, ...payload, lossless: true, messages, tools }; + } // Default tier: never persist full prompts, message text, tool schemas, or // tool names here. // The optional fourth argument is reduced to content-free provenance only. - let promptProvenance = null; - if (provenanceInput) { - try { - promptProvenance = buildPromptTraceProvenance( - provenanceInput.messages, - provenanceInput.tools, - provenanceInput.runtimeMode, - ); - } catch { /* provenance must never break a model request */ } - } - return _appendEvent(runId, 'llm_request', { - step, - ...payload, - ...(promptProvenance ? { promptProvenance } : {}), + let promptProvenance = null; + if (provenanceInput) { + try { promptProvenance = buildPromptTraceProvenance(provenanceInput.messages, provenanceInput.tools, provenanceInput.runtimeMode); } catch {} + } + return { step, ...payload, ...(promptProvenance ? { promptProvenance } : {}) }; }); } @@ -370,25 +370,21 @@ export function recordLLMResponse(runId, step, { content, toolCalls, usage, late }); } -export async function recordToolCall(runId, step, { name, args, result, latencyMs }) { +export function recordToolCall(runId, step, { name, args, result, latencyMs }) { // Truncate very large tool results (a11y trees can be huge). Keep the first // 20KB verbatim by default — plenty for debugging flow — and 200KB in the // opt-in lossless tier; note the truncation either way. - const state = await _ensureRunState(runId); - const cap = state?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; - let shortResult = result; - try { - const s = typeof result === 'string' ? result : JSON.stringify(result); - if (s && s.length > cap) { - shortResult = { _truncated: true, length: s.length, head: s.slice(0, cap) }; + return _appendEvent(runId, 'tool', (state) => { + if (state?.lossless === true && (state.losslessBytes || 0) >= LOSSILESS_RUN_CAP) { + return { step, name, args: args || null, result: { _truncated: true, length: 0, head: '(per-run lossless budget reached)' }, latencyMs: latencyMs || null }; } - } catch {} - return _appendEvent(runId, 'tool', { - step, - name, - args: args || null, - result: shortResult, - latencyMs: latencyMs || null, + const cap = state?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; + let shortResult = result; + try { + const s = typeof result === 'string' ? result : JSON.stringify(result); + if (s && s.length > cap) shortResult = { _truncated: true, length: s.length, head: s.slice(0, cap) }; + } catch {} + return { step, name, args: args || null, result: shortResult, latencyMs: latencyMs || null }; }); } diff --git a/src/chrome/src/ui/traces.js b/src/chrome/src/ui/traces.js index 6ac7328be..a4aaede0a 100644 --- a/src/chrome/src/ui/traces.js +++ b/src/chrome/src/ui/traces.js @@ -8,6 +8,7 @@ import { deleteRun, clearAllRuns, } from '../trace/recorder.js'; import { isKnownKind, isIgnorableKind } from '../trace/event-model.js'; +import { sanitizeTraceExport } from '../agent/trace-export.js'; import { t } from './i18n.js'; import { escapeHtml, escapeAttr } from './utils.js'; @@ -543,7 +544,7 @@ document.getElementById('btn-export').addEventListener('click', async () => { exportedByWebBrainVersion: chrome.runtime.getManifest().version || '', schema: 'webbrain-trace/1', }; - const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); + const blob = new Blob([JSON.stringify(sanitizeTraceExport(payload), null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; diff --git a/src/firefox/src/agent/trace-export.js b/src/firefox/src/agent/trace-export.js index 1e5cf7264..793533959 100644 --- a/src/firefox/src/agent/trace-export.js +++ b/src/firefox/src/agent/trace-export.js @@ -34,17 +34,36 @@ const SECRET_PATTERNS = [ /\bsk-[A-Za-z0-9_-]{8,}/g, /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, ]; +const SENSITIVE_TRACE_KEY = /(?:authorization|cookie|password|passwd|passphrase|passcode|pincode|(?:verification|confirmation|security|auth|email|twofactor|2fa|mfa|onetime)code|secret|credential|privatekey|apikey|token|accesskeyid|secretaccesskey)$/i; +const SENSITIVE_TRACE_KEY_EXACT = new Set(['code', 'pin', 'otp', 'cvv', 'cvc', 'ssn']); + +function isSensitiveTraceKey(key) { + const normalized = String(key || '').replace(/[^a-z0-9]/gi, ''); + return SENSITIVE_TRACE_KEY.test(normalized) || SENSITIVE_TRACE_KEY_EXACT.has(normalized); +} function maskSecrets(text) { let out = String(text ?? ''); for (const pattern of SECRET_PATTERNS) out = out.replace(pattern, '[redacted]'); out = out .replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [redacted]') - .replace(/((?:^|[^a-zA-Z0-9_])["']?(?:api[_ -]?key|access[_ -]?token|token|secret|password|passwd)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;}\]]+)/gi, '$1[redacted]') + .replace(/((?:^|[^a-zA-Z0-9_])["']?(?:authorization|cookie|password|passwd|passphrase|passcode|pincode|(?:verification|confirmation|security|auth|email|twofactor|2fa|mfa|onetime|recovery)[_ -]?code|secret|credential|private[_ -]?key|api[_ -]?key|(?:access|refresh)[_ -]?token|client[_ -]?secret|token|access[_ -]?key[_ -]?id|secret[_ -]?access[_ -]?key|otp)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;}\]]+)/gi, '$1[redacted]') .replace(/([?&](?:api[_-]?key|access[_-]?token|token|key)=)[^&\s]+/gi, '$1[redacted]'); return out; } +function redactExportValue(value, key = '') { + if (isSensitiveTraceKey(key)) return '[redacted]'; + if (typeof value === 'string') return maskSecrets(value); + if (Array.isArray(value)) return value.map(item => redactExportValue(item)); + if (!value || typeof value !== 'object') return value; + return Object.fromEntries(Object.entries(value).map(([childKey, item]) => [childKey, redactExportValue(item, childKey)])); +} + +export function sanitizeTraceExport(payload) { + return payload?.run?.lossless === true ? redactExportValue(payload) : payload; +} + // Lossless requests carry the full message/tool shape. Render a bounded, // masked preview per message so the export stays readable without dumping // every token of a 500 KB request. diff --git a/src/firefox/src/trace/recorder.js b/src/firefox/src/trace/recorder.js index 89efc7ebd..bd45ea1f9 100644 --- a/src/firefox/src/trace/recorder.js +++ b/src/firefox/src/trace/recorder.js @@ -26,6 +26,7 @@ const DB_VERSION = 2; // opt-in debugging tier from exhausting IndexedDB on a single long run. const LOSSILESS_RESULT_CAP = 200_000; const LOSSILESS_REQUEST_CAP = 500_000; +const LOSSILESS_RUN_CAP = 5_000_000; let _dbPromise = null; function openDB() { @@ -146,8 +147,8 @@ async function peekRunFlags(db, runId) { const record = await promisifyReq( tx(db, ['runs'], 'readonly').objectStore('runs').get(runId), ); - return { lossless: record?.lossless === true }; - } catch { return { lossless: false }; } + return { lossless: record?.lossless === true, losslessBytes: record?.losslessBytes || 0 }; + } catch { return { lossless: false, losslessBytes: 0 }; } } const _runStateLoads = new Map(); @@ -163,7 +164,7 @@ async function _ensureRunState(runId, db = null) { const resolvedDb = db || await openDB(); const seq = await _peekSeq(resolvedDb, runId); const flags = await peekRunFlags(resolvedDb, runId); - const state = { seq, lossless: flags.lossless }; + const state = { seq, lossless: flags.lossless, losslessBytes: Number(flags.losslessBytes) || 0 }; _runState.set(runId, state); return state; } catch { return null; } @@ -259,14 +260,14 @@ export async function startRun(meta) { tabTitle: meta.tabTitle || '', mode: meta.mode || 'act', attachments: normalizeTraceAttachments(meta.attachments), - ...(lossless ? { lossless: true } : {}), + ...(lossless ? { lossless: true, losslessBytes: 0 } : {}), stepCount: 0, totalInputTokens: 0, totalOutputTokens: 0, finalContent: null, }; await promisifyReq(tx(db, ['runs']).objectStore('runs').put(record)); - _runState.set(runId, { seq: 0, model: record.model, providerId: record.providerId, lossless }); + _runState.set(runId, { seq: 0, model: record.model, providerId: record.providerId, lossless, losslessBytes: 0 }); return runId; } catch (e) { console.warn('[trace] startRun failed:', e); @@ -278,9 +279,10 @@ async function _appendEventNow(runId, kind, data) { if (!(await tracingEnabled())) return; try { const db = await openDB(); - await _ensureRunState(runId, db); + const state = await _ensureRunState(runId, db); + const resolvedData = typeof data === 'function' ? data(state) : data; const seq = _newSeq(runId); - const ev = makeEvent(runId, seq, kind, data); + const ev = makeEvent(runId, seq, kind, resolvedData); if (!ev) { // Unknown kind or unserializable data: skip the write and surface the // bug at recording time instead of storing a ghost event. @@ -288,6 +290,16 @@ async function _appendEventNow(runId, kind, data) { return null; } await promisifyReq(tx(db, ['events']).objectStore('events').put(ev)); + if (state?.lossless === true && (kind === 'llm_request' || kind === 'tool')) { + let bytes = 0; + try { bytes = JSON.stringify(resolvedData).length; } catch {} + state.losslessBytes = (state.losslessBytes || 0) + bytes; + const run = await promisifyReq(tx(db, ['runs']).objectStore('runs').get(runId)); + if (run?.lossless === true) { + run.losslessBytes = state.losslessBytes; + await promisifyReq(tx(db, ['runs']).objectStore('runs').put(run)); + } + } return seq; } catch (e) { console.warn('[trace] appendEvent failed:', e); @@ -298,38 +310,26 @@ function _appendEvent(runId, kind, data) { return _queueRunWrite(runId, () => _appendEventNow(runId, kind, data)); } -export async function recordLLMRequest(runId, step, payload, provenanceInput = null) { +export function recordLLMRequest(runId, step, payload, provenanceInput = null) { // Lossless tier (opt-in): persist the request's full message/tool shape for // deep debugging and request reconstruction. Clamp oversized requests so // one request cannot exhaust IndexedDB. - const state = await _ensureRunState(runId); - if (state?.lossless === true && provenanceInput) { - const { messages, tools } = clampLosslessRequest(provenanceInput.messages || null, provenanceInput.tools || null); - return _appendEvent(runId, 'llm_request', { - step, - ...payload, - lossless: true, - messages, - tools, - }); - } + return _appendEvent(runId, 'llm_request', (state) => { + if (state?.lossless === true && provenanceInput) { + if ((state.losslessBytes || 0) >= LOSSILESS_RUN_CAP) { + return { step, ...payload, lossless: true, messages: { _truncated: true, length: 0, head: '(per-run lossless budget reached)', toolNames: [] }, tools: null }; + } + const { messages, tools } = clampLosslessRequest(provenanceInput.messages || null, provenanceInput.tools || null); + return { step, ...payload, lossless: true, messages, tools }; + } // Default tier: never persist full prompts, message text, tool schemas, or // tool names here. // The optional fourth argument is reduced to content-free provenance only. - let promptProvenance = null; - if (provenanceInput) { - try { - promptProvenance = buildPromptTraceProvenance( - provenanceInput.messages, - provenanceInput.tools, - provenanceInput.runtimeMode, - ); - } catch { /* provenance must never break a model request */ } - } - return _appendEvent(runId, 'llm_request', { - step, - ...payload, - ...(promptProvenance ? { promptProvenance } : {}), + let promptProvenance = null; + if (provenanceInput) { + try { promptProvenance = buildPromptTraceProvenance(provenanceInput.messages, provenanceInput.tools, provenanceInput.runtimeMode); } catch {} + } + return { step, ...payload, ...(promptProvenance ? { promptProvenance } : {}) }; }); } @@ -353,25 +353,21 @@ export function recordLLMResponse(runId, step, { content, toolCalls, usage, late }); } -export async function recordToolCall(runId, step, { name, args, result, latencyMs }) { +export function recordToolCall(runId, step, { name, args, result, latencyMs }) { // Truncate very large tool results (a11y trees can be huge). Keep the first // 20KB verbatim by default — plenty for debugging flow — and 200KB in the // opt-in lossless tier; note the truncation either way. - const state = await _ensureRunState(runId); - const cap = state?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; - let shortResult = result; - try { - const s = typeof result === 'string' ? result : JSON.stringify(result); - if (s && s.length > cap) { - shortResult = { _truncated: true, length: s.length, head: s.slice(0, cap) }; + return _appendEvent(runId, 'tool', (state) => { + if (state?.lossless === true && (state.losslessBytes || 0) >= LOSSILESS_RUN_CAP) { + return { step, name, args: args || null, result: { _truncated: true, length: 0, head: '(per-run lossless budget reached)' }, latencyMs: latencyMs || null }; } - } catch {} - return _appendEvent(runId, 'tool', { - step, - name, - args: args || null, - result: shortResult, - latencyMs: latencyMs || null, + const cap = state?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; + let shortResult = result; + try { + const s = typeof result === 'string' ? result : JSON.stringify(result); + if (s && s.length > cap) shortResult = { _truncated: true, length: s.length, head: s.slice(0, cap) }; + } catch {} + return { step, name, args: args || null, result: shortResult, latencyMs: latencyMs || null }; }); } diff --git a/src/firefox/src/ui/traces.js b/src/firefox/src/ui/traces.js index 99a7c2b25..fe56c0702 100644 --- a/src/firefox/src/ui/traces.js +++ b/src/firefox/src/ui/traces.js @@ -8,6 +8,7 @@ import { deleteRun, clearAllRuns, } from '../trace/recorder.js'; import { isKnownKind, isIgnorableKind } from '../trace/event-model.js'; +import { sanitizeTraceExport } from '../agent/trace-export.js'; import { t } from './i18n.js'; import { escapeHtml, escapeAttr } from './utils.js'; @@ -543,7 +544,7 @@ document.getElementById('btn-export').addEventListener('click', async () => { exportedByWebBrainVersion: browser.runtime.getManifest().version || '', schema: 'webbrain-trace/1', }; - const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); + const blob = new Blob([JSON.stringify(sanitizeTraceExport(payload), null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; diff --git a/test/run.js b/test/run.js index 1ffd2f8e8..01727cec9 100644 --- a/test/run.js +++ b/test/run.js @@ -286,10 +286,10 @@ const { ); // trace-export.js is pure ESM — the /export --traces serializer, tested here. -const { tracesToMarkdown } = await import( +const { tracesToMarkdown, sanitizeTraceExport } = await import( 'file://' + path.join(ROOT, 'src/chrome/src/agent/trace-export.js').replace(/\\/g, '/') ); -const { tracesToMarkdown: tracesToMarkdownFx } = await import( +const { tracesToMarkdown: tracesToMarkdownFx, sanitizeTraceExport: sanitizeTraceExportFx } = await import( 'file://' + path.join(ROOT, 'src/firefox/src/agent/trace-export.js').replace(/\\/g, '/') ); const { webbrainTraceToAtif } = await import( @@ -8889,8 +8889,8 @@ test('trace lossless tier: recorder branches on the tier and clamps payloads', ( assert.match(recorderSource, /await _ensureRunState\(runId\)[\s\S]*?state\?\.lossless === true \? LOSSILESS_RESULT_CAP : 20_000/, `${browser}: tool-result cap does not branch on tier`); assert.match(recorderSource, /peekRunFlags|lossless: record\?\.lossless === true|lossless = record\?\.lossless === true/, `${browser}: SW-eviction recovery does not restore the tier`); assert.match(recorderSource, /async function _ensureRunState\(runId(?:, db = null)?\)/, `${browser}: recorder has no shared SW-recovery state loader`); - assert.match(recorderSource, /async function recordLLMRequest[\s\S]*?await _ensureRunState\(runId\)[\s\S]*?state\?\.lossless === true/, `${browser}: request tier is checked before SW recovery`); - assert.match(recorderSource, /async function recordToolCall[\s\S]*?await _ensureRunState\(runId\)[\s\S]*?state\?\.lossless === true/, `${browser}: tool tier is checked before SW recovery`); + assert.match(recorderSource, /function recordLLMRequest[\s\S]*?_appendEvent\(runId, 'llm_request', \(state\)[\s\S]*?state\?\.lossless === true/, `${browser}: request recovery is not serialized inside the write queue`); + assert.match(recorderSource, /function recordToolCall[\s\S]*?_appendEvent\(runId, 'tool', \(state\)[\s\S]*?state\?\.lossless === true/, `${browser}: tool recovery is not serialized inside the write queue`); assert.match(recorderSource, /\.\.\.\(lossless \? \{ lossless: true \} : \{\}\)/, `${browser}: default run records still serialize a false lossless field`); assert.match(recorderSource, /LOSSILESS_TOOLS_CAP|clampLosslessRequest|tools: \{ _truncated/, `${browser}: lossless tool schemas are not independently bounded`); // Default tier must keep the content-free provenance path. @@ -8963,6 +8963,37 @@ test('trace lossless tier: export renders masked request previews only for lossl assert.doesNotMatch(markdown, /\*\*system:\*\*|\*\*user:\*\*/, 'default tier export must not render messages'); }); +test('trace lossless tier: exports redact the complete credential-key catalog', () => { + const sentinel = { + refresh_token: 'refresh-sentinel', + client_secret: 'client-secret-sentinel', + private_key: 'private-key-sentinel', + otp: 'otp-sentinel', + recovery_code: 'recovery-sentinel', + }; + for (const [label, serialize] of [['chrome', tracesToMarkdown], ['firefox', tracesToMarkdownFx]]) { + const { markdown } = serialize([{ run: { runId: label, status: 'done' }, events: [{ + kind: 'llm_request', + data: { lossless: true, messages: [{ role: 'user', content: JSON.stringify(sentinel) }], tools: [] }, + }] }]); + for (const value of Object.values(sentinel)) { + assert.doesNotMatch(markdown, new RegExp(value), `${label}: Markdown leaked ${value}`); + } + } +}); + +test('trace lossless tier: JSON exports redact lossless event credentials only', () => { + const payload = { + run: { runId: 'lossless-json', lossless: true }, + events: [{ data: { messages: [{ content: '{"refresh_token":"refresh-sentinel","private_key":"private-sentinel"}' }] } }], + }; + for (const [label, sanitize] of [['chrome', sanitizeTraceExport], ['firefox', sanitizeTraceExportFx]]) { + const exported = JSON.stringify(sanitize(payload)); + assert.doesNotMatch(exported, /refresh-sentinel|private-sentinel/, `${label}: JSON export leaked a credential`); + assert.match(exported, /\[redacted\]/, `${label}: JSON export did not mark credentials`); + } +}); + test('trace lossless tier: default-tier privacy guard survives in both builds', () => { const chromeRecorder = fs.readFileSync(path.join(ROOT, 'src/chrome/src/trace/recorder.js'), 'utf8'); const firefoxRecorder = fs.readFileSync(path.join(ROOT, 'src/firefox/src/trace/recorder.js'), 'utf8'); From 049daa8225aa12d66e9ec1be864057df0dfb50f8 Mon Sep 17 00:00:00 2001 From: alectimison-maker Date: Sat, 22 Aug 2026 08:35:09 +0800 Subject: [PATCH 4/6] fix(trace): evict oldest lossless runs within budget --- src/chrome/src/trace/recorder.js | 17 +++++++++++++++++ src/firefox/src/trace/recorder.js | 15 +++++++++++++++ test/run.js | 3 ++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/chrome/src/trace/recorder.js b/src/chrome/src/trace/recorder.js index c822520b6..705b22b54 100644 --- a/src/chrome/src/trace/recorder.js +++ b/src/chrome/src/trace/recorder.js @@ -27,6 +27,7 @@ const DB_VERSION = 2; const LOSSILESS_RESULT_CAP = 200_000; const LOSSILESS_REQUEST_CAP = 500_000; const LOSSILESS_RUN_CAP = 5_000_000; +const LOSSILESS_TOTAL_CAP = 50_000_000; let _dbPromise = null; function openDB() { @@ -314,6 +315,7 @@ async function _appendEventNow(runId, kind, data) { if (run?.lossless === true) { run.losslessBytes = state.losslessBytes; await promisifyReq(tx(db, ['runs']).objectStore('runs').put(run)); + await evictOldestLosslessRuns(runId); } } return seq; @@ -322,6 +324,21 @@ async function _appendEventNow(runId, kind, data) { } } +// Retain the active run and evict completed lossless runs oldest first when +// their aggregate recorded payload crosses the global budget. +async function evictOldestLosslessRuns(activeRunId) { + const runs = await listRuns({ limit: 500 }); + let total = runs.reduce((sum, run) => sum + (run?.lossless === true ? Number(run.losslessBytes) || 0 : 0), 0); + const candidates = runs + .filter(run => run?.lossless === true && run.runId !== activeRunId && run.status !== 'running') + .sort((a, b) => (a.startedAt || 0) - (b.startedAt || 0)); + for (const run of candidates) { + if (total <= LOSSILESS_TOTAL_CAP) break; + await deleteRun(run.runId); + total -= Number(run.losslessBytes) || 0; + } +} + function _appendEvent(runId, kind, data) { return _queueRunWrite(runId, () => _appendEventNow(runId, kind, data)); } diff --git a/src/firefox/src/trace/recorder.js b/src/firefox/src/trace/recorder.js index bd45ea1f9..7b9e27871 100644 --- a/src/firefox/src/trace/recorder.js +++ b/src/firefox/src/trace/recorder.js @@ -27,6 +27,7 @@ const DB_VERSION = 2; const LOSSILESS_RESULT_CAP = 200_000; const LOSSILESS_REQUEST_CAP = 500_000; const LOSSILESS_RUN_CAP = 5_000_000; +const LOSSILESS_TOTAL_CAP = 50_000_000; let _dbPromise = null; function openDB() { @@ -298,6 +299,7 @@ async function _appendEventNow(runId, kind, data) { if (run?.lossless === true) { run.losslessBytes = state.losslessBytes; await promisifyReq(tx(db, ['runs']).objectStore('runs').put(run)); + await evictOldestLosslessRuns(runId); } } return seq; @@ -306,6 +308,19 @@ async function _appendEventNow(runId, kind, data) { } } +async function evictOldestLosslessRuns(activeRunId) { + const runs = await listRuns({ limit: 500 }); + let total = runs.reduce((sum, run) => sum + (run?.lossless === true ? Number(run.losslessBytes) || 0 : 0), 0); + const candidates = runs + .filter(run => run?.lossless === true && run.runId !== activeRunId && run.status !== 'running') + .sort((a, b) => (a.startedAt || 0) - (b.startedAt || 0)); + for (const run of candidates) { + if (total <= LOSSILESS_TOTAL_CAP) break; + await deleteRun(run.runId); + total -= Number(run.losslessBytes) || 0; + } +} + function _appendEvent(runId, kind, data) { return _queueRunWrite(runId, () => _appendEventNow(runId, kind, data)); } diff --git a/test/run.js b/test/run.js index 01727cec9..f80817373 100644 --- a/test/run.js +++ b/test/run.js @@ -8173,7 +8173,7 @@ test('trace recorder: writes go through the event model and stamp the format ver const recorderSource = fs.readFileSync(path.join(ROOT, `src/${browser}/src/trace/recorder.js`), 'utf8'); assert.match(recorderSource, /import \{ TRACE_FORMAT_VERSION, makeEvent \} from '\.\/event-model\.js';/, `${browser}: recorder does not import the event model`); assert.match(recorderSource, /traceFormatVersion: TRACE_FORMAT_VERSION/, `${browser}: run record does not stamp the format version`); - assert.match(recorderSource, /const ev = makeEvent\(runId, seq, kind, data\);/, `${browser}: event writes bypass the envelope`); + assert.match(recorderSource, /const resolvedData = typeof data === 'function' \? data\(state\) : data;[\s\S]*?const ev = makeEvent\(runId, seq, kind, resolvedData\);/, `${browser}: queued event writes bypass the envelope`); assert.match(recorderSource, /dropped invalid event/, `${browser}: invalid-event drop is not surfaced`); assert.match(recorderSource, /const marker = makeEvent\(runId, seq, 'screenshot'/, `${browser}: screenshot marker bypasses the envelope`); } @@ -8893,6 +8893,7 @@ test('trace lossless tier: recorder branches on the tier and clamps payloads', ( assert.match(recorderSource, /function recordToolCall[\s\S]*?_appendEvent\(runId, 'tool', \(state\)[\s\S]*?state\?\.lossless === true/, `${browser}: tool recovery is not serialized inside the write queue`); assert.match(recorderSource, /\.\.\.\(lossless \? \{ lossless: true \} : \{\}\)/, `${browser}: default run records still serialize a false lossless field`); assert.match(recorderSource, /LOSSILESS_TOOLS_CAP|clampLosslessRequest|tools: \{ _truncated/, `${browser}: lossless tool schemas are not independently bounded`); + assert.match(recorderSource, /evictOldestLosslessRuns[\s\S]*?status !== 'running'[\s\S]*?sort\(\(a, b\) => \(a\.startedAt \|\| 0\) - \(b\.startedAt \|\| 0\)\)[\s\S]*?await deleteRun\(run\.runId\)/, `${browser}: lossless runs are not evicted oldest-first`); // Default tier must keep the content-free provenance path. assert.match(recorderSource, /buildPromptTraceProvenance\(/, `${browser}: default tier lost its provenance reduction`); } From 59f3c0d75bcd7220650826e717aa2da3758d0aae Mon Sep 17 00:00:00 2001 From: Codex release-state test Date: Sat, 22 Aug 2026 11:05:54 +0300 Subject: [PATCH 5/6] fix(trace): redact recovery_code in JSON exports and refresh lossless tier pins - add recovery to the sensitive-key code group in both catalogs (chrome cloud-runs.js, firefox trace-export.js) so credentials stored as structured args no longer survive sanitizeTraceExport - extend the JSON export test with a structured-args case and a non-sensitive passthrough assertion - update stale lossless-tier pins to the post-recovery write queue shape (losslessBytes stamp, queue-callback tier branch, tool cap) --- src/chrome/src/cloud-runs.js | 2 +- src/firefox/src/agent/trace-export.js | 2 +- test/run.js | 15 ++++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/chrome/src/cloud-runs.js b/src/chrome/src/cloud-runs.js index 1a9a027de..1a1ab19fb 100644 --- a/src/chrome/src/cloud-runs.js +++ b/src/chrome/src/cloud-runs.js @@ -14,7 +14,7 @@ const CLOUD_PERSIST_BYTES_LIMIT = 4 * 1024 * 1024; const TERMINAL_STATUSES = new Set(['completed', 'failed', 'aborted']); // Suffix match on normalized keys (non-alnum stripped). Avoid bare `pin` as a // suffix — it over-matches `spin`, `mapPin`, etc. Short exact keys live in the set. -const SENSITIVE_CLOUD_KEY = /(?:authorization|cookie|password|passwd|passphrase|passcode|pincode|(?:verification|confirmation|security|auth|email|twofactor|2fa|mfa|onetime)code|secret|credential|privatekey|apikey|token|accesskeyid|secretaccesskey)$/i; +const SENSITIVE_CLOUD_KEY = /(?:authorization|cookie|password|passwd|passphrase|passcode|pincode|(?:verification|confirmation|security|auth|email|twofactor|2fa|mfa|onetime|recovery)code|secret|credential|privatekey|apikey|token|accesskeyid|secretaccesskey)$/i; const SENSITIVE_CLOUD_KEY_EXACT = new Set(['code', 'pin', 'otp', 'cvv', 'cvc', 'ssn']); const LARGE_IMAGE_KEY = /(?:attachimage|screenshot|image|imagedata|dataurl)$/i; const CLOUD_TEXT_ENTRY_TOOLS = new Set(['set_field', 'type_ax', 'type_text', 'iframe_type']); diff --git a/src/firefox/src/agent/trace-export.js b/src/firefox/src/agent/trace-export.js index 793533959..c738cbd50 100644 --- a/src/firefox/src/agent/trace-export.js +++ b/src/firefox/src/agent/trace-export.js @@ -34,7 +34,7 @@ const SECRET_PATTERNS = [ /\bsk-[A-Za-z0-9_-]{8,}/g, /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, ]; -const SENSITIVE_TRACE_KEY = /(?:authorization|cookie|password|passwd|passphrase|passcode|pincode|(?:verification|confirmation|security|auth|email|twofactor|2fa|mfa|onetime)code|secret|credential|privatekey|apikey|token|accesskeyid|secretaccesskey)$/i; +const SENSITIVE_TRACE_KEY = /(?:authorization|cookie|password|passwd|passphrase|passcode|pincode|(?:verification|confirmation|security|auth|email|twofactor|2fa|mfa|onetime|recovery)code|secret|credential|privatekey|apikey|token|accesskeyid|secretaccesskey)$/i; const SENSITIVE_TRACE_KEY_EXACT = new Set(['code', 'pin', 'otp', 'cvv', 'cvc', 'ssn']); function isSensitiveTraceKey(key) { diff --git a/test/run.js b/test/run.js index f80817373..db79870a1 100644 --- a/test/run.js +++ b/test/run.js @@ -8882,16 +8882,16 @@ test('trace lossless tier: recorder branches on the tier and clamps payloads', ( const recorderSource = fs.readFileSync(path.join(ROOT, `src/${browser}/src/trace/recorder.js`), 'utf8'); assert.match(recorderSource, /async function losslessTraceEnabled\(\)/, `${browser}: losslessTraceEnabled missing`); assert.match(recorderSource, /const lossless = meta\.lossless === true \|\| await losslessTraceEnabled\(\);/, `${browser}: tier decision missing in startRun`); - assert.match(recorderSource, /\.\.\.\(lossless \? \{ lossless: true \} : \{\}\)/, `${browser}: run record does not stamp the tier`); + assert.match(recorderSource, /\.\.\.\(lossless \? \{ lossless: true, losslessBytes: 0 \} : \{\}\)/, `${browser}: run record does not stamp the tier`); assert.match(recorderSource, /const LOSSILESS_RESULT_CAP = 200_000;/, `${browser}: lossless result cap missing`); assert.match(recorderSource, /const LOSSILESS_REQUEST_CAP = 500_000;/, `${browser}: lossless request cap missing`); - assert.match(recorderSource, /await _ensureRunState\(runId\)[\s\S]*?state\?\.lossless === true && provenanceInput/, `${browser}: request lossless branch missing`); - assert.match(recorderSource, /await _ensureRunState\(runId\)[\s\S]*?state\?\.lossless === true \? LOSSILESS_RESULT_CAP : 20_000/, `${browser}: tool-result cap does not branch on tier`); + assert.match(recorderSource, /_appendEvent\(runId, 'llm_request', \(state\)[\s\S]*?state\?\.lossless === true && provenanceInput/, `${browser}: request lossless branch missing`); + assert.match(recorderSource, /const cap = state\?\.lossless === true \? LOSSILESS_RESULT_CAP : 20_000;/, `${browser}: tool-result cap does not branch on tier`); assert.match(recorderSource, /peekRunFlags|lossless: record\?\.lossless === true|lossless = record\?\.lossless === true/, `${browser}: SW-eviction recovery does not restore the tier`); assert.match(recorderSource, /async function _ensureRunState\(runId(?:, db = null)?\)/, `${browser}: recorder has no shared SW-recovery state loader`); assert.match(recorderSource, /function recordLLMRequest[\s\S]*?_appendEvent\(runId, 'llm_request', \(state\)[\s\S]*?state\?\.lossless === true/, `${browser}: request recovery is not serialized inside the write queue`); assert.match(recorderSource, /function recordToolCall[\s\S]*?_appendEvent\(runId, 'tool', \(state\)[\s\S]*?state\?\.lossless === true/, `${browser}: tool recovery is not serialized inside the write queue`); - assert.match(recorderSource, /\.\.\.\(lossless \? \{ lossless: true \} : \{\}\)/, `${browser}: default run records still serialize a false lossless field`); + assert.match(recorderSource, /\.\.\.\(lossless \? \{ lossless: true, losslessBytes: 0 \} : \{\}\)/, `${browser}: default run records still serialize a false lossless field`); assert.match(recorderSource, /LOSSILESS_TOOLS_CAP|clampLosslessRequest|tools: \{ _truncated/, `${browser}: lossless tool schemas are not independently bounded`); assert.match(recorderSource, /evictOldestLosslessRuns[\s\S]*?status !== 'running'[\s\S]*?sort\(\(a, b\) => \(a\.startedAt \|\| 0\) - \(b\.startedAt \|\| 0\)\)[\s\S]*?await deleteRun\(run\.runId\)/, `${browser}: lossless runs are not evicted oldest-first`); // Default tier must keep the content-free provenance path. @@ -8986,12 +8986,17 @@ test('trace lossless tier: exports redact the complete credential-key catalog', test('trace lossless tier: JSON exports redact lossless event credentials only', () => { const payload = { run: { runId: 'lossless-json', lossless: true }, - events: [{ data: { messages: [{ content: '{"refresh_token":"refresh-sentinel","private_key":"private-sentinel"}' }] } }], + events: [ + { data: { messages: [{ content: '{"refresh_token":"refresh-sentinel","private_key":"private-sentinel"}' }] } }, + { data: { name: 'fill_form', args: { recovery_code: 'recovery-sentinel', api_key: 'api-sentinel', notes: 'plain-sentinel' } } }, + ], }; for (const [label, sanitize] of [['chrome', sanitizeTraceExport], ['firefox', sanitizeTraceExportFx]]) { const exported = JSON.stringify(sanitize(payload)); assert.doesNotMatch(exported, /refresh-sentinel|private-sentinel/, `${label}: JSON export leaked a credential`); + assert.doesNotMatch(exported, /recovery-sentinel|api-sentinel/, `${label}: JSON export leaked a credential stored under a sensitive args key`); assert.match(exported, /\[redacted\]/, `${label}: JSON export did not mark credentials`); + assert.match(exported, /plain-sentinel/, `${label}: JSON export redacted a non-sensitive key`); } }); From 3e4ddccb91baf7cb72015f17f38147ff7e3af56c Mon Sep 17 00:00:00 2001 From: Codex release-state test Date: Sat, 22 Aug 2026 11:19:17 +0300 Subject: [PATCH 6/6] fix(trace): gate lossless eviction on a cached total and scan all runs - evictOldestLosslessRuns keeps a running aggregate so under-budget writes skip the store-wide scan entirely (one scan per worker lifetime to seed it, resynced on every eviction pass and clearAllRuns) - the eviction scan walks every run instead of listRuns({ limit: 500 }), so older lossless runs count toward the 50 MB budget and stay evictable once past that window - budget-reached truncation markers now carry the dropped payload's true length instead of length: 0 --- src/chrome/src/trace/recorder.js | 63 +++++++++++++++++++++++++++---- src/firefox/src/trace/recorder.js | 63 +++++++++++++++++++++++++++---- test/run.js | 4 ++ 3 files changed, 115 insertions(+), 15 deletions(-) diff --git a/src/chrome/src/trace/recorder.js b/src/chrome/src/trace/recorder.js index 705b22b54..86539f6ac 100644 --- a/src/chrome/src/trace/recorder.js +++ b/src/chrome/src/trace/recorder.js @@ -315,7 +315,7 @@ async function _appendEventNow(runId, kind, data) { if (run?.lossless === true) { run.losslessBytes = state.losslessBytes; await promisifyReq(tx(db, ['runs']).objectStore('runs').put(run)); - await evictOldestLosslessRuns(runId); + await evictOldestLosslessRuns(runId, bytes); } } return seq; @@ -325,18 +325,60 @@ async function _appendEventNow(runId, kind, data) { } // Retain the active run and evict completed lossless runs oldest first when -// their aggregate recorded payload crosses the global budget. -async function evictOldestLosslessRuns(activeRunId) { - const runs = await listRuns({ limit: 500 }); - let total = runs.reduce((sum, run) => sum + (run?.lossless === true ? Number(run.losslessBytes) || 0 : 0), 0); +// their aggregate recorded payload crosses the global budget. A cached running +// total keeps under-budget writes free of any store-wide scan; the scan itself +// walks every run rather than a newest-N window so old lossless runs still +// count toward the budget and stay evictable. +let _losslessTotalEstimate = null; + +async function _scanLosslessTotal() { + const db = await openDB(); + let total = 0; + await new Promise((resolve) => { + const req = tx(db, ['runs'], 'readonly').objectStore('runs').openCursor(); + req.onsuccess = () => { + const c = req.result; + if (!c) return resolve(); + if (c.value?.lossless === true) total += Number(c.value.losslessBytes) || 0; + c.continue(); + }; + req.onerror = () => resolve(); + }); + _losslessTotalEstimate = total; + return total; +} + +async function evictOldestLosslessRuns(activeRunId, addedBytes = 0) { + if (_losslessTotalEstimate === null) { + // First lossless write of this worker lifetime: learn the true total. + // The persisted run record already includes this write's bytes. + await _scanLosslessTotal(); + } else { + _losslessTotalEstimate += addedBytes; + if (_losslessTotalEstimate <= LOSSILESS_TOTAL_CAP) return; + } + const db = await openDB(); + const runs = []; + await new Promise((resolve) => { + const req = tx(db, ['runs'], 'readonly').objectStore('runs').openCursor(); + req.onsuccess = () => { + const c = req.result; + if (!c) return resolve(); + if (c.value?.lossless === true) runs.push(c.value); + c.continue(); + }; + req.onerror = () => resolve(); + }); + let total = runs.reduce((sum, run) => sum + (Number(run.losslessBytes) || 0), 0); const candidates = runs - .filter(run => run?.lossless === true && run.runId !== activeRunId && run.status !== 'running') + .filter(run => run.runId !== activeRunId && run.status !== 'running') .sort((a, b) => (a.startedAt || 0) - (b.startedAt || 0)); for (const run of candidates) { if (total <= LOSSILESS_TOTAL_CAP) break; await deleteRun(run.runId); total -= Number(run.losslessBytes) || 0; } + _losslessTotalEstimate = total; } function _appendEvent(runId, kind, data) { @@ -351,7 +393,9 @@ export function recordLLMRequest(runId, step, payload, provenanceInput = null) { return _appendEvent(runId, 'llm_request', (state) => { if (state?.lossless === true && provenanceInput) { if ((state.losslessBytes || 0) >= LOSSILESS_RUN_CAP) { - return { step, ...payload, lossless: true, messages: { _truncated: true, length: 0, head: '(per-run lossless budget reached)', toolNames: [] }, tools: null }; + let length = null; + try { length = JSON.stringify({ messages: provenanceInput.messages || null, tools: provenanceInput.tools || null }).length; } catch {} + return { step, ...payload, lossless: true, messages: { _truncated: true, length, head: '(per-run lossless budget reached)', toolNames: boundedToolNames(provenanceInput.tools) }, tools: null }; } const { messages, tools } = clampLosslessRequest(provenanceInput.messages || null, provenanceInput.tools || null); return { step, ...payload, lossless: true, messages, tools }; @@ -393,7 +437,9 @@ export function recordToolCall(runId, step, { name, args, result, latencyMs }) { // opt-in lossless tier; note the truncation either way. return _appendEvent(runId, 'tool', (state) => { if (state?.lossless === true && (state.losslessBytes || 0) >= LOSSILESS_RUN_CAP) { - return { step, name, args: args || null, result: { _truncated: true, length: 0, head: '(per-run lossless budget reached)' }, latencyMs: latencyMs || null }; + let length = null; + try { const s = typeof result === 'string' ? result : JSON.stringify(result); length = s ? s.length : 0; } catch {} + return { step, name, args: args || null, result: { _truncated: true, length, head: '(per-run lossless budget reached)' }, latencyMs: latencyMs || null }; } const cap = state?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; let shortResult = result; @@ -684,6 +730,7 @@ export async function deleteRun(runId) { } export async function clearAllRuns() { + _losslessTotalEstimate = null; const db = await openDB(); const t = tx(db, ['runs', 'events', 'shots']); await promisifyReq(t.objectStore('runs').clear()); diff --git a/src/firefox/src/trace/recorder.js b/src/firefox/src/trace/recorder.js index 7b9e27871..d7bfee84d 100644 --- a/src/firefox/src/trace/recorder.js +++ b/src/firefox/src/trace/recorder.js @@ -299,7 +299,7 @@ async function _appendEventNow(runId, kind, data) { if (run?.lossless === true) { run.losslessBytes = state.losslessBytes; await promisifyReq(tx(db, ['runs']).objectStore('runs').put(run)); - await evictOldestLosslessRuns(runId); + await evictOldestLosslessRuns(runId, bytes); } } return seq; @@ -308,17 +308,61 @@ async function _appendEventNow(runId, kind, data) { } } -async function evictOldestLosslessRuns(activeRunId) { - const runs = await listRuns({ limit: 500 }); - let total = runs.reduce((sum, run) => sum + (run?.lossless === true ? Number(run.losslessBytes) || 0 : 0), 0); +// Retain the active run and evict completed lossless runs oldest first when +// their aggregate recorded payload crosses the global budget. A cached running +// total keeps under-budget writes free of any store-wide scan; the scan itself +// walks every run rather than a newest-N window so old lossless runs still +// count toward the budget and stay evictable. +let _losslessTotalEstimate = null; + +async function _scanLosslessTotal() { + const db = await openDB(); + let total = 0; + await new Promise((resolve) => { + const req = tx(db, ['runs'], 'readonly').objectStore('runs').openCursor(); + req.onsuccess = () => { + const c = req.result; + if (!c) return resolve(); + if (c.value?.lossless === true) total += Number(c.value.losslessBytes) || 0; + c.continue(); + }; + req.onerror = () => resolve(); + }); + _losslessTotalEstimate = total; + return total; +} + +async function evictOldestLosslessRuns(activeRunId, addedBytes = 0) { + if (_losslessTotalEstimate === null) { + // First lossless write of this worker lifetime: learn the true total. + // The persisted run record already includes this write's bytes. + await _scanLosslessTotal(); + } else { + _losslessTotalEstimate += addedBytes; + if (_losslessTotalEstimate <= LOSSILESS_TOTAL_CAP) return; + } + const db = await openDB(); + const runs = []; + await new Promise((resolve) => { + const req = tx(db, ['runs'], 'readonly').objectStore('runs').openCursor(); + req.onsuccess = () => { + const c = req.result; + if (!c) return resolve(); + if (c.value?.lossless === true) runs.push(c.value); + c.continue(); + }; + req.onerror = () => resolve(); + }); + let total = runs.reduce((sum, run) => sum + (Number(run.losslessBytes) || 0), 0); const candidates = runs - .filter(run => run?.lossless === true && run.runId !== activeRunId && run.status !== 'running') + .filter(run => run.runId !== activeRunId && run.status !== 'running') .sort((a, b) => (a.startedAt || 0) - (b.startedAt || 0)); for (const run of candidates) { if (total <= LOSSILESS_TOTAL_CAP) break; await deleteRun(run.runId); total -= Number(run.losslessBytes) || 0; } + _losslessTotalEstimate = total; } function _appendEvent(runId, kind, data) { @@ -332,7 +376,9 @@ export function recordLLMRequest(runId, step, payload, provenanceInput = null) { return _appendEvent(runId, 'llm_request', (state) => { if (state?.lossless === true && provenanceInput) { if ((state.losslessBytes || 0) >= LOSSILESS_RUN_CAP) { - return { step, ...payload, lossless: true, messages: { _truncated: true, length: 0, head: '(per-run lossless budget reached)', toolNames: [] }, tools: null }; + let length = null; + try { length = JSON.stringify({ messages: provenanceInput.messages || null, tools: provenanceInput.tools || null }).length; } catch {} + return { step, ...payload, lossless: true, messages: { _truncated: true, length, head: '(per-run lossless budget reached)', toolNames: boundedToolNames(provenanceInput.tools) }, tools: null }; } const { messages, tools } = clampLosslessRequest(provenanceInput.messages || null, provenanceInput.tools || null); return { step, ...payload, lossless: true, messages, tools }; @@ -374,7 +420,9 @@ export function recordToolCall(runId, step, { name, args, result, latencyMs }) { // opt-in lossless tier; note the truncation either way. return _appendEvent(runId, 'tool', (state) => { if (state?.lossless === true && (state.losslessBytes || 0) >= LOSSILESS_RUN_CAP) { - return { step, name, args: args || null, result: { _truncated: true, length: 0, head: '(per-run lossless budget reached)' }, latencyMs: latencyMs || null }; + let length = null; + try { const s = typeof result === 'string' ? result : JSON.stringify(result); length = s ? s.length : 0; } catch {} + return { step, name, args: args || null, result: { _truncated: true, length, head: '(per-run lossless budget reached)' }, latencyMs: latencyMs || null }; } const cap = state?.lossless === true ? LOSSILESS_RESULT_CAP : 20_000; let shortResult = result; @@ -664,6 +712,7 @@ export async function deleteRun(runId) { } export async function clearAllRuns() { + _losslessTotalEstimate = null; const db = await openDB(); const t = tx(db, ['runs', 'events', 'shots']); await promisifyReq(t.objectStore('runs').clear()); diff --git a/test/run.js b/test/run.js index db79870a1..4930af10c 100644 --- a/test/run.js +++ b/test/run.js @@ -8894,6 +8894,10 @@ test('trace lossless tier: recorder branches on the tier and clamps payloads', ( assert.match(recorderSource, /\.\.\.\(lossless \? \{ lossless: true, losslessBytes: 0 \} : \{\}\)/, `${browser}: default run records still serialize a false lossless field`); assert.match(recorderSource, /LOSSILESS_TOOLS_CAP|clampLosslessRequest|tools: \{ _truncated/, `${browser}: lossless tool schemas are not independently bounded`); assert.match(recorderSource, /evictOldestLosslessRuns[\s\S]*?status !== 'running'[\s\S]*?sort\(\(a, b\) => \(a\.startedAt \|\| 0\) - \(b\.startedAt \|\| 0\)\)[\s\S]*?await deleteRun\(run\.runId\)/, `${browser}: lossless runs are not evicted oldest-first`); + assert.match(recorderSource, /_losslessTotalEstimate \+= addedBytes;[\s\S]*?if \(_losslessTotalEstimate <= LOSSILESS_TOTAL_CAP\) return;/, `${browser}: eviction scan is not gated by a cached running total`); + assert.match(recorderSource, /async function _scanLosslessTotal\(\)[\s\S]*?objectStore\('runs'\)\.openCursor\(\)/, `${browser}: lossless total must scan every run, not a newest-N window`); + assert.match(recorderSource, /clearAllRuns\(\) \{[\s\S]*?_losslessTotalEstimate = null;/, `${browser}: clearAllRuns does not reset the lossless total cache`); + assert.doesNotMatch(recorderSource, /length: 0, head: '\(per-run lossless budget reached\)'/, `${browser}: budget-reached markers lost the true payload length`); // Default tier must keep the content-free provenance path. assert.match(recorderSource, /buildPromptTraceProvenance\(/, `${browser}: default tier lost its provenance reduction`); }