fix: salvage truncated worker envelopes and log worker lifecycle

Models occasionally drop the closing tag of the result envelope while
the JSON itself is complete; the strict parser rejected these and one
repair attempt could not always recover, cascading into worker_error
and dropping valid attachments. The parser now falls back to
brace-balanced salvage when the closing tag is missing, and worker
lifecycle events (start, resume, invalid envelope, repair, settle,
worker_error) are logged.
This commit is contained in:
zenord
2026-08-19 15:44:53 +08:00
parent 6b506b8c55
commit 9c3139380d
5 changed files with 183 additions and 11 deletions
+13
View File
@@ -12,6 +12,7 @@ const logFile = process.env.FAKE_ACP_LOG;
const log = (entry) => { if (logFile) fs.appendFileSync(logFile, `${JSON.stringify(entry)}\n`); };
const assistantEnvelope = (reply, actions) => `${reply}\n<GORI_ASSISTANT_ACTION_V2>${JSON.stringify({ reply, actions })}</GORI_ASSISTANT_ACTION_V2>`;
const workerEnvelope = (result) => `worker reply\n<GORI_WORKER_RESULT_V2>${JSON.stringify(result)}</GORI_WORKER_RESULT_V2>`;
const workerEnvelopeTruncated = (result) => `worker reply\n<GORI_WORKER_RESULT_V2>${JSON.stringify(result)}`;
const app = acp.agent({ name: "fake-acp-agent" })
.onRequest(acp.methods.agent.initialize, ({ params }) => {
@@ -92,6 +93,7 @@ const app = acp.agent({ name: "fake-acp-agent" })
else if (userText === "stop") response = assistantEnvelope("stopping", [{ type: "stop" }]);
else if (userText === "cancel proposal") response = assistantEnvelope("cancelling", [{ type: "cancel" }]);
else if (userText === "ack pending") response = assistantEnvelope("任务「Test proposal」还在等你确认。", []);
else if (userText === "truncated assistant") response = `ok\n<GORI_ASSISTANT_ACTION_V2>${JSON.stringify({ reply: "ok", actions: [] })}`;
else if (userText === "invalid assistant") response = "invalid without envelope";
else response = assistantEnvelope("ok", []);
await update(client, params.sessionId, response);
@@ -121,6 +123,17 @@ const app = acp.agent({ name: "fake-acp-agent" })
if (process.env.FAKE_ACP_WORKER_GATE_FILE) {
while (!fs.existsSync(process.env.FAKE_ACP_WORKER_GATE_FILE)) await new Promise((resolve) => setTimeout(resolve, 5));
}
if (goal.includes("truncbad")) {
await update(client, params.sessionId, `worker reply\n<GORI_WORKER_RESULT_V2>{"status":"PENDING","summary":"cut off mid json"`);
return { stopReason: "end_turn" };
}
if (goal.includes("truncattach")) {
const outbox = path.join(process.cwd(), ".gori-outbox");
fs.mkdirSync(outbox, { recursive: true });
fs.writeFileSync(path.join(outbox, "shot.png"), Buffer.concat([PNG_HEADER, Buffer.from("fake-png-payload-truncated")]));
await update(client, params.sessionId, workerEnvelopeTruncated({ status: "PENDING", summary: "made a pic", attachments: [{ path: ".gori-outbox/shot.png", mimeType: "image/png" }] }));
return { stopReason: "end_turn" };
}
if (goal.includes("attachbad")) {
await update(client, params.sessionId, workerEnvelope({ status: "PENDING", summary: "made a pic", attachments: [{ path: "/tmp/evil.png" }, { path: "not-an-image.txt" }] }));
return { stopReason: "end_turn" };