fix(webui): withdraw request writes when a refresh resolves archived
A same-thread refresh kept the last resolved live controls until the session and history reads finished. When the fresh project lookup finds the project archived or the archive state unknown, withdraw the unguarded thread controls and remount the request panel fenced before those reads.
This commit is contained in:
1 parent
4c85d37842
commit
e75c339470
4 files changed
+43
-6
No files matched your search
@@ -6295,3 +6295,5 @@ R5 quote fallback room (#797 D3, 2026-11-06, MVP3 lead): r4 review P2 found that
|
||||
2026-11-06 | #797 MVP3 project archive r7 — WebUI implementation | Before: r6 head 51892cf95 lost known archived state during failed refresh, reopening repository Save and room Open and exposing guarded thread controls. After: settings, thread and room derive archive state from the displayed metadata through pending and failed reads; successful live reads restore controls, and refusal clearing remains intact. Node 26 final WebUI 658 pass / 0 fail / 0 cancelled / 0 skipped; four browser regressions fail on exact r6 for expected behavior. An earlier diagnostic whole suite had one 1440px remote-fixture timeout; candidate and r6 isolated reruns each passed 4/4, and the final whole suite passed that case. Room Send remains disabled on failed membership reads as before. Full iterations and comparison: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r7-report.md and project-archive-r7-evidence/. Bus/harness/CLI not run. Queue refused this linked worktree; no push, merge, deployment or server guard change. Local implementation candidate for independent review.
|
||||
|
||||
2026-11-06 UTC | #797 MVP3 project archive r8 — WebUI implementation | Before: r7 head 5bf07f35f temporarily withdrew live thread controls and showed archive-unavailable notices on every same-thread refresh without a failed read. After: a non-navigation redraw retains the resolved archive state only for the same thread with its project in retained metadata; missing retained metadata on failed reads remains unknown. Two delayed refresh ticks retain live request, session, share, cancel and revert controls; a thread switch still withdraws them and stays unknown after failure. Node 26: exact-r7 negative reproduces disabled session control, switch control passes; focused archive browser files 11/11; full WebUI 660 pass / 0 fail / 0 cancelled / 0 skipped, versus r7 658/0. The 1440px remote fixture passed. Bus/harness/CLI not run; project-files excluded. Queue refused linked worktree; no queue move, push, merge, deployment or server guard change. Local candidate for independent review. Evidence/report: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r8-evidence/ and /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r8-report.md.
|
||||
|
||||
2026-11-06 UTC | #797 MVP3 project archive r9 — WebUI fix for the r8 review P1 | Before: on r8 head 4c85d3784, a same-thread redraw kept the last resolved live controls. If the fresh lookup then found the project archived (listed or resolved) or unknown, the mounted request panel still offered mode, cancel, revoke, resend and answer, and the session, share, unshare, cancel and revert controls stayed enabled until the slower session and history reads finished. After: right after the fresh project lookup, a same-thread refresh that resolves archived or unknown withdraws those controls and remounts the request panel fenced, before the session read. The navigation and initial-load paths are unchanged. New browser test with the sessions read held open covers listed archived, resolved archived and unknown, and asserts no write. Node 26: negative control on exact r8 fails for behavior ("listed archived: [data-request-mode] offered while the sessions read is pending"); the r8 reviewer probe goes from 3/1 on r8 to 4/0; focused archive browser files 12/12; whole WebUI 661 pass / 0 fail / 0 cancelled / 0 skipped, versus r8 660/0. Bus/harness/CLI not run; project-files excluded. No push, merge, deployment or server guard change. Local candidate for independent review. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r9-evidence/.
|
||||
@@ -2095,3 +2095,4 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-11-06 | Codex implementation sub-agent | #797 project archive r6 WebUI fixes on 8c3f48113 | Known-archived unguarded writes suppressed in controls and handlers; final Node 26 WebUI 654/0/0/0, five new tests and changed navigation assertion fail on baseline as expected, real Store/Broker guarded-refusal probes 4/4. Full inventory, diagnostic iterations and evidence: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r6-report.md. Local candidate for independent review; queue refused linked worktree; no push, merge or deployment.
|
||||
2026-11-06 | Codex implementation sub-agent | #797 project archive r7 WebUI correction on 51892cf95 | Retained archived state fences settings, thread and room writes across failed refresh; final Node 26 WebUI 658/0/0/0, four exact-baseline behavioral negatives, 1440px diagnostic timeout and candidate/base isolated reruns documented. Room Send still requires a successful membership read. Evidence and handoff: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r7-report.md. Local candidate for independent review; queue refused linked worktree; no push, merge or deployment.
|
||||
2026-11-06 | Codex implementation sub-agent | #797 project archive r8 WebUI correction on 5bf07f35f | Same-thread refresh retains resolved live archive state; switch and failed-lookup withdrawal retained. Exact-r7 behavioral negative reproduced; Node 26 archive browser files 11/11 and full WebUI 660/0/0/0. Evidence and handoff: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r8-report.md. Local candidate for independent review; queue refused linked worktree; no push, merge or deployment.
|
||||
2026-11-06 | mvp3-lead-797 (Claude, #797 lane) | project archive r9 WebUI fix on 4c85d3784 for the r8 review P1 | A same-thread refresh that resolves archived or unknown withdraws request-panel and unguarded thread writes before the session read. r8 negative fails for behavior; reviewer probe 4/0; focused 12/12; whole WebUI 661/0/0/0. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r9-evidence/. Local candidate for independent review; no push, merge or deployment.
|
||||
@@ -489,17 +489,20 @@
|
||||
current.projectArchived = current.projects.some(p => p.id === (kind === 'project' ? id : current.thread?.project) && p.archived);
|
||||
if (kind === 'thread' && old?.kind === 'thread' && old.id === id && !navigated && old.archiveUnknown === false && old.projects.some(p => p.id === old.thread?.project)) current.archiveUnknown = false;
|
||||
if (current.projectArchived) current.archiveUnknown = false;
|
||||
if (navigated || !old) container.innerHTML = '<p class="chat-loading" role="status">Reading chat…</p>';
|
||||
else if (current.archiveUnknown) {
|
||||
// Keep the previous conversation in place while its archive lookup is pending,
|
||||
// but immediately withdraw controls whose server paths lack archive guards.
|
||||
// Keep the previous conversation in place, but immediately withdraw controls whose
|
||||
// server paths lack archive guards, and the request panel's writes.
|
||||
const withdraw = () => {
|
||||
for (const el of container.querySelectorAll('#chat-session-form input, #chat-session-form button, #chat-share-form input, #chat-share-form button, [data-unshare], #chat-cancel, [data-revert]')) el.disabled = true;
|
||||
window.MosaicRequests?.mount?.(container, { archiveUnknown: true });
|
||||
window.MosaicRequests?.mount?.(container, { archiveUnknown: !!current.archiveUnknown, projectArchived: !!current.projectArchived });
|
||||
if (!current.archiveUnknown) return;
|
||||
const notice = document.createElement('p');
|
||||
notice.setAttribute('role', 'status');
|
||||
notice.textContent = 'Archive status unavailable. Some changes are unavailable; sending may be refused; refresh to check.';
|
||||
container.querySelector('.chat-area')?.prepend(notice);
|
||||
}
|
||||
};
|
||||
const withdrawn = !(navigated || !old) && current.archiveUnknown;
|
||||
if (navigated || !old) container.innerHTML = '<p class="chat-loading" role="status">Reading chat…</p>';
|
||||
else if (current.archiveUnknown) withdraw(); // archive lookup pending
|
||||
try {
|
||||
if (user) {
|
||||
const config = await fetch('/api/config', { cache: 'no-store', redirect: 'manual', signal });
|
||||
@@ -545,6 +548,10 @@
|
||||
}
|
||||
else if (projectId) current.archiveUnknown = false;
|
||||
current.projectArchived = current.projects.some(p => p.id === projectId && p.archived);
|
||||
// A same-thread refresh kept the controls of the last resolved (live) state. If this
|
||||
// lookup now finds the project archived or unknown, withdraw them before the slower
|
||||
// session and history reads, not at the next paint.
|
||||
if (kind === 'thread' && old && !navigated && !withdrawn && (current.archiveUnknown || current.projectArchived)) withdraw();
|
||||
const catalog = kind === 'thread' && !current.shared ? await request(`threads/${id}/sessions`, undefined, signal).catch(err => {
|
||||
if (err.code === 'access-refused' && !match[3]) return null;
|
||||
throw err;
|
||||
|
||||
@@ -18,6 +18,7 @@ async function boot(t) {
|
||||
window.fetch=async(url,init={})=>{
|
||||
const path=String(url),method=(init.method||'GET').toUpperCase();window.__trace.push({path,method});
|
||||
if(path==='/api/chat/projects' && window.__pauseProjects) await new Promise(resolve=>window.__releaseProjects=resolve);
|
||||
if(path.endsWith('/sessions') && window.__pauseSessions) await new Promise(resolve=>window.__releaseSessions=resolve);
|
||||
if((path==='/api/chat/threads' || path==='/api/chat/threads?shared=true') && window.__pauseThreads) await new Promise(resolve=>(window.__releaseThreads ||= []).push(resolve));
|
||||
if(path==='/api/chat/projects' && window.__failProjects) return new Response(JSON.stringify({error:'unavailable'}),{status:window.__failProjects===true?503:window.__failProjects});
|
||||
if(path==='/api/config'){const r=await original(url,init),v=await r.json();return new Response(JSON.stringify({...v,projectRemotes:true,projectRemoteWrites:true}));}
|
||||
@@ -73,6 +74,32 @@ test('switching threads withdraws controls while unresolved and keeps unknown af
|
||||
assert.match(await b.evaluate(`document.querySelector('.chat-area').textContent`), /Archive status unavailable/);
|
||||
assert.equal(await b.evaluate(`!!document.querySelector('#chat-session-form, [data-request-cancel]')`), false);
|
||||
});
|
||||
test('same-thread refresh withdraws writes once archived or unknown resolves, before session reads', async t => {
|
||||
const { b } = await boot(t), path = `/projects/threads/${THREAD}`;
|
||||
const variants = [
|
||||
['listed archived', `window.__liveProjects=[${JSON.stringify(archived)}];`, /This project is archived/],
|
||||
['resolved archived', `window.__liveProjects=[];window.__archiveRows=[${JSON.stringify(archived)}];`, /This project is archived/],
|
||||
['unknown', `window.__liveProjects=[];window.__archiveRows=[];window.__archiveFailure='network';`, /Archive status unavailable/],
|
||||
];
|
||||
for (const [name, change, after] of variants) {
|
||||
await b.evaluate(`window.__archiveFailure=false;true`); await setLive(b); await refresh(b, path);
|
||||
await until(b, `document.querySelector('[data-request-mode]') && document.querySelector('#chat-session-form button') && !document.querySelector('#chat-session-form button').disabled`);
|
||||
await b.evaluate(`${change}window.__pauseSessions=true;window.__releaseSessions=null;window.__trace=[];window.__refresh=window.MosaicChat.render({path:${JSON.stringify(path)},container:document.querySelector('#bus-view'),canSend:true,navigated:false});true`);
|
||||
await until(b, `window.__releaseSessions`);
|
||||
// The archive state is resolved; the sessions read and the repaint are still pending.
|
||||
for (const selector of ['[data-request-mode]', '[data-request-cancel]', '[data-request-revoke]', '[data-request-resend]', '.request-answer button'])
|
||||
assert.equal(await b.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`), false, `${name}: ${selector} offered while the sessions read is pending`);
|
||||
for (const selector of ['#chat-session-form input', '#chat-session-form button', '#chat-share-form button', '[data-unshare]', '#chat-cancel', '[data-revert]'])
|
||||
assert.equal(await b.evaluate(`[...document.querySelectorAll(${JSON.stringify(selector)})].every(e=>e.disabled)`), true, `${name}: ${selector} enabled while the sessions read is pending`);
|
||||
for (const [attr, value] of [['mode', 'autonomous'], ['cancel', ''], ['cancel-confirm', ''], ['resend', ''], ['revoke', '']]) await force(b, `<button data-request-${attr}="${value}">Go</button>`, `[data-request-${attr}]`);
|
||||
await force(b, '<form class="request-answer"><textarea>answer</textarea><button>Send</button></form>', '.request-answer', true);
|
||||
await noWrites(b);
|
||||
await b.evaluate(`window.__pauseSessions=false;window.__releaseSessions();window.__refresh.then(()=>true)`);
|
||||
assert.match(await b.evaluate(`document.querySelector('.chat-area').textContent`), after, name);
|
||||
assert.equal(await b.evaluate(`!!document.querySelector('[data-request-mode], [data-request-cancel]')`), false, `${name}: request writes after the repaint`);
|
||||
await noWrites(b);
|
||||
}
|
||||
});
|
||||
async function setArchived(b) {
|
||||
await b.evaluate(`window.__liveProjects=[];window.__archiveRows=[${JSON.stringify(archived)}];true`);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user