fix(webui): record request misses only from the request read (#797)
The chat.js fetch wrapper recorded a request-not-found from /api/requests/<id>/workspace as a missing request and then served a synthetic 404 to the request panel's rereads, failing 11 request-browser tests. Only the request's own read records a miss now. Adds a regression test that fails on 9d3eb9e11, and bounds the r3 held-404 test's check on its refresh reads, which raced Files' cached repaint under load.
This commit is contained in:
1 parent
9d3eb9e115
commit
1186cf7b1e
4 files changed
+58
-3
No files matched your search
@@ -6318,3 +6318,4 @@ R5 quote fallback room (#797 D3, 2026-11-06, MVP3 lead): r4 review P2 found that
|
||||
2026-11-06 UTC | #797 round A task evidence and request polling | Before: advance_request omitted missing per-task push/review chains, the coder continuation seeded the first task regardless of evidence, and missing request IDs were polled repeatedly. After: the broker names all incomplete task chains and supplies the same read-only result to continuation selection; scoped harness serialization exposes the refusal with the existing 300-character bound; thread-page readers suppress confirmed missing IDs until a new caught-up thread message. The owner authorized the narrow harness socket addition. Exact-base controls: five behavioral failures in the initial control and one in the bounded harness control. Node 26 serial named-file gate: 142 pass, 0 fail, 0 skip. An intermediate rerun was deliberately stopped after the owner clarified the bound; its log is retained. Queue refused this linked worktree. Local candidate for independent review; no push or deployment. Report and evidence: /mnt/storage/scratch/mosaic-lead/mvp3/roundA-fix-report.md and /mnt/storage/scratch/mosaic-lead/mvp3/roundA-fix-evidence/.
|
||||
2026-11-06 UTC | #797 round A fix r2 (Sol r1 P2 + two P3) | Before (97d670da9): a remembered request-not-found was retried only when the newest message on a caught-up thread changed, so a request opened with no new message stayed hidden; misses grew with paged history; an aborted read got a fabricated 404. After: misses are also cleared when the owner opens the thread (navigated) and when a mosaic-request-state for the id is seen anywhere on the page; timer refreshes and repaints still never retry. Misses are pruned to ids in the retained rows (200-row cap). An already-aborted read bypasses the memory and keeps the native AbortError. Remaining limit: the thread page reads only messages, so an event-only open with the thread left open and no navigation shows at the next message, navigation or reload. Closing that needs a server-visible thread signal, outside this change. New tests fail on 97d670da9 chat.js (2/2); webui chat-sessions-access, chat and browser files 40/0/0 (Node 26 serial). Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/roundA-fix-r2-evidence/.
|
||||
2026-11-06 UTC | #797 round A fix r3 (Sol r2 P2) | Before (16384bb12): a mosaic-request-state for an id advanced the thread's epoch only when a remembered miss was deleted, so a 404 still in flight from before the observation could record the miss afterwards and timer refreshes kept fabricating 404s for an open request. After: each observed state advances that id's own generation on every thread that retains it; an in-flight 404 records a miss only if both the thread epoch and the id generation are unchanged. Other ids' slow misses still settle. Generations are pruned to retained rows with the misses. New held-404 test fails on 16384bb12 chat.js at recovery; webui chat-sessions-access, chat and browser files 41/0/0 (Node 26 serial). Lead ruling: no third review round. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/roundA-fix-r3-evidence/.
|
||||
2026-11-06 UTC | #797 round A fix r4 (Lead push gate red on 9d3eb9e11) | Before (9d3eb9e11): the chat.js fetch wrapper recorded a request-not-found from /api/requests/<id>/workspace as a missing request, then served a synthetic 404 to every later read of that id, including request panel rereads; request-browser.test.mjs failed 11/24 (its stub has no workspace route), base 6b84fe136 24/0. After: only the request's own read records a miss; a remembered miss still answers its workspace read in the page. New regression test fails on 9d3eb9e11 chat.js and passes on the fix. The r3 held-404 test raced Files' cached repaint under concurrency 3 (678/1; alone 5/5 pass); it now waits, bounded, for the refresh's reads to reach the server and still fails on 16384bb12. request-browser 24/0; whole webui suite except project-files 679/0/0/0 (Node 26, concurrency 3). Correction: the r1-r3 author gate ran only chat-sessions-access, chat and browser and omitted request-browser.test.mjs, which also exercises chat.js; that omission let this regression reach the push gate. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/roundA-fix-push-gate/r4-*.
|
||||
@@ -2107,3 +2107,4 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-11-06 UTC | Codex implementation sub-agent | #797 roundA-fix on 6b84fe136 | Named per-task push/review gaps, shared the check with coder continuation and stopped missing-request thread polling; owner authorized bounded harness socket reason forwarding. Exact-base behavioral negatives and Node 26 six-file gate 142/0/0/0 retained in /mnt/storage/scratch/mosaic-lead/mvp3/roundA-fix-evidence/; local candidate for independent review, no push or deployment. Queue refused linked worktree.
|
||||
2026-11-06 UTC | mvp3-lead-797 (Claude, #797 lane) | roundA-fix r2 on 97d670da9 for Sol r1 P2/P3 | Missing-request memory now also clears on thread open and on an observed request state, is pruned to retained rows, and keeps AbortError; event-only open without navigation documented as a remaining limit. Negative 0/2 on 97d670da9, webui gate 40/0/0. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/roundA-fix-r2-evidence/.
|
||||
2026-11-06 UTC | mvp3-lead-797 (Claude, #797 lane) | roundA-fix r3 on 16384bb12 for Sol r2 P2 | A request state observed before an in-flight 404 settles now fences that miss via a per-id generation. Negative fails on 16384bb12, webui gate 41/0/0. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/roundA-fix-r3-evidence/.
|
||||
2026-11-06 UTC | mvp3-lead-797 (Claude, #797 lane) | roundA-fix r4 on 9d3eb9e11 for the Lead's red webui push gate | Workspace 404s no longer record a request miss; the r3 held-404 test waits for its refresh reads. request-browser 24/0, whole webui 679/0/0/0; r1-r3 gate omission of request-browser recorded in BUILD-LOG. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/roundA-fix-push-gate/r4-*.
|
||||
@@ -208,7 +208,9 @@
|
||||
const id = path[1], epoch = s.requestEpoch ?? 0, seen = s.requestSeen?.get(id) ?? 0;
|
||||
if (s.missingRequests?.has(id)) return new Response(JSON.stringify({ error: 'request-not-found' }), { status: 404, headers: { 'content-type': 'application/json' } });
|
||||
const response = await fetchPage(url, init);
|
||||
if (response.status === 404) {
|
||||
// Only the request's own read records a miss. A workspace 404 is about the workspace and must
|
||||
// not hide a request whose state still reads.
|
||||
if (response.status === 404 && !path[0].endsWith('/workspace')) {
|
||||
const body = await response.clone().json().catch(() => null);
|
||||
if (body?.error === 'request-not-found' && !out() && (s.requestEpoch ?? 0) === epoch && (s.requestSeen?.get(id) ?? 0) === seen) {
|
||||
(s.missingRequests ??= new Set()).add(id);
|
||||
|
||||
@@ -278,6 +278,57 @@ test('a request state observed before an in-flight 404 settles keeps that miss f
|
||||
const before = calls.length;
|
||||
await render(false);
|
||||
await wait(`document.querySelector('[data-chat-summary]')?.textContent.includes(${JSON.stringify(ids[1])}) && document.querySelector('[data-chat-files]')?.textContent.includes('draft-request-late.html')`);
|
||||
assert.ok(calls.slice(before).some(c => c.id === ids[1] && c.part === 'get'));
|
||||
assert.ok(calls.slice(before).some(c => c.id === ids[1] && c.part === 'workspace'));
|
||||
// Files paints its cached copy first and reads behind it, so wait (bounded) for this refresh's
|
||||
// own reads to reach the server. A remembered miss is answered in the page and never arrives.
|
||||
const reached = part => calls.slice(before).some(c => c.id === ids[1] && c.part === part);
|
||||
for (let i = 0; i < 150 && !(reached('get') && reached('workspace')); i++) await new Promise(r => setTimeout(r, 20));
|
||||
assert.ok(reached('get'));
|
||||
assert.ok(reached('workspace'));
|
||||
});
|
||||
|
||||
test('a workspace request-not-found never hides a request whose state still reads', async t => {
|
||||
const project = '10000000-0000-4000-8000-000000000001', thread = '20000000-0000-4000-8000-000000000001', session = '30000000-0000-4000-8000-000000000001';
|
||||
const id = 'request-real';
|
||||
const rows = [{ id: '40000000-0000-4000-8000-000000000001', from_role: 'human', body: 'Build it', request: id, at: '2026-11-06T12:00:00Z' }];
|
||||
const calls = [];
|
||||
let b;
|
||||
const server = createServer((req, res) => {
|
||||
const url = new URL(req.url, 'http://fixture');
|
||||
const json = (status, value) => { res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(value)); };
|
||||
if (url.pathname === '/') { res.writeHead(200, { 'content-type': 'text/html' }); return res.end('<!doctype html><body><div id="view"></div><script src="/files.js"></script><script src="/chat.js"></script><script src="/requests.js"></script></body>'); }
|
||||
if (['/files.js', '/chat.js', '/requests.js'].includes(url.pathname)) { res.writeHead(200, { 'content-type': 'text/javascript' }); return res.end(readFileSync(new URL('../src/public' + url.pathname, import.meta.url))); }
|
||||
if (url.pathname === '/api/config') return json(200, { requests: true, requestWrites: true });
|
||||
if (url.pathname === '/api/chat/projects') return json(200, { rows: [{ id: project, name: 'Project', repo: 'timer' }] });
|
||||
if (url.pathname === '/api/chat/projects/archived') return json(200, { rows: [] });
|
||||
if (url.pathname === '/api/chat/threads') return json(200, { rows: url.searchParams.has('shared') ? [] : [{ id: thread, project, name: 'Thread', instance: 'assistant' }] });
|
||||
if (url.pathname.endsWith('/sessions')) return json(200, { rows: [{ id: session, name: 'Default', default: true, state: 'active', revision: 1 }] });
|
||||
if (url.pathname.endsWith('/shares')) return json(200, { rows: [] });
|
||||
if (url.pathname.includes('/messages')) return json(200, { rows, after: rows.at(-1).id, hasMore: false });
|
||||
const match = /^\/api\/requests\/([^/]+)(?:\/(workspace|draft))?$/.exec(url.pathname);
|
||||
if (match) {
|
||||
const [, rid, part] = match; calls.push({ id: rid, part: part ?? 'get' });
|
||||
// Only the workspace route answers request-not-found; the request's own state reads.
|
||||
if (part === 'workspace') return json(404, { error: 'request-not-found' });
|
||||
if (part === 'draft') return json(200, { candidate: null, prd: null, requirements: [], snapshot: { id: null, revision: 0 } });
|
||||
return json(200, { version: 1, request: rid, revision: 1, phase: 'discovery', project: 'timer', mode: 'guided', mode_revision: 1, snapshot: null, candidate: null, tasks: [], delivery: null, question: null, wait: null });
|
||||
}
|
||||
return json(404, { error: 'not-configured' });
|
||||
});
|
||||
t.after(async () => { await b?.close(); await new Promise(resolve => server.close(resolve)); });
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
b = await browser({ machine: false }); await b.navigate(`http://127.0.0.1:${server.address().port}/`);
|
||||
const path = `/projects/threads/${thread}`;
|
||||
const render = () => b.evaluate(`window.MosaicChat.render({path:${JSON.stringify(path)},container:document.querySelector('#view'),canSend:true,navigated:false})`);
|
||||
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(${expression})return true;await new Promise(r=>setTimeout(r,20));}throw Error('fixture wait failed: '+${JSON.stringify(expression)});})()`);
|
||||
await render();
|
||||
await wait(`document.querySelector('[data-chat-summary]')?.textContent.includes(${JSON.stringify(id)})`);
|
||||
await b.evaluate('new Promise(r=>setTimeout(()=>r(true),200))');
|
||||
assert.ok(calls.some(c => c.part === 'workspace'), 'the workspace 404 was served');
|
||||
// A refresh after that 404 still reads the request from the network and still shows it.
|
||||
const before = calls.length;
|
||||
await render();
|
||||
await wait(`document.querySelector('[data-chat-summary]')?.textContent.includes(${JSON.stringify(id)})`);
|
||||
await b.evaluate('new Promise(r=>setTimeout(()=>r(true),200))');
|
||||
assert.ok(calls.slice(before).some(c => c.id === id && c.part === 'get'), 'the request state is read again, not served a remembered miss');
|
||||
assert.equal(await b.evaluate(`(async()=>(await fetch('/api/requests/${id}')).status)()`), 200);
|
||||
});
|
||||
Reference in new issue
Block a user