Fix: print JSON on live-poll --reply success (#800)

Successful --reply was exit 0 with empty stdout, so agents could not tell delivery from a hang. Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-09-10 08:47:39 +05:00
committed by Abdul Wahab
co-authored by Cursor
parent 3bdb9ff06c
commit 67d018fe05
3 changed files with 58 additions and 4 deletions
+55 -1
View File
@@ -596,6 +596,18 @@ fn write_carbonize_banner(event: &Map<String, Value>, io: &mut Io) {
} }
} }
fn reply_ack_json(reply: &Reply) -> Value {
let mut m = Map::new();
m.insert("ok".into(), json!(true));
m.insert("id".into(), json!(reply.id));
m.insert("status".into(), json!(reply.ty));
if let Some(f) = &reply.file {
m.insert("file".into(), json!(f));
}
m.insert("_instructions".into(), json!("Poll again now."));
Value::Object(m)
}
/// JS: printPollEvent(event) — a wire-supplied `_instructions` must never /// JS: printPollEvent(event) — a wire-supplied `_instructions` must never
/// win over the locally generated one (#488). /// win over the locally generated one (#488).
fn print_poll_event(event: &mut Value, io: &mut Io) { fn print_poll_event(event: &mut Value, io: &mut Io) {
@@ -718,7 +730,13 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
} }
}; };
return match post_reply(&base, &token, &reply) { return match post_reply(&base, &token, &reply) {
Ok(()) => 0, Ok(()) => {
println(
io,
&serde_json::to_string(&reply_ack_json(&reply)).unwrap_or_default(),
);
0
}
Err(PollError::ConnRefused) => { Err(PollError::ConnRefused) => {
io.err(&format!( io.err(&format!(
"Live server not running. Start one with: {}\n", "Live server not running. Start one with: {}\n",
@@ -839,4 +857,40 @@ mod tests {
})); }));
assert!(parsed.get("_instructions").is_none(), "{}", parsed); assert!(parsed.get("_instructions").is_none(), "{}", parsed);
} }
#[test]
fn reply_ack_json_includes_file_when_present() {
let reply = Reply {
id: "ab12cd34".into(),
ty: "done".into(),
message: None,
file: Some("index.html".into()),
data: None,
source_event_type: None,
};
let parsed = reply_ack_json(&reply);
assert_eq!(parsed["ok"], json!(true));
assert_eq!(parsed["id"], json!("ab12cd34"));
assert_eq!(parsed["status"], json!("done"));
assert_eq!(parsed["file"], json!("index.html"));
assert_eq!(parsed["_instructions"], json!("Poll again now."));
}
#[test]
fn reply_ack_json_omits_file_when_absent() {
let reply = Reply {
id: "ab12cd34".into(),
ty: "steer_done".into(),
message: None,
file: None,
data: None,
source_event_type: None,
};
let parsed = reply_ack_json(&reply);
assert_eq!(parsed["ok"], json!(true));
assert_eq!(parsed["id"], json!("ab12cd34"));
assert_eq!(parsed["status"], json!("steer_done"));
assert!(parsed.get("file").is_none(), "{}", parsed);
assert_eq!(parsed["_instructions"], json!("Poll again now."));
}
} }
+1 -1
View File
@@ -1740,7 +1740,7 @@ Conventions: every script's "run directly" guard is `process.argv[1]` ending wit
#### `live-poll.mjs` -> `impeccable poll` #### `live-poll.mjs` -> `impeccable poll`
- Invoked from live.md poll loop; `--reply` forms quoted in `_instructions` (see instructions.mjs strings in 6.3/below). - Invoked from live.md poll loop; `--reply` forms quoted in `_instructions` (see instructions.mjs strings in 6.3/below).
- Args: `--stream`, `--timeout=MS` (one-shot total, default 600000), `--types=A,B`, `--ack-timeout=MS` (stream, default 600000), `--reply <id> <status> [--file PATH] [--data JSON] [message]`, `--help`. `--reply` errors (stderr, exit 1): `Usage: node "<abs>/live-poll.mjs" --reply <id> <status> [--file path] [--data '<json>'] [message]` + `Missing event id after --reply.` / `The value after --reply must be the event id, not the status "done". Use --reply EVENT_ID done.` / `Missing reply status after event id "X".`; `--data must be valid JSON: <err>`. - Args: `--stream`, `--timeout=MS` (one-shot total, default 600000), `--types=A,B`, `--ack-timeout=MS` (stream, default 600000), `--reply <id> <status> [--file PATH] [--data JSON] [message]`, `--help`. `--reply` success (stdout, exit 0): one compact JSON line `{ok:true,id,status,file? (only when --file was passed),_instructions:'Poll again now.'}`. `--reply` errors (stderr, exit 1): `Usage: node "<abs>/live-poll.mjs" --reply <id> <status> [--file path] [--data '<json>'] [message]` + `Missing event id after --reply.` / `The value after --reply must be the event id, not the status "done". Use --reply EVENT_ID done.` / `Missing reply status after event id "X".`; `--data must be valid JSON: <err>`.
- Needs `server.json`; else stderr `No running live server found. Start one with: node "<abs>/live.mjs"` exit 1. - Needs `server.json`; else stderr `No running live server found. Start one with: node "<abs>/live.mjs"` exit 1.
- One-shot: loops `GET /poll?token&timeout=<slice ≤270000>&leaseMs=600000[&types]` until an event or total deadline; prints one JSON line (`console.log(JSON.stringify(event))`) with `_instructions` added by `instructionsForEvent` (unless already present). For `accept`/`discard`: spawns `node live-accept.mjs --id ID (--discard | --variant N) [--page-url U] [--param-values JSON]` (30 s), sets `event._acceptResult` (parse failure/throw → `{handled:false, mode:'error', error}`), then POSTs completion `{id, type: completionType, sourceEventType: event.type, message: _acceptResult.error, file: _acceptResult.file, data: {carbonize:true}?}` where completionType = discard: `discarded` if handled else `error`; accept: `agent_done` if handled&carbonize, `complete` if handled, `error` if mode error or (svelte-component unhandled), else `agent_done`; sets `event._completionAck = {ok:true, type}` (+ `final:false, requiresComplete:true, nextCommand:'live-complete.mjs --id <id>', message:'Carbonize cleanup must be verified, then the session must be completed explicitly before polling again.'` for carbonize) or `{ok:false, error}`. Stderr banners: manual_edit_apply → 4-line banner starting `Manual Apply action required: edit source, then reply with \`live-poll.mjs --reply <id> done --data '<json>'\`.`; carbonize → `⚠ Carbonize cleanup REQUIRED before next poll. After cleanup, run live-complete.mjs --id <id>. See reference/live.md "Required after accept".` - One-shot: loops `GET /poll?token&timeout=<slice ≤270000>&leaseMs=600000[&types]` until an event or total deadline; prints one JSON line (`console.log(JSON.stringify(event))`) with `_instructions` added by `instructionsForEvent` (unless already present). For `accept`/`discard`: spawns `node live-accept.mjs --id ID (--discard | --variant N) [--page-url U] [--param-values JSON]` (30 s), sets `event._acceptResult` (parse failure/throw → `{handled:false, mode:'error', error}`), then POSTs completion `{id, type: completionType, sourceEventType: event.type, message: _acceptResult.error, file: _acceptResult.file, data: {carbonize:true}?}` where completionType = discard: `discarded` if handled else `error`; accept: `agent_done` if handled&carbonize, `complete` if handled, `error` if mode error or (svelte-component unhandled), else `agent_done`; sets `event._completionAck = {ok:true, type}` (+ `final:false, requiresComplete:true, nextCommand:'live-complete.mjs --id <id>', message:'Carbonize cleanup must be verified, then the session must be completed explicitly before polling again.'` for carbonize) or `{ok:false, error}`. Stderr banners: manual_edit_apply → 4-line banner starting `Manual Apply action required: edit source, then reply with \`live-poll.mjs --reply <id> done --data '<json>'\`.`; carbonize → `⚠ Carbonize cleanup REQUIRED before next poll. After cleanup, run live-complete.mjs --id <id>. See reference/live.md "Required after accept".`
- Stream: stderr `[impeccable-poll] stream mode: one JSON object per line on stdout; use --reply while this process stays running`; after each reply-needing event waits (poll `/status` every 400 ms) until the id leaves `pendingEvents` (else `Timed out waiting for --reply on event <id>` exit 1); returns on `exit`. - Stream: stderr `[impeccable-poll] stream mode: one JSON object per line on stdout; use --reply while this process stays running`; after each reply-needing event waits (poll `/status` every 400 ms) until the id leaves `pendingEvents` (else `Timed out waiting for --reply on event <id>` exit 1); returns on `exit`.
@@ -26,7 +26,7 @@
"signal": null "signal": null
}, },
{ {
"stdout": "", "stdout": "{\"ok\":true,\"id\":\"ab12cd34\",\"status\":\"done\",\"file\":\"index.html\",\"_instructions\":\"Poll again now.\"}\n",
"stderr": "", "stderr": "",
"exit": 0, "exit": 0,
"signal": null "signal": null
@@ -44,7 +44,7 @@
"signal": null "signal": null
}, },
{ {
"stdout": "", "stdout": "{\"ok\":true,\"id\":\"ab12cd34\",\"status\":\"steer_done\",\"_instructions\":\"Poll again now.\"}\n",
"stderr": "", "stderr": "",
"exit": 0, "exit": 0,
"signal": null "signal": null