Skip to content

Commit 39ab862

Browse files
committed
Preserve active question state on reload
1 parent 9051902 commit 39ab862

3 files changed

Lines changed: 8 additions & 4 deletions

File tree

‎docs/implementation/read-only-review.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,5 @@ PR #11 round 9 returned no inline findings and identified shutdown ordering in i
106106
Current PR validation: 187 unit/integration tests, 32 browser tests, typecheck, and diff checks. Earlier counts above identify the stage when each behavior was added.
107107

108108
The documentation-only review after adding `AGENTS.md` exposed one more concrete lifecycle race: an expired persisted attempt could show Retry while its cancelled provider was still settling. Question polling now exposes whether the local invocation remains active, displays Finishing cancellation, and keeps polling without exposing Retry until settlement. A browser regression advances the persisted clock while leaving the invocation unresolved, then confirms Retry appears only after settlement.
109+
110+
The follow-up review exposed the same active-job marker missing from the initial review response. Reloading during provider settlement could therefore expose Retry and stop polling. Initial loads and action responses now include the marker used by question polling; the browser regression reloads during settlement and confirms Retry remains hidden until the invocation finishes.

‎test/browser/review.spec.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ test('hides Retry until a timed-out invocation has actually settled',async({page
289289
await expect(page.getByText('Agent · Answering…',{exact:true})).toBeVisible();
290290
const note=app.service.store.getReviewNotes(config.identity).find(note=>note.text==='Slow question')!;
291291
app.service.store.finishAnswer(config.identity,note.id,note.answer!.attempt,{status:'failed',error:'Agent timed out. Try again.'});
292+
await page.reload();
292293
await expect(page.getByText('Agent · Finishing cancellation…',{exact:true})).toBeVisible({timeout:10000});
293294
await expect(page.getByRole('button',{name:'Retry answer',exact:true})).toHaveCount(0);
294295
settle('Late answer');

‎web/server.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const publicRoot = new URL('./public/', import.meta.url);
88
export async function startServer(config: ReviewConfig, port = 4318, questionAgent?: QuestionAgent) {
99
const service = new ReviewService(config), token = randomBytes(32).toString('hex');
1010
const questions=new Questions(service,questionAgent);
11+
const load=()=>{const view=service.load();return {...view,notes:view.notes.map(note=>({...note,answerActive:questions.isRunning(note.id)}))};};
1112
const server = createServer(async (req, res) => {
1213
const address = server.address(); const actualPort = address && typeof address !== 'string' ? address.port : port;
1314
const origin = `http://127.0.0.1:${actualPort}`;
@@ -21,8 +22,8 @@ export async function startServer(config: ReviewConfig, port = 4318, questionAge
2122
const supplied = req.headers['x-codeboost-token'];
2223
if (typeof supplied !== 'string' || !/^[a-f0-9]{64}$/.test(supplied) || !timingSafeEqual(Buffer.from(supplied), Buffer.from(token))) { json(403, { error: 'Open the private local URL printed by the CLI.' }); return; }
2324
if (req.method === 'GET' && path === '/api/settings') { json(200,{questionProvider:service.store.questionProvider()});return; }
24-
if (req.method === 'GET' && path === '/api/questions') { json(200,{notes:service.load().notes.map(note=>({...note,answerActive:questions.isRunning(note.id)}))});return; }
25-
if (req.method === 'GET' && path === '/api/review') { json(200, service.load()); return; }
25+
if (req.method === 'GET' && path === '/api/questions') { json(200,{notes:load().notes});return; }
26+
if (req.method === 'GET' && path === '/api/review') { json(200, load()); return; }
2627
if (req.method !== 'POST' || !['/api/action','/api/settings'].includes(path) || req.headers['content-type'] !== 'application/json') { json(405, { error: 'Unsupported request.' }); return; }
2728
const chunks: Buffer[] = []; let size = 0;
2829
for await (const chunk of req) { size += chunk.length; if (size > 16384) { json(413, { error: 'Request too large.' }); return; } chunks.push(chunk); }
@@ -31,15 +32,15 @@ export async function startServer(config: ReviewConfig, port = 4318, questionAge
3132
if(path==='/api/settings') {service.store.setQuestionProvider(input.questionProvider);json(200,{questionProvider:service.store.questionProvider()});return;}
3233
if(input.action==='retry-question') {
3334
const view=service.load();if(input.token!==view.token)throw new Error('Stale review state. Refresh and retry.');
34-
questions.start(input.id,view);json(200,service.load());return;
35+
questions.start(input.id,view);json(200,load());return;
3536
}
3637
const view=service.act(input);
3738
if(view.createdNoteId && input.kind==='question') {
3839
try {questions.start(view.createdNoteId,view);} catch(error) {
3940
// The saved question remains visible and retryable when capacity is reached.
4041
}
4142
}
42-
json(200,service.load());return;
43+
json(200,load());return;
4344
}
4445
if (req.method !== 'GET') { json(405, { error: 'Method not allowed.' }); return; }
4546
const files: Record<string, [string, string]> = { '/': ['index.html', 'text/html'], '/app.js': ['app.js', 'text/javascript'], '/style.css': ['style.css', 'text/css'] };

0 commit comments

Comments
 (0)