Skip to content

Commit 4e2e27a

Browse files
mchwangclaude
andcommitted
Start no planning agent for a suggestion request handled after shutdown began
Ask and the runner stop admission in shutdown step 1, but the suggestion coordinator only closed in step 6. A request admitted before shutdown and handled during the drain allocated a pending request and started a planning invocation, which step 6 then cancelled. It now answers 503, records nothing, and the UI may resend it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 359865c commit 4e2e27a

2 files changed

Lines changed: 26 additions & 0 deletions

File tree

‎test/runner-planning-feedback.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { mkdtempSync, rmSync } from 'node:fs';
22
import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
4+
import { request as httpRequest } from 'node:http';
5+
import { DatabaseSync } from 'node:sqlite';
46
import { createHash, randomUUID } from 'node:crypto';
57
import { choiceKeys } from '../core/approvals.ts';
68
import { afterEach, describe, expect, it, vi } from 'vitest';
@@ -160,6 +162,28 @@ describe('planning API for lane G', () => {
160162
for (let i = 0; i < 50 && (await api(app, 'GET', `/api/plan/suggestions/${ready}`)).body.state !== 'ready'; i++) await new Promise(r => setTimeout(r, 20));
161163
expect((await api(app, 'POST', `/api/plan/suggestions/${ready}/cancel`, { actionId: randomUUID() })).body.result).toEqual({ state: 'cancelled' });
162164
});
165+
it('starts no planning agent for a suggestion request that finishes arriving after shutdown began', async () => {
166+
const { deps, calls } = planning();
167+
const { app, config, close } = await serve({ planning: deps });
168+
const view = await review(app), url = new URL(app.url);
169+
const text = JSON.stringify({ expectedRevision: view.plan.revision, snapshotId: view.snapshot.id, feedback: '', actionId: randomUUID() });
170+
let finish!: () => void;
171+
const response = new Promise<number>((resolve, reject) => {
172+
const req = httpRequest({ host: url.hostname, port: url.port, path: '/api/plan/suggestions', method: 'POST',
173+
headers: { 'x-codeboost-token': app.token, 'content-type': 'application/json', 'content-length': Buffer.byteLength(text) } }, res => { res.resume(); resolve(res.statusCode!); });
174+
req.on('error', reject); req.write(text.slice(0, 5)); finish = () => req.end(text.slice(5));
175+
});
176+
await new Promise(r => setTimeout(r, 20));
177+
const closing = close();
178+
await new Promise(r => setTimeout(r, 20));
179+
finish();
180+
expect(await response).toBe(503);
181+
await closing;
182+
expect(calls).toHaveLength(0);
183+
// No suggestion request was allocated either.
184+
const db = new DatabaseSync(config.database);
185+
try { expect(db.prepare('SELECT COUNT(*) AS n FROM requests').get()).toEqual({ n: 0 }); } finally { db.close(); }
186+
});
163187
it('settles a pending suggestion at shutdown instead of leaving it pending', async () => {
164188
const { deps, calls } = planning();
165189
const { app, config, close } = await serve({ planning: deps });

‎web/server.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export async function startServer(config: ReviewConfig, port = 4318, questionAge
9797
return { revision: service.store.importRevision(input.source, input.format as 'json' | 'yaml', service.planContext(), input.expectedRevision as number).revision };
9898
}
9999
if (started) {
100+
// Like Ask and the runner, no new agent starts once shutdown began; 503 is not recorded, so the UI may resend.
101+
if (stopping) throw new ShuttingDownError();
100102
if (!suggestions || !planning) throw new GuardRefusal('Planning agent not available yet.');
101103
const plan = service.store.getPlan(identity), snapshot = service.store.getSnapshot(identity);
102104
if (input.expectedRevision !== plan.revision || input.snapshotId !== snapshot.id) throw new GuardRefusal('Stale plan revision or snapshot. Reload before asking for suggestions.');

0 commit comments

Comments
 (0)