mirror of https://github.com/garrytan/gstack.git
fix: write ALL feedback to disk so agent can poll in background mode
The agent backgrounds $D serve (Claude Code can't block on a subprocess and do other work simultaneously). With stdout-only feedback delivery, the agent never sees regenerate/remix feedback. Fix: write feedback-pending.json (regenerate/remix) and feedback.json (submit) to disk next to the board HTML. Agent polls the filesystem instead of reading stdout. Both channels (stdout + disk) are always active so foreground mode still works. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
229db44b8f
commit
b57de95a7f
|
|
@ -21,7 +21,14 @@
|
||||||
* │
|
* │
|
||||||
* └──(timeout)──► exit 1
|
* └──(timeout)──► exit 1
|
||||||
*
|
*
|
||||||
* Stdout: feedback JSON only (one line per feedback event)
|
* Feedback delivery (two channels, both always active):
|
||||||
|
* Stdout: feedback JSON (one line per event) — for foreground mode
|
||||||
|
* Disk: feedback-pending.json (regenerate/remix) or feedback.json (submit)
|
||||||
|
* written next to the HTML file — for background mode polling
|
||||||
|
*
|
||||||
|
* The agent typically backgrounds $D serve and polls for feedback-pending.json.
|
||||||
|
* When found: read it, delete it, generate new variants, POST /api/reload.
|
||||||
|
*
|
||||||
* Stderr: structured telemetry (SERVE_STARTED, SERVE_FEEDBACK_RECEIVED, etc.)
|
* Stderr: structured telemetry (SERVE_STARTED, SERVE_FEEDBACK_RECEIVED, etc.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -120,14 +127,17 @@ export async function serve(options: ServeOptions): Promise<void> {
|
||||||
|
|
||||||
console.error(`SERVE_FEEDBACK_RECEIVED: type=${action}`);
|
console.error(`SERVE_FEEDBACK_RECEIVED: type=${action}`);
|
||||||
|
|
||||||
// Print feedback JSON to stdout (agent reads this)
|
// Print feedback JSON to stdout (for foreground mode)
|
||||||
console.log(JSON.stringify(body));
|
console.log(JSON.stringify(body));
|
||||||
|
|
||||||
if (isSubmit) {
|
// ALWAYS write feedback to disk so the agent can poll for it
|
||||||
// Write feedback.json next to the HTML file
|
// (agent typically backgrounds $D serve, can't read stdout)
|
||||||
const feedbackPath = path.join(path.dirname(html), "feedback.json");
|
const feedbackDir = path.dirname(html);
|
||||||
fs.writeFileSync(feedbackPath, JSON.stringify(body, null, 2));
|
const feedbackFile = isSubmit ? "feedback.json" : "feedback-pending.json";
|
||||||
|
const feedbackPath = path.join(feedbackDir, feedbackFile);
|
||||||
|
fs.writeFileSync(feedbackPath, JSON.stringify(body, null, 2));
|
||||||
|
|
||||||
|
if (isSubmit) {
|
||||||
state = "done";
|
state = "done";
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,10 +84,10 @@ describe('Serve HTTP endpoints', () => {
|
||||||
try { body = await req.json(); } catch { return Response.json({ error: 'Invalid JSON' }, { status: 400 }); }
|
try { body = await req.json(); } catch { return Response.json({ error: 'Invalid JSON' }, { status: 400 }); }
|
||||||
if (typeof body !== 'object' || body === null) return Response.json({ error: 'Expected JSON object' }, { status: 400 });
|
if (typeof body !== 'object' || body === null) return Response.json({ error: 'Expected JSON object' }, { status: 400 });
|
||||||
const isSubmit = body.regenerated === false;
|
const isSubmit = body.regenerated === false;
|
||||||
|
const feedbackFile = isSubmit ? 'feedback.json' : 'feedback-pending.json';
|
||||||
|
fs.writeFileSync(path.join(tmpDir, feedbackFile), JSON.stringify(body, null, 2));
|
||||||
if (isSubmit) {
|
if (isSubmit) {
|
||||||
state = 'done';
|
state = 'done';
|
||||||
const feedbackPath = path.join(tmpDir, 'feedback.json');
|
|
||||||
fs.writeFileSync(feedbackPath, JSON.stringify(body, null, 2));
|
|
||||||
return Response.json({ received: true, action: 'submitted' });
|
return Response.json({ received: true, action: 'submitted' });
|
||||||
}
|
}
|
||||||
state = 'regenerating';
|
state = 'regenerating';
|
||||||
|
|
@ -160,8 +160,12 @@ describe('Serve HTTP endpoints', () => {
|
||||||
expect(written.ratings.A).toBe(4);
|
expect(written.ratings.A).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POST /api/feedback with regenerate sets state to regenerating', async () => {
|
test('POST /api/feedback with regenerate sets state and writes feedback-pending.json', async () => {
|
||||||
state = 'serving';
|
state = 'serving';
|
||||||
|
// Clean up any prior pending file
|
||||||
|
const pendingPath = path.join(tmpDir, 'feedback-pending.json');
|
||||||
|
if (fs.existsSync(pendingPath)) fs.unlinkSync(pendingPath);
|
||||||
|
|
||||||
const feedback = {
|
const feedback = {
|
||||||
preferred: 'B',
|
preferred: 'B',
|
||||||
ratings: { A: 3, B: 5, C: 2 },
|
ratings: { A: 3, B: 5, C: 2 },
|
||||||
|
|
@ -185,6 +189,12 @@ describe('Serve HTTP endpoints', () => {
|
||||||
const progress = await fetch(`${baseUrl}/api/progress`);
|
const progress = await fetch(`${baseUrl}/api/progress`);
|
||||||
const pd = await progress.json();
|
const pd = await progress.json();
|
||||||
expect(pd.status).toBe('regenerating');
|
expect(pd.status).toBe('regenerating');
|
||||||
|
|
||||||
|
// Agent can poll for feedback-pending.json
|
||||||
|
expect(fs.existsSync(pendingPath)).toBe(true);
|
||||||
|
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
||||||
|
expect(pending.regenerated).toBe(true);
|
||||||
|
expect(pending.regenerateAction).toBe('different');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POST /api/feedback with remix contains remixSpec', async () => {
|
test('POST /api/feedback with remix contains remixSpec', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue