fix(chat): send writes with an empty responseText and point card declines to resolve-from-comment

#21035 refused any write whose call_api input had a responseText key, even
responseText: {} beside a valid body, which looped a DeepSeek chat until its
window closed. An empty or default responseText is now dropped and the write is
sent once; a responseText that sets a window is still refused.

The cancel and withdraw summaries from #21035 named withdraw as the agent route,
so an agent told "do not publish it" left a person-created card pending. The
summaries and chat directive now say withdraw is only for the agent's own
obsolete card and a person's decline is recorded with resolve-from-comment when
the card's resolver policy allows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 committed 2026-11-12 01:46:01 -06:00
1 parent 3c058c37e6
commit 3f20fb2bcc
8 files changed
+67 -20

No files matched your search

@@ -811,9 +811,9 @@ describe("runner API request boundary", () => {
await expect(executeRunnerApi({ operationId: createProject, body: { name: "Once" }, responseText: { offsetBytes: 4 } }, context, io(request))).rejects.toThrow("This request was not sent");
expect(request).not.toHaveBeenCalled();
});
// Full chat cohort 4: write calls carried responseText beside (or instead of) their body. Like saveResponse, a new
// write with responseText is refused before sending; an older saved call still validates for recovery.
it.each([{}, { limitBytes: 8192 }, { offsetBytes: 0, limitBytes: 4096 }])("refuses a new write that carries responseText before sending: %j", async responseText => {
// Full chat cohort 4: write calls carried responseText beside (or instead of) their body. A new write whose
// responseText sets a window is refused before sending; an older saved call still validates for recovery.
it.each([{ limitBytes: 8192 }, { offsetBytes: 0, limitBytes: 4096 }])("refuses a new write whose responseText sets a window before sending: %j", async responseText => {
const request = vi.fn<typeof fetch>(async () => Response.json({ id: "company-a", name: "Acorn Studio" }));
const call = { operationId: "PATCH /api/companies/{companyId}", pathParams: { companyId: "company-a" }, body: { name: "Acorn Studio" }, responseText };
const error = await executeRunnerApi(call, context, io(request)).then(() => null, (caught: Error) => caught);
@@ -822,6 +822,25 @@ describe("runner API request boundary", () => {
expect(error?.message).toContain("Resend PATCH /api/companies/{companyId} once without responseText, with its request fields in body");
expect(validateRunnerApiCall(call, context).input).not.toHaveProperty("responseText");
});
// Full chat cohort 5 (inbox-search.restore-archive): DeepSeek sent responseText: {} with a valid body on every
// inbox-archive POST, and 24 refusals in a row ran out the chat window. An empty or default read option changes
// nothing on a write, so it is dropped and the write is sent once.
it.each([
{ responseText: {} },
{ responseText: { offsetBytes: 0 } },
{ saveResponse: false },
{ responseText: {}, saveResponse: false },
])("sends a write once when its read options are empty or default: %j", async options => {
const request = vi.fn<typeof fetch>(async () => Response.json({ ok: true }));
const result = await executeRunnerApi({ operationId: "POST /api/issues/{id}/inbox-archive", pathParams: { id: "issue-a" }, body: {}, ...options }, context, io(request));
expect(request).toHaveBeenCalledTimes(1);
const [url, init] = request.mock.calls[0]!;
expect(String(url)).toMatch(/\/api\/issues\/issue-a\/inbox-archive$/);
expect(init?.method).toBe("POST");
expect(JSON.parse(String(init?.body))).toEqual({});
expect(result).toMatchObject({ ok: true, status: 200 });
expect(result).not.toHaveProperty("responseText");
});
it("still sends the same write once without responseText", async () => {
const request = vi.fn<typeof fetch>(async () => Response.json({ id: "company-a", name: "Acorn Studio" }));
const result = await executeRunnerApi({ operationId: "PATCH /api/companies/{companyId}", pathParams: { companyId: "company-a" }, body: { name: "Acorn Studio" } }, context, io(request));