Conversation
Vite's Rollup worker bundler wraps CJS modules in require_libXXX stubs
that break Comunica v5's circular actor/bus/mediator dep graph, causing
every queryGraph call to fail at runtime despite unit tests passing.
Two root causes identified via live browser reproduction:
1. node:diagnostics_channel (lru-cache) — Vite's browser-external
Proxy stub returns undefined for all property accesses, so
(0, L.channel)("lru-cache:metrics") throws at init time.
2. `global` (promise-polyfill) — not defined in browser workers;
cascades into broken __commonJS wrappers for downstream actors.
Fix:
- Add vite-plugin-worker-comunica.ts: intercepts the Comunica import
in worker.plugins and substitutes an esbuild-compiled flat ESM bundle
(same strategy as InProcessWorker tests that already pass).
- optimizeDeps.esbuildOptions: stub node:diagnostics_channel + define
global→globalThis so the dep-optimizer pre-bundle is also clean.
- e2e/sparql-worker.spec.ts: browser regression test covering INSERT,
SELECT, CONSTRUCT, DELETE, and malformed-SPARQL error path.
fix(worker): make Comunica QueryEngine work in Vite browser worker
isAiStreaming() rewritten with four UI-agnostic signals: 1. input disabled/aria-busy (textarea UIs) 2. visible spinner by class name 3. stop/abort button by aria-label OR exact text content 4. MutationObserver fallback — DOM silence for 1.5 s = done; catches icon-only stop buttons and thinking phases where none of the above signals fire (e.g. OWUI qwen3) Removed #send-message-button dependency and type=submit fallback that matched all OWUI buttons and caused false negatives during the qwen3 thinking phase. help() now opens with a full SILENTLY IGNORED format list covering every common wrong format, including the method="toolName" mistake qwen3 made on first attempt.
Adds everything needed to run an interactive OWUI relay session via Playwright MCP browser without external Node processes (BroadcastChannel must share the MCP browser process). - .playwright/: fresh-setup, send-starter, send-task, send-pizza send-task is a generic injection wrapper; send-pizza is a thin wrapper over it - e2e/demo-openwebui-pizza.spec.ts: recorded demo spec with silent-ignore warning in SYSTEM_PROMPT - playwright.openwebui.config.ts: Playwright config for OWUI tests - public/demo-stage-owui.html: side-by-side OWUI + Ontosphere stage - scripts/owui-auth.mjs: saves auth cookie to .playwright/owui-auth.json - docs/owui-relay-session.md: comprehensive session setup guide - CLAUDE.md: pointer to the session guide - package.json: demo:owui:auth and demo:owui:video scripts - .gitignore: exclude owui-auth.json (contains session tokens)
…akes Make isAiStreaming() fully generic — no chat-UI-specific selectors. Now uses four layered signals: textarea disabled/aria, visible spinner classes, stop-button aria-label/text, and MutationObserver DOM-silence fallback (STREAM_QUIET_MS=4000ms). Covers qwen3:8b icon-only stop button and any UI without standard spinner markup. Also add WRONG/RIGHT examples for queryGraph (sparql≠query) and setViewMode (mode≠viewMode) to help() — both consistently misused by qwen3.
MutationObserver fired mid-stream causing race conditions with thinking models (qwen3). Fire-on-idle polls every 500ms, waits for isAiStreaming() = false, reads the complete page text, extracts all tool calls at once. - relay: replace MutationObserver block with idlePoll() (500ms interval) - relay: pre-seed dispatchedSigs before poll starts to skip INSTR examples - relay: isAiStreaming() now checks spinners, stop-word buttons, DOM quiet - playwright: send-starter uses Good Response button (not __vgIsStreaming) - playwright: fresh-setup selects mistral-small3.1 (no thinking blocks)
…solutions discoverability to CLAUDE.md
…o bookmarklet plugin doSubmit() deadlocked: content injection causes DOM mutations → isAiStreaming() returns true for STREAM_QUIET_MS (4s) → submit poll blocks. By the time injectResult is called we already confirmed model idle via fire-on-idle; no need to re-check. vite-plugin-bookmarklet: addWatchFile so virtual module invalidates on source change.
…RESPONSE] UUID prefix insertFromPaste (appends) was promoted to Path 1 to sync Svelte for btn.click(). But OWUI pre-fills TipTap with [RESPONSE] <uuid> internal state in some conditions; append produces [RESPONSE] UUID + result text, model echoes the UUID. setContent(text, true) REPLACES all editor content. Enter-based submit reads TipTap state directly so Svelte async sync no longer matters. Path order: 1. setContent / raw PM dispatch (replace — correct for any pre-filled state) 2. insertFromPaste (append — fallback for non-TipTap contenteditable) 3. insertText (last resort)
…textarea Clear TipTap before insertFromPaste so [RESPONSE] UUID pre-fill is gone before append — paste to empty = clean insert, Svelte syncs, button enabled. Enter fires only for textarea UIs; TipTap (OWUI) maps Enter to new paragraph not submit — btn.click() is the correct path after Svelte syncs via paste.
hasContent fired submitInput on the first synchronous poll — before setContent's microtask had synced Svelte's prompt store. Button was still disabled → no click → silent fail or premature fire with stale content. doSubmit now polls btnEnabled only, giving the ~50 ms microtask queue time to flush. insertFromPaste (and the pre-clear step) removed; setContent atomically replaces any [RESPONSE] UUID pre-fill and is the sole inject path.
…nt submit
Remove all fallback injection paths (raw PM dispatch, insertFromPaste,
insertText). One path: setContent(text, true) to atomically replace content,
tiptap.on('transaction') to submit once Svelte's onTransaction sync completes.
300 ms safety timeout handles edge cases where the event never fires.
Eliminates the doSubmit polling loop and the entire multi-path injection chain
that caused UUID prefix bugs and premature submit races.
Two fixes:
1. Text-stability fallback in idlePoll: track innerText change time
separately from DOM mutation rate. If text unchanged for STREAM_QUIET_MS,
consider idle regardless of background DOM mutations. Fixes FhGenie where
continuous UI updates (progress indicator, animations) kept isAiStreaming()
permanently true, blocking all tool-call dispatch.
2. Delta extraction: process only text.slice(lastIdleText.length) per poll
instead of full page text + global dispatchedSigs. Same call in a later
turn fires again ("call it again" works). Per-turn Set still deduplicates
duplicate calls within a single response.
…utton isAiStreaming() signal 3: add SVG path prefix matching for icon-only stop buttons. FhGenie (Fluent UI) replaces the send arrow with a Dismiss24Regular X icon (path 'M8.22 8.22') during generation — no aria-label or text, so stop-word matching missed it. submitInput(): extend send button heuristic to also match class name containing 'send' or 'submit'. FhGenie's button has class _questionInputSendButton_* with no matching text or aria-label.
isAiStreaming() rewrite: - Primary signal: find send/submit button (by id, tree-climb, aria/text/class) - If send button found: check for X icon (M8.22 SVG path = Fluent UI Dismiss, FhGenie's stop affordance) or disabled-with-content (OWUI pattern) - If send button not found: fallback to aria-disabled/busy, spinners, stop words - Remove DOM mutation rate signal (signal 4) — caused permanent false positives on FhGenie due to background UI updates idlePoll rewrite: - Remove text-stability complexity (no longer needed) - On idle: full page text scan with global dispatchedSigs - Pre-seed dispatchedSigs at inject to skip pre-existing calls - Clean and predictable: one mechanism, no delta/stability bookkeeping
React re-renders asynchronously after the native value setter + input event. Calling submitInput() synchronously hits a still-disabled button (React hasn't re-rendered yet) and Enter fires before the value is in React state — both ignored. 50ms timeout lets React flush its state update before clicking.
Injecting while OWUI transitions out of streaming state causes a race: setContent fires before the editor accepts input, pasting lands too early. Poll isAiStreaming() (up to 10s) before calling setContent. Safe now that signal 4 (DOM mutation rate) is removed — content injection no longer triggers false positives in isAiStreaming().
… input idlePoll was firing on every idle tick including while user types or pastes. The starter prompt contains JSON-RPC example calls — relay dispatched them immediately on paste before the AI ever responded. Track prevStreaming: only dispatch on the single tick where streaming just ended (true→false transition). User messages arrive while always-idle so the transition never fires for them.
getPageText() uses a TreeWalker to collect body text skipping the input element subtree. Tool calls typed or injected into the input (INSTR examples, relay result text) can never be dispatched. All three scan sites updated: idlePoll, pre-seed, and waitForIdle.
getPageText() now strips the chat input's current content via string subtraction — prevents relay from dispatching tool calls the user typed or pasted into the input field. prevStreaming transition guard removed: getPageText() is the sole protection against dispatching user content, so the streaming→idle edge-detect is redundant and prevents legitimate re-dispatch after multi-turn conversations.
…T walker String subtraction was fragile — TipTap reformats pasted content (strips backticks, changes whitespace) so inp.innerText != body.innerText substring. The previous TreeWalker used SHOW_TEXT only: FILTER_REJECT on a text node is treated as FILTER_SKIP (no subtree effect), so the whole input was included. With SHOW_ELEMENT|SHOW_TEXT, FILTER_REJECT on the input element correctly skips its entire subtree. Textarea value is never in body.innerText, so TEXTAREA inputs skip the walker entirely.
Replace transaction-event+setTimeout(300) submit trigger with a waitSubmit loop that polls findSendButton() every 100ms, requiring 2 consecutive enabled ticks before clicking. Also simplify isAiStreaming() send-button branch: any disabled state means not ready, dropping the "disabled AND has content" heuristic that wrongly returned idle during model thinking with empty input. Together these ensure: - Pre-paste (waitReady): blocks while button is disabled (thinking phase) - Post-paste (waitSubmit): waits for Svelte to re-enable the button after setContent, not for the TipTap transaction which fires before the framework has flushed its render cycle Co-Authored-By: Thomas Hanke <thomas.hanke@iwm.fraunhofer.de>
…pty input FhGenie disables its send button when the input is empty (not just during generation). The previous change (disabled→streaming) broke FhGenie dispatch. New logic for the disabled branch: - enabled → idle (return false immediately) - disabled + has content → streaming (OWUI pattern) - disabled + empty → fall through to spinner/aria signals OWUI thinking is caught by the [class*="thinking"] spinner selector. FhGenie idle (disabled, empty, no spinner) correctly returns false.
… dispatch isAiStreaming() send-button branch: revert fallthrough for disabled+empty. FhGenie disables the send button when input is empty (not just during generation), and its Stream-mode toolbar elements match [class*="streaming"], causing the fallback spinner check to return true indefinitely. Correct logic: disabled+empty = idle (return false directly, like original). OWUI during generation replaces the send button with a stop button, so findSendButton() returns null and the fallback stop-word/spinner path handles it. idlePoll: require 2 consecutive idle ticks (1 s stable) before extracting. Prevents user starter-prompt examples from being dispatched in the brief window (~200ms) between user submit and model starting to stream.
Svelte needs more time to flush state after setContent and enable the send button reliably. 2 ticks (200ms) was sometimes too short.
Adds getAssistantText() that queries [data-message-author-role="assistant"] (OWUI), [data-role="assistant"], and aria-log fallbacks to restrict getPageText() to AI responses only. Prevents dispatching tool-call examples embedded in the user's starter prompt before the AI has responded.
Replaces the stableTicks heuristic with a loop that calls setContent, waits 600ms for OWUI's post-stream annotation phase to complete, then polls every 300ms checking isAiStreaming() before clicking submit. Success is detected by TipTap editor.isEmpty — a cleared input means OWUI accepted the message, no fixed tick count needed.
- relayBridge: log tool results/errors to console; fix unknown-tool path to emit console.error; add runLayout to toastLabel switch; rewrite buildCanvasSummary to use getNodes/getLinks instead of removed getGraphState - .playwright/pizza-demo-setup.js: new — bootstrap OWUI relay session with format INSTR and Socratic Turn 0 starter - .playwright/turn-driver.js: new — drive T1–T6 Socratic questions via __vgIsStreaming idle detection (replaces Good Response button polling) - .playwright/session-log-start.sh: new — persistent session log aggregator - fresh-setup.js / send-starter.js / send-pizza.js: model corrected to qwen3:4b - e2e/demo-pizza-socratic.spec.ts: new demo video spec — operator asks Socratic questions, model discovers OWL classes → subClassOf → hasPart → layout - docs/demo-scripts/pizza-ontology.md: recording guide with turn-by-turn expected tool calls and fallback nudges - .gitignore: add debug.png
4 MB mp4 / 6 MB webm — Socratic pizza ontology session, 7 turns, full class hierarchy + hasPart links + dagre-tb layout + introspection.
added 28 commits
May 8, 2026 16:44
collect-demo-videos.mjs matched result dirs by longest spec name first, preventing pizza-tutorial-chat from overwriting pizza-tutorial video.
…ects clearInferred only wiped data but never triggered a canvas refresh, so inferred links and decorations persisted after clearing. Fix re-emits all data subjects so the onSubjectsChange handler removes inferred links and refreshes element decorations through the existing update path. MCP clearInferred tool now routes through the registered canvas callback (same codepath as the UI button) when available, falling back to the bare dataProvider call when the canvas is not mounted. Tests updated to cover both paths and the error case.
qwen3:4b consistently passes subjectIri (inferring from addTriple parameter naming). This alias prevents silent failures when the model uses the wrong param name.
…cording - Switch demo:video to build+warmup approach: start dev server, prime Vite dep-optimization cache with a headless page load, then record. Eliminates mid-test page reloads caused by Vite's first-run dep scan. - Add workers:1 to playwright.demo.config.ts to prevent parallel-startup race conditions between browser instances sharing the same origin. - Re-record all 6 demo videos with updated MCP tool names (addTriple) and relay/layout improvements from the skolemization branch merge.
12-minute live qwen3:4b session — 11-turn Socratic pizza ontology arc. Idle-trimmed to 5.2 min (57% saved), 27 MB mp4.
After waitQuiet for the starter/help() cycle, check that at least one
relay result message ("[Ontosphere →") exists in the chat. If not, the
model failed to load or ignored the starter — throw immediately instead
of burning 11 turns recording an empty canvas.
…rt guard - Track MCP call count via __demoMcpCallCount__ in appFrame (replaces unreliable DOM selector check for relay result messages) - 13-min session, 1769 inferred triples — idle-trimmed to 3.4 min (74%)
…length stability isAiStreaming() gives false negatives for qwen3 (send button stays enabled during generation), causing the idle-poll to fire after just 1 s of apparent quiet and dispatch tool calls from partial model output — multiple sends per turn, hacked-apart Socratic conversation. Replace with content-length-based stability: require 24 consecutive 500 ms ticks (12 s) with identical getPageText() length before dispatching. Any content change resets the counter, so thinking pauses never trigger early dispatch. isAiStreaming() is intentionally not consulted.
…o block T4 rewritten to explicitly list all 12 required triples (equivalentClass, rdf:type owl:Restriction, owl:onProperty, owl:someValuesFrom) for each pizza class. qwen3:4b was creating named restriction nodes but omitting onProperty and someValuesFrom — OWL-RL cls-svf1 never fired, pizzas stayed unclassified. shorten-idle.py: add no_cut_last flag (arg 7). When true, the last detected freeze block is kept at full length rather than trimmed to digest_sec. collect-demo-videos.mjs: pass no_cut_last=1 for openwebui-socratic so the model's final classification response is not cut away in post-processing.
… way' claim; raise idle-cut threshold - addTriple description: remove prohibition on blank-node restriction construction; document 4-call pattern (_:b0/_:b1 labels) with owl:Restriction emphasis and distinct-label-per-restriction warning (same label = same IRI = collapse) - loadRdf description: 'only way' → 'simplest way'; add reference to addTriple alternative so both paths are documented and neither is forbidden - mcp.json: sync addTriple + loadRdf descriptions to match relay manifest - T4 Socratic question: replace 12-triple directive list with concept-led question that lets qwen3 choose addTriple or loadRdf guided by the updated manifest - collect-demo-videos.mjs: raise min_freeze 8s → 20s to preserve qwen3 CoT thinking phases (hidden <details> blocks freeze visible chat for 20–60s; 8s was cutting them) Validation: loadRdf Turtle path produces correct owl:Restriction nodes with onProperty + someValuesFrom; runReasoning classifies pizza1→SalamiPizza, pizza2→HawaiianPizza, pizza3→MargheritaPizza via cls-svf1 + cax-eqc chain.
- addTriple description: replace BLANK NODE LIMITATION with 4-call blank-node restriction pattern (_:b0/_:b1 distinct labels, owl:Restriction not owl:Class) - loadRdf description: add explicit no-@Prefix instruction with ex: pattern example - Top-level manifest bullet: both restriction paths documented - links.ts error message: no-@Prefix example in fallback Turtle hint
…@Prefix - Step 6 abort guard: wait for mcpCallCount>0 before starting waitQuiet — avoids false abort when qwen3 CoT thinking (20-60s in <details>) exhausts the 15s silence window before help() fires - sanitizeTurtle: strip pre-loaded @Prefix lines (often malformed by qwen3 with spaces: `< http://...>`) and fix whitespace inside angle brackets before parse - Applies sanitize before validateTurtleSnippet so helpful error hints still work
pizza1→SalamiPizza, pizza2→HawaiianPizza, pizza3→MargheritaPizza all classified by OWL-RL cls-svf1 via blank-node equivalentClass restrictions.
SELECT rows and CONSTRUCT triples now use prefix:local notation (e.g. ex:SalamiPizza, owl:NamedIndividual) instead of full IRIs. Blank-node skolem IRIs (urn:vg:bnode:*) are shown as _:bnode. Makes relay-injected results readable in the OWUI chat.
…on timing - waitRelayFlush: add 45s stability window so multi-burst relay batches (spaced 28–38s apart) are fully drained before the next turn is sent; previous single-idle-detection caused HawaiianPizza/MargheritaPizza restrictions to be sent after the spec had moved on - Turn loop restructured: AFTER_CAPTIONS + TURN_TOPICS both fire within 7s of relay flush so they survive shorten-idle's 20s min_freeze threshold - T8/T9 split: dedicated turns for pizza2 and pizza3 prevent qwen3 from duplicating pizza2 edges when building pizza3 - getNodeDetails: abbreviate IRI types/predicates/objects via abbreviateIri so model sees ex:Pizza not http://example.org/Pizza - Re-recorded demo: all 3 pizzas classified (SalamiPizza, HawaiianPizza, MargheritaPizza), captions preserved in compressed output
- Exact version pin (0.2.0) forces re-audit of inlined codec on any future upgrade - copy-konclude-assets.mjs copies worker.js/konclude.mjs/konclude.wasm from node_modules/rdf-reasoner-konclude/dist/ to public/rdf-reasoner-konclude/ on every npm install, eliminating manual drift - Chains with existing patch-package hook in postinstall - public/rdf-reasoner-konclude/ added to .gitignore (auto-generated, ~5 MB WASM)
…e v0.2.0 binary protocol Replace the NTriples round-trip with zero-copy ArrayBuffer transfer: - Inline InternTable/encodeToBuffers/decodeBuffers from ts/intern.ts (pinned 0.2.0) - Replace _call rest-params with explicit (method, args[], transfer?) signature - Rewrite reason() to use loadTripleBuffer → realization → getInferredTripleBuffer - Remove _serializeToNTriples() and all NTriples-related code - Remove Emscripten pthread-exit ErrorEvent filter (not needed in v0.2.0) - Bump worker URL cache-bust from v=15 to v=16
…raph Konclude returns its full taxonomy including triples already in the source. KoncludeReasoner.reason() now builds a (s,p,o) key set from the source quads before reasoning and skips any decoded triple whose key is already present. Only genuinely new inferences land in urn:konclude:inferred. Also fix pre-existing unnecessary \@ escape in regex (line 729).
… + MCP param - appConfigStore: add reasonerBackend field (default 'konclude') + setReasonerBackend - ConfigurationPanel: Reasoner Backend radio (OWL DL / N3 Rules); ruleset picker only shown when N3 is selected - rdfManager.impl/workerProtocol: runReasoning() accepts reasonerBackend, reads from config if not passed, forwards to worker - MCP runReasoning: reasonerBackend param + updated description + capabilities list - server.js + vite.config.ts: add COOP/COEP headers (required for SharedArrayBuffer) - README: update reasoning section — Konclude default, N3 as legacy/advanced
…onent test - nodes.test.ts: add getNamespaces mock; update assertions for abbreviated IRIs - graph.test.ts: update SELECT/CONSTRUCT assertions for prefix-shortened IRIs - Remove NodePropertyEditor.autopopulate.test.tsx (it.skip; jsdom can't drive Radix UI combobox portals — belongs in Playwright if ever revived)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Konclude v0.2.0 migration (main feature)
KoncludeReasonerfrom the deprecated NTriples string round-tripprotocol to the v0.2.0 zero-copy binary ArrayBuffer wire protocol
(
loadTripleBuffer/realization/getInferredTripleBuffer){id, result/error}message routing; eliminated exit-code noise
scripts/copy-konclude-assets.mjs) sothe WASM bundle is served from
public/rdf-reasoner-konclude/automatically(s\0p\0o)key set — only genuinely new triples land inurn:konclude:inferredsame-origin/credentialless) to bothserver.jsand
vite.config.tsforSharedArrayBuffersupportrdf-reasoner-koncludeto exact pin0.2.0; version bumped to1.3.0Reasoner backend selector
available as the legacy/advanced option
ConfigurationPanel(OWL DL / N3 Rules);N3 ruleset picker shown only when N3 is selected
runReasoningMCP tool andrdfManager.runReasoning()acceptreasonerBackend: 'konclude' | 'n3';getCapabilitiesreturnsreasonerBackends: ['konclude', 'n3']handleRunReasoning(React layer) and missing forwarding ofreasonerBackendinto
reasoningRequestin the worker dispatchSPARQL worker
QueryEnginefailing inside Vite browser workers; rewiredthe worker bundle so Comunica loads correctly
queryGraphnow returns prefix-shortened IRIs (ex:Alicenot the full IRI);blank-node skolem IRIs mapped to
_:bnodeBlank node skolemization
urn:vg:bnode:IRIs at storewrite time; deterministic across reloads
addTripleaccepts blank-node IRI restrictions in OWL axiomsMCP tool improvements
loadRdf: improved validation; injects missing built-in@prefixdeclarations;permits both OWL restriction paths
addNode: acceptssubjectIrias alias foririgetNodeDetails: abbreviates returned IRIs using registered prefixesexpandNode: navigates canvas to the node after expandingqueryGraph: IRI shortening, ASK rejection, prefix injection, bare-IRIPREFIX normalisation
OWUI relay
setContent+ transaction event submit (no fallbacks)callQueue/isProcessing) instead of unreliable send-button statewaitRelayFlushuses 45 s stability windowBroadcastChannelscoped correctly viadocker-devoriginDemo & docs
README.mdreasoning section updated: Konclude as default, N3 as advanceddocs/solutions/entries for fire-on-idle relay pattern and relay architectureTests
reasoning_konclude_demo.test.ts: verifies Konclude infersex:Person subClassOf owl:Thingand direct hierarchy edges; documentsecho count (12 echoed, 15 genuinely new)
reasonerBackend: 'n3'nodes.test.ts(missinggetNamespacesmock) andgraph.test.ts(assertions updated for prefix-shortened IRIs)
NodePropertyEditor.autopopulate.test.tsx(
it.skip; jsdom cannot drive Radix UI combobox portals)