diff --git a/scripts/web-live-dom-probe.mjs b/scripts/web-live-dom-probe.mjs index 776cca4c..12668904 100644 --- a/scripts/web-live-dom-probe.mjs +++ b/scripts/web-live-dom-probe.mjs @@ -649,7 +649,7 @@ async function createFreshSession(page, args, actions) { const alignment = await waitForFreshSessionAlignment(page, args, before, timeoutMs); const after = alignment.after; actions.push({ action: "fresh-session", selector, settled: Boolean(settled), aligned: alignment.ok, timeoutMs, shellReady, before, after, alignment }); - if (!settled || !alignment.ok) throw new Error(`fresh session did not settle: ${JSON.stringify({ settled: Boolean(settled), alignment, timeoutMs })}`); + if (!alignment.ok) throw new Error(`fresh session did not settle: ${JSON.stringify({ settled: Boolean(settled), alignment, timeoutMs })}`); return true; } @@ -661,16 +661,16 @@ async function waitForFreshSessionAlignment(page, args, before, timeoutMs) { while (Date.now() - startedAt < timeoutMs) { const check = freshSessionAlignment(before, after); bestCandidate = bestCandidate ?? freshSessionCandidate(before, after); - attempts.push({ elapsedMs: Date.now() - startedAt, ok: check.ok, reason: check.reason, conversationId: check.conversationId, routeConversationId: check.routeConversationId, selectedConversationId: check.selectedConversationId, apiConversationPresent: check.apiConversationPresent }); + attempts.push(freshSessionAttemptSummary(check, Date.now() - startedAt)); if (check.ok) return { ok: true, reason: null, attempts, after, check }; await page.waitForTimeout(500); after = await collectSessionState(page, args); } const check = freshSessionAlignment(before, after); bestCandidate = bestCandidate ?? freshSessionCandidate(before, after); - attempts.push({ elapsedMs: Date.now() - startedAt, ok: check.ok, reason: check.reason, conversationId: check.conversationId, routeConversationId: check.routeConversationId, selectedConversationId: check.selectedConversationId, apiConversationPresent: check.apiConversationPresent }); - if (!check.ok && bestCandidate?.conversationId) { - const repair = await realignFreshSession(page, args, bestCandidate.conversationId); + attempts.push(freshSessionAttemptSummary(check, Date.now() - startedAt)); + if (!check.ok && bestCandidate?.id) { + const repair = await realignFreshSession(page, args, bestCandidate.id); after = await collectSessionState(page, args); const repaired = freshSessionAlignment(before, after); return { ok: repaired.ok, reason: repaired.reason, attempts, after, check: repaired, repair }; @@ -678,19 +678,39 @@ async function waitForFreshSessionAlignment(page, args, before, timeoutMs) { return { ok: false, reason: check.reason, attempts, after, check }; } -function freshSessionCandidate(before, after) { - const previousConversationId = firstNonEmpty(before?.routeConversationId, before?.api?.workspace?.selectedConversationId, before?.activeTab?.conversationId); - const items = Array.isArray(after?.api?.conversations?.items) ? after.api.conversations.items : []; - return items.find((item) => item.conversationId && item.conversationId !== previousConversationId && Number(item.messagesCount ?? 0) === 0) ?? null; +function freshSessionAttemptSummary(check, elapsedMs) { + return { + elapsedMs, + ok: check.ok, + reason: check.reason, + id: check.id, + routeId: check.routeId, + selectedConversationId: check.selectedConversationId, + activeTabSessionId: check.activeTabSessionId, + activeTabConversationId: check.activeTabConversationId, + apiConversationPresent: check.apiConversationPresent, + workspaceApiAvailable: check.workspaceApiAvailable, + conversationsApiAvailable: check.conversationsApiAvailable + }; } -async function realignFreshSession(page, args, conversationId) { - const selected = await selectConversationViaApi(page, args, conversationId, "web-live-dom-probe-fresh-realign"); - const target = new URL(`/workbench/sessions/${encodeURIComponent(conversationId)}`, args.url).toString(); +function freshSessionCandidate(before, after) { + const previousConversationId = sessionAuthorityId(before); + const items = Array.isArray(after?.api?.conversations?.items) ? after.api.conversations.items : []; + const apiCandidate = items.find((item) => item.conversationId && item.conversationId !== previousConversationId && Number(item.messagesCount ?? 0) === 0); + if (apiCandidate) return { ...apiCandidate, id: apiCandidate.conversationId, source: "api-conversations" }; + const tabs = Array.isArray(after?.sessionTabs) ? after.sessionTabs : []; + const tabCandidate = tabs.find((item) => firstNonEmpty(item.sessionId, item.conversationId) && firstNonEmpty(item.sessionId, item.conversationId) !== previousConversationId && item.running !== "true"); + return tabCandidate ? { ...tabCandidate, id: firstNonEmpty(tabCandidate.sessionId, tabCandidate.conversationId), source: "session-tab" } : null; +} + +async function realignFreshSession(page, args, sessionOrConversationId) { + const selected = await selectConversationViaApi(page, args, sessionOrConversationId, "web-live-dom-probe-fresh-realign").catch((error) => ({ ok: false, phase: "select-conversation", error: error instanceof Error ? error.message : String(error) })); + const target = new URL(`/workbench/sessions/${encodeURIComponent(sessionOrConversationId)}`, args.url).toString(); await page.goto(target, { waitUntil: "domcontentloaded", timeout: args.timeoutMs }); await page.locator("#workspace").waitFor({ state: "visible", timeout: args.timeoutMs }); await page.locator("#command-input").waitFor({ state: "visible", timeout: args.timeoutMs }); - return { action: "fresh-session-realign", conversationId, target, selected }; + return { action: "fresh-session-realign", id: sessionOrConversationId, target, selected }; } async function selectConversationViaApi(page, args, conversationId, updatedByClient) { @@ -719,15 +739,19 @@ async function selectConversationViaApi(page, args, conversationId, updatedByCli } function freshSessionAlignment(before, after) { - const previousConversationId = firstNonEmpty(before?.routeConversationId, before?.api?.workspace?.selectedConversationId, before?.activeTab?.conversationId); - const conversationId = firstNonEmpty(after?.routeConversationId, after?.api?.workspace?.selectedConversationId, after?.activeTab?.conversationId); + const previousConversationId = sessionAuthorityId(before); + const conversationId = sessionAuthorityId(after); const selectedConversationId = after?.api?.workspace?.selectedConversationId ?? null; const routeConversationId = after?.routeConversationId ?? null; - const apiConversation = conversationId ? after?.api?.conversations?.items?.find((item) => item.conversationId === conversationId) ?? null : null; + const activeTabSessionId = after?.activeTab?.sessionId ?? null; + const activeTabConversationId = after?.activeTab?.conversationId ?? null; + const workspaceApiAvailable = after?.api?.workspace?.ok === true; + const conversationsApiAvailable = after?.api?.conversations?.ok === true; + const apiConversation = conversationId ? after?.api?.conversations?.items?.find((item) => item.conversationId === conversationId || item.sessionId === conversationId) ?? null : null; const routeMatches = Boolean(routeConversationId && conversationId && routeConversationId === conversationId); - const workspaceMatches = Boolean(selectedConversationId && conversationId && selectedConversationId === conversationId); - const apiConversationPresent = Boolean(apiConversation); - const activeTabMatches = !after?.activeTab?.conversationId || after.activeTab.conversationId === conversationId; + const workspaceMatches = workspaceApiAvailable ? Boolean(selectedConversationId && conversationId && selectedConversationId === conversationId) : true; + const apiConversationPresent = conversationsApiAvailable ? Boolean(apiConversation) : true; + const activeTabMatches = Boolean(!activeTabSessionId && !activeTabConversationId) || activeTabSessionId === conversationId || activeTabConversationId === conversationId; const changed = Boolean(conversationId && conversationId !== previousConversationId); const commandReady = after?.command?.inputDisabled === false; const empty = after?.messageCount === 0; @@ -740,7 +764,11 @@ function freshSessionAlignment(before, after) { : !activeTabMatches ? "fresh-session-active-tab-not-aligned" : !commandReady ? "fresh-session-command-not-ready" : "fresh-session-not-empty"; - return { ok, reason, previousConversationId, conversationId, routeConversationId, selectedConversationId, apiConversationPresent, activeTabConversationId: after?.activeTab?.conversationId ?? null, commandReady, command: after?.command ?? null, messageCount: after?.messageCount ?? null }; + return { ok, reason, previousConversationId, conversationId, id: conversationId, routeId: routeConversationId, routeConversationId, selectedConversationId, workspaceApiAvailable, conversationsApiAvailable, apiConversationPresent, activeTabConversationId, activeTabSessionId, commandReady, command: after?.command ?? null, messageCount: after?.messageCount ?? null }; +} + +function sessionAuthorityId(state) { + return firstNonEmpty(state?.routeConversationId, state?.api?.workspace?.selectedConversationId, state?.activeTab?.conversationId, state?.activeTab?.sessionId); } async function waitForSessionShellReady(page, args, actions) { @@ -763,6 +791,14 @@ async function waitForSessionShellReady(page, args, actions) { async function collectSessionState(page, args = null) { const dom = await page.evaluate(() => { const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']"); + const sessionTabs = [...document.querySelectorAll(".session-tab")].map((tab) => ({ + title: tab.querySelector("strong")?.textContent?.trim() ?? tab.getAttribute("title") ?? null, + conversationId: tab.getAttribute("data-conversation-id") ?? null, + sessionId: tab.getAttribute("data-session-id") ?? null, + status: tab.getAttribute("data-status") ?? null, + running: tab.getAttribute("data-running") ?? null, + active: tab.getAttribute("data-active") === "true" || tab.getAttribute("aria-selected") === "true" + })); const routeMatch = window.location.pathname.match(/\/workbench\/sessions\/([^/]+)/u) ?? window.location.pathname.match(/\/workspace\/sessions\/([^/]+)/u); return { finalUrl: window.location.href, @@ -778,6 +814,7 @@ async function collectSessionState(page, args = null) { running: activeTab.getAttribute("data-running") ?? null } : null, tabCount: document.querySelectorAll(".session-tab").length, + sessionTabs: sessionTabs.slice(0, 20), messageCount: document.querySelectorAll(".message-card").length, emptyVisible: Boolean(document.querySelector("[data-conversation-empty]")), command: { @@ -1088,6 +1125,7 @@ function compactSessionState(state) { selectedConversationId: state.api?.workspace?.selectedConversationId ?? null, revision: state.api?.workspace?.revision ?? null, activeTab: state.activeTab ?? null, + sessionTabs: Array.isArray(state.sessionTabs) ? state.sessionTabs.slice(0, 6) : [], messageCount: state.messageCount ?? null, command: state.command ?? null, conversationsCount: state.api?.conversations?.count ?? null