fix: sessionId durchreichen bei target_tab/sync/risk_request

Ereignisse ohne sessionId passierten den Panel-Filter ungehindert, wodurch
ein frisch geoeffneter Chat den Arbeits-Tab oder Verlauf eines parallel
laufenden Chats angezeigt bekam.
This commit is contained in:
2026-09-01 09:40:00 +02:00
parent 02d3d11523
commit ef180164ea
2 changed files with 41 additions and 24 deletions
+39 -22
View File
@@ -386,15 +386,19 @@ function reportToolDone(
} }
/** Sendet den aktuellen Target-Tab ans Panel und überträgt den Glow auf den neuen Tab. */ /** Sendet den aktuellen Target-Tab ans Panel und überträgt den Glow auf den neuen Tab. */
async function notifyTargetTab(): Promise<void> { async function notifyTargetTab(sessionId?: string): Promise<void> {
const targetTabId = getTargetTabId(); // sessionId mitstempeln: ohne sie zeigte ein frisch geöffneter Chat den
// Arbeits-Tab eines ANDEREN, parallel laufenden Chats an — Ereignisse ohne
// sessionId passieren den Filter im Panel ungehindert.
const sid = sessionId ?? activeSessionId;
const targetTabId = getTargetTabId(sid);
if (targetTabId === null) { if (targetTabId === null) {
sendToPanel({ type: 'target_tab', tabId: null }); sendToPanel({ type: 'target_tab', tabId: null, sessionId: sid });
return; return;
} }
try { try {
const tab = await chrome.tabs.get(targetTabId); const tab = await chrome.tabs.get(targetTabId);
sendToPanel({ type: 'target_tab', tabId: targetTabId, title: tab.title, url: tab.url }); sendToPanel({ type: 'target_tab', tabId: targetTabId, title: tab.title, url: tab.url, sessionId: sid });
// Nur während eines echten Laufs anzeigen. // Nur während eines echten Laufs anzeigen.
if (agentRunActive) notifyGlow(true); if (agentRunActive) notifyGlow(true);
// Tab in den Vordergrund bringen wenn focusWorkingTab aktiv // Tab in den Vordergrund bringen wenn focusWorkingTab aktiv
@@ -403,7 +407,7 @@ async function notifyTargetTab(): Promise<void> {
await chrome.tabs.update(targetTabId, { active: true }).catch(() => {}); await chrome.tabs.update(targetTabId, { active: true }).catch(() => {});
} }
} catch { } catch {
sendToPanel({ type: 'target_tab', tabId: null }); sendToPanel({ type: 'target_tab', tabId: null, sessionId: sid });
} }
} }
@@ -434,7 +438,7 @@ async function runTool(name: string, input: Record<string, unknown>, sessionId?:
} }
const tabId = await currentTabId(sessionId); const tabId = await currentTabId(sessionId);
const { allowed, verdict } = await gate(tabId, name, input, (v) => askUser(v, name)); const { allowed, verdict } = await gate(tabId, name, input, (v) => askUser(v, name, sessionId));
if (!allowed) { if (!allowed) {
return { return {
@@ -464,11 +468,14 @@ async function runTool(name: string, input: Record<string, unknown>, sessionId?:
} }
/** Fragt den Nutzer. Ohne offenes Panel wird gehandelt — Autonomie hat Vorrang. */ /** Fragt den Nutzer. Ohne offenes Panel wird gehandelt — Autonomie hat Vorrang. */
function askUser(verdict: RiskVerdict, toolName: string): Promise<boolean> { function askUser(verdict: RiskVerdict, toolName: string, sessionId?: string): Promise<boolean> {
if (!panelPort) return Promise.resolve(true); if (!panelPort) return Promise.resolve(true);
const requestId = crypto.randomUUID(); const requestId = crypto.randomUUID();
sendToPanel({ type: 'risk_request', requestId, name: toolName, verdict, timeoutMs: RISK_TIMEOUT_MS }); sendToPanel({
type: 'risk_request', requestId, name: toolName, verdict,
timeoutMs: RISK_TIMEOUT_MS, sessionId: sessionId ?? activeSessionId,
});
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@@ -505,7 +512,7 @@ async function getActiveTab(sessionId?: string): Promise<chrome.tabs.Tab> {
const [candidate] = await chrome.tabs.query({ active: true, currentWindow: true }); const [candidate] = await chrome.tabs.query({ active: true, currentWindow: true });
if (candidate?.id != null && await tabOwner(candidate.id, sessionId) === 'own') { if (candidate?.id != null && await tabOwner(candidate.id, sessionId) === 'own') {
setTargetTabId(candidate.id, sessionId); setTargetTabId(candidate.id, sessionId);
notifyTargetTab(); notifyTargetTab(sessionId);
return candidate; return candidate;
} }
@@ -516,14 +523,14 @@ async function getActiveTab(sessionId?: string): Promise<chrome.tabs.Tab> {
if (!hasOwnGroup && candidate?.id != null if (!hasOwnGroup && candidate?.id != null
&& await tabOwner(candidate.id, sessionId) === 'user') { && await tabOwner(candidate.id, sessionId) === 'user') {
setTargetTabId(candidate.id, sessionId); setTargetTabId(candidate.id, sessionId);
notifyTargetTab(); notifyTargetTab(sessionId);
return candidate; return candidate;
} }
const fresh = await chrome.tabs.create({ url: 'about:blank', active: false }); const fresh = await chrome.tabs.create({ url: 'about:blank', active: false });
if (!fresh.id) throw new ToolError('NO_ACTIVE_TAB', 'Kein Tab verfügbar', false); if (!fresh.id) throw new ToolError('NO_ACTIVE_TAB', 'Kein Tab verfügbar', false);
setTargetTabId(fresh.id, sessionId); setTargetTabId(fresh.id, sessionId);
notifyTargetTab(); notifyTargetTab(sessionId);
return fresh; return fresh;
} }
@@ -970,11 +977,11 @@ async function dispatchTool(name: string, input: Record<string, unknown>, sessio
if (input.newTab) { if (input.newTab) {
const newTab = await chrome.tabs.create({ url, active: false }); const newTab = await chrome.tabs.create({ url, active: false });
setTargetTabId(newTab.id!); setTargetTabId(newTab.id!);
notifyTargetTab(); notifyTargetTab(sessionId);
return { tabId: newTab.id, url, loading: true, hint: 'Seite lädt — browser_wait oder screenshot nach kurzer Pause.', _method: 'synthetic' }; return { tabId: newTab.id, url, loading: true, hint: 'Seite lädt — browser_wait oder screenshot nach kurzer Pause.', _method: 'synthetic' };
} }
await chrome.tabs.update(tabId!, { url }); await chrome.tabs.update(tabId!, { url });
notifyTargetTab(); notifyTargetTab(sessionId);
return { tabId, url, loading: true, hint: 'Seite lädt — browser_wait oder screenshot nach kurzer Pause.', _method: 'synthetic' }; return { tabId, url, loading: true, hint: 'Seite lädt — browser_wait oder screenshot nach kurzer Pause.', _method: 'synthetic' };
} }
@@ -1019,7 +1026,7 @@ async function dispatchTool(name: string, input: Record<string, unknown>, sessio
// sessionId mitgeben: sonst landet der neue Tab in der Gruppe der gerade // sessionId mitgeben: sonst landet der neue Tab in der Gruppe der gerade
// SICHTBAREN Konversation statt in der des aufrufenden Chats. // SICHTBAREN Konversation statt in der des aufrufenden Chats.
setTargetTabId(newTab.id!, sessionId); setTargetTabId(newTab.id!, sessionId);
notifyTargetTab(); notifyTargetTab(sessionId);
return { tabId: newTab.id, url, redirectedFrom: url !== requestedUrl ? requestedUrl : undefined, _method: 'synthetic' }; return { tabId: newTab.id, url, redirectedFrom: url !== requestedUrl ? requestedUrl : undefined, _method: 'synthetic' };
} }
@@ -1037,7 +1044,7 @@ async function dispatchTool(name: string, input: Record<string, unknown>, sessio
await chrome.windows.update(t.windowId!, { focused: true }); await chrome.windows.update(t.windowId!, { focused: true });
} }
setTargetTabId(tid, sessionId); setTargetTabId(tid, sessionId);
notifyTargetTab(); notifyTargetTab(sessionId);
return { ok: true, tabId: tid, _method: 'synthetic' }; return { ok: true, tabId: tid, _method: 'synthetic' };
} }
@@ -1912,7 +1919,7 @@ chrome.runtime.onConnect.addListener((port) => {
} else { } else {
// Der Broker-Kanal kennt keine Bilder — lieber sagen als still schlucken. // Der Broker-Kanal kennt keine Bilder — lieber sagen als still schlucken.
if (msg.images?.length) { if (msg.images?.length) {
sendToPanel({ type: 'log', level: 'warn', sendToPanel({ type: 'log', level: 'warn', sessionId,
text: `${msg.images.length} Bild(er) ignoriert: Bildanhänge gehen nur im Antrieb "direct".` }); text: `${msg.images.length} Bild(er) ignoriert: Bildanhänge gehen nur im Antrieb "direct".` });
} }
// Aktuelle Tab-URL mitschicken — der Broker baut daraus den Kontext (z.B. Mercedes-Wissen). // Aktuelle Tab-URL mitschicken — der Broker baut daraus den Kontext (z.B. Mercedes-Wissen).
@@ -1929,12 +1936,22 @@ chrome.runtime.onConnect.addListener((port) => {
} }
case 'sync': { case 'sync': {
// Nachliefern statt neu erzeugen: der Puffer ist die Wahrheit. // Nachliefern statt neu erzeugen: der Puffer ist die Wahrheit.
// ABER nur die Ereignisse DIESER Konversation: der Puffer ist global,
// ein frisch geöffneter Chat bekam sonst den kompletten Verlauf aller
// parallel laufenden Chats nachgespielt ("es fühlt sich wie eine an").
// Ereignisse ohne sessionId sind global (connection_status u.ä.) und
// bleiben drin.
const from = typeof msg.from === 'number' ? msg.from : 0; const from = typeof msg.from === 'number' ? msg.from : 0;
const missed = eventBuffer.filter(e => e.seq > from); const sid = (msg as any).sessionId || activeSessionId;
const missed = eventBuffer.filter(e => {
if (e.seq <= from) return false;
const evSid = (e.msg as any).sessionId;
return !evSid || evSid === sid;
});
if (panelPort) { if (panelPort) {
try { try {
panelPort.postMessage({ panelPort.postMessage({
type: 'sync_start', count: missed.length, running: isAgentRunning(), type: 'sync_start', count: missed.length, running: isAgentRunning(sid),
latest: eventSeq, startedAt: runStartedAt || undefined, latest: eventSeq, startedAt: runStartedAt || undefined,
}); });
// Direkt posten, nicht ueber sendToPanel — sonst landet alles erneut im Puffer. // Direkt posten, nicht ueber sendToPanel — sonst landet alles erneut im Puffer.
@@ -1962,17 +1979,17 @@ chrome.runtime.onConnect.addListener((port) => {
case 'set_config': { case 'set_config': {
const next = await patchConfig(msg.patch); const next = await patchConfig(msg.patch);
if (msg.patch.brokerUrl) { ws?.close(); setTimeout(connect, 300); } if (msg.patch.brokerUrl) { ws?.close(); setTimeout(connect, 300); }
sendToPanel({ type: 'state', config: next, connected: ws?.readyState === WebSocket.OPEN, running: isAgentRunning(), version: EXTENSION_VERSION, runningSessions: getRunningSessions() }); sendToPanel({ type: 'state', config: next, connected: ws?.readyState === WebSocket.OPEN, running: isAgentRunning(activeSessionId), version: EXTENSION_VERSION, sessionId: activeSessionId, runningSessions: getRunningSessions() });
break; break;
} }
case 'get_state': { case 'get_state': {
const cfg = await loadConfig(); const cfg = await loadConfig();
sendToPanel({ type: 'state', config: cfg, connected: ws?.readyState === WebSocket.OPEN, running: isAgentRunning(), version: EXTENSION_VERSION, runningSessions: getRunningSessions() }); sendToPanel({ type: 'state', config: cfg, connected: ws?.readyState === WebSocket.OPEN, running: isAgentRunning(activeSessionId), version: EXTENSION_VERSION, sessionId: activeSessionId, runningSessions: getRunningSessions() });
break; break;
} }
case 'run_tool': { case 'run_tool': {
const callId = crypto.randomUUID(); const callId = crypto.randomUUID();
sendToPanel({ type: 'tool_executing', callId, name: msg.name, input: msg.input }); sendToPanel({ type: 'tool_executing', callId, name: msg.name, input: msg.input, sessionId: activeSessionId });
const started = performance.now(); const started = performance.now();
try { try {
const result = await runTool(msg.name, msg.input); const result = await runTool(msg.name, msg.input);
@@ -1996,7 +2013,7 @@ chrome.runtime.onConnect.addListener((port) => {
const sid = (msg as any).sessionId; const sid = (msg as any).sessionId;
if (sid) { if (sid) {
activeSessionId = sid; activeSessionId = sid;
notifyTargetTab(); notifyTargetTab(sid);
sendToPanel({ sendToPanel({
type: 'state', type: 'state',
config: await loadConfig(), config: await loadConfig(),
+2 -2
View File
@@ -212,7 +212,7 @@ function openPort(): void {
} }
port = p; port = p;
// Alles nachfordern, was waehrend geschlossenem Panel aufgelaufen ist. // Alles nachfordern, was waehrend geschlossenem Panel aufgelaufen ist.
try { p.postMessage({ type: 'sync', from: lastSeq }); } catch { /* Reconnect greift */ } try { p.postMessage({ type: 'sync', from: lastSeq, sessionId: activeConvId }); } catch { /* Reconnect greift */ }
p.onMessage.addListener((msg) => { p.onMessage.addListener((msg) => {
// Sequenznummer merken: nach einem Abriss wird genau ab hier nachgeliefert. // Sequenznummer merken: nach einem Abriss wird genau ab hier nachgeliefert.
@@ -2263,7 +2263,7 @@ function showRunningBanner(startedAt?: number): void {
bar.remove(); bar.remove();
// Alles ab Null nachfordern: der Puffer im Worker hat den vollen Lauf. // Alles ab Null nachfordern: der Puffer im Worker hat den vollen Lauf.
lastSeq = 0; lastSeq = 0;
try { port?.postMessage({ type: 'sync', from: 0 }); } catch { /* egal */ } try { port?.postMessage({ type: 'sync', from: 0, sessionId: activeConvId }); } catch { /* egal */ }
}); });
bar.append(btn); bar.append(btn);
appendToChat(bar); appendToChat(bar);