mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-12 08:28:40 -04:00
perf(chat): batch live thinking rendering and bound timer updates (#5931)
* perf(chat): batch live thinking DOM updates
* test(chat): cover live thinking scheduler lifecycle
* fix(chat): guard background stop-state, restore live thinking text, drop source-text tests
- _closeOpenThinkingMarkup no longer overwrites currentAccumulated for
backgrounded streams. It now mirrors the guard the delta path already uses
(`if (!_isBg) currentAccumulated = accumulated`). Without it a backgrounded
stream's text is written into the foreground session's stop-state, which
abortCurrentRequest and detachCurrentStream then put in the wrong bubble.
- Split _extractLiveThinkingText into _liveThinkingText (strip every think tag)
and _closedThinkingText (via extractThinkingBlocks). Slicing from the first
<think> to the first </think> pinned the live box to "The" for the rest of the
stream on the `<think>The</think>` + untagged-thinking pattern that the
hasUnclosedThink detection deliberately keeps streaming through.
- The background transition now flushes with rich:true, so a stream that
backgrounds mid-thinking isn't left as pre-wrap plain text permanently.
- Move the throttle to static/js/liveThinkingThrottle.js and import it. The
.mjs suite imports the module instead of slicing it out of chat.js with
vm.runInNewContext and marker comments.
- Replace the source-text assertions in tests/test_live_thinking_scheduler_js.py
with behavioral coverage, per tests/TESTING_STANDARD.md. The .mjs suite grows
from 3 to 6 cases.
- Collapse the duplicated tool_start/agent_step finalizers into one
_endLiveThinkingSection().
* fix(chat): hoist thinking teardown out of the try block so catch can reach it
In an ES module a function declared inside `try { }` is scoped to that block,
and `catch` is a sibling scope rather than a nested one. _closeOpenThinkingMarkup
was declared inside the try and called from catch, so the call threw
ReferenceError and killed the rest of the error path: the stream never
finalized and the thinking block was never torn down.
Declare _closeOpenThinkingMarkup and a new _endThinkingOnTerminalPath next to
the existing _flushLiveThinking / _cancelLiveThinkingWork outer lets and assign
them inside the try, which is the pattern those two already use for exactly
this reason.
Verified against a live stream in a browser: before, clicking stop mid-thinking
logged "_closeOpenThinkingMarkup is not defined" and left no finalized thinking
section; after, the block collapses to "View thinking process" correctly.
* perf(chat): extract live thinking at commit cadence
* fix(chat): bound live thinking work
* test(chat): update stream invariant assertions
---------
Co-authored-by: Léo <leograndcontact@gmail.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""Source-level wiring guards for live-thinking stream lifecycle.
|
||||
|
||||
The pure scheduler suite covers timing behavior. These assertions pin the
|
||||
browser-only integration seams that are impractical to import without the full
|
||||
application DOM.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_CHAT = (Path(__file__).resolve().parent.parent / "static" / "js" / "chat.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def _between(start: str, end: str) -> str:
|
||||
return _CHAT.split(start, 1)[1].split(end, 1)[0]
|
||||
|
||||
|
||||
def test_in_thinking_delta_short_circuits_before_cumulative_normalization():
|
||||
delta_handler = _between(
|
||||
"let _delta = json.delta;",
|
||||
"} else if (json.type === 'research_progress')",
|
||||
)
|
||||
delta_path = _between(
|
||||
"// Detect thinking-in-progress:",
|
||||
"} else if (json.type === 'research_progress')",
|
||||
)
|
||||
guard = "if (!_thinkingAnalysisGate.shouldAnalyze(roundText, {"
|
||||
normalize = "markdownModule.normalizeThinkingMarkup(roundText)"
|
||||
assert guard in delta_path
|
||||
assert delta_path.index(guard) < delta_path.index(normalize)
|
||||
assert "_queueLiveThinking(roundText);" in delta_path
|
||||
assert "createThinkingAnalysisGate" in _CHAT
|
||||
projector_append = "_roundDisplayProjector.append(_delta, roundText);"
|
||||
assert projector_append in delta_handler
|
||||
assert delta_handler.index(projector_append) < delta_handler.index(guard)
|
||||
assert "_renderStream({ knownNormal: true, displayText: _roundDisplayProjector.current() });" in delta_path
|
||||
assert "_replyDisplayProjector.append(_delta, roundReplyText)" in delta_path
|
||||
|
||||
|
||||
def test_short_close_grace_expires_without_another_delta():
|
||||
assert "function _scheduleThinkingGrace()" in _CHAT
|
||||
grace = _between(
|
||||
"function _scheduleThinkingGrace()",
|
||||
"function _replyAfterClosedThinking",
|
||||
)
|
||||
assert "setTimeout(() =>" in grace
|
||||
assert "_finishLiveThinkingTransition();" in grace
|
||||
cancel = _between("_cancelLiveThinkingWork = () =>", "function _finalizeLiveThinking")
|
||||
assert "_cancelThinkingGrace();" in cancel
|
||||
delta_path = _between(
|
||||
"// Detect thinking-in-progress:",
|
||||
"} else if (json.type === 'research_progress')",
|
||||
)
|
||||
false_close = _between(
|
||||
"// Detect false close:",
|
||||
"if (hasUnclosedThink && !isThinking)",
|
||||
)
|
||||
assert "Do NOT require a prior unclosed delta" in false_close
|
||||
assert "_afterClose &&" in false_close
|
||||
assert "&& isThinking" not in false_close.split("let _falseCloseDeadline", 1)[1].split("if (isThinking)", 1)[0]
|
||||
assert "_thinkingRecheckAt = _falseCloseDeadline || 0;" in delta_path
|
||||
|
||||
|
||||
def test_terminal_paths_use_one_authoritative_rich_round_render():
|
||||
tool_path = _between(
|
||||
"} else if (json.type === 'tool_start') {",
|
||||
"} else if (json.type === 'tool_output') {",
|
||||
)
|
||||
assert "_endLiveThinkingSection({ rich: false });" in tool_path
|
||||
assert tool_path.count("_finalizeRoundRender();") == 1
|
||||
assert "_renderStream();" not in tool_path
|
||||
|
||||
agent_path = _between(
|
||||
"} else if (json.type === 'agent_step') {",
|
||||
"} else if (json.type === 'budget_exceeded') {",
|
||||
)
|
||||
assert "_endLiveThinkingSection({ rich: false });" in agent_path
|
||||
assert agent_path.count("_finalizeRoundRender();") == 1
|
||||
assert "if (!roundFinalized)" not in agent_path
|
||||
|
||||
catch_path = _between(
|
||||
"// foreground session's text.\n const _isBgCatch",
|
||||
"} finally {",
|
||||
)
|
||||
assert "if (_isBgCatch)" in catch_path
|
||||
assert "_cancelLiveThinkingWork();" in catch_path
|
||||
assert "_catchTerminalView = _finalizeInterruptedView();" in catch_path
|
||||
assert "_finalizeRoundRender();" not in catch_path
|
||||
assert "_endThinkingOnTerminalPath({ rich: false });" in catch_path
|
||||
assert "const _catchViewHolder = _catchTerminalView?.holder || holder;" in catch_path
|
||||
|
||||
round_finalizer = _between(
|
||||
"_finalizeRoundRender = () => {",
|
||||
"_finalizeInterruptedView = () => {",
|
||||
)
|
||||
assert "if (roundFinalized) return roundFinalization;" in round_finalizer
|
||||
assert round_finalizer.index("processWithThinking") < round_finalizer.rindex("roundFinalized = true;")
|
||||
assert "lastContentRoundHolder = terminalHolder;" in round_finalizer
|
||||
|
||||
interrupted_finalizer = _between(
|
||||
"_finalizeInterruptedView = () => {",
|
||||
"function _replyAfterClosedThinking",
|
||||
)
|
||||
assert "finalization?.hasContent" in interrupted_finalizer
|
||||
assert "lastContentRoundHolder || finalization?.holder" in interrupted_finalizer
|
||||
|
||||
stop_path = _between(
|
||||
"// Render whatever was accumulated so far",
|
||||
"// Reset button state",
|
||||
)
|
||||
assert "const _stoppedViewHolder = _terminalView?.holder || currentHolder;" in stop_path
|
||||
assert "_stoppedViewHolder.querySelector('.body').appendChild(stoppedIndicator);" in stop_path
|
||||
|
||||
done_path = _between(
|
||||
"if (data === '[DONE]') {",
|
||||
"try {\n const json = JSON.parse(data);",
|
||||
)
|
||||
assert "_finalizeLiveThinking(_closedThinkingText(roundText), false);" in done_path
|
||||
assert "_renderStream();" not in done_path
|
||||
|
||||
post_loop = _between(
|
||||
"if (!_streamSawDone) {",
|
||||
"// --- Final render (skip if stream was ever backgrounded or currently in background) ---",
|
||||
)
|
||||
assert "_cancelLiveThinkingWork();" in post_loop
|
||||
assert "_renderStream();" not in post_loop
|
||||
|
||||
recovery_path = _between(
|
||||
"function _tryAutoRecover(holder, accumulated, sessionId)",
|
||||
"function _removeStallBanner()",
|
||||
)
|
||||
assert "processWithThinking" not in recovery_path
|
||||
|
||||
|
||||
def test_detach_synchronously_cancels_delayed_view_work():
|
||||
registration = _between("_activeStreams.set(streamSessionId", "_syncForegroundStreamGlobals();")
|
||||
assert "cancelViewWork: () => _cancelLiveThinkingWork()" in registration
|
||||
|
||||
detach = _between("export function detachCurrentStream", "// _notifyStreamComplete")
|
||||
cancel = "if (active.cancelViewWork) active.cancelViewWork();"
|
||||
background = "_backgroundStreams.set(sessionId"
|
||||
assert cancel in detach
|
||||
assert detach.index(cancel) < detach.index(background)
|
||||
Reference in New Issue
Block a user