fix(routes): preserve live cards on cross-profile 409 collisions

Gate follow-up for #7710. Three front-end handlers treated EVERY 409 as a
domain-specific stale/expired signal, so the new session_profile_mismatch
response would have:

- messages.js: hidden a LIVE clarify card and mislabelled it expired
- ui.js: retired a LIVE compression-recovery card with a false note
- commands.js: rendered a false 'Compression failed' error state

Each now routes through _sessionProfileMismatchFromError() first and leaves
the card standing. Also captures the status in the import_cli test mock so a
wrong status can no longer pass (the mock swallowed it into *_).
This commit is contained in:
nesquena-hermes authored and n committed 2026-09-22 10:21:22 +00:00
1 parent d698fb4772
commit 0378ed4e34
4 files changed
+38 -4

No files matched your search

+6
View File
@@ -991,6 +991,12 @@ async function resumeManualCompressionForSession(sid){
// No active compression job or transient server error — not a real failure. // No active compression job or transient server error — not a real failure.
// 404: route missed or session gone; 5xx: backend exception during status check. // 404: route missed or session gone; 5xx: backend exception during status check.
if(e&&(!e.status||e.status===404||e.status>=500)) return; if(e&&(!e.status||e.status===404||e.status>=500)) return;
// #7710: a cross-profile refusal now arrives as 409
// (``session_profile_mismatch``) where it used to be a 404 that hit the
// benign early-return above. It is not a compression failure, so do not
// render the error state or locally settle the compression UI.
if(e&&e.status===409&&typeof _sessionProfileMismatchFromError==='function'
&&_sessionProfileMismatchFromError(e)) return;
if(S.session&&S.session.session_id===sid&&typeof setCompressionUi==='function'){ if(S.session&&S.session.session_id===sid&&typeof setCompressionUi==='function'){
const visibleMessages=_manualCompressionVisibleMessages(); const visibleMessages=_manualCompressionVisibleMessages();
setCompressionUi({ setCompressionUi({
+13
View File
@@ -8988,6 +8988,19 @@ async function respondClarify(response) {
// not tear B down on A's late 409. The SSE/poll path will re-render the // not tear B down on A's late 409. The SSE/poll path will re-render the
// next prompt's card from scratch via ``showClarifyCard`` either way. // next prompt's card from scratch via ``showClarifyCard`` either way.
if (e && e.status === 409) { if (e && e.status === 409) {
// #7710: a cross-profile refusal now also arrives as 409
// (``session_profile_mismatch``). The prompt is NOT expired — the write
// was refused because the session belongs to another profile. Treating
// it as expired would hide a live clarification card and mislabel the
// cause, so leave the card standing and report the real reason.
if (typeof _sessionProfileMismatchFromError === 'function'
&& _sessionProfileMismatchFromError(e)) {
_clarifySetControlsDisabled(false, false);
if (typeof setStatus === "function") {
setStatus("Clarify: session belongs to a different profile");
}
return;
}
if (_clarifyId === clarifyId) { if (_clarifyId === clarifyId) {
// Same card still showing — dismiss it and rescue the typed draft. // Same card still showing — dismiss it and rescue the typed draft.
// Order matters: ``_stashClarifyDraft`` (called from // Order matters: ``_stashClarifyDraft`` (called from
+10
View File
@@ -508,6 +508,16 @@ async function startCompressionRecovery(btn){
const composer=$('msg'); const composer=$('msg');
if(composer&&typeof composer.focus==='function') composer.focus(); if(composer&&typeof composer.focus==='function') composer.focus();
}catch(e){ }catch(e){
// #7710: a cross-profile refusal now also arrives as 409
// (``session_profile_mismatch``). That is NOT a stale recovery action —
// the card is still valid, the request was simply refused because the
// session belongs to another profile. Retiring it would hide a live card
// and show a false "conversation already moved on" note.
if(e&&e.status===409&&typeof _sessionProfileMismatchFromError==='function'
&&_sessionProfileMismatchFromError(e)){
if(typeof setStatus==='function') setStatus('Session belongs to a different profile');
return;
}
// A 409 means this session no longer has an active recovery action (the // A 409 means this session no longer has an active recovery action (the
// session already moved on — e.g. a substantive prompt cleared it). The // session already moved on — e.g. a substantive prompt cleared it). The
// persisted card in the transcript is stale, so retire it and show a neutral // persisted card in the transcript is stale, so retire it and show a neutral
@@ -44,10 +44,14 @@ def _capture(monkeypatch):
# #7710: the real ``j`` signature is ``j(handler, payload, status=200, # #7710: the real ``j`` signature is ``j(handler, payload, status=200,
# extra_headers=None, *, pretty=True)``; the mock has to accept the # extra_headers=None, *, pretty=True)``; the mock has to accept the
# status keyword so the helper's 409 ``session_profile_mismatch`` # status keyword so the helper's 409 ``session_profile_mismatch``
# payload can be captured here. # payload can be captured here. Capture the STATUS too — swallowing it
monkeypatch.setattr( # into ``*_`` would let a wrong status (200/404) pass this test.
routes, "j", lambda h, o, *_, **__: (cap.__setitem__("ok", o), True)[1] def _fake_j(h, o, status=200, *_, **__):
) cap["ok"] = o
cap["ok_status"] = status
return True
monkeypatch.setattr(routes, "j", _fake_j)
monkeypatch.setattr( monkeypatch.setattr(
routes, routes,
"bad", "bad",
@@ -98,6 +102,7 @@ def test_import_cli_existing_foreign_profile_unqualified_request_404(monkeypatch
) )
assert "ok" in cap, f"expected 409 session_profile_mismatch, got {cap}" assert "ok" in cap, f"expected 409 session_profile_mismatch, got {cap}"
assert cap["ok_status"] == 409, f"expected status 409, got {cap.get('ok_status')}"
assert cap["ok"] == { assert cap["ok"] == {
"error": "Session belongs to a different profile", "error": "Session belongs to a different profile",
"code": "session_profile_mismatch", "code": "session_profile_mismatch",