#!/usr/bin/env node 'use strict'; const { spawn } = require('node:child_process'); const fs = require('node:fs'); const fsPromises = require('node:fs/promises'); const os = require('node:os'); const path = require('node:path'); const { buildOperationPlan, } = require('./lib/logseq-electron-op-sim.cjs'); const DEFAULT_URL = 'http://localhost:3001/#/'; const DEFAULT_SESSION_NAME = 'logseq-op-sim'; const DEFAULT_CHROME_PROFILE = 'auto'; const DEFAULT_INSTANCES = 1; const DEFAULT_OPS = 50; const DEFAULT_OP_PROFILE = 'fast'; const DEFAULT_OP_TIMEOUT_MS = 1000; const DEFAULT_ROUNDS = 1; const DEFAULT_UNDO_REDO_DELAY_MS = 350; const DEFAULT_HEADED = true; const DEFAULT_AUTO_CONNECT = false; const DEFAULT_RESET_SESSION = true; const DEFAULT_TARGET_GRAPH = 'db1'; const DEFAULT_E2E_PASSWORD = '12345'; const DEFAULT_SWITCH_GRAPH_TIMEOUT_MS = 100000; const DEFAULT_CHROME_LAUNCH_ARGS = [ '--new-window', '--no-first-run', '--no-default-browser-check', ]; const RENDERER_READY_TIMEOUT_MS = 30000; const RENDERER_READY_POLL_DELAY_MS = 250; const FALLBACK_PAGE_NAME = 'op-sim-scratch'; const DEFAULT_VERIFY_CHECKSUM = true; const DEFAULT_CLEANUP_TODAY_PAGE = true; const DEFAULT_CAPTURE_REPLAY = true; const DEFAULT_SYNC_SETTLE_TIMEOUT_MS = 3000; const AGENT_BROWSER_ACTION_TIMEOUT_MS = 1000000; const PROCESS_TIMEOUT_MS = 1000000; const AGENT_BROWSER_RETRY_COUNT = 2; const BOOTSTRAP_EVAL_TIMEOUT_MS = 150000; const RENDERER_EVAL_BASE_TIMEOUT_MS = 30000; const DEFAULT_ARTIFACT_BASE_DIR = path.join('tmp', 'db-sync-repro'); function usage() { return [ 'Usage: node scripts/sync-open-chrome-tab-simulate.cjs [options]', '', 'Options:', ` --url URL to open (default: ${DEFAULT_URL})`, ` --session agent-browser session name (default: ${DEFAULT_SESSION_NAME})`, ` --instances Number of concurrent browser instances (default: ${DEFAULT_INSTANCES})`, ` --graph Graph name to switch/download before ops (default: ${DEFAULT_TARGET_GRAPH})`, ` --e2e-password Password for E2EE modal if prompted (default: ${DEFAULT_E2E_PASSWORD})`, ' --profile Chrome profile to reuse login state (default: auto)', ' auto = prefer Default, then logseq.com', ' none = do not pass --profile to agent-browser (isolated profile)', ' profile labels are mapped to Chrome profile names', ' --executable-path Chrome executable path (default: auto-detect system Chrome)', ' --auto-connect Enable auto-connect to an already running Chrome instance', ' --no-auto-connect Disable auto-connect to a running Chrome instance', ' --no-reset-session Do not close the target agent-browser session before starting', ` --switch-timeout-ms Timeout for graph switch/download bootstrap (default: ${DEFAULT_SWITCH_GRAPH_TIMEOUT_MS})`, ` --ops Total operations across all instances per round (must be >= 1, default: ${DEFAULT_OPS})`, ` --op-profile Operation profile: fast|full (default: ${DEFAULT_OP_PROFILE})`, ` --op-timeout-ms Timeout per operation in renderer (default: ${DEFAULT_OP_TIMEOUT_MS})`, ' --seed Deterministic seed for operation ordering/jitter', ' --replay Replay a prior captured artifact run', ` --rounds Number of operation rounds per instance (default: ${DEFAULT_ROUNDS})`, ` --undo-redo-delay-ms Wait time after undo/redo command (default: ${DEFAULT_UNDO_REDO_DELAY_MS})`, ` --sync-settle-timeout-ms Timeout waiting for local/remote tx to settle before checksum verify (default: ${DEFAULT_SYNC_SETTLE_TIMEOUT_MS})`, ' --verify-checksum Run dev checksum diagnostics after each round (default: enabled)', ' --no-verify-checksum Skip post-round checksum diagnostics', ' --capture-replay Capture initial DB + per-op tx stream for local replay (default: enabled)', ' --no-capture-replay Skip replay capture payloads', ' --cleanup-today-page Delete today page after simulation (default: enabled)', ' --no-cleanup-today-page Keep today page unchanged after simulation', ' --headless Run agent-browser in headless mode', ' --print-only Print parsed args only, do not run simulation', ' -h, --help Show this message', ].join('\n'); } function parsePositiveInteger(value, flagName) { const parsed = Number.parseInt(value, 10); if (!Number.isInteger(parsed) || parsed <= 0) { throw new Error(`${flagName} must be a positive integer`); } return parsed; } function parseNonNegativeInteger(value, flagName) { const parsed = Number.parseInt(value, 10); if (!Number.isInteger(parsed) || parsed < 0) { throw new Error(`${flagName} must be a non-negative integer`); } return parsed; } function parseArgs(argv) { const result = { url: DEFAULT_URL, session: DEFAULT_SESSION_NAME, instances: DEFAULT_INSTANCES, graph: DEFAULT_TARGET_GRAPH, e2ePassword: DEFAULT_E2E_PASSWORD, profile: DEFAULT_CHROME_PROFILE, executablePath: null, autoConnect: DEFAULT_AUTO_CONNECT, resetSession: DEFAULT_RESET_SESSION, switchTimeoutMs: DEFAULT_SWITCH_GRAPH_TIMEOUT_MS, ops: DEFAULT_OPS, opProfile: DEFAULT_OP_PROFILE, opTimeoutMs: DEFAULT_OP_TIMEOUT_MS, seed: null, replay: null, rounds: DEFAULT_ROUNDS, undoRedoDelayMs: DEFAULT_UNDO_REDO_DELAY_MS, syncSettleTimeoutMs: DEFAULT_SYNC_SETTLE_TIMEOUT_MS, verifyChecksum: DEFAULT_VERIFY_CHECKSUM, captureReplay: DEFAULT_CAPTURE_REPLAY, cleanupTodayPage: DEFAULT_CLEANUP_TODAY_PAGE, headed: DEFAULT_HEADED, printOnly: false, }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--help' || arg === '-h') { return { ...result, help: true }; } if (arg === '--print-only') { result.printOnly = true; continue; } if (arg === '--headless') { result.headed = false; continue; } if (arg === '--verify-checksum') { result.verifyChecksum = true; continue; } if (arg === '--no-verify-checksum') { result.verifyChecksum = false; continue; } if (arg === '--cleanup-today-page') { result.cleanupTodayPage = true; continue; } if (arg === '--no-cleanup-today-page') { result.cleanupTodayPage = false; continue; } if (arg === '--capture-replay') { result.captureReplay = true; continue; } if (arg === '--no-capture-replay') { result.captureReplay = false; continue; } if (arg === '--no-auto-connect') { result.autoConnect = false; continue; } if (arg === '--auto-connect') { result.autoConnect = true; continue; } if (arg === '--no-reset-session') { result.resetSession = false; continue; } const next = argv[i + 1]; if (arg === '--url') { if (typeof next !== 'string' || next.length === 0) { throw new Error('--url must be a non-empty string'); } result.url = next; i += 1; continue; } if (arg === '--session') { if (typeof next !== 'string' || next.length === 0) { throw new Error('--session must be a non-empty string'); } result.session = next; i += 1; continue; } if (arg === '--graph') { if (typeof next !== 'string' || next.length === 0) { throw new Error('--graph must be a non-empty string'); } result.graph = next; i += 1; continue; } if (arg === '--e2e-password') { if (typeof next !== 'string' || next.length === 0) { throw new Error('--e2e-password must be a non-empty string'); } result.e2ePassword = next; i += 1; continue; } if (arg === '--instances') { result.instances = parsePositiveInteger(next, '--instances'); i += 1; continue; } if (arg === '--profile') { if (typeof next !== 'string' || next.length === 0) { throw new Error('--profile must be a non-empty string'); } result.profile = next; i += 1; continue; } if (arg === '--executable-path') { if (typeof next !== 'string' || next.length === 0) { throw new Error('--executable-path must be a non-empty string'); } result.executablePath = next; i += 1; continue; } if (arg === '--ops') { result.ops = parsePositiveInteger(next, '--ops'); i += 1; continue; } if (arg === '--op-profile') { if (typeof next !== 'string' || next.length === 0) { throw new Error('--op-profile must be a non-empty string'); } const normalized = next.toLowerCase(); if (normalized !== 'fast' && normalized !== 'full') { throw new Error('--op-profile must be one of: fast, full'); } result.opProfile = normalized; i += 1; continue; } if (arg === '--op-timeout-ms') { result.opTimeoutMs = parsePositiveInteger(next, '--op-timeout-ms'); i += 1; continue; } if (arg === '--seed') { if (typeof next !== 'string' || next.length === 0) { throw new Error('--seed must be a non-empty string'); } result.seed = next; i += 1; continue; } if (arg === '--replay') { if (typeof next !== 'string' || next.length === 0) { throw new Error('--replay must be a non-empty path'); } result.replay = next; i += 1; continue; } if (arg === '--rounds') { result.rounds = parsePositiveInteger(next, '--rounds'); i += 1; continue; } if (arg === '--undo-redo-delay-ms') { result.undoRedoDelayMs = parseNonNegativeInteger(next, '--undo-redo-delay-ms'); i += 1; continue; } if (arg === '--sync-settle-timeout-ms') { result.syncSettleTimeoutMs = parsePositiveInteger(next, '--sync-settle-timeout-ms'); i += 1; continue; } if (arg === '--switch-timeout-ms') { result.switchTimeoutMs = parsePositiveInteger(next, '--switch-timeout-ms'); i += 1; continue; } throw new Error(`Unknown argument: ${arg}`); } if (result.ops < 1) { throw new Error('--ops must be at least 1'); } if (result.rounds < 1) { throw new Error('--rounds must be at least 1'); } return result; } function spawnAndCapture(cmd, args, options = {}) { const { input, timeoutMs = PROCESS_TIMEOUT_MS, env = process.env, } = options; return new Promise((resolve, reject) => { const child = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'], env, }); let stdout = ''; let stderr = ''; let timedOut = false; const timer = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); }, timeoutMs); child.stdout.on('data', (payload) => { stdout += payload.toString(); }); child.stderr.on('data', (payload) => { stderr += payload.toString(); }); child.once('error', (error) => { clearTimeout(timer); reject(error); }); child.once('exit', (code) => { clearTimeout(timer); if (timedOut) { reject(new Error(`Command timed out after ${timeoutMs}ms: ${cmd} ${args.join(' ')}`)); return; } if (code === 0) { resolve({ code, stdout, stderr }); return; } const detail = stderr.trim() || stdout.trim(); reject( new Error( `Command failed: ${cmd} ${args.join(' ')} (exit ${code})` + (detail ? `\n${detail}` : '') ) ); }); if (typeof input === 'string') { child.stdin.write(input); } child.stdin.end(); }); } function parseJsonOutput(output) { const text = output.trim(); if (!text) { throw new Error('Expected JSON output from agent-browser but got empty output'); } try { return JSON.parse(text); } catch (_error) { const lines = text.split(/\r?\n/).filter(Boolean); const lastLine = lines[lines.length - 1]; try { return JSON.parse(lastLine); } catch (error) { throw new Error('Failed to parse JSON output from agent-browser: ' + String(error.message || error)); } } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function hashSeed(input) { const text = String(input ?? ''); let hash = 2166136261; for (let i = 0; i < text.length; i += 1) { hash ^= text.charCodeAt(i); hash = Math.imul(hash, 16777619); } return hash >>> 0; } function createSeededRng(seedInput) { let state = hashSeed(seedInput); if (state === 0) { state = 0x9e3779b9; } return () => { state = (state + 0x6D2B79F5) >>> 0; let payload = state; payload = Math.imul(payload ^ (payload >>> 15), payload | 1); payload ^= payload + Math.imul(payload ^ (payload >>> 7), payload | 61); return ((payload ^ (payload >>> 14)) >>> 0) / 4294967296; }; } function deriveSeed(baseSeed, ...parts) { return hashSeed([String(baseSeed ?? ''), ...parts.map((it) => String(it))].join('::')); } function sanitizeForFilename(value) { return String(value || 'default').replace(/[^a-zA-Z0-9._-]+/g, '-'); } async function pathExists(targetPath) { try { await fsPromises.access(targetPath); return true; } catch (_error) { return false; } } async function copyIfExists(sourcePath, destPath) { if (!(await pathExists(sourcePath))) return false; await fsPromises.mkdir(path.dirname(destPath), { recursive: true }); await fsPromises.cp(sourcePath, destPath, { force: true, recursive: true, }); return true; } async function detectChromeUserDataRoot() { const home = os.homedir(); const candidates = []; if (process.platform === 'darwin') { candidates.push(path.join(home, 'Library', 'Application Support', 'Google', 'Chrome')); } else if (process.platform === 'win32') { const localAppData = process.env.LOCALAPPDATA; if (localAppData) { candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data')); } } else { candidates.push(path.join(home, '.config', 'google-chrome')); candidates.push(path.join(home, '.config', 'chromium')); } for (const candidate of candidates) { if (await pathExists(candidate)) return candidate; } return null; } async function createIsolatedChromeUserDataDir(sourceProfileName, instanceIndex) { const sourceRoot = await detectChromeUserDataRoot(); if (!sourceRoot) { throw new Error('Cannot find Chrome user data root to clone auth profile'); } const sourceProfileDir = path.join(sourceRoot, sourceProfileName); if (!(await pathExists(sourceProfileDir))) { throw new Error(`Cannot find Chrome profile directory to clone: ${sourceProfileDir}`); } const targetRoot = path.join( os.tmpdir(), `logseq-op-sim-user-data-${sanitizeForFilename(sourceProfileName)}-${instanceIndex}` ); const targetDefaultProfileDir = path.join(targetRoot, 'Default'); await fsPromises.rm(targetRoot, { recursive: true, force: true }); await fsPromises.mkdir(targetDefaultProfileDir, { recursive: true }); await copyIfExists(path.join(sourceRoot, 'Local State'), path.join(targetRoot, 'Local State')); const entries = [ 'Network', 'Cookies', 'Local Storage', 'Session Storage', 'IndexedDB', 'WebStorage', 'Preferences', 'Secure Preferences', ]; for (const entry of entries) { await copyIfExists( path.join(sourceProfileDir, entry), path.join(targetDefaultProfileDir, entry) ); } return targetRoot; } function buildChromeLaunchArgs(url) { return [ `--app=${url}`, ...DEFAULT_CHROME_LAUNCH_ARGS, ]; } function isRetryableAgentBrowserError(error) { const message = String(error?.message || error || ''); return ( /daemon may be busy or unresponsive/i.test(message) || /resource temporarily unavailable/i.test(message) || /os error 35/i.test(message) || /EAGAIN/i.test(message) || /inspected target navigated or closed/i.test(message) || /execution context was destroyed/i.test(message) || /cannot find context with specified id/i.test(message) || /target closed/i.test(message) || /session closed/i.test(message) || /cdp command timed out/i.test(message) || /cdp response channel closed/i.test(message) || /operation timed out\. the page may still be loading/i.test(message) ); } async function listChromeProfiles() { try { const { stdout } = await spawnAndCapture('agent-browser', ['profiles']); const lines = stdout.split(/\r?\n/); const profiles = []; for (const line of lines) { const match = line.match(/^\s+(.+?)\s+\((.+?)\)\s*$/); if (!match) continue; profiles.push({ profile: match[1].trim(), label: match[2].trim(), }); } return profiles; } catch (_error) { return []; } } async function detectChromeProfile() { const profiles = await listChromeProfiles(); if (profiles.length > 0) { const defaultProfile = profiles.find((item) => item.profile === 'Default'); if (defaultProfile) return defaultProfile.profile; return profiles[0].profile; } return 'Default'; } async function detectChromeExecutablePath() { const candidates = [ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', `${process.env.HOME || ''}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`, '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', ].filter(Boolean); for (const candidate of candidates) { try { await fsPromises.access(candidate, fs.constants.X_OK); return candidate; } catch (_error) { // keep trying } } return null; } function expandHome(inputPath) { if (typeof inputPath !== 'string') return inputPath; if (!inputPath.startsWith('~')) return inputPath; return path.join(os.homedir(), inputPath.slice(1)); } function looksLikePath(value) { return value.includes('/') || value.includes('\\') || value.startsWith('~') || value.startsWith('.'); } async function resolveProfileArgument(profile) { if (!profile) return null; if (looksLikePath(profile)) { return expandHome(profile); } let profileName = profile; const profiles = await listChromeProfiles(); if (profiles.length > 0) { const byLabel = profiles.find((item) => item.label.toLowerCase() === profile.toLowerCase()); if (byLabel) { profileName = byLabel.profile; } } return profileName; } async function runAgentBrowser(session, commandArgs, options = {}) { const { retries = AGENT_BROWSER_RETRY_COUNT, ...commandOptions } = options; const env = { ...process.env, AGENT_BROWSER_DEFAULT_TIMEOUT: String(AGENT_BROWSER_ACTION_TIMEOUT_MS), }; const globalFlags = ['--session', session]; if (commandOptions.headed) { globalFlags.push('--headed'); } if (commandOptions.autoConnect) { globalFlags.push('--auto-connect'); } if (commandOptions.profile) { globalFlags.push('--profile', commandOptions.profile); } if (commandOptions.state) { globalFlags.push('--state', commandOptions.state); } if (Array.isArray(commandOptions.launchArgs) && commandOptions.launchArgs.length > 0) { globalFlags.push('--args', commandOptions.launchArgs.join(',')); } if (commandOptions.executablePath) { globalFlags.push('--executable-path', commandOptions.executablePath); } let lastError = null; for (let attempt = 0; attempt <= retries; attempt += 1) { try { const { stdout, stderr } = await spawnAndCapture( 'agent-browser', [...globalFlags, ...commandArgs, '--json'], { ...commandOptions, env, } ); const parsed = parseJsonOutput(stdout); if (!parsed || parsed.success !== true) { const fallback = String(parsed?.error || '').trim() || stderr.trim() || stdout.trim(); throw new Error('agent-browser command failed: ' + (fallback || 'unknown error')); } return parsed; } catch (error) { lastError = error; if (attempt >= retries || !isRetryableAgentBrowserError(error)) { throw error; } await sleep((attempt + 1) * 250); } } throw lastError || new Error('agent-browser command failed'); } function urlMatchesTarget(candidate, targetUrl) { if (typeof candidate !== 'string' || typeof targetUrl !== 'string') return false; if (candidate === targetUrl) return true; if (candidate.startsWith(targetUrl)) return true; try { const candidateUrl = new URL(candidate); const target = new URL(targetUrl); return ( candidateUrl.origin === target.origin && candidateUrl.pathname === target.pathname ); } catch (_error) { return false; } } async function ensureActiveTabOnTargetUrl(session, targetUrl, runOptions) { const currentUrlResult = await runAgentBrowser(session, ['get', 'url'], runOptions); const currentUrl = currentUrlResult?.data?.url; if (urlMatchesTarget(currentUrl, targetUrl)) { return; } const tabList = await runAgentBrowser(session, ['tab', 'list'], runOptions); const tabs = Array.isArray(tabList?.data?.tabs) ? tabList.data.tabs : []; const matchedTab = tabs.find((tab) => urlMatchesTarget(tab?.url, targetUrl)); if (matchedTab && Number.isInteger(matchedTab.index)) { await runAgentBrowser(session, ['tab', String(matchedTab.index)], runOptions); return; } const created = await runAgentBrowser(session, ['tab', 'new', targetUrl], runOptions); const createdIndex = created?.data?.index; if (Number.isInteger(createdIndex)) { await runAgentBrowser(session, ['tab', String(createdIndex)], runOptions); } } function buildRendererProgram(config) { return `(() => (async () => { const config = ${JSON.stringify(config)}; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const createSeededRng = (seedInput) => { const text = String(seedInput ?? ''); let hash = 2166136261; for (let i = 0; i < text.length; i += 1) { hash ^= text.charCodeAt(i); hash = Math.imul(hash, 16777619); } let state = hash >>> 0; if (state === 0) state = 0x9e3779b9; return () => { state = (state + 0x6D2B79F5) >>> 0; let payload = state; payload = Math.imul(payload ^ (payload >>> 15), payload | 1); payload ^= payload + Math.imul(payload ^ (payload >>> 7), payload | 61); return ((payload ^ (payload >>> 14)) >>> 0) / 4294967296; }; }; const nextRandom = createSeededRng(config.seed); const randomItem = (items) => items[Math.floor(nextRandom() * items.length)]; const shuffle = (items) => { const arr = Array.isArray(items) ? [...items] : []; for (let i = arr.length - 1; i > 0; i -= 1) { const j = Math.floor(nextRandom() * (i + 1)); const tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } return arr; }; const describeError = (error) => String(error?.message || error); const asPageName = (pageLike) => { if (typeof pageLike === 'string' && pageLike.length > 0) return pageLike; if (!pageLike || typeof pageLike !== 'object') return null; if (typeof pageLike.name === 'string' && pageLike.name.length > 0) return pageLike.name; if (typeof pageLike.originalName === 'string' && pageLike.originalName.length > 0) return pageLike.originalName; if (typeof pageLike.title === 'string' && pageLike.title.length > 0) return pageLike.title; return null; }; const waitForEditorReady = async () => { const deadline = Date.now() + config.readyTimeoutMs; let lastError = null; while (Date.now() < deadline) { try { if ( globalThis.logseq?.api && typeof logseq.api.get_current_block === 'function' && ( typeof logseq.api.get_current_page === 'function' || typeof logseq.api.get_today_page === 'function' ) && typeof logseq.api.append_block_in_page === 'function' ) { return; } } catch (error) { lastError = error; } await sleep(config.readyPollDelayMs); } if (lastError) { throw new Error('Logseq editor readiness timed out: ' + describeError(lastError)); } throw new Error('Logseq editor readiness timed out: logseq.api is unavailable'); }; const runPrefix = typeof config.runPrefix === 'string' && config.runPrefix.length > 0 ? config.runPrefix : config.markerPrefix; const checksumWarningToken = 'db-sync/checksum-mismatch'; const txRejectedWarningToken = 'db-sync/tx-rejected'; const missingEntityWarningToken = 'nothing found for entity id'; const applyRemoteTxWarningToken = 'frontend.worker.sync.handle-message/apply-remote-tx'; const numericEntityIdWarningToken = 'non-transact outliner ops contain numeric entity ids'; const checksumWarningTokenLower = checksumWarningToken.toLowerCase(); const txRejectedWarningTokenLower = txRejectedWarningToken.toLowerCase(); const missingEntityWarningTokenLower = missingEntityWarningToken.toLowerCase(); const applyRemoteTxWarningTokenLower = applyRemoteTxWarningToken.toLowerCase(); const numericEntityIdWarningTokenLower = numericEntityIdWarningToken.toLowerCase(); const fatalWarningStateKey = '__logseqOpFatalWarnings'; const fatalWarningPatchKey = '__logseqOpFatalWarningPatchInstalled'; const consoleCaptureStateKey = '__logseqOpConsoleCaptureStore'; const wsCaptureStateKey = '__logseqOpWsCaptureStore'; const wsCapturePatchKey = '__logseqOpWsCapturePatchInstalled'; const MAX_DIAGNOSTIC_EVENTS = 3000; const runStartedAtMs = Date.now(); const chooseRunnableOperation = (requestedOperation, operableCount) => { if ( requestedOperation === 'move' || requestedOperation === 'delete' || requestedOperation === 'indent' ) { return operableCount >= 2 ? requestedOperation : 'add'; } if ( requestedOperation === 'copyPaste' || requestedOperation === 'copyPasteTreeToEmptyTarget' ) { return operableCount >= 1 ? requestedOperation : 'add'; } return requestedOperation; }; const stringifyConsoleArg = (value) => { if (typeof value === 'string') return value; try { return JSON.stringify(value); } catch (_error) { return String(value); } }; const pushBounded = (target, value, max = MAX_DIAGNOSTIC_EVENTS) => { if (!Array.isArray(target)) return; target.push(value); if (target.length > max) { target.splice(0, target.length - max); } }; const consoleCaptureStore = window[consoleCaptureStateKey] && typeof window[consoleCaptureStateKey] === 'object' ? window[consoleCaptureStateKey] : {}; window[consoleCaptureStateKey] = consoleCaptureStore; const consoleCaptureEntry = Array.isArray(consoleCaptureStore[config.markerPrefix]) ? consoleCaptureStore[config.markerPrefix] : []; consoleCaptureStore[config.markerPrefix] = consoleCaptureEntry; const wsCaptureStore = window[wsCaptureStateKey] && typeof window[wsCaptureStateKey] === 'object' ? window[wsCaptureStateKey] : {}; window[wsCaptureStateKey] = wsCaptureStore; const wsCaptureEntry = wsCaptureStore[config.markerPrefix] && typeof wsCaptureStore[config.markerPrefix] === 'object' ? wsCaptureStore[config.markerPrefix] : { outbound: [], inbound: [], installed: false, installReason: null }; wsCaptureStore[config.markerPrefix] = wsCaptureEntry; const installFatalWarningTrap = () => { const warningList = Array.isArray(window[fatalWarningStateKey]) ? window[fatalWarningStateKey] : []; window[fatalWarningStateKey] = warningList; if (window[fatalWarningPatchKey]) return; window[fatalWarningPatchKey] = true; const trapMethod = (method) => { const original = console[method]; if (typeof original !== 'function') return; console[method] = (...args) => { try { const text = args.map(stringifyConsoleArg).join(' '); pushBounded(consoleCaptureEntry, { level: method, text, createdAt: Date.now(), }); const textLower = text.toLowerCase(); if ( textLower.includes(checksumWarningTokenLower) || textLower.includes(txRejectedWarningTokenLower) || textLower.includes(numericEntityIdWarningTokenLower) || ( textLower.includes(missingEntityWarningTokenLower) && textLower.includes(applyRemoteTxWarningTokenLower) ) ) { const kind = textLower.includes(checksumWarningTokenLower) ? 'checksum_mismatch' : ( textLower.includes(txRejectedWarningTokenLower) ? 'tx_rejected' : ( textLower.includes(numericEntityIdWarningTokenLower) ? 'numeric_entity_id_in_non_transact_op' : 'missing_entity_id' ) ); warningList.push({ kind, level: method, text, createdAt: Date.now(), }); } } catch (_error) { // noop } return original.apply(console, args); }; }; trapMethod('warn'); trapMethod('error'); trapMethod('log'); }; const toWsText = (value) => { if (typeof value === 'string') return value.slice(0, 4000); if (value instanceof ArrayBuffer) { return '[ArrayBuffer byteLength=' + value.byteLength + ']'; } if (typeof Blob !== 'undefined' && value instanceof Blob) { return '[Blob size=' + value.size + ']'; } try { return JSON.stringify(value).slice(0, 4000); } catch (_error) { return String(value).slice(0, 4000); } }; const installWsCapture = () => { try { if (!globalThis.WebSocket) { wsCaptureEntry.installed = false; wsCaptureEntry.installReason = 'WebSocket unavailable'; return; } if (window[wsCapturePatchKey] !== true) { const OriginalWebSocket = window.WebSocket; const originalSend = OriginalWebSocket.prototype.send; OriginalWebSocket.prototype.send = function patchedSend(payload) { try { pushBounded(wsCaptureEntry.outbound, { createdAt: Date.now(), url: typeof this?.url === 'string' ? this.url : null, readyState: Number.isInteger(this?.readyState) ? this.readyState : null, payload: toWsText(payload), }); } catch (_error) { // noop } return originalSend.call(this, payload); }; window.WebSocket = function LogseqWsCapture(...args) { const ws = new OriginalWebSocket(...args); try { ws.addEventListener('message', (event) => { try { pushBounded(wsCaptureEntry.inbound, { createdAt: Date.now(), url: typeof ws?.url === 'string' ? ws.url : null, readyState: Number.isInteger(ws?.readyState) ? ws.readyState : null, payload: toWsText(event?.data), }); } catch (_error) { // noop } }); } catch (_error) { // noop } return ws; }; window.WebSocket.prototype = OriginalWebSocket.prototype; Object.setPrototypeOf(window.WebSocket, OriginalWebSocket); for (const key of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) { window.WebSocket[key] = OriginalWebSocket[key]; } window[wsCapturePatchKey] = true; } wsCaptureEntry.installed = true; wsCaptureEntry.installReason = null; } catch (error) { wsCaptureEntry.installed = false; wsCaptureEntry.installReason = describeError(error); } }; const latestFatalWarning = () => { const warningList = Array.isArray(window[fatalWarningStateKey]) ? window[fatalWarningStateKey] : []; return warningList.length > 0 ? warningList[warningList.length - 1] : null; }; const parseCreatedAtMs = (value) => { if (value == null) return null; if (typeof value === 'number' && Number.isFinite(value)) return value; if (value instanceof Date) { const ms = value.getTime(); return Number.isFinite(ms) ? ms : null; } const ms = new Date(value).getTime(); return Number.isFinite(ms) ? ms : null; }; const getRtcLogList = () => { try { if (!globalThis.logseq?.api?.get_state_from_store) return []; const logs = logseq.api.get_state_from_store(['rtc/logs']); if (Array.isArray(logs) && logs.length > 0) return logs; const latest = logseq.api.get_state_from_store(['rtc/log']); return latest && typeof latest === 'object' ? [latest] : []; } catch (_error) { return []; } }; const latestChecksumMismatchRtcLog = () => { try { const logs = getRtcLogList(); for (let i = logs.length - 1; i >= 0; i -= 1) { const entry = logs[i]; if (!entry || typeof entry !== 'object') continue; const createdAtMs = parseCreatedAtMs(entry['created-at'] || entry.createdAt); if (Number.isFinite(createdAtMs) && createdAtMs < runStartedAtMs) continue; const type = String(entry.type || '').toLowerCase(); const localChecksum = String( entry['local-checksum'] || entry.localChecksum || entry.local_checksum || '' ); const remoteChecksum = String( entry['remote-checksum'] || entry.remoteChecksum || entry.remote_checksum || '' ); const hasMismatchType = type.includes('checksum-mismatch'); const hasDifferentChecksums = localChecksum.length > 0 && remoteChecksum.length > 0 && localChecksum !== remoteChecksum; if (!hasMismatchType && !hasDifferentChecksums) continue; return { type: entry.type || null, messageType: entry['message-type'] || entry.messageType || null, localTx: entry['local-tx'] || entry.localTx || null, remoteTx: entry['remote-tx'] || entry.remoteTx || null, localChecksum, remoteChecksum, createdAt: entry['created-at'] || entry.createdAt || null, raw: entry, }; } return null; } catch (_error) { return null; } }; const latestTxRejectedRtcLog = () => { try { const logs = getRtcLogList(); for (let i = logs.length - 1; i >= 0; i -= 1) { const entry = logs[i]; if (!entry || typeof entry !== 'object') continue; const createdAtMs = parseCreatedAtMs(entry['created-at'] || entry.createdAt); if (Number.isFinite(createdAtMs) && createdAtMs < runStartedAtMs) continue; const type = String(entry.type || '').toLowerCase(); if (!type.includes('tx-rejected')) continue; return { type: entry.type || null, messageType: entry['message-type'] || entry.messageType || null, reason: entry.reason || null, remoteTx: entry['t'] || entry.t || null, createdAt: entry['created-at'] || entry.createdAt || null, raw: entry, }; } return null; } catch (_error) { return null; } }; const failIfFatalSignalSeen = () => { const txRejectedRtcLog = latestTxRejectedRtcLog(); if (txRejectedRtcLog) { throw new Error('tx rejected rtc-log detected: ' + JSON.stringify(txRejectedRtcLog)); } const warning = latestFatalWarning(); if (!warning) return; const details = String(warning.text || '').slice(0, 500); if (warning.kind === 'tx_rejected') { throw new Error('tx-rejected warning detected: ' + details); } if (warning.kind === 'missing_entity_id') { throw new Error('missing-entity-id warning detected: ' + details); } if (warning.kind === 'numeric_entity_id_in_non_transact_op') { throw new Error('numeric-entity-id-in-non-transact-op warning detected: ' + details); } // checksum mismatch is recorded for diagnostics but is non-fatal in simulation. }; const clearFatalSignalState = () => { try { const warningList = Array.isArray(window[fatalWarningStateKey]) ? window[fatalWarningStateKey] : null; if (warningList) { warningList.length = 0; } } catch (_error) { // noop } try { if (Array.isArray(consoleCaptureEntry)) { consoleCaptureEntry.length = 0; } } catch (_error) { // noop } try { if (Array.isArray(wsCaptureEntry?.outbound)) { wsCaptureEntry.outbound.length = 0; } if (Array.isArray(wsCaptureEntry?.inbound)) { wsCaptureEntry.inbound.length = 0; } } catch (_error) { // noop } try { if (globalThis.logseq?.api?.set_state_from_store) { logseq.api.set_state_from_store(['rtc/log'], null); logseq.api.set_state_from_store(['rtc/logs'], []); } } catch (_error) { // noop } }; const withTimeout = async (promise, timeoutMs, label) => { if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { return promise; } let timer = null; try { return await Promise.race([ promise, new Promise((_, reject) => { timer = setTimeout(() => { reject(new Error(label + ' timed out after ' + timeoutMs + 'ms')); }, timeoutMs); }), ]); } finally { if (timer) clearTimeout(timer); } }; const flattenBlocks = (nodes, acc = []) => { if (!Array.isArray(nodes)) return acc; for (const node of nodes) { if (!node) continue; acc.push(node); if (Array.isArray(node.children) && node.children.length > 0) { flattenBlocks(node.children, acc); } } return acc; }; const isClientBlock = (block) => typeof block?.content === 'string' && block.content.startsWith(config.markerPrefix); const isOperableBlock = (block) => typeof block?.content === 'string' && block.content.startsWith(runPrefix); const isClientRootBlock = (block) => typeof block?.content === 'string' && block.content === (config.markerPrefix + ' client-root'); let operationPageName = null; const listPageBlocks = async () => { if ( typeof operationPageName === 'string' && operationPageName.length > 0 && typeof logseq.api.get_page_blocks_tree === 'function' ) { const tree = await logseq.api.get_page_blocks_tree(operationPageName); return flattenBlocks(tree, []); } const tree = await logseq.api.get_current_page_blocks_tree(); return flattenBlocks(tree, []); }; const listOperableBlocks = async () => { const flattened = await listPageBlocks(); return flattened.filter(isOperableBlock); }; const listManagedBlocks = async () => { const operableBlocks = await listOperableBlocks(); return operableBlocks.filter(isClientBlock); }; const ensureClientRootBlock = async (anchorBlock) => { const existing = (await listOperableBlocks()).find(isClientRootBlock); if (existing?.uuid) return existing; const inserted = await logseq.api.insert_block(anchorBlock.uuid, config.markerPrefix + ' client-root', { sibling: true, before: false, focus: false, }); if (!inserted?.uuid) { throw new Error('Failed to create client root block'); } return inserted; }; const pickIndentCandidate = async (blocks) => { for (const candidate of shuffle(blocks)) { const prev = await logseq.api.get_previous_sibling_block(candidate.uuid); if (prev?.uuid) return candidate; } return null; }; const pickOutdentCandidate = async (blocks) => { for (const candidate of shuffle(blocks)) { const full = await logseq.api.get_block(candidate.uuid, { includeChildren: false }); const parentId = full?.parent?.id; const pageId = full?.page?.id; if (parentId && pageId && parentId !== pageId) { return candidate; } } return null; }; const getPreviousSiblingUuid = async (uuid) => { const prev = await logseq.api.get_previous_sibling_block(uuid); return prev?.uuid || null; }; const ensureIndentCandidate = async (blocks, anchorBlock, opIndex) => { const existing = await pickIndentCandidate(blocks); if (existing?.uuid) return existing; const baseTarget = blocks.length > 0 ? randomItem(blocks) : anchorBlock; const base = await logseq.api.insert_block(baseTarget.uuid, config.markerPrefix + ' indent-base-' + opIndex, { sibling: true, before: false, focus: false, }); if (!base?.uuid) { throw new Error('Failed to create indent base block'); } const candidate = await logseq.api.insert_block(base.uuid, config.markerPrefix + ' indent-candidate-' + opIndex, { sibling: true, before: false, focus: false, }); if (!candidate?.uuid) { throw new Error('Failed to create indent candidate block'); } return candidate; }; const runIndent = async (candidate) => { const prevUuid = await getPreviousSiblingUuid(candidate.uuid); if (!prevUuid) { throw new Error('No previous sibling for indent candidate'); } await logseq.api.move_block(candidate.uuid, prevUuid, { before: false, children: true, }); }; const ensureOutdentCandidate = async (blocks, anchorBlock, opIndex) => { const existing = await pickOutdentCandidate(blocks); if (existing?.uuid) return existing; const candidate = await ensureIndentCandidate(blocks, anchorBlock, opIndex); await runIndent(candidate); return candidate; }; const runOutdent = async (candidate) => { const full = await logseq.api.get_block(candidate.uuid, { includeChildren: false }); const parentId = full?.parent?.id; const pageId = full?.page?.id; if (!parentId || !pageId || parentId === pageId) { throw new Error('Outdent candidate is not nested'); } const parent = await logseq.api.get_block(parentId, { includeChildren: false }); if (!parent?.uuid) { throw new Error('Cannot resolve parent block for outdent'); } await logseq.api.move_block(candidate.uuid, parent.uuid, { before: false, children: false, }); }; const pickRandomGroup = (blocks, minSize = 1, maxSize = 3) => { const pool = shuffle(blocks); const lower = Math.max(1, Math.min(minSize, pool.length)); const upper = Math.max(lower, Math.min(maxSize, pool.length)); const size = lower + Math.floor(nextRandom() * (upper - lower + 1)); return pool.slice(0, size); }; const toBatchTree = (block) => ({ content: typeof block?.content === 'string' ? block.content : '', children: Array.isArray(block?.children) ? block.children.map(toBatchTree) : [], }); const getAnchor = async () => { const deadline = Date.now() + config.readyTimeoutMs; let lastError = null; while (Date.now() < deadline) { try { if (typeof logseq.api.get_today_page === 'function') { const todayPage = await logseq.api.get_today_page(); const todayPageName = asPageName(todayPage); if (todayPageName) { operationPageName = todayPageName; const seeded = await logseq.api.append_block_in_page( todayPageName, config.markerPrefix + ' anchor', {} ); if (seeded?.uuid) return seeded; } } if (typeof logseq.api.get_current_page === 'function') { const currentPage = await logseq.api.get_current_page(); const currentPageName = asPageName(currentPage); if (currentPageName) { operationPageName = currentPageName; const seeded = await logseq.api.append_block_in_page( currentPageName, config.markerPrefix + ' anchor', {} ); if (seeded?.uuid) return seeded; } } const currentBlock = await logseq.api.get_current_block(); if (currentBlock && currentBlock.uuid) { return currentBlock; } { operationPageName = config.fallbackPageName; const seeded = await logseq.api.append_block_in_page( config.fallbackPageName, config.markerPrefix + ' anchor', {} ); if (seeded?.uuid) return seeded; } } catch (error) { lastError = error; } await sleep(config.readyPollDelayMs); } if (lastError) { throw new Error('Unable to resolve anchor block: ' + describeError(lastError)); } throw new Error('Unable to resolve anchor block: open a graph and page, then retry'); }; const parseRtcTxText = (text) => { if (typeof text !== 'string' || text.length === 0) return null; const localMatch = text.match(/:local-tx\\s+(-?\\d+)/); const remoteMatch = text.match(/:remote-tx\\s+(-?\\d+)/); if (!localMatch || !remoteMatch) return null; return { localTx: Number.parseInt(localMatch[1], 10), remoteTx: Number.parseInt(remoteMatch[1], 10), }; }; const readRtcTx = () => { const node = document.querySelector('[data-testid="rtc-tx"]'); if (!node) return null; return parseRtcTxText((node.textContent || '').trim()); }; const waitForRtcSettle = async () => { const deadline = Date.now() + config.syncSettleTimeoutMs; let stableHits = 0; let last = null; while (Date.now() < deadline) { const current = readRtcTx(); if (current && Number.isFinite(current.localTx) && Number.isFinite(current.remoteTx)) { last = current; if (current.localTx === current.remoteTx) { stableHits += 1; if (stableHits >= 3) return { ok: true, ...current }; } else { stableHits = 0; } } await sleep(250); } return { ok: false, ...(last || {}), reason: 'rtc-tx did not settle before timeout' }; }; const extractNotificationTexts = () => Array.from( document.querySelectorAll('.ui__notifications-content .text-sm.leading-5.font-medium.whitespace-pre-line') ) .map((el) => (el.textContent || '').trim()) .filter(Boolean); const parseChecksumNotification = (text) => { if (typeof text !== 'string' || !text.includes('Checksum recomputed.')) return null; const match = text.match( /Recomputed:\\s*([0-9a-fA-F]{16})\\s*,\\s*local:\\s*([^,]+)\\s*,\\s*remote:\\s*([^,\\.]+)/ ); if (!match) { return { raw: text, parsed: false, reason: 'notification did not match expected checksum format', }; } const normalize = (value) => { const trimmed = String(value || '').trim(); if (trimmed === '') return null; return trimmed; }; const recomputed = normalize(match[1]); const local = normalize(match[2]); const remote = normalize(match[3]); const localMatches = recomputed === local; const remoteMatches = recomputed === remote; const localRemoteMatch = local === remote; return { raw: text, parsed: true, recomputed, local, remote, localMatches, remoteMatches, localRemoteMatch, matched: localMatches && remoteMatches && localRemoteMatch, }; }; const runChecksumDiagnostics = async () => { const settle = await waitForRtcSettle(); if (!settle.ok) { return { ok: false, settle, reason: settle.reason || 'sync did not settle', }; } const before = new Set(extractNotificationTexts()); const commandCandidates = ['dev/recompute-checksum', ':dev/recompute-checksum']; let invoked = null; let invokeError = null; for (const command of commandCandidates) { try { await logseq.api.invoke_external_command(command); invoked = command; invokeError = null; break; } catch (error) { invokeError = error; } } if (!invoked) { return { ok: false, settle, reason: 'failed to invoke checksum command', error: describeError(invokeError), }; } const deadline = Date.now() + Math.max(10000, config.readyTimeoutMs); let seen = null; while (Date.now() < deadline) { const current = extractNotificationTexts(); for (const text of current) { if (before.has(text)) continue; const parsed = parseChecksumNotification(text); if (parsed) { return { ok: Boolean(parsed.matched), settle, invoked, ...parsed, }; } seen = text; } await sleep(250); } return { ok: false, settle, invoked, reason: 'checksum notification not found before timeout', seen, }; }; const replayCaptureEnabled = config.captureReplay !== false; const replayCaptureStoreKey = '__logseqOpReplayCaptureStore'; const replayAttrByNormalizedName = { uuid: ':block/uuid', title: ':block/title', name: ':block/name', parent: ':block/parent', page: ':block/page', order: ':block/order', }; const replayCaptureState = { installed: false, installReason: null, enabled: false, currentOpIndex: null, txLog: [], }; const replayCaptureStoreRoot = window[replayCaptureStoreKey] && typeof window[replayCaptureStoreKey] === 'object' ? window[replayCaptureStoreKey] : {}; window[replayCaptureStoreKey] = replayCaptureStoreRoot; const replayCaptureStoreEntry = replayCaptureStoreRoot[config.markerPrefix] && typeof replayCaptureStoreRoot[config.markerPrefix] === 'object' ? replayCaptureStoreRoot[config.markerPrefix] : {}; replayCaptureStoreEntry.markerPrefix = config.markerPrefix; replayCaptureStoreEntry.updatedAt = Date.now(); replayCaptureStoreEntry.initialDb = null; replayCaptureStoreEntry.opLog = []; replayCaptureStoreEntry.txCapture = { enabled: replayCaptureEnabled, installed: false, installReason: null, totalTx: 0, txLog: [], }; replayCaptureStoreRoot[config.markerPrefix] = replayCaptureStoreEntry; const readAny = (value, keys) => { if (!value || typeof value !== 'object') return undefined; for (const key of keys) { if (Object.prototype.hasOwnProperty.call(value, key)) { return value[key]; } } return undefined; }; const normalizeReplayAttr = (value) => { if (typeof value !== 'string') return null; const text = value.trim(); if (!text) return null; if (text.startsWith(':')) { return text; } if (text.includes('/')) { return ':' + text; } return replayAttrByNormalizedName[text] || null; }; const normalizeReplayDatom = (datom) => { if (!datom || typeof datom !== 'object') return null; const eRaw = readAny(datom, ['e', ':e']); const aRaw = readAny(datom, ['a', ':a']); const vRaw = readAny(datom, ['v', ':v']); const addedRaw = readAny(datom, ['added', ':added']); const e = Number(eRaw); if (!Number.isInteger(e)) return null; const attr = normalizeReplayAttr(typeof aRaw === 'string' ? aRaw : String(aRaw || '')); if (!attr) return null; let v = vRaw; if (attr === ':block/uuid' && typeof vRaw === 'string') { v = vRaw; } else if ((attr === ':block/parent' || attr === ':block/page') && Number.isFinite(Number(vRaw))) { v = Number(vRaw); } return { e, a: attr, v, added: addedRaw !== false, }; }; const installReplayTxCapture = () => { if (!replayCaptureEnabled) { replayCaptureState.installReason = 'disabled by config'; replayCaptureStoreEntry.txCapture.installReason = replayCaptureState.installReason; return; } const core = window.LSPluginCore; if (!core || typeof core.hookDb !== 'function') { replayCaptureState.installReason = 'LSPluginCore.hookDb unavailable'; replayCaptureStoreEntry.txCapture.installReason = replayCaptureState.installReason; return; } const sinkKey = '__logseqOpReplayCaptureSinks'; const patchInstalledKey = '__logseqOpReplayCapturePatchInstalled'; const sinks = Array.isArray(window[sinkKey]) ? window[sinkKey] : []; window[sinkKey] = sinks; const sink = (type, payload) => { try { if (replayCaptureState.enabled && String(type || '') === 'changed' && payload && typeof payload === 'object') { const rawDatoms = readAny(payload, ['txData', ':tx-data', 'tx-data', 'tx_data']); const datoms = Array.isArray(rawDatoms) ? rawDatoms.map(normalizeReplayDatom).filter(Boolean) : []; if (datoms.length > 0) { const entry = { capturedAt: Date.now(), opIndex: Number.isInteger(replayCaptureState.currentOpIndex) ? replayCaptureState.currentOpIndex : null, datoms, }; replayCaptureState.txLog.push(entry); replayCaptureStoreEntry.txCapture.txLog.push(entry); replayCaptureStoreEntry.txCapture.totalTx = replayCaptureStoreEntry.txCapture.txLog.length; replayCaptureStoreEntry.updatedAt = Date.now(); } } } catch (_error) { // keep capture best-effort } }; sinks.push(sink); if (window[patchInstalledKey] !== true) { const original = core.hookDb.bind(core); core.hookDb = (type, payload, pluginId) => { try { const listeners = Array.isArray(window[sinkKey]) ? window[sinkKey] : []; for (const listener of listeners) { if (typeof listener === 'function') { listener(type, payload); } } } catch (_error) { // keep hook best-effort } return original(type, payload, pluginId); }; window[patchInstalledKey] = true; } replayCaptureState.installed = true; replayCaptureState.enabled = true; replayCaptureState.installReason = null; replayCaptureStoreEntry.txCapture.installed = true; replayCaptureStoreEntry.txCapture.enabled = true; replayCaptureStoreEntry.txCapture.installReason = null; replayCaptureStoreEntry.updatedAt = Date.now(); }; const flattenAnyObjects = (value, acc = []) => { if (Array.isArray(value)) { for (const item of value) flattenAnyObjects(item, acc); return acc; } if (value && typeof value === 'object') { acc.push(value); } return acc; }; const normalizeSnapshotBlock = (block) => { if (!block || typeof block !== 'object') return null; const id = Number(readAny(block, ['id', 'db/id', ':db/id'])); const uuid = readAny(block, ['uuid', 'block/uuid', ':block/uuid']); if (!Number.isInteger(id) || typeof uuid !== 'string' || uuid.length === 0) return null; const parent = readAny(block, ['parent', 'block/parent', ':block/parent']); const page = readAny(block, ['page', 'block/page', ':block/page']); const parentId = Number(readAny(parent, ['id', 'db/id', ':db/id'])); const pageId = Number(readAny(page, ['id', 'db/id', ':db/id'])); const title = readAny(block, ['title', 'block/title', ':block/title']); const name = readAny(block, ['name', 'block/name', ':block/name']); const order = readAny(block, ['order', 'block/order', ':block/order']); return { id, uuid, parentId: Number.isInteger(parentId) ? parentId : null, pageId: Number.isInteger(pageId) ? pageId : null, title: typeof title === 'string' ? title : null, name: typeof name === 'string' ? name : null, order: typeof order === 'string' ? order : null, }; }; const captureInitialDbSnapshot = async () => { if (!replayCaptureEnabled) { return { ok: false, reason: 'disabled by config', blockCount: 0, blocks: [], }; } if (typeof logseq.api.datascript_query !== 'function') { return { ok: false, reason: 'datascript_query API unavailable', blockCount: 0, blocks: [], }; } try { const query = '[:find (pull ?b [:db/id :block/uuid :block/title :block/name :block/order {:block/parent [:db/id :block/uuid]} {:block/page [:db/id :block/uuid]}]) :where [?b :block/uuid]]'; const raw = await logseq.api.datascript_query(query); const objects = flattenAnyObjects(raw, []); const blocks = objects .map(normalizeSnapshotBlock) .filter(Boolean); const dedup = new Map(); for (const block of blocks) { dedup.set(block.id, block); } const normalized = Array.from(dedup.values()) .sort((a, b) => a.id - b.id); return { ok: true, blockCount: normalized.length, blocks: normalized, }; } catch (error) { return { ok: false, reason: describeError(error), blockCount: 0, blocks: [], }; } }; const snapshotBlocksToStateMap = (blocks) => { const stateMap = new Map(); if (!Array.isArray(blocks)) return stateMap; for (const block of blocks) { if (!block || typeof block !== 'object') continue; const id = Number(block.id); if (!Number.isInteger(id)) continue; stateMap.set(id, { id, uuid: typeof block.uuid === 'string' ? block.uuid : null, title: typeof block.title === 'string' ? block.title : null, name: typeof block.name === 'string' ? block.name : null, order: typeof block.order === 'string' ? block.order : null, parentId: Number.isInteger(block.parentId) ? block.parentId : null, pageId: Number.isInteger(block.pageId) ? block.pageId : null, }); } return stateMap; }; const captureChecksumStateMap = async () => { const snapshot = await captureInitialDbSnapshot(); return { ok: snapshot.ok === true, reason: snapshot.reason || null, state: snapshotBlocksToStateMap(snapshot.blocks), }; }; const replayDatomEntriesFromStateDiff = (beforeMap, afterMap) => { const datoms = []; const allIds = new Set(); for (const id of beforeMap.keys()) allIds.add(id); for (const id of afterMap.keys()) allIds.add(id); const scalarAttrs = [ ['uuid', ':block/uuid'], ['title', ':block/title'], ['name', ':block/name'], ['order', ':block/order'], ]; const refAttrs = [ ['parentId', ':block/parent'], ['pageId', ':block/page'], ]; for (const id of allIds) { const before = beforeMap.get(id) || null; const after = afterMap.get(id) || null; for (const [key, attr] of scalarAttrs) { const beforeValue = before ? before[key] : null; const afterValue = after ? after[key] : null; if (beforeValue === afterValue) continue; if (typeof beforeValue === 'string') { datoms.push({ e: id, a: attr, v: beforeValue, added: false }); } if (typeof afterValue === 'string') { datoms.push({ e: id, a: attr, v: afterValue, added: true }); } } for (const [key, attr] of refAttrs) { const beforeValue = before ? before[key] : null; const afterValue = after ? after[key] : null; if (beforeValue === afterValue) continue; if (Number.isInteger(beforeValue)) { datoms.push({ e: id, a: attr, v: beforeValue, added: false }); } if (Number.isInteger(afterValue)) { datoms.push({ e: id, a: attr, v: afterValue, added: true }); } } } return datoms; }; const counts = { add: 0, delete: 0, move: 0, indent: 0, outdent: 0, undo: 0, redo: 0, copyPaste: 0, copyPasteTreeToEmptyTarget: 0, fallbackAdd: 0, errors: 0, }; const errors = []; const operationLog = []; const phaseTimeoutMs = Math.max(5000, Number(config.readyTimeoutMs || 0) + 5000); const opReadTimeoutMs = Math.max(2000, Number(config.opTimeoutMs || 0) * 2); installFatalWarningTrap(); installWsCapture(); clearFatalSignalState(); await withTimeout(waitForEditorReady(), phaseTimeoutMs, 'waitForEditorReady'); failIfFatalSignalSeen(); const anchor = await withTimeout(getAnchor(), phaseTimeoutMs, 'getAnchor'); await withTimeout(ensureClientRootBlock(anchor), phaseTimeoutMs, 'ensureClientRootBlock'); installReplayTxCapture(); const initialManaged = await withTimeout(listManagedBlocks(), phaseTimeoutMs, 'listManagedBlocks'); if (!initialManaged.length) { await withTimeout( logseq.api.insert_block(anchor.uuid, config.markerPrefix + ' seed', { sibling: true, before: false, focus: false, }), phaseTimeoutMs, 'insert seed block' ); } const initialDb = await withTimeout( captureInitialDbSnapshot(), phaseTimeoutMs, 'captureInitialDbSnapshot' ); replayCaptureStoreEntry.initialDb = initialDb; replayCaptureStoreEntry.updatedAt = Date.now(); let replaySnapshotState = { ok: initialDb?.ok === true, reason: initialDb?.reason || null, state: snapshotBlocksToStateMap(initialDb?.blocks), }; const appendReplayFallbackTxFromSnapshot = async (opIndex) => { if (!replayCaptureEnabled) return; const nextSnapshot = await captureChecksumStateMap(); if (!nextSnapshot || nextSnapshot.ok !== true || !nextSnapshot.state) { replaySnapshotState = nextSnapshot; return; } if (!replaySnapshotState || replaySnapshotState.ok !== true || !replaySnapshotState.state) { replaySnapshotState = nextSnapshot; return; } const alreadyCaptured = replayCaptureState.txLog.some((entry) => entry?.opIndex === opIndex); const datoms = replayDatomEntriesFromStateDiff(replaySnapshotState.state, nextSnapshot.state); if (!alreadyCaptured && datoms.length > 0) { const entry = { capturedAt: Date.now(), opIndex, source: 'snapshot-diff', datoms, }; replayCaptureState.txLog.push(entry); replayCaptureStoreEntry.txCapture.txLog.push(entry); replayCaptureStoreEntry.txCapture.totalTx = replayCaptureStoreEntry.txCapture.txLog.length; replayCaptureStoreEntry.updatedAt = Date.now(); } replaySnapshotState = nextSnapshot; }; let executed = 0; for (let i = 0; i < config.plan.length; i += 1) { failIfFatalSignalSeen(); const requested = config.plan[i]; const operable = await withTimeout( listOperableBlocks(), opReadTimeoutMs, 'listOperableBlocks before operation' ); let operation = chooseRunnableOperation(requested, operable.length); if (operation !== requested) { counts.fallbackAdd += 1; } try { await sleep(Math.floor(nextRandom() * 10)); replayCaptureState.currentOpIndex = i; const runOperation = async () => { if (operation === 'add') { const target = operable.length > 0 ? randomItem(operable) : anchor; const content = nextRandom() < 0.2 ? '' : config.markerPrefix + ' add-' + i; const asChild = operable.length > 0 && nextRandom() < 0.35; const inserted = await logseq.api.insert_block(target.uuid, content, { sibling: !asChild, before: false, focus: false, }); return { kind: 'add', targetUuid: target.uuid || null, insertedUuid: inserted?.uuid || null, content, sibling: !asChild, before: false, }; } if (operation === 'copyPaste') { const pageBlocks = await listPageBlocks(); const copyPool = (operable.length > 0 ? operable : pageBlocks).filter((b) => b?.uuid); if (copyPool.length === 0) { throw new Error('No blocks available for copyPaste'); } const source = randomItem(copyPool); const target = randomItem(copyPool); await logseq.api.select_block(source.uuid); await logseq.api.invoke_external_command('logseq.editor/copy'); const latestSource = await logseq.api.get_block(source.uuid); const sourceContent = latestSource?.content || source.content || ''; const copiedContent = config.markerPrefix + ' copy-' + i + (sourceContent ? ' :: ' + sourceContent : ''); const inserted = await logseq.api.insert_block(target.uuid, copiedContent, { sibling: true, before: false, focus: false, }); return { kind: 'copyPaste', sourceUuid: source.uuid || null, targetUuid: target.uuid || null, insertedUuid: inserted?.uuid || null, copiedContent, }; } if (operation === 'copyPasteTreeToEmptyTarget') { const pageBlocks = await listPageBlocks(); const treePool = (operable.length >= 2 ? operable : pageBlocks).filter((b) => b?.uuid); if (treePool.length < 2) { throw new Error('Not enough blocks available for multi-block copy'); } const sources = pickRandomGroup(treePool, 2, 4); const sourceTrees = []; for (const source of sources) { const sourceTree = await logseq.api.get_block(source.uuid, { includeChildren: true }); if (sourceTree?.uuid) { sourceTrees.push(sourceTree); } } if (sourceTrees.length === 0) { throw new Error('Failed to load source tree blocks'); } const treeTarget = operable.length > 0 ? randomItem(operable) : anchor; const emptyTarget = await logseq.api.insert_block(treeTarget.uuid, '', { sibling: true, before: false, focus: false, }); if (!emptyTarget?.uuid) { throw new Error('Failed to create empty target block'); } await logseq.api.update_block(emptyTarget.uuid, ''); const payload = sourceTrees.map((tree, idx) => { const node = toBatchTree(tree); const origin = typeof node.content === 'string' && node.content.length > 0 ? ' :: ' + node.content : ''; node.content = config.markerPrefix + ' tree-copy-' + i + '-' + idx + origin; return node; }); let fallbackToSingleTree = false; try { await logseq.api.insert_batch_block(emptyTarget.uuid, payload, { sibling: false }); } catch (_error) { fallbackToSingleTree = true; for (const tree of sourceTrees) { await logseq.api.insert_batch_block(emptyTarget.uuid, toBatchTree(tree), { sibling: false }); } } return { kind: 'copyPasteTreeToEmptyTarget', treeTargetUuid: treeTarget.uuid || null, emptyTargetUuid: emptyTarget.uuid || null, sourceUuids: sourceTrees.map((tree) => tree?.uuid).filter(Boolean), payloadSize: payload.length, fallbackToSingleTree, }; } if (operation === 'move') { const source = randomItem(operable); const candidates = operable.filter((block) => block.uuid !== source.uuid); const target = randomItem(candidates); const before = nextRandom() < 0.5; await logseq.api.move_block(source.uuid, target.uuid, { before, children: false, }); return { kind: 'move', sourceUuid: source.uuid || null, targetUuid: target.uuid || null, before, children: false, }; } if (operation === 'indent') { const candidate = await ensureIndentCandidate(operable, anchor, i); const prevUuid = await getPreviousSiblingUuid(candidate.uuid); if (!prevUuid) { throw new Error('No previous sibling for indent candidate'); } await logseq.api.move_block(candidate.uuid, prevUuid, { before: false, children: true, }); return { kind: 'indent', candidateUuid: candidate.uuid || null, targetUuid: prevUuid, before: false, children: true, }; } if (operation === 'outdent') { const candidate = await ensureOutdentCandidate(operable, anchor, i); const full = await logseq.api.get_block(candidate.uuid, { includeChildren: false }); const parentId = full?.parent?.id; const pageId = full?.page?.id; if (!parentId || !pageId || parentId === pageId) { throw new Error('Outdent candidate is not nested'); } const parent = await logseq.api.get_block(parentId, { includeChildren: false }); if (!parent?.uuid) { throw new Error('Cannot resolve parent block for outdent'); } await logseq.api.move_block(candidate.uuid, parent.uuid, { before: false, children: false, }); return { kind: 'outdent', candidateUuid: candidate.uuid || null, targetUuid: parent.uuid || null, before: false, children: false, }; } if (operation === 'delete') { const candidates = operable.filter((block) => block.uuid !== anchor.uuid && !isClientRootBlock(block)); const victimPool = candidates.length > 0 ? candidates : operable; const victim = randomItem(victimPool); if (isClientRootBlock(victim)) { throw new Error('Skip deleting protected client root block'); } await logseq.api.remove_block(victim.uuid); return { kind: 'delete', victimUuid: victim.uuid || null, }; } if (operation === 'undo') { await logseq.api.invoke_external_command('logseq.editor/undo'); await sleep(config.undoRedoDelayMs); return { kind: 'undo' }; } if (operation === 'redo') { await logseq.api.invoke_external_command('logseq.editor/redo'); await sleep(config.undoRedoDelayMs); return { kind: 'redo' }; } return { kind: operation }; }; const opDetail = await withTimeout(runOperation(), config.opTimeoutMs, operation + ' operation'); failIfFatalSignalSeen(); try { await withTimeout( appendReplayFallbackTxFromSnapshot(i), opReadTimeoutMs, 'appendReplayFallbackTxFromSnapshot' ); } catch (_error) { // best-effort fallback capture } counts[operation] += 1; executed += 1; const opEntry = { index: i, requested, executedAs: operation, detail: opDetail || null }; operationLog.push(opEntry); replayCaptureStoreEntry.opLog.push(opEntry); replayCaptureStoreEntry.updatedAt = Date.now(); } catch (error) { counts.errors += 1; errors.push({ index: i, requested, attempted: operation, message: String(error?.message || error), }); try { const recoveryOperable = await withTimeout( listOperableBlocks(), opReadTimeoutMs, 'listOperableBlocks for recovery' ); const target = recoveryOperable.length > 0 ? randomItem(recoveryOperable) : anchor; await withTimeout( logseq.api.insert_block(target.uuid, config.markerPrefix + ' recovery-' + i, { sibling: true, before: false, focus: false, }), opReadTimeoutMs, 'insert recovery block' ); counts.add += 1; executed += 1; try { await withTimeout( appendReplayFallbackTxFromSnapshot(i), opReadTimeoutMs, 'appendReplayFallbackTxFromSnapshot-recovery' ); } catch (_error) { // best-effort fallback capture } const opEntry = { index: i, requested, executedAs: 'add', detail: { kind: 'recovery-add', targetUuid: target.uuid || null, }, }; operationLog.push(opEntry); replayCaptureStoreEntry.opLog.push(opEntry); replayCaptureStoreEntry.updatedAt = Date.now(); } catch (recoveryError) { errors.push({ index: i, requested, attempted: 'recovery-add', message: String(recoveryError?.message || recoveryError), }); break; } } finally { replayCaptureState.currentOpIndex = null; } } let checksum = null; const warnings = []; failIfFatalSignalSeen(); if (config.verifyChecksum) { try { checksum = await withTimeout( runChecksumDiagnostics(), Math.max( 45000, Number(config.syncSettleTimeoutMs || 0) + Number(config.readyTimeoutMs || 0) + 10000 ), 'runChecksumDiagnostics' ); } catch (error) { checksum = { ok: false, reason: String(error?.message || error), timedOut: true, }; } if (!checksum.ok) { warnings.push({ index: config.plan.length, requested: 'verifyChecksum', attempted: 'verifyChecksum', message: checksum.reason || 'checksum mismatch', checksum, }); } } const finalManaged = await withTimeout(listManagedBlocks(), phaseTimeoutMs, 'final listManagedBlocks'); replayCaptureState.enabled = false; const replayTxCapture = { enabled: replayCaptureEnabled, installed: replayCaptureState.installed === true, installReason: replayCaptureState.installReason, totalTx: replayCaptureState.txLog.length, txLog: replayCaptureState.txLog, }; replayCaptureStoreEntry.txCapture = replayTxCapture; replayCaptureStoreEntry.updatedAt = Date.now(); return { ok: errors.length === 0, requestedOps: config.plan.length, executedOps: executed, counts, markerPrefix: config.markerPrefix, anchorUuid: anchor.uuid, finalManagedCount: finalManaged.length, sampleManaged: finalManaged.slice(0, 5).map((block) => ({ uuid: block.uuid, content: block.content, })), errorCount: errors.length, errors: errors.slice(0, 20), warnings: warnings.slice(0, 20), rtcLogs: getRtcLogList(), consoleLogs: Array.isArray(consoleCaptureEntry) ? [...consoleCaptureEntry] : [], wsMessages: { installed: wsCaptureEntry?.installed === true, installReason: wsCaptureEntry?.installReason || null, outbound: Array.isArray(wsCaptureEntry?.outbound) ? [...wsCaptureEntry.outbound] : [], inbound: Array.isArray(wsCaptureEntry?.inbound) ? [...wsCaptureEntry.inbound] : [], }, requestedPlan: Array.isArray(config.plan) ? [...config.plan] : [], opLog: operationLog, opLogSample: operationLog.slice(0, 20), initialDb, txCapture: replayTxCapture, checksum, }; })())()`; } function buildCleanupTodayPageProgram(config = {}) { const cleanupConfig = { cleanupTodayPage: true, ...(config || {}), }; return `(() => (async () => { const config = ${JSON.stringify(cleanupConfig)}; const asPageName = (pageLike) => { if (typeof pageLike === 'string' && pageLike.length > 0) return pageLike; if (!pageLike || typeof pageLike !== 'object') return null; if (typeof pageLike.name === 'string' && pageLike.name.length > 0) return pageLike.name; if (typeof pageLike.originalName === 'string' && pageLike.originalName.length > 0) return pageLike.originalName; if (typeof pageLike.title === 'string' && pageLike.title.length > 0) return pageLike.title; return null; }; const purgePageBlocks = async (pageName) => { if (!pageName) { return { ok: false, pageName, reason: 'empty page name' }; } if (!globalThis.logseq?.api?.get_page_blocks_tree || !globalThis.logseq?.api?.remove_block) { return { ok: false, pageName, reason: 'page block APIs unavailable' }; } let tree = []; try { tree = await logseq.api.get_page_blocks_tree(pageName); } catch (error) { return { ok: false, pageName, reason: 'failed to read page tree: ' + String(error?.message || error) }; } const topLevel = Array.isArray(tree) ? tree.map((block) => block?.uuid).filter(Boolean) : []; for (const uuid of topLevel) { try { await logseq.api.remove_block(uuid); } catch (_error) { // best-effort cleanup; continue deleting remaining blocks } } return { ok: true, pageName, removedBlocks: topLevel.length, }; }; try { const pages = []; if (!globalThis.logseq?.api?.get_today_page) { return { ok: false, reason: 'today page API unavailable' }; } const today = await logseq.api.get_today_page(); const todayName = asPageName(today); if (todayName) { pages.push(todayName); } const uniquePages = Array.from(new Set(pages.filter(Boolean))); const pageResults = []; for (const pageName of uniquePages) { const pageResult = await purgePageBlocks(pageName); let deleted = false; let deleteError = null; if (globalThis.logseq?.api?.delete_page) { try { await logseq.api.delete_page(pageName); deleted = true; } catch (error) { deleteError = String(error?.message || error); } } pageResults.push({ ...pageResult, deleted, deleteError, }); } return { ok: pageResults.every((item) => item.ok), pages: pageResults, }; } catch (error) { return { ok: false, reason: String(error?.message || error) }; } })())()`; } function buildGraphBootstrapProgram(config) { return `(() => (async () => { const config = ${JSON.stringify(config)}; const lower = (value) => String(value || '').toLowerCase(); const targetGraphLower = lower(config.graphName); const stateKey = '__logseqOpBootstrapState'; const state = (window[stateKey] && typeof window[stateKey] === 'object') ? window[stateKey] : {}; window[stateKey] = state; if (state.targetGraph !== config.graphName || state.runId !== config.runId) { state.initialGraphName = null; state.initialRepoName = null; state.initialTargetMatched = null; state.passwordAttempts = 0; state.refreshCount = 0; state.graphDetected = false; state.graphCardClicked = false; state.passwordSubmitted = false; state.actionTriggered = false; state.gotoGraphsOk = false; state.gotoGraphsError = null; state.downloadStarted = false; state.downloadCompleted = false; state.downloadCompletionSource = null; state.lastDownloadLog = null; state.lastRefreshAt = 0; state.lastGraphClickAt = 0; state.targetStateStableHits = 0; state.switchAttempts = 0; } state.runId = config.runId; state.targetGraph = config.graphName; if (typeof state.passwordAttempts !== 'number') state.passwordAttempts = 0; if (typeof state.refreshCount !== 'number') state.refreshCount = 0; if (typeof state.graphDetected !== 'boolean') state.graphDetected = false; if (typeof state.graphCardClicked !== 'boolean') state.graphCardClicked = false; if (typeof state.passwordSubmitted !== 'boolean') state.passwordSubmitted = false; if (typeof state.actionTriggered !== 'boolean') state.actionTriggered = false; if (typeof state.gotoGraphsOk !== 'boolean') state.gotoGraphsOk = false; if (typeof state.gotoGraphsError !== 'string' && state.gotoGraphsError !== null) state.gotoGraphsError = null; if (typeof state.downloadStarted !== 'boolean') state.downloadStarted = false; if (typeof state.downloadCompleted !== 'boolean') state.downloadCompleted = false; if (typeof state.downloadCompletionSource !== 'string' && state.downloadCompletionSource !== null) { state.downloadCompletionSource = null; } if (typeof state.lastDownloadLog !== 'object' && state.lastDownloadLog !== null) { state.lastDownloadLog = null; } if (typeof state.initialRepoName !== 'string' && state.initialRepoName !== null) { state.initialRepoName = null; } if (typeof state.initialTargetMatched !== 'boolean' && state.initialTargetMatched !== null) { state.initialTargetMatched = null; } if (typeof state.lastRefreshAt !== 'number') { state.lastRefreshAt = 0; } if (typeof state.lastGraphClickAt !== 'number') { state.lastGraphClickAt = 0; } if (typeof state.targetStateStableHits !== 'number') { state.targetStateStableHits = 0; } if (typeof state.switchAttempts !== 'number') { state.switchAttempts = 0; } const setInputValue = (input, value) => { if (!input) return; const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; if (setter) { setter.call(input, value); } else { input.value = value; } input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); }; const dispatchClick = (node) => { if (!(node instanceof HTMLElement)) return false; try { node.scrollIntoView({ block: 'center', inline: 'center' }); } catch (_error) { // ignore scroll failures } try { node.focus(); } catch (_error) { // ignore focus failures } try { node.click(); } catch (_error) { // continue with explicit events } node.dispatchEvent(new MouseEvent('mousedown', { view: window, bubbles: true, cancelable: true })); node.dispatchEvent(new MouseEvent('mouseup', { view: window, bubbles: true, cancelable: true })); node.dispatchEvent(new MouseEvent('click', { view: window, bubbles: true, cancelable: true })); return true; }; const graphNameMatchesTarget = (graphName) => { const value = lower(graphName); if (!value) return false; return ( value === targetGraphLower || value.endsWith('/' + targetGraphLower) || value.endsWith('_' + targetGraphLower) || value.includes('logseq_db_' + targetGraphLower) ); }; const stateMatchesTarget = (repoName, graphName) => { const hasRepo = typeof repoName === 'string' && repoName.length > 0; const hasGraph = typeof graphName === 'string' && graphName.length > 0; const repoMatches = hasRepo ? graphNameMatchesTarget(repoName) : false; const graphMatches = hasGraph ? graphNameMatchesTarget(graphName) : false; if (hasRepo && hasGraph) { return repoMatches && graphMatches; } if (hasRepo) return repoMatches; if (hasGraph) return graphMatches; return false; }; const listGraphCards = () => Array.from(document.querySelectorAll('div[data-testid^="logseq_db_"]')); const findGraphCard = () => { const exact = document.querySelector('div[data-testid="logseq_db_' + config.graphName + '"]'); if (exact) return exact; const byTestId = listGraphCards() .find((card) => lower(card.getAttribute('data-testid')).includes(targetGraphLower)); if (byTestId) return byTestId; return listGraphCards() .find((card) => lower(card.textContent).includes(targetGraphLower)); }; const clickRefresh = () => { const candidates = Array.from(document.querySelectorAll('button,span,a')); const refreshNode = candidates.find((el) => (el.textContent || '').trim() === 'Refresh'); const clickable = refreshNode ? (refreshNode.closest('button') || refreshNode) : null; return dispatchClick(clickable); }; const clickGraphCard = (card) => { if (!card) return false; const anchors = Array.from(card.querySelectorAll('a')); const exactAnchor = anchors.find((el) => lower(el.textContent).trim() === targetGraphLower); const looseAnchor = anchors.find((el) => lower(el.textContent).includes(targetGraphLower)); const anyAnchor = anchors[0]; const actionButton = Array.from(card.querySelectorAll('button')) .find((el) => lower(el.textContent).includes(targetGraphLower)); const target = exactAnchor || looseAnchor || anyAnchor || actionButton || card; return dispatchClick(target); }; const getCurrentGraphName = async () => { try { if (!globalThis.logseq?.api?.get_current_graph) return null; const current = await logseq.api.get_current_graph(); if (!current || typeof current !== 'object') return null; if (typeof current.name === 'string' && current.name.length > 0) return current.name; if (typeof current.url === 'string' && current.url.length > 0) { const parts = current.url.split('/').filter(Boolean); return parts[parts.length - 1] || null; } } catch (_error) { // ignore } return null; }; const getCurrentRepoName = () => { try { if (!globalThis.logseq?.api?.get_state_from_store) return null; const value = logseq.api.get_state_from_store(['git/current-repo']); return typeof value === 'string' && value.length > 0 ? value : null; } catch (_error) { return null; } }; const getDownloadingGraphUuid = () => { try { if (!globalThis.logseq?.api?.get_state_from_store) return null; return logseq.api.get_state_from_store(['rtc/downloading-graph-uuid']); } catch (_error) { return null; } }; const getRtcLog = () => { try { if (!globalThis.logseq?.api?.get_state_from_store) return null; return logseq.api.get_state_from_store(['rtc/log']); } catch (_error) { return null; } }; const asLower = (value) => String(value || '').toLowerCase(); const parseRtcDownloadLog = (value) => { if (!value || typeof value !== 'object') return null; const type = value.type || value['type'] || null; const typeLower = asLower(type); if (!typeLower.includes('rtc.log/download')) return null; const subType = value['sub-type'] || value.subType || value.subtype || value.sub_type || null; const graphUuid = value['graph-uuid'] || value.graphUuid || value.graph_uuid || null; const message = value.message || null; return { type: String(type || ''), subType: String(subType || ''), graphUuid: graphUuid ? String(graphUuid) : null, message: message ? String(message) : null, }; }; const probeGraphReady = async () => { try { if (!globalThis.logseq?.api?.get_current_page_blocks_tree) { return { ok: false, reason: 'get_current_page_blocks_tree unavailable' }; } await logseq.api.get_current_page_blocks_tree(); return { ok: true, reason: null }; } catch (error) { return { ok: false, reason: String(error?.message || error) }; } }; const initialGraphName = await getCurrentGraphName(); const initialRepoName = getCurrentRepoName(); const initialTargetMatched = stateMatchesTarget(initialRepoName, initialGraphName); if (!state.initialGraphName && initialGraphName) { state.initialGraphName = initialGraphName; } if (!state.initialRepoName && initialRepoName) { state.initialRepoName = initialRepoName; } if (state.initialTargetMatched === null) { state.initialTargetMatched = initialTargetMatched; } const shouldForceSelection = (config.forceSelection === true && !state.graphCardClicked && !state.downloadStarted) || !initialTargetMatched; let onGraphsPage = location.hash.includes('/graphs'); if ((shouldForceSelection || !initialTargetMatched) && !onGraphsPage) { try { location.hash = '#/graphs'; state.gotoGraphsOk = true; } catch (error) { state.gotoGraphsError = String(error?.message || error); } onGraphsPage = location.hash.includes('/graphs'); } const modal = document.querySelector('.e2ee-password-modal-content'); const passwordModalVisible = !!modal; let passwordAttempted = false; let passwordSubmittedThisStep = false; if (modal) { const passwordInputs = Array.from( modal.querySelectorAll('input[type="password"], .ls-toggle-password-input input, input') ); if (passwordInputs.length >= 2) { setInputValue(passwordInputs[0], config.password); setInputValue(passwordInputs[1], config.password); passwordAttempted = true; } else if (passwordInputs.length === 1) { setInputValue(passwordInputs[0], config.password); passwordAttempted = true; } if (passwordAttempted) { state.passwordAttempts += 1; } const submitButton = Array.from(modal.querySelectorAll('button')) .find((button) => /(submit|open|unlock|confirm|enter)/i.test((button.textContent || '').trim())); if (submitButton && !submitButton.disabled) { passwordSubmittedThisStep = dispatchClick(submitButton); state.passwordSubmitted = state.passwordSubmitted || passwordSubmittedThisStep; state.actionTriggered = state.actionTriggered || passwordSubmittedThisStep; } } let graphCardClickedThisStep = false; let refreshClickedThisStep = false; if (location.hash.includes('/graphs')) { const card = findGraphCard(); if (card) { const now = Date.now(); state.graphDetected = true; if (!state.graphCardClicked && now - state.lastGraphClickAt >= 500) { graphCardClickedThisStep = clickGraphCard(card); if (graphCardClickedThisStep) { state.lastGraphClickAt = now; state.switchAttempts += 1; } state.graphCardClicked = state.graphCardClicked || graphCardClickedThisStep; state.actionTriggered = state.actionTriggered || graphCardClickedThisStep; } } else { const now = Date.now(); if (now - state.lastRefreshAt >= 2000) { refreshClickedThisStep = clickRefresh(); if (refreshClickedThisStep) { state.refreshCount += 1; state.lastRefreshAt = now; } } } } const downloadingGraphUuid = getDownloadingGraphUuid(); if (downloadingGraphUuid) { state.actionTriggered = true; state.downloadStarted = true; } const rtcDownloadLog = parseRtcDownloadLog(getRtcLog()); if (rtcDownloadLog) { state.lastDownloadLog = rtcDownloadLog; const subTypeLower = asLower(rtcDownloadLog.subType); const messageLower = asLower(rtcDownloadLog.message); if (subTypeLower.includes('download-progress') || subTypeLower.includes('downloadprogress')) { state.downloadStarted = true; } if ( (subTypeLower.includes('download-completed') || subTypeLower.includes('downloadcompleted')) && messageLower.includes('ready') ) { state.downloadStarted = true; state.downloadCompleted = true; state.downloadCompletionSource = 'rtc-log'; } } const currentGraphName = await getCurrentGraphName(); const currentRepoName = getCurrentRepoName(); const onGraphsPageFinal = location.hash.includes('/graphs'); const repoMatchesTarget = graphNameMatchesTarget(currentRepoName); const graphMatchesTarget = graphNameMatchesTarget(currentGraphName); const switchedToTargetGraph = stateMatchesTarget(currentRepoName, currentGraphName) && !onGraphsPageFinal; if (switchedToTargetGraph) { state.targetStateStableHits += 1; } else { state.targetStateStableHits = 0; } if ( !switchedToTargetGraph && !onGraphsPageFinal && !passwordModalVisible && !state.downloadStarted && !state.graphCardClicked ) { try { location.hash = '#/graphs'; state.gotoGraphsOk = true; } catch (error) { state.gotoGraphsError = String(error?.message || error); } } const needsReadinessProbe = switchedToTargetGraph && !passwordModalVisible && !downloadingGraphUuid; const readyProbe = needsReadinessProbe ? await probeGraphReady() : { ok: false, reason: 'skipped' }; if (state.downloadStarted && !state.downloadCompleted && readyProbe.ok) { state.downloadCompleted = true; state.downloadCompletionSource = 'db-ready-probe'; } const downloadLifecycleSatisfied = !state.downloadStarted || state.downloadCompleted; const requiresAction = config.requireAction !== false; const ok = switchedToTargetGraph && !passwordModalVisible && !downloadingGraphUuid && readyProbe.ok && downloadLifecycleSatisfied && (!requiresAction || state.actionTriggered) && state.targetStateStableHits >= 2; const availableCards = listGraphCards().slice(0, 10).map((card) => ({ dataTestId: card.getAttribute('data-testid'), text: (card.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 120), })); return { ok, targetGraph: config.graphName, initialGraphName: state.initialGraphName || null, initialRepoName: state.initialRepoName || null, initialTargetMatched: state.initialTargetMatched, currentGraphName, currentRepoName, gotoGraphsOk: state.gotoGraphsOk, gotoGraphsError: state.gotoGraphsError, onGraphsPage: onGraphsPageFinal, downloadingGraphUuid, switchedToTargetGraph, repoMatchesTarget, graphMatchesTarget, readyProbe, actionTriggered: state.actionTriggered, graphDetected: state.graphDetected, graphCardClicked: state.graphCardClicked, graphCardClickedThisStep, switchAttempts: state.switchAttempts, refreshCount: state.refreshCount, refreshClickedThisStep, passwordAttempts: state.passwordAttempts, passwordAttempted, passwordModalVisible, passwordSubmitted: state.passwordSubmitted, passwordSubmittedThisStep, downloadStarted: state.downloadStarted, downloadCompleted: state.downloadCompleted, downloadCompletionSource: state.downloadCompletionSource, targetStateStableHits: state.targetStateStableHits, lastDownloadLog: state.lastDownloadLog, availableCards, }; })())()`; } async function runGraphBootstrap(sessionName, args, runOptions) { const deadline = Date.now() + args.switchTimeoutMs; const bootstrapRunId = `${Date.now()}-${Math.random().toString(16).slice(2)}`; let lastBootstrap = null; while (Date.now() < deadline) { const bootstrapProgram = buildGraphBootstrapProgram({ runId: bootstrapRunId, graphName: args.graph, password: args.e2ePassword, forceSelection: true, requireAction: true, }); const bootstrapEvaluation = await runAgentBrowser( sessionName, ['eval', '--stdin'], { input: bootstrapProgram, timeoutMs: BOOTSTRAP_EVAL_TIMEOUT_MS, ...runOptions, } ); const bootstrap = bootstrapEvaluation?.data?.result; if (!bootstrap || typeof bootstrap !== 'object') { throw new Error('Graph bootstrap returned empty state for session ' + sessionName); } lastBootstrap = bootstrap; if (bootstrap.ok) { return bootstrap; } await sleep(250); } throw new Error( 'Failed to switch/download graph "' + args.graph + '" within timeout. ' + 'Last bootstrap state: ' + JSON.stringify(lastBootstrap) ); } function buildGraphProbeProgram(graphName) { return `(() => (async () => { const target = ${JSON.stringify(String(graphName || ''))}.toLowerCase(); const lower = (v) => String(v || '').toLowerCase(); const matches = (value) => { const v = lower(value); if (!v) return false; return v === target || v.endsWith('/' + target) || v.endsWith('_' + target) || v.includes('logseq_db_' + target); }; let currentGraphName = null; let currentRepoName = null; try { if (globalThis.logseq?.api?.get_current_graph) { const current = await logseq.api.get_current_graph(); currentGraphName = current?.name || current?.url || null; } } catch (_error) { // ignore } try { if (globalThis.logseq?.api?.get_state_from_store) { currentRepoName = logseq.api.get_state_from_store(['git/current-repo']) || null; } } catch (_error) { // ignore } const repoMatchesTarget = matches(currentRepoName); const graphMatchesTarget = matches(currentGraphName); const onGraphsPage = location.hash.includes('/graphs'); const stableTarget = (repoMatchesTarget || graphMatchesTarget) && !onGraphsPage; return { targetGraph: ${JSON.stringify(String(graphName || ''))}, currentGraphName, currentRepoName, repoMatchesTarget, graphMatchesTarget, onGraphsPage, stableTarget, }; })())()`; } async function ensureTargetGraphBeforeOps(sessionName, args, runOptions) { let lastProbe = null; let lastBootstrap = null; for (let attempt = 0; attempt < 4; attempt += 1) { const probeEval = await runAgentBrowser( sessionName, ['eval', '--stdin'], { input: buildGraphProbeProgram(args.graph), ...runOptions, } ); const probe = probeEval?.data?.result; lastProbe = probe; if (probe?.stableTarget) { return { ok: true, probe, bootstrap: lastBootstrap }; } lastBootstrap = await runGraphBootstrap(sessionName, args, runOptions); } throw new Error( 'Target graph verification failed before ops. ' + 'Last probe: ' + JSON.stringify(lastProbe) + '. ' + 'Last bootstrap: ' + JSON.stringify(lastBootstrap) ); } function buildSessionNames(baseSession, instances) { if (instances <= 1) return [baseSession]; const sessions = []; for (let i = 0; i < instances; i += 1) { sessions.push(`${baseSession}-${i + 1}`); } return sessions; } function buildSimulationOperationPlan(totalOps, profile) { if (profile === 'full') { return buildOperationPlan(totalOps); } const fastOperationOrder = [ 'add', 'add', 'move', 'delete', 'indent', 'outdent', 'add', 'move', ]; const plan = []; for (let i = 0; i < totalOps; i += 1) { plan.push(fastOperationOrder[i % fastOperationOrder.length]); } return plan; } function shuffleOperationPlan(plan, rng = Math.random) { const shuffled = Array.isArray(plan) ? [...plan] : []; for (let i = shuffled.length - 1; i > 0; i -= 1) { const j = Math.floor(rng() * (i + 1)); const tmp = shuffled[i]; shuffled[i] = shuffled[j]; shuffled[j] = tmp; } return shuffled; } function computeRendererEvalTimeoutMs(syncSettleTimeoutMs, opCount) { return Math.max( 1200000, RENDERER_EVAL_BASE_TIMEOUT_MS + (syncSettleTimeoutMs * 2) + (opCount * 500) + 30000 ); } function buildReplayCaptureProbeProgram(markerPrefix) { return `(() => { const key = '__logseqOpReplayCaptureStore'; const consoleKey = '__logseqOpConsoleCaptureStore'; const wsKey = '__logseqOpWsCaptureStore'; const marker = ${JSON.stringify(String(markerPrefix || ''))}; const store = window[key]; const consoleStore = window[consoleKey]; const wsStore = window[wsKey]; const entry = store && typeof store === 'object' ? store[marker] : null; const consoleEntry = consoleStore && typeof consoleStore === 'object' ? consoleStore[marker] : null; const wsEntry = wsStore && typeof wsStore === 'object' ? wsStore[marker] : null; if (!entry && !consoleEntry && !wsEntry) return null; return { replayCapture: entry && typeof entry === 'object' ? entry : null, consoleLogs: Array.isArray(consoleEntry) ? consoleEntry : [], wsMessages: wsEntry && typeof wsEntry === 'object' ? wsEntry : null, }; })()`; } async function collectFailureReplayCapture(sessionName, markerPrefix, runOptions) { try { const evaluation = await runAgentBrowser( sessionName, ['eval', '--stdin'], { input: buildReplayCaptureProbeProgram(markerPrefix), timeoutMs: 20000, ...runOptions, } ); const value = evaluation?.data?.result; return value && typeof value === 'object' ? value : null; } catch (_error) { return null; } } function summarizeRounds(rounds) { return rounds.reduce( (acc, round) => { const roundCounts = round?.counts && typeof round.counts === 'object' ? round.counts : {}; for (const [k, v] of Object.entries(roundCounts)) { acc.counts[k] = (acc.counts[k] || 0) + (Number(v) || 0); } acc.requestedOps += Number(round.requestedOps || 0); acc.executedOps += Number(round.executedOps || 0); acc.errorCount += Number(round.errorCount || 0); if (round.ok !== true) { acc.failedRounds.push(round.round); } return acc; }, { counts: {}, requestedOps: 0, executedOps: 0, errorCount: 0, failedRounds: [] } ); } async function runSimulationForSession(sessionName, index, args, sharedConfig) { if (args.resetSession) { try { await runAgentBrowser(sessionName, ['close'], { autoConnect: false, headed: false, }); } catch (_error) { // session may not exist yet } } const runOptions = { headed: args.headed, autoConnect: args.autoConnect, profile: sharedConfig.instanceProfiles[index] ?? null, launchArgs: sharedConfig.effectiveLaunchArgs, executablePath: sharedConfig.effectiveExecutablePath, }; await runAgentBrowser(sessionName, ['open', args.url], runOptions); await ensureActiveTabOnTargetUrl(sessionName, args.url, runOptions); const rounds = []; let bootstrap = null; const fixedPlanForInstance = sharedConfig.fixedPlansByInstance instanceof Map ? sharedConfig.fixedPlansByInstance.get(index + 1) : null; const rendererEvalTimeoutMs = computeRendererEvalTimeoutMs( args.syncSettleTimeoutMs, Array.isArray(fixedPlanForInstance) && fixedPlanForInstance.length > 0 ? fixedPlanForInstance.length : sharedConfig.plan.length ); for (let round = 0; round < args.rounds; round += 1) { const roundSeed = deriveSeed( sharedConfig.seed ?? sharedConfig.runId, sessionName, index + 1, round + 1 ); const roundRng = createSeededRng(roundSeed); bootstrap = await runGraphBootstrap(sessionName, args, runOptions); const clientPlan = Array.isArray(fixedPlanForInstance) && fixedPlanForInstance.length > 0 ? [...fixedPlanForInstance] : shuffleOperationPlan(sharedConfig.plan, roundRng); const markerPrefix = `${sharedConfig.runPrefix}r${round + 1}-client-${index + 1}-`; const rendererProgram = buildRendererProgram({ runPrefix: sharedConfig.runPrefix, markerPrefix, plan: clientPlan, seed: roundSeed, undoRedoDelayMs: args.undoRedoDelayMs, readyTimeoutMs: RENDERER_READY_TIMEOUT_MS, readyPollDelayMs: RENDERER_READY_POLL_DELAY_MS, syncSettleTimeoutMs: args.syncSettleTimeoutMs, opTimeoutMs: args.opTimeoutMs, fallbackPageName: FALLBACK_PAGE_NAME, verifyChecksum: args.verifyChecksum, captureReplay: args.captureReplay, }); try { const evaluation = await runAgentBrowser( sessionName, ['eval', '--stdin'], { input: rendererProgram, timeoutMs: rendererEvalTimeoutMs, ...runOptions, } ); const value = evaluation?.data?.result; if (!value) { throw new Error(`Unexpected empty result from agent-browser eval (round ${round + 1})`); } rounds.push({ round: round + 1, ...value, }); } catch (error) { const captured = await collectFailureReplayCapture(sessionName, markerPrefix, runOptions); if (captured && typeof captured === 'object') { const replayCapture = captured.replayCapture && typeof captured.replayCapture === 'object' ? captured.replayCapture : {}; const fallbackOpLog = Array.isArray(replayCapture.opLog) ? replayCapture.opLog : []; const fallbackTxCapture = replayCapture.txCapture && typeof replayCapture.txCapture === 'object' ? replayCapture.txCapture : null; const fallbackInitialDb = replayCapture.initialDb && typeof replayCapture.initialDb === 'object' ? replayCapture.initialDb : null; const fallbackConsoleLogs = Array.isArray(captured.consoleLogs) ? captured.consoleLogs : []; const fallbackWsMessages = captured.wsMessages && typeof captured.wsMessages === 'object' ? captured.wsMessages : null; const fallbackExecutedOps = fallbackOpLog.length; const roundResult = { round: round + 1, ok: false, requestedOps: clientPlan.length, executedOps: fallbackExecutedOps, counts: {}, markerPrefix, anchorUuid: null, finalManagedCount: 0, sampleManaged: [], errorCount: 1, errors: [ { index: fallbackExecutedOps, requested: 'eval', attempted: 'eval', message: String(error?.message || error), }, ], requestedPlan: Array.isArray(clientPlan) ? [...clientPlan] : [], opLog: fallbackOpLog, opLogSample: fallbackOpLog.slice(0, 20), initialDb: fallbackInitialDb, txCapture: fallbackTxCapture, consoleLogs: fallbackConsoleLogs, wsMessages: fallbackWsMessages, checksum: null, recoveredFromEvalFailure: true, }; rounds.push(roundResult); } error.partialResult = { ok: false, rounds: [...rounds], ...summarizeRounds(rounds), }; throw error; } } const summary = summarizeRounds(rounds); const value = { ok: summary.failedRounds.length === 0, rounds, requestedOps: summary.requestedOps, executedOps: summary.executedOps, counts: summary.counts, errorCount: summary.errorCount, failedRounds: summary.failedRounds, }; value.runtime = { session: sessionName, instanceIndex: index + 1, effectiveProfile: runOptions.profile, effectiveLaunchArgs: sharedConfig.effectiveLaunchArgs, effectiveExecutablePath: sharedConfig.effectiveExecutablePath, bootstrap, rounds: args.rounds, opProfile: args.opProfile, opTimeoutMs: args.opTimeoutMs, seed: args.seed, verifyChecksum: args.verifyChecksum, captureReplay: args.captureReplay, cleanupTodayPage: args.cleanupTodayPage, autoConnect: args.autoConnect, headed: args.headed, }; return value; } async function runPostSimulationCleanup(sessionName, index, args, sharedConfig) { if (!args.cleanupTodayPage) return null; const runOptions = { headed: args.headed, autoConnect: args.autoConnect, profile: sharedConfig.instanceProfiles[index] ?? null, launchArgs: sharedConfig.effectiveLaunchArgs, executablePath: sharedConfig.effectiveExecutablePath, }; const cleanupEval = await runAgentBrowser( sessionName, ['eval', '--stdin'], { input: buildCleanupTodayPageProgram({ cleanupTodayPage: args.cleanupTodayPage, }), timeoutMs: 30000, ...runOptions, } ); return cleanupEval?.data?.result || null; } function formatFailureText(reason) { return String(reason?.stack || reason?.message || reason); } function classifySimulationFailure(reason) { const text = formatFailureText(reason).toLowerCase(); if ( text.includes('checksum mismatch rtc-log detected') || text.includes('db-sync/checksum-mismatch') || text.includes(':rtc.log/checksum-mismatch') ) { return 'checksum_mismatch'; } if ( text.includes('tx rejected rtc-log detected') || text.includes('tx-rejected warning detected') || text.includes('db-sync/tx-rejected') || text.includes(':rtc.log/tx-rejected') ) { return 'tx_rejected'; } if ( text.includes('missing-entity-id warning detected') || text.includes('nothing found for entity id') ) { return 'missing_entity_id'; } if ( text.includes('numeric-entity-id-in-non-transact-op warning detected') || text.includes('non-transact outliner ops contain numeric entity ids') ) { return 'numeric_entity_id_in_non_transact_op'; } return 'other'; } function buildRejectedResultEntry(sessionName, index, reason, failFastState) { const failureType = classifySimulationFailure(reason); const error = formatFailureText(reason); const partialResult = reason && typeof reason === 'object' && reason.partialResult && typeof reason.partialResult === 'object' ? reason.partialResult : null; const peerCancelledByFailFast = (failFastState?.reasonType === 'checksum_mismatch' || failFastState?.reasonType === 'tx_rejected' || failFastState?.reasonType === 'missing_entity_id' || failFastState?.reasonType === 'numeric_entity_id_in_non_transact_op') && Number.isInteger(failFastState?.sourceIndex) && failFastState.sourceIndex !== index; if (peerCancelledByFailFast) { const cancelledReason = failFastState.reasonType === 'tx_rejected' ? 'cancelled_due_to_peer_tx_rejected' : ( failFastState.reasonType === 'missing_entity_id' ? 'cancelled_due_to_peer_missing_entity_id' : ( failFastState.reasonType === 'numeric_entity_id_in_non_transact_op' ? 'cancelled_due_to_peer_numeric_entity_id_in_non_transact_op' : 'cancelled_due_to_peer_checksum_mismatch' ) ); return { session: sessionName, instanceIndex: index + 1, ok: false, cancelled: true, cancelledReason, peerInstanceIndex: failFastState.sourceIndex + 1, error, failureType: 'peer_cancelled', result: partialResult, }; } return { session: sessionName, instanceIndex: index + 1, ok: false, error, failureType, result: partialResult, }; } function extractChecksumMismatchDetailsFromError(errorText) { const text = String(errorText || ''); const marker = 'checksum mismatch rtc-log detected:'; const markerIndex = text.toLowerCase().indexOf(marker); if (markerIndex === -1) return null; const afterMarker = text.slice(markerIndex + marker.length); const match = afterMarker.match(/\{[\s\S]*?\}/); if (!match) return null; try { const parsed = JSON.parse(match[0]); if (!parsed || typeof parsed !== 'object') return null; return parsed; } catch (_error) { return null; } } function extractTxRejectedDetailsFromError(errorText) { const text = String(errorText || ''); const marker = 'tx rejected rtc-log detected:'; const markerIndex = text.toLowerCase().indexOf(marker); if (markerIndex === -1) return null; const afterMarker = text.slice(markerIndex + marker.length); const match = afterMarker.match(/\{[\s\S]*?\}/); if (!match) return null; try { const parsed = JSON.parse(match[0]); if (!parsed || typeof parsed !== 'object') return null; return parsed; } catch (_error) { return null; } } function buildRunArtifact({ output, args, runContext, failFastState }) { const safeOutput = output && typeof output === 'object' ? output : {}; const resultItems = Array.isArray(safeOutput.results) ? safeOutput.results : []; const clients = resultItems.map((item) => { const errorText = item?.error ? String(item.error) : null; const mismatch = errorText ? extractChecksumMismatchDetailsFromError(errorText) : null; const txRejected = errorText ? extractTxRejectedDetailsFromError(errorText) : null; const rounds = Array.isArray(item?.result?.rounds) ? item.result.rounds.map((round) => ({ round: Number(round?.round || 0), requestedOps: Number(round?.requestedOps || 0), executedOps: Number(round?.executedOps || 0), errorCount: Number(round?.errorCount || 0), requestedPlan: Array.isArray(round?.requestedPlan) ? round.requestedPlan : [], opLog: Array.isArray(round?.opLog) ? round.opLog : [], errors: Array.isArray(round?.errors) ? round.errors : [], warnings: Array.isArray(round?.warnings) ? round.warnings : [], initialDb: round?.initialDb && typeof round.initialDb === 'object' ? round.initialDb : null, txCapture: round?.txCapture && typeof round.txCapture === 'object' ? round.txCapture : null, consoleLogs: Array.isArray(round?.consoleLogs) ? round.consoleLogs : [], wsMessages: round?.wsMessages && typeof round.wsMessages === 'object' ? round.wsMessages : null, })) : []; return { session: item?.session || null, instanceIndex: Number.isInteger(item?.instanceIndex) ? item.instanceIndex : null, ok: Boolean(item?.ok), cancelled: item?.cancelled === true, cancelledReason: item?.cancelledReason || null, failureType: item?.failureType || null, error: errorText, mismatch, txRejected, requestedOps: Number(item?.result?.requestedOps || 0), executedOps: Number(item?.result?.executedOps || 0), errorCount: Number(item?.result?.errorCount || 0), failedRounds: Array.isArray(item?.result?.failedRounds) ? item.result.failedRounds : [], requestedPlan: Array.isArray(item?.result?.rounds?.[0]?.requestedPlan) ? item.result.rounds[0].requestedPlan : [], opLogTail: Array.isArray(item?.result?.rounds?.[0]?.opLog) ? item.result.rounds[0].opLog.slice(-50) : [], opLogSample: Array.isArray(item?.result?.rounds?.[0]?.opLogSample) ? item.result.rounds[0].opLogSample : [], errors: Array.isArray(item?.result?.rounds?.[0]?.errors) ? item.result.rounds[0].errors : [], rounds, }; }); return { createdAt: new Date().toISOString(), runId: runContext?.runId || null, runPrefix: runContext?.runPrefix || null, args: args || {}, summary: { ok: Boolean(safeOutput.ok), instances: Number(safeOutput.instances || clients.length || 0), successCount: Number(safeOutput.successCount || 0), failureCount: Number(safeOutput.failureCount || 0), }, failFast: { triggered: Boolean(failFastState?.triggered), sourceIndex: Number.isInteger(failFastState?.sourceIndex) ? failFastState.sourceIndex : null, reasonType: failFastState?.reasonType || null, }, mismatchCount: clients.filter((item) => item.mismatch).length, txRejectedCount: clients.filter((item) => item.txRejected).length, clients, }; } function extractReplayContext(artifact) { const argsOverride = artifact && typeof artifact.args === 'object' && artifact.args ? { ...artifact.args } : {}; const fixedPlansByInstance = new Map(); const clients = Array.isArray(artifact?.clients) ? artifact.clients : []; for (const client of clients) { const instanceIndex = Number(client?.instanceIndex); if (!Number.isInteger(instanceIndex) || instanceIndex <= 0) continue; if (!Array.isArray(client?.requestedPlan)) continue; fixedPlansByInstance.set(instanceIndex, [...client.requestedPlan]); } return { argsOverride, fixedPlansByInstance, }; } async function writeRunArtifact(artifact, baseDir = DEFAULT_ARTIFACT_BASE_DIR) { const runId = String(artifact?.runId || Date.now()); const artifactDir = path.join(baseDir, runId); await fsPromises.mkdir(artifactDir, { recursive: true }); await fsPromises.writeFile( path.join(artifactDir, 'artifact.json'), JSON.stringify(artifact, null, 2), 'utf8' ); const clients = Array.isArray(artifact?.clients) ? artifact.clients : []; for (let i = 0; i < clients.length; i += 1) { const client = clients[i]; const clientIndex = Number.isInteger(client?.instanceIndex) && client.instanceIndex > 0 ? client.instanceIndex : i + 1; const clientDir = path.join(artifactDir, 'clients', `client-${clientIndex}`); await fsPromises.mkdir(clientDir, { recursive: true }); const rounds = Array.isArray(client?.rounds) ? client.rounds : []; for (let j = 0; j < rounds.length; j += 1) { const round = rounds[j]; const roundIndex = Number.isInteger(round?.round) && round.round > 0 ? round.round : j + 1; const roundPrefix = `round-${roundIndex}`; await fsPromises.writeFile( path.join(clientDir, `${roundPrefix}-client-ops.json`), JSON.stringify( { requestedPlan: Array.isArray(round?.requestedPlan) ? round.requestedPlan : [], opLog: Array.isArray(round?.opLog) ? round.opLog : [], txCapture: round?.txCapture && typeof round.txCapture === 'object' ? round.txCapture : null, errors: Array.isArray(round?.errors) ? round.errors : [], warnings: Array.isArray(round?.warnings) ? round.warnings : [], }, null, 2 ), 'utf8' ); await fsPromises.writeFile( path.join(clientDir, `${roundPrefix}-console-log.json`), JSON.stringify( Array.isArray(round?.consoleLogs) ? round.consoleLogs : [], null, 2 ), 'utf8' ); await fsPromises.writeFile( path.join(clientDir, `${roundPrefix}-ws-messages.json`), JSON.stringify( round?.wsMessages && typeof round.wsMessages === 'object' ? round.wsMessages : { installed: false, outbound: [], inbound: [] }, null, 2 ), 'utf8' ); } } return artifactDir; } async function main() { let args; try { args = parseArgs(process.argv.slice(2)); } catch (error) { console.error(error.message); console.error('\n' + usage()); process.exit(1); return; } if (args.help) { console.log(usage()); return; } let replayContext = { sourceArtifactPath: null, fixedPlansByInstance: new Map(), }; if (args.replay) { const replayPath = path.resolve(args.replay); const replayContent = await fsPromises.readFile(replayPath, 'utf8'); const replayArtifact = JSON.parse(replayContent); const extractedReplay = extractReplayContext(replayArtifact); args = { ...args, ...extractedReplay.argsOverride, replay: args.replay, }; replayContext = { sourceArtifactPath: replayPath, fixedPlansByInstance: extractedReplay.fixedPlansByInstance, }; } const preview = { url: args.url, session: args.session, instances: args.instances, graph: args.graph, e2ePassword: args.e2ePassword, switchTimeoutMs: args.switchTimeoutMs, profile: args.profile, executablePath: args.executablePath, autoConnect: args.autoConnect, resetSession: args.resetSession, ops: args.ops, opProfile: args.opProfile, opTimeoutMs: args.opTimeoutMs, seed: args.seed, replay: args.replay, rounds: args.rounds, undoRedoDelayMs: args.undoRedoDelayMs, syncSettleTimeoutMs: args.syncSettleTimeoutMs, verifyChecksum: args.verifyChecksum, captureReplay: args.captureReplay, cleanupTodayPage: args.cleanupTodayPage, headed: args.headed, }; if (args.printOnly) { console.log(JSON.stringify(preview, null, 2)); return; } await spawnAndCapture('agent-browser', ['--version']); const sessionNames = buildSessionNames(args.session, args.instances); let effectiveProfile; if (args.profile === 'none') { effectiveProfile = null; } else if (args.profile === 'auto') { const autoName = await detectChromeProfile(); effectiveProfile = await resolveProfileArgument(autoName); } else { effectiveProfile = await resolveProfileArgument(args.profile); } const effectiveExecutablePath = args.executablePath || (await detectChromeExecutablePath()); const effectiveLaunchArgs = effectiveProfile ? buildChromeLaunchArgs(args.url) : null; const instanceProfiles = []; if (args.instances <= 1 || !effectiveProfile) { for (let i = 0; i < args.instances; i += 1) { instanceProfiles.push(effectiveProfile); } } else if (looksLikePath(effectiveProfile)) { for (let i = 0; i < args.instances; i += 1) { instanceProfiles.push(effectiveProfile); } } else { for (let i = 0; i < args.instances; i += 1) { const isolated = await createIsolatedChromeUserDataDir(effectiveProfile, i + 1); instanceProfiles.push(isolated); } } const runId = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`; const sharedConfig = { runId, runPrefix: `op-sim-${runId}-`, seed: args.seed, replaySource: replayContext.sourceArtifactPath, fixedPlansByInstance: replayContext.fixedPlansByInstance, captureReplay: args.captureReplay, effectiveProfile, instanceProfiles, effectiveLaunchArgs, effectiveExecutablePath, plan: buildSimulationOperationPlan( Math.max(1, Math.ceil(args.ops / args.instances)), args.opProfile ), }; const failFastState = { triggered: false, sourceIndex: null, reasonType: null, }; const closeOtherSessions = async (excludeIndex) => { await Promise.all( sessionNames.map((sessionName, index) => { if (index === excludeIndex) return Promise.resolve(); return runAgentBrowser(sessionName, ['close'], { autoConnect: false, headed: false, }).catch(() => null); }) ); }; const tasks = sessionNames.map((sessionName, index) => (async () => { try { return await runSimulationForSession(sessionName, index, args, sharedConfig); } catch (error) { if (!failFastState.triggered) { failFastState.triggered = true; failFastState.sourceIndex = index; failFastState.reasonType = classifySimulationFailure(error); await closeOtherSessions(index); } throw error; } })() ); const settled = await Promise.allSettled(tasks); let cleanupTodayPage = null; try { cleanupTodayPage = await runPostSimulationCleanup( sessionNames[0], 0, args, sharedConfig ); } catch (error) { cleanupTodayPage = { ok: false, reason: String(error?.message || error), }; } const expectedOpsForInstance = (instanceIndex) => { const fixedPlan = sharedConfig.fixedPlansByInstance instanceof Map ? sharedConfig.fixedPlansByInstance.get(instanceIndex) : null; const perRound = Array.isArray(fixedPlan) && fixedPlan.length > 0 ? fixedPlan.length : sharedConfig.plan.length; return perRound * args.rounds; }; if (sessionNames.length === 1) { const single = settled[0]; if (single.status === 'rejected') { const rejected = buildRejectedResultEntry( sessionNames[0], 0, single.reason, failFastState ); const value = { ...(rejected.result && typeof rejected.result === 'object' ? rejected.result : {}), ok: false, error: rejected.error || formatFailureText(single.reason), failureType: rejected.failureType || 'other', cleanupTodayPage, }; if (rejected.cancelled) value.cancelled = true; if (rejected.cancelledReason) value.cancelledReason = rejected.cancelledReason; if (Number.isInteger(rejected.peerInstanceIndex)) { value.peerInstanceIndex = rejected.peerInstanceIndex; } try { const singleOutput = { ok: false, instances: 1, successCount: 0, failureCount: 1, results: [{ session: sessionNames[0], instanceIndex: 1, ok: false, result: value, error: rejected.error, failureType: rejected.failureType, cancelled: rejected.cancelled, cancelledReason: rejected.cancelledReason, peerInstanceIndex: rejected.peerInstanceIndex, }], }; const artifact = buildRunArtifact({ output: singleOutput, args, runContext: sharedConfig, failFastState, }); value.artifactDir = await writeRunArtifact(artifact); } catch (error) { value.artifactError = String(error?.message || error); } console.log(JSON.stringify(value, null, 2)); process.exitCode = 2; return; } const value = single.value; value.cleanupTodayPage = cleanupTodayPage; try { const singleOutput = { ok: value.ok, instances: 1, successCount: value.ok ? 1 : 0, failureCount: value.ok ? 0 : 1, results: [{ session: sessionNames[0], instanceIndex: 1, ok: value.ok, result: value, }], }; const artifact = buildRunArtifact({ output: singleOutput, args, runContext: sharedConfig, failFastState, }); value.artifactDir = await writeRunArtifact(artifact); } catch (error) { value.artifactError = String(error?.message || error); } console.log(JSON.stringify(value, null, 2)); if (!value.ok || value.executedOps < expectedOpsForInstance(1)) { process.exitCode = 2; } return; } const results = settled.map((entry, idx) => { const sessionName = sessionNames[idx]; if (entry.status === 'fulfilled') { const value = entry.value; const passed = Boolean(value?.ok) && Number(value?.executedOps || 0) >= expectedOpsForInstance(idx + 1); return { session: sessionName, instanceIndex: idx + 1, ok: passed, result: { ...value, cleanupTodayPage: idx === 0 ? cleanupTodayPage : null, }, }; } return buildRejectedResultEntry(sessionName, idx, entry.reason, failFastState); }); const successCount = results.filter((item) => item.ok).length; const output = { ok: successCount === results.length, instances: results.length, successCount, failureCount: results.length - successCount, results, }; try { const artifact = buildRunArtifact({ output, args, runContext: sharedConfig, failFastState, }); output.artifactDir = await writeRunArtifact(artifact); } catch (error) { output.artifactError = String(error?.message || error); } console.log(JSON.stringify(output, null, 2)); if (!output.ok) { process.exitCode = 2; } } if (require.main === module) { main().catch((error) => { console.error(error.stack || String(error)); process.exit(1); }); } module.exports = { parseArgs, isRetryableAgentBrowserError, buildCleanupTodayPageProgram, classifySimulationFailure, buildRejectedResultEntry, extractChecksumMismatchDetailsFromError, extractTxRejectedDetailsFromError, buildRunArtifact, extractReplayContext, createSeededRng, shuffleOperationPlan, };