Defer non-critical session switch refreshes
This commit is contained in:
1 parent
e24e6c54a6
commit
93d8907a3e
7 files changed
+192
-50
No files matched your search
+108
-20
@@ -32,6 +32,7 @@ let _pendingCarryForwardSnapshot = null;
|
||||
let _draftSaveTimer = null;
|
||||
const _DRAFT_SAVE_DELAY_MS = 400;
|
||||
const NEW_CHAT_DRAFT_SESSION_KEY = 'hermes-new-chat-draft-session';
|
||||
const _composerDraftKnownPayloadSessions = new Set();
|
||||
|
||||
function _profileMatchesActiveProfile(profile, activeProfile){
|
||||
const eventName = (typeof profile === 'string' && profile.trim()) ? profile.trim() : 'default';
|
||||
@@ -98,21 +99,64 @@ async function _restoreRememberedNewChatDraftSession() {
|
||||
function _saveComposerDraft(sid, text, files) {
|
||||
if (!sid) return;
|
||||
clearTimeout(_draftSaveTimer);
|
||||
const normalizedText = String(text || '');
|
||||
const normalizedFiles = Array.isArray(files) ? files.filter(Boolean) : [];
|
||||
if (_composerDraftHasPayload(normalizedText, normalizedFiles)) {
|
||||
_composerDraftKnownPayloadSessions.add(sid);
|
||||
}
|
||||
_draftSaveTimer = setTimeout(() => {
|
||||
api('/api/session/draft', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ session_id: sid, text: text || '', files: files || [] }),
|
||||
body: JSON.stringify({ session_id: sid, text: normalizedText, files: normalizedFiles }),
|
||||
}).then(() => {
|
||||
_rememberComposerDraftPayloadState(sid, normalizedText, normalizedFiles);
|
||||
}).catch(() => {});
|
||||
}, _DRAFT_SAVE_DELAY_MS);
|
||||
}
|
||||
|
||||
function _composerDraftHasPayload(text, files) {
|
||||
return !!(String(text || '') || (Array.isArray(files) && files.filter(Boolean).length));
|
||||
}
|
||||
|
||||
function _sessionComposerDraftHasPayload(session) {
|
||||
const draft = session && session.composer_draft;
|
||||
return !!(draft && _composerDraftHasPayload(draft.text, draft.files));
|
||||
}
|
||||
|
||||
function _rememberComposerDraftPayloadState(sid, text, files) {
|
||||
if (!sid) return;
|
||||
const normalizedText = String(text || '');
|
||||
const normalizedFiles = Array.isArray(files) ? files.filter(Boolean) : [];
|
||||
if (_composerDraftHasPayload(normalizedText, normalizedFiles)) {
|
||||
_composerDraftKnownPayloadSessions.add(sid);
|
||||
} else {
|
||||
_composerDraftKnownPayloadSessions.delete(sid);
|
||||
}
|
||||
if (S.session && S.session.session_id === sid) {
|
||||
S.session.composer_draft = { text: normalizedText, files: normalizedFiles };
|
||||
}
|
||||
}
|
||||
|
||||
// Immediate save used before session switches.
|
||||
function _saveComposerDraftNow(sid, text, files) {
|
||||
if (!sid) return;
|
||||
if (!sid) return Promise.resolve();
|
||||
clearTimeout(_draftSaveTimer);
|
||||
const normalizedText = String(text || '');
|
||||
const normalizedFiles = Array.isArray(files) ? files.filter(Boolean) : [];
|
||||
// Most chat switches leave an empty composer. Avoid putting the switch path
|
||||
// behind a network POST unless there is new local draft content or an existing
|
||||
// server draft that must be cleared.
|
||||
if (!_composerDraftHasPayload(normalizedText, normalizedFiles)
|
||||
&& S.session && S.session.session_id === sid
|
||||
&& !_sessionComposerDraftHasPayload(S.session)
|
||||
&& !_composerDraftKnownPayloadSessions.has(sid)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return api('/api/session/draft', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ session_id: sid, text: text || '', files: files || [] }),
|
||||
body: JSON.stringify({ session_id: sid, text: normalizedText, files: normalizedFiles }),
|
||||
}).then(() => {
|
||||
_rememberComposerDraftPayloadState(sid, normalizedText, normalizedFiles);
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -161,9 +205,11 @@ function _clearComposerDraft(sid) {
|
||||
if (!sid) return;
|
||||
clearTimeout(_draftSaveTimer);
|
||||
_clearRememberedNewChatDraftSession(sid);
|
||||
api('/api/session/draft', {
|
||||
return api('/api/session/draft', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ session_id: sid, text: '' }),
|
||||
}).then(() => {
|
||||
_rememberComposerDraftPayloadState(sid, '', []);
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -1021,11 +1067,17 @@ async function newSession(flash, options={}){
|
||||
}
|
||||
updateQueueBadge(S.session.session_id);
|
||||
syncTopbar();renderMessages();
|
||||
const dirLoad=loadDir('.');
|
||||
// loadDir('.') is fire-and-forget while the workspace panel is closed:
|
||||
// waiting would block new-chat/profile-switch flow for users who never open
|
||||
// the file tree. When visible, wait so the file list lands with the session.
|
||||
if(options&&options.awaitWorkspaceLoad) await dirLoad;
|
||||
// Keep new-chat first paint instant. The workspace tree / git badge can
|
||||
// refresh right after paint unless this caller explicitly needs it loaded
|
||||
// before continuing (profile/default-workspace binding path).
|
||||
if(options&&options.awaitWorkspaceLoad){
|
||||
await loadDir('.');
|
||||
}else if(typeof _deferWorkspaceRefreshForSession==='function'){
|
||||
_deferWorkspaceRefreshForSession(S.session.session_id);
|
||||
}else{
|
||||
const _dirP=loadDir('.');
|
||||
if(_dirP&&typeof _dirP.catch==='function') _dirP.catch(()=>{});
|
||||
}
|
||||
// don't call renderSessionList here - callers do it when needed
|
||||
})();
|
||||
try{
|
||||
@@ -1308,14 +1360,15 @@ async function loadSession(sid){
|
||||
S._pendingSessionToolsets=null;
|
||||
if(typeof populateModelDropdown==='function'){
|
||||
const modelRefreshSid=sid;
|
||||
const isActiveModelRefreshSession=()=>!!(S.session&&S.session.session_id===modelRefreshSid);
|
||||
if(!S._bootReady&&typeof window!=='undefined'&&typeof window._startBootModelDropdown==='function'){
|
||||
Promise.resolve().then(()=>{
|
||||
if(_loadingSessionId!==modelRefreshSid) return;
|
||||
if(!isActiveModelRefreshSession()) return undefined;
|
||||
return window._startBootModelDropdown();
|
||||
}).catch(()=>{});
|
||||
}else{
|
||||
const modelRefreshPromise=Promise.resolve().then(()=>{
|
||||
if(_loadingSessionId!==modelRefreshSid) return;
|
||||
const modelRefreshPromise=_deferSessionSideEffect(modelRefreshSid,()=>{
|
||||
if(!isActiveModelRefreshSession()) return undefined;
|
||||
return populateModelDropdown({freshness:'session_visit'});
|
||||
}).catch(()=>{});
|
||||
if(typeof window!=='undefined') window._modelDropdownReady=modelRefreshPromise;
|
||||
@@ -1521,7 +1574,7 @@ async function loadSession(sid){
|
||||
const liveTurn=document.getElementById('liveAssistantTurn');
|
||||
if(!liveTurn||!liveTurn.querySelector('.tool-call-group[data-tool-worklog-group="1"]')) ensureLiveWorklogShell();
|
||||
}
|
||||
loadDir('.');
|
||||
_deferWorkspaceRefreshForSession(sid);
|
||||
setBusy(true);setComposerStatus('');
|
||||
startApprovalPolling(sid);
|
||||
if(typeof startClarifyPolling==='function') startClarifyPolling(sid);
|
||||
@@ -1600,7 +1653,7 @@ async function loadSession(sid){
|
||||
if(typeof ensureLiveWorklogShell==='function') ensureLiveWorklogShell();
|
||||
else appendThinking();
|
||||
}
|
||||
loadDir('.');
|
||||
_deferWorkspaceRefreshForSession(sid);
|
||||
updateQueueBadge(sid);
|
||||
startApprovalPolling(sid);
|
||||
if(typeof startClarifyPolling==='function') startClarifyPolling(sid);
|
||||
@@ -1614,11 +1667,10 @@ async function loadSession(sid){
|
||||
updateQueueBadge(sid);
|
||||
syncTopbar();renderMessages(sameSessionForceReload?{preserveScroll:true}:undefined);
|
||||
if(typeof resumeManualCompressionForSession==='function') resumeManualCompressionForSession(sid);
|
||||
const _dirP=loadDir('.');
|
||||
// Workspace refresh is guarded by session id inside loadDir(); do not
|
||||
// block session-load completion, draft restore, or model resolution on
|
||||
// file-tree IO for users focused on the chat.
|
||||
if(_dirP&&typeof _dirP.catch==='function') _dirP.catch(()=>{});
|
||||
// Workspace refresh is guarded by session id inside loadDir(); keep it
|
||||
// after the transcript's first paint so chat switching is not competing
|
||||
// with file-tree / git badge IO.
|
||||
_deferWorkspaceRefreshForSession(sid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2116,9 +2168,45 @@ async function _generateHandoffSummary(sid, rounds) {
|
||||
// retry.
|
||||
}
|
||||
|
||||
function _afterSessionFirstPaint(fn, delayMs=0){
|
||||
return new Promise((resolve)=>{
|
||||
const invoke=()=>{
|
||||
try{ resolve(typeof fn==='function' ? fn() : undefined); }
|
||||
catch(_){ resolve(undefined); }
|
||||
};
|
||||
const run=()=>{
|
||||
if(typeof requestIdleCallback==='function'){
|
||||
requestIdleCallback(invoke,{timeout:1500});
|
||||
}else{
|
||||
setTimeout(invoke, delayMs);
|
||||
}
|
||||
};
|
||||
if(typeof requestAnimationFrame==='function'){
|
||||
requestAnimationFrame(()=>requestAnimationFrame(run));
|
||||
}else{
|
||||
setTimeout(run, delayMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _deferSessionSideEffect(sid, fn, delayMs=0){
|
||||
if(!sid||typeof fn!=='function') return Promise.resolve();
|
||||
return _afterSessionFirstPaint(()=>{
|
||||
if(!S.session||S.session.session_id!==sid) return undefined;
|
||||
return fn();
|
||||
},delayMs);
|
||||
}
|
||||
|
||||
function _deferWorkspaceRefreshForSession(sid, opts={}){
|
||||
_deferSessionSideEffect(sid,()=>{
|
||||
const load=loadDir('.', opts);
|
||||
if(load&&typeof load.catch==='function') load.catch(()=>{});
|
||||
},150);
|
||||
}
|
||||
|
||||
function _resolveSessionModelForDisplaySoon(sid){
|
||||
if(!sid) return;
|
||||
setTimeout(async()=>{
|
||||
_deferSessionSideEffect(sid,async()=>{
|
||||
try{
|
||||
const data=await api(`/api/session?session_id=${encodeURIComponent(sid)}&messages=0&resolve_model=1`);
|
||||
const model=data&&data.session&&data.session.model;
|
||||
|
||||
@@ -290,13 +290,15 @@ def test_load_session_attaches_sse_before_auxiliary_work():
|
||||
# appendThinking, and renderMessages now takes a preserveScroll arg — so the
|
||||
# old contiguous "syncTopbar();renderMessages();appendThinking();loadDir('.');"
|
||||
# literal no longer exists. Assert each auxiliary call individually; all must
|
||||
# still run AFTER attachLiveStream (the invariant this test protects).
|
||||
# still run AFTER attachLiveStream (the invariant this test protects). The
|
||||
# workspace refresh is now scheduled through a first-paint deferral helper
|
||||
# rather than calling loadDir('.') inline.
|
||||
for marker in (
|
||||
"updateSendBtn();",
|
||||
"syncTopbar();",
|
||||
"renderMessages(",
|
||||
"appendThinking();",
|
||||
"loadDir('.');",
|
||||
"_deferWorkspaceRefreshForSession(sid);",
|
||||
"updateQueueBadge(sid);",
|
||||
"startApprovalPolling(sid)",
|
||||
):
|
||||
@@ -876,7 +878,9 @@ def test_load_session_restores_worklog_shell_before_reattach_replay():
|
||||
body = _function_body(SESSIONS_JS, "loadSession")
|
||||
fallback_pos = body.find("if(!restoredLiveTurn){")
|
||||
assert fallback_pos != -1, "loadSession must have a live-turn fallback branch"
|
||||
fallback_block = body[fallback_pos:body.find("loadDir('.')", fallback_pos)]
|
||||
refresh_pos = body.find("_deferWorkspaceRefreshForSession(sid);", fallback_pos)
|
||||
assert refresh_pos != -1, "fallback path should still schedule workspace refresh after first paint"
|
||||
fallback_block = body[fallback_pos:refresh_pos]
|
||||
clear_pos = fallback_block.find("clearLiveToolCards();")
|
||||
shell_pos = fallback_block.find("ensureLiveWorklogShell()")
|
||||
legacy_pos = fallback_block.find("else appendThinking();")
|
||||
@@ -939,7 +943,9 @@ def test_restore_succeeded_reconnect_skips_unkeyed_restored_tool_duplicates():
|
||||
assert "hasRestoredLiveToolRows&&!liveToolReplayId(tc)" in helper_block
|
||||
restore_block = body[restore_replay_pos:fallback_pos]
|
||||
assert "replayPersistedLiveToolCards({skipUnkeyedRestoredDuplicates:true});" in restore_block
|
||||
assert "replayPersistedLiveToolCards();" in body[fallback_pos:body.find("loadDir('.')", fallback_pos)]
|
||||
refresh_pos = body.find("_deferWorkspaceRefreshForSession(sid);", fallback_pos)
|
||||
assert refresh_pos != -1, "fallback path should still schedule workspace refresh after first paint"
|
||||
assert "replayPersistedLiveToolCards();" in body[fallback_pos:refresh_pos]
|
||||
|
||||
|
||||
def test_merge_inflight_tail_preserves_all_segmented_live_progress():
|
||||
|
||||
@@ -670,22 +670,39 @@ def test_populate_model_dropdown_accepts_session_visit_freshness_and_guards_stal
|
||||
assert live_tail.count("requestSeq!==null&&requestSeq!==_modelDropdownRequestSeq") >= 4
|
||||
|
||||
|
||||
def test_load_session_schedules_async_session_visit_model_refresh_after_metadata_load():
|
||||
def test_load_session_schedules_session_visit_model_refresh_before_message_load():
|
||||
body = _extract_function_body(_read_static("sessions.js"), "async function loadSession(")
|
||||
|
||||
assign_idx = body.index("S.session=data.session")
|
||||
boot_guard_idx = body.index("if(!S._bootReady&&typeof window!=='undefined'&&typeof window._startBootModelDropdown==='function'){")
|
||||
boot_promise_idx = body.index("Promise.resolve().then(()=>{", boot_guard_idx)
|
||||
boot_refresh_idx = body.index("return window._startBootModelDropdown();")
|
||||
else_idx = body.index("}else{", boot_refresh_idx)
|
||||
promise_idx = body.index("const modelRefreshPromise=Promise.resolve().then(", else_idx)
|
||||
ready_idx = body.index("window._modelDropdownReady=modelRefreshPromise")
|
||||
refresh_idx = body.index("populateModelDropdown({freshness:'session_visit'})")
|
||||
stale_guard_idx = body.index("_loadingSessionId!==modelRefreshSid")
|
||||
message_load_idx = body.index("await _ensureMessagesLoaded(sid)", assign_idx)
|
||||
failure_return_idx = body.index("return;", message_load_idx)
|
||||
model_block_idx = body.index("if(typeof populateModelDropdown==='function')", assign_idx)
|
||||
guard_helper_idx = body.index("const isActiveModelRefreshSession", model_block_idx)
|
||||
promise_idx = body.index("const modelRefreshPromise=_deferSessionSideEffect", model_block_idx)
|
||||
ready_idx = body.index("window._modelDropdownReady=modelRefreshPromise", promise_idx)
|
||||
refresh_idx = body.index("populateModelDropdown({freshness:'session_visit'})", promise_idx)
|
||||
|
||||
assert assign_idx < boot_guard_idx < boot_promise_idx < boot_refresh_idx < else_idx < promise_idx < refresh_idx < ready_idx
|
||||
assert boot_promise_idx < stale_guard_idx < boot_refresh_idx
|
||||
assert promise_idx < body.index("_loadingSessionId!==modelRefreshSid", promise_idx) < refresh_idx
|
||||
assert assign_idx < model_block_idx < message_load_idx < failure_return_idx
|
||||
assert model_block_idx < promise_idx < refresh_idx < ready_idx
|
||||
assert guard_helper_idx < promise_idx
|
||||
assert "_loadingSessionId!==modelRefreshSid" not in body[model_block_idx:ready_idx], (
|
||||
"deferred model refresh must guard on the active session, not _loadingSessionId, "
|
||||
"because loadSession clears _loadingSessionId when the first paint is complete"
|
||||
)
|
||||
|
||||
|
||||
def test_session_visit_model_refresh_is_deferred_until_after_first_paint():
|
||||
sessions = _read_static("sessions.js")
|
||||
defer_helper = _extract_function_body(sessions, "function _afterSessionFirstPaint(")
|
||||
side_effect_helper = _extract_function_body(sessions, "function _deferSessionSideEffect(")
|
||||
load_body = _extract_function_body(sessions, "async function loadSession(")
|
||||
|
||||
assert "requestAnimationFrame(()=>requestAnimationFrame(run))" in defer_helper
|
||||
assert "requestIdleCallback(invoke,{timeout:1500})" in defer_helper
|
||||
assert "return _afterSessionFirstPaint(()=>" in side_effect_helper
|
||||
assert "const modelRefreshPromise=_deferSessionSideEffect" in load_body
|
||||
assert "isActiveModelRefreshSession()" in load_body
|
||||
assert "return populateModelDropdown({freshness:'session_visit'});" in load_body
|
||||
|
||||
|
||||
def test_boot_model_dropdown_clears_cached_ready_on_401():
|
||||
|
||||
@@ -76,6 +76,26 @@ def test_session_switch_awaits_immediate_draft_flush_before_loading_target():
|
||||
)
|
||||
|
||||
|
||||
def test_immediate_empty_draft_flush_clears_locally_known_server_draft():
|
||||
assert "const _composerDraftKnownPayloadSessions = new Set();" in SESSIONS_JS
|
||||
save_start = SESSIONS_JS.find("function _saveComposerDraft(")
|
||||
save_end = SESSIONS_JS.find("function _composerDraftHasPayload", save_start)
|
||||
now_start = SESSIONS_JS.find("function _saveComposerDraftNow(")
|
||||
now_end = SESSIONS_JS.find("// Restore composer draft", now_start)
|
||||
assert save_start != -1 and save_end != -1
|
||||
assert now_start != -1 and now_end != -1
|
||||
save_body = SESSIONS_JS[save_start:save_end]
|
||||
now_body = SESSIONS_JS[now_start:now_end]
|
||||
assert "_composerDraftKnownPayloadSessions.add(sid);" in save_body, (
|
||||
"debounced non-empty draft saves must mark the session as having server-side draft payload"
|
||||
)
|
||||
assert "_rememberComposerDraftPayloadState(sid, normalizedText, normalizedFiles);" in save_body
|
||||
assert "!_composerDraftKnownPayloadSessions.has(sid)" in now_body, (
|
||||
"empty immediate switch-away saves may only be skipped when no local/server draft payload is known"
|
||||
)
|
||||
assert "_rememberComposerDraftPayloadState(sid, normalizedText, normalizedFiles);" in now_body
|
||||
|
||||
|
||||
def test_pre_switch_draft_flush_rechecks_stale_loading_guard():
|
||||
"""The awaited draft-save in loadSession yields the event loop. On a rapid
|
||||
session switch (B then quickly C) the stale B continuation must bail out
|
||||
@@ -117,6 +137,12 @@ def test_clear_composer_draft_forgets_same_new_chat_candidate():
|
||||
assert "_clearRememberedNewChatDraftSession(sid);" in body, (
|
||||
"sending a draft must stop New Chat from restoring that now-cleared candidate"
|
||||
)
|
||||
assert "return api('/api/session/draft'" in body, (
|
||||
"clear path should return its POST promise for callers/tests that need to await it"
|
||||
)
|
||||
assert "_rememberComposerDraftPayloadState(sid, '', []);" in body, (
|
||||
"clear path must also clear local draft-payload tracking so send-then-switch can skip redundant empty flushes"
|
||||
)
|
||||
|
||||
|
||||
def test_boot_restore_preserves_zero_message_session_with_composer_draft():
|
||||
|
||||
@@ -80,30 +80,30 @@ class TestLoadSessionIdleOverlap:
|
||||
|
||||
found = False
|
||||
for pos in positions:
|
||||
# Window widened 600→850 (#3326 added a reload-width-hint comment +
|
||||
# conditional preserveScroll arg in the idle branch, pushing the
|
||||
# _dirP.catch line further from the S.busy=false anchor).
|
||||
block = SESSIONS_JS[pos : pos + 850]
|
||||
has_loaddir = "loadDir('.')" in block
|
||||
# Window widened 600→950 (#3326 added a reload-width-hint comment +
|
||||
# conditional preserveScroll arg in the idle branch; the workspace
|
||||
# refresh now routes through a first-paint deferral helper instead of
|
||||
# a direct loadDir('.') call).
|
||||
block = SESSIONS_JS[pos : pos + 950]
|
||||
has_deferred_workspace = "_deferWorkspaceRefreshForSession(sid);" in block
|
||||
# #3326 added an optional {preserveScroll} arg to the idle-path render
|
||||
# call; match the call form rather than the bare `renderMessages()`.
|
||||
has_render = "renderMessages(" in block
|
||||
if has_loaddir and has_render:
|
||||
if has_deferred_workspace and has_render:
|
||||
found = True
|
||||
assert "highlightCode()" not in block, (
|
||||
"The idle path should rely on renderMessages()'s consolidated "
|
||||
"post-render pass instead of running a second highlight pass."
|
||||
)
|
||||
assert "_dirP" in block and "await _dirP" not in block, (
|
||||
"loadDir() should refresh the workspace without blocking "
|
||||
"session-load completion."
|
||||
assert block.index("renderMessages(") < block.index("_deferWorkspaceRefreshForSession(sid);"), (
|
||||
"workspace refresh should be deferred until after the session "
|
||||
"transcript render starts."
|
||||
)
|
||||
assert "_dirP.catch" in block
|
||||
break
|
||||
|
||||
assert found, (
|
||||
"Could not find the idle path in loadSession that calls both "
|
||||
"renderMessages and loadDir."
|
||||
"renderMessages and _deferWorkspaceRefreshForSession."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -133,8 +133,12 @@ class TestParallelizedFetches:
|
||||
fn = self._get_switch_fn()
|
||||
assert "awaitWorkspaceLoad: workspaceVisible" in fn
|
||||
sessions_js = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
||||
assert "if(options&&options.awaitWorkspaceLoad) await dirLoad" in sessions_js
|
||||
assert "loadDir('.') is fire-and-forget while the workspace panel is closed" in sessions_js
|
||||
assert "if(options&&options.awaitWorkspaceLoad){" in sessions_js
|
||||
assert "await loadDir('.')" in sessions_js
|
||||
assert "typeof _deferWorkspaceRefreshForSession==='function'" in sessions_js
|
||||
assert "_deferWorkspaceRefreshForSession(S.session.session_id)" in sessions_js
|
||||
assert "const _dirP=loadDir('.')" in sessions_js
|
||||
assert "Keep new-chat first paint instant" in sessions_js
|
||||
|
||||
|
||||
class TestSpinnerCss:
|
||||
|
||||
@@ -319,7 +319,8 @@ def test_server_runtime_journal_snapshot_restores_structured_inflight_state():
|
||||
helper_pos = SESSIONS_SRC.index("function _serverLiveSnapshotToolId")
|
||||
helper_block = SESSIONS_SRC[helper_pos : helper_pos + 3600]
|
||||
load_pos = SESSIONS_SRC.index("async function loadSession")
|
||||
load_block = SESSIONS_SRC[load_pos : load_pos + 20000]
|
||||
load_end = SESSIONS_SRC.index("// ── Handoff hint logic", load_pos)
|
||||
load_block = SESSIONS_SRC[load_pos:load_end]
|
||||
|
||||
assert "runtime_journal_snapshot" in load_block
|
||||
assert "_serverLiveSnapshotInflight(S.session.runtime_journal_snapshot" in load_block
|
||||
|
||||
Reference in new issue
Block a user