From 9fc1e3490f7b7515d0e9d439c6533aaec06da032 Mon Sep 17 00:00:00 2001 From: Muhammad Ahmed Cheema Date: Wed, 12 Aug 2026 12:28:08 -0700 Subject: [PATCH 1/2] The money verb was waiting for a keypress that could never come The first ceremony failed twice at the exact same wall: order submit timed out at 30 seconds while everything before it succeeded. The CLI's submit has an interactive confirmation, the one flag list we never read in full because submit was the one call we never rehearsed, and a headless worker has no keypress to give. --yes now rides every submit; the human yes already happened, in the owner's inbox. Two companions from the same post-mortem: submit gets a 120 second timeout of its own, since payment authorization is the slowest call in the CLI and the read-call ceiling starved it; and when an uncertain submit reconciles against order history and finds nothing landed, the cart reopens instead of staying sealed forever, which is the stranding the ceremony proved. A regression test pins --yes and the patient timeout to the submit args. For the record, the machine failed safe both times: honest unknown outcomes, an immediate history reconcile by the agent, zero dollars moved. --- lib/rendi/doordash.test.ts | 16 +++++++++++++++- lib/rendi/doordash.ts | 24 +++++++++++++++++------- trigger/tools/doordash-submit.ts | 4 ++++ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/lib/rendi/doordash.test.ts b/lib/rendi/doordash.test.ts index 99a29f6..dff17b8 100644 --- a/lib/rendi/doordash.test.ts +++ b/lib/rendi/doordash.test.ts @@ -8,7 +8,7 @@ import { paymentMethodsFixture, searchFixture, } from "./doordash.fixtures.ts"; -import { intentFor, runDd, search } from "./doordash.ts"; +import { intentFor, orderSubmit, runDd, search } from "./doordash.ts"; import { ddAddressList, ddItemDetails, @@ -172,6 +172,20 @@ describe("search location doctrine", () => { }); }); +describe("orderSubmit", () => { + it("consents for the headless worker and waits out payment auth", async () => { + impl.mockResolvedValueOnce(envelope({ success: true, order_uuid: "o-1" })); + await orderSubmit({ cartUuid: "c-1", tipCents: 142 }, "g"); + const [, args, options] = impl.mock.calls[0] as [ + string, + string[], + { timeout: number }, + ]; + expect(args).toContain("--yes"); + expect(options.timeout).toBe(120_000); + }); +}); + // promisify is imported so the custom-symbol mock stays anchored to the // same node:util instance the wrapper uses. void promisify; diff --git a/lib/rendi/doordash.ts b/lib/rendi/doordash.ts index 21c46ff..d0b4a5e 100644 --- a/lib/rendi/doordash.ts +++ b/lib/rendi/doordash.ts @@ -48,13 +48,18 @@ export function intentFor(goal: string): string { ].join("\n"); } -export async function runDd(args: string[], goal: string): Promise { +export async function runDd( + args: string[], + goal: string, + opts?: { timeoutMs?: number }, +): Promise { + const timeoutMs = opts?.timeoutMs ?? TIMEOUT_MS; let stdout: string; try { ({ stdout } = await run( cliPath(), ["--json-output", ...args, "--intent", intentFor(goal)], - { timeout: TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }, + { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, )); } catch (error) { const failure = error as { @@ -64,9 +69,7 @@ export async function runDd(args: string[], goal: string): Promise { code?: string | number; }; if (failure.killed) { - throw new DdUncertainError( - `dd-cli timed out after ${TIMEOUT_MS / 1000}s`, - ); + throw new DdUncertainError(`dd-cli timed out after ${timeoutMs / 1000}s`); } if (failure.code === "ENOENT") { throw new Error("dd-cli is not installed; nothing was sent to DoorDash"); @@ -97,8 +100,9 @@ async function parsed( schema: T, args: string[], goal: string, + opts?: { timeoutMs?: number }, ): Promise> { - return schema.parse(await runDd(args, goal)); + return schema.parse(await runDd(args, goal, opts)); } // The saved default address is the CLI's own location doctrine for "near @@ -381,11 +385,17 @@ export async function orderSubmit( input.cartUuid, "--tip-cents", String(input.tipCents), + // A headless worker has no keypress to give: without --yes the CLI + // waits on its interactive confirmation forever. The human yes + // already happened, in the owner's inbox. + "--yes", ]; if (input.fulfillment) args.push("--fulfillment", input.fulfillment); if (input.scheduledTime) args.push("--scheduled-time", input.scheduledTime); if (input.priority) args.push("--priority"); - return parsed(ddSubmitResult, args, goal); + // Payment authorization is the CLI's slowest call; the read-call + // timeout starved it during the first ceremony. + return parsed(ddSubmitResult, args, goal, { timeoutMs: 120_000 }); } export async function orderStatus(orderUuid: string, goal: string) { diff --git a/trigger/tools/doordash-submit.ts b/trigger/tools/doordash-submit.ts index 840da7c..e0d8d1f 100644 --- a/trigger/tools/doordash-submit.ts +++ b/trigger/tools/doordash-submit.ts @@ -248,6 +248,10 @@ export const doordashSubmit = tool({ status: "unknown", errorMessage: error.message, }); + // History says nothing landed, so the cart is intact by + // definition; leaving it sealed would strand it, which is + // what the first ceremony proved. + await setCartStatus(approval.cartUuid, "open"); return { outcome: "unknown", storeName: snapshot.storeName, From 01965cd304589de3bbe366ac88f100f41db9ba4f Mon Sep 17 00:00:00 2001 From: Muhammad Ahmed Cheema Date: Wed, 12 Aug 2026 12:37:42 -0700 Subject: [PATCH 2/2] Unknown outcomes settle instead of guessing Codex round one, one finding, accepted in full. Reopening the cart on an immediate history miss conflated two different truths: history saying nothing landed, and history failing to answer at all, while even an honest empty read seconds after a timeout cannot rule out an order still materializing. So the cart now stays sealed on unknown, and calling doordash-submit again with the same approval id IS the settle loop: consumption answers alreadyConsumed, the tool asks history again, adopts the order if one landed, and releases the cart only when a successful later look confirms nothing ever did. The reconciler moved into the wrapper as a tri-state (found, none, unavailable) with tests for each face, and both submit paths settle through one shared poll. --- lib/rendi/doordash.test.ts | 37 ++++++++- lib/rendi/doordash.ts | 23 ++++++ trigger/rendi/agent.md | 14 ++-- trigger/tools/doordash-submit.ts | 137 ++++++++++++++++++++++--------- 4 files changed, 165 insertions(+), 46 deletions(-) diff --git a/lib/rendi/doordash.test.ts b/lib/rendi/doordash.test.ts index dff17b8..ed3e8df 100644 --- a/lib/rendi/doordash.test.ts +++ b/lib/rendi/doordash.test.ts @@ -8,7 +8,13 @@ import { paymentMethodsFixture, searchFixture, } from "./doordash.fixtures.ts"; -import { intentFor, orderSubmit, runDd, search } from "./doordash.ts"; +import { + findUnrecordedOrder, + intentFor, + orderSubmit, + runDd, + search, +} from "./doordash.ts"; import { ddAddressList, ddItemDetails, @@ -186,6 +192,35 @@ describe("orderSubmit", () => { }); }); +describe("findUnrecordedOrder", () => { + it("an unreachable history is not an empty one", async () => { + impl.mockRejectedValueOnce({ code: 1, stderr: "network is down" }); + const rec = await findUnrecordedOrder("s-1", new Set(), "g"); + expect(rec).toEqual({ unavailable: true }); + }); + + it("an empty history answers found null", async () => { + impl.mockResolvedValueOnce(envelope({ orders: [], success: true })); + const rec = await findUnrecordedOrder("s-1", new Set(), "g"); + expect(rec).toEqual({ found: null }); + }); + + it("adopts only orders the ledger has never seen, at this store", async () => { + impl.mockResolvedValueOnce( + envelope({ + success: true, + orders: [ + { order_uuid: "known-1", store_id: "s-1" }, + { order_uuid: "other-store", store_id: "s-2" }, + { order_uuid: "fresh-1", store_id: "s-1" }, + ], + }), + ); + const rec = await findUnrecordedOrder("s-1", new Set(["known-1"]), "g"); + expect(rec).toEqual({ found: "fresh-1" }); + }); +}); + // promisify is imported so the custom-symbol mock stays anchored to the // same node:util instance the wrapper uses. void promisify; diff --git a/lib/rendi/doordash.ts b/lib/rendi/doordash.ts index d0b4a5e..2eecd3e 100644 --- a/lib/rendi/doordash.ts +++ b/lib/rendi/doordash.ts @@ -228,6 +228,29 @@ export async function orderHistory( return parsed(ddOrderHistory, args, goal); } +// After an uncertain submit, order history is the truth, but only when +// it actually answers: an unreachable history proves nothing, and +// conflating it with an empty one is how a live order gets orphaned. +export type OrderReconciliation = + | { found: string | null } + | { unavailable: true }; + +export async function findUnrecordedOrder( + storeId: string, + known: Set, + goal: string, +): Promise { + const history = await orderHistory({ max: 5, days: 1 }, goal).catch( + () => null, + ); + if (!history) return { unavailable: true }; + const found = history.orders.find( + (order) => + String(order.store_id ?? "") === storeId && !known.has(order.order_uuid), + ); + return { found: found?.order_uuid ?? null }; +} + export async function orderReceipt(orderUuid: string, goal: string) { return parsed( ddLenient, diff --git a/trigger/rendi/agent.md b/trigger/rendi/agent.md index ffc1abf..7373764 100644 --- a/trigger/rendi/agent.md +++ b/trigger/rendi/agent.md @@ -184,11 +184,15 @@ message wakes you with the approval id; call doordash-submit with exactly that id and narrate the outcome honestly, including action_required and failed, where the answer is never to resubmit. An unknown outcome means the response was lost, not that the order -failed: wait a minute, check order-history, and reconcile honestly -before saying anything definitive. If the owner says they entered the -code but no [order approved] message ever arrived, calling -doordash-submit with the approval id from your own request is safe; -the machine refuses unless the code was truly verified. +failed: the cart stays sealed, and calling doordash-submit again with +the SAME approval id after a minute is the settle path. The machine +checks order history itself, adopts the order if it landed, or reopens +the cart once a successful look confirms nothing ever did; never +request a fresh approval while an unknown is unsettled. If the owner +says they entered the code but no [order approved] message ever +arrived, calling doordash-submit with the approval id from your own +request is equally safe; the machine refuses unless the code was truly +verified. Spend caps are enforced below you; when a request is denied for caps, say so plainly. Anything changed after the code was sent voids it, and a fresh approval is one call away. diff --git a/trigger/tools/doordash-submit.ts b/trigger/tools/doordash-submit.ts index e0d8d1f..ae8fba4 100644 --- a/trigger/tools/doordash-submit.ts +++ b/trigger/tools/doordash-submit.ts @@ -32,27 +32,43 @@ async function recordOutcome( .where(eq(ddOrders.id, orderId)); } -// After an uncertain submit, order history is the truth: an order at -// this store that our ledger has never seen is the one whose response -// was lost. Returns its uuid, or null when nothing landed. -async function reconcileFromHistory( - storeId: string, - goal: string, -): Promise { - const history = await dd - .orderHistory({ max: 5, days: 1 }, goal) - .catch(() => null); - if (!history) return null; - const known = new Set( +async function knownOrderUuids(): Promise> { + return new Set( (await getDb().select({ orderUuid: ddOrders.orderUuid }).from(ddOrders)) .map((row) => row.orderUuid) - .filter(Boolean), + .filter((uuid): uuid is string => Boolean(uuid)), ); - const found = history.orders.find( - (order) => - String(order.store_id ?? "") === storeId && !known.has(order.order_uuid), +} + +// Poll an accepted order to a terminal state, bounded, and leave the +// ledger and cart telling the same story. +async function settleOrder( + orderRowId: number, + orderUuid: string, + cartUuid: string, + goal: string, +): Promise<{ status: string; errorMessage: string | null }> { + let status = "pending"; + let errorMessage: string | null = null; + for (let attempt = 0; attempt < STATUS_POLLS; attempt++) { + const check = await dd.orderStatus(orderUuid, goal).catch(() => null); + if (check) { + status = check.status; + errorMessage = check.error_message ?? null; + if (status !== "pending") break; + } + await new Promise((resolve) => setTimeout(resolve, STATUS_POLL_MS)); + } + await recordOutcome(orderRowId, { status, errorMessage }); + await setCartStatus( + cartUuid, + status === "successful" + ? "placed" + : status === "pending" + ? "placing" + : "failed", ); - return found?.order_uuid ?? null; + return { status, errorMessage }; } // The finalizer, with zero degrees of freedom: everything about the @@ -108,6 +124,50 @@ export const doordashSubmit = tool({ }; } } + // Unknown with no uuid is the timed-out submit whose truth was + // still unwritten. Re-calling this tool IS the settle loop: adopt + // the order if history shows one landed, release the cart once a + // successful look confirms nothing ever did. + if (existing.status === "unknown" && !existing.orderUuid) { + const snapshot = await getCartSnapshot(existing.cartUuid); + if (snapshot) { + const rec = await dd.findUnrecordedOrder( + snapshot.storeId, + await knownOrderUuids(), + goal, + ); + if ("unavailable" in rec) { + return { + outcome: "unknown", + note: "order history is unreachable right now; the cart stays sealed, try this same approval id again shortly", + }; + } + if (rec.found) { + await recordOutcome(existing.id, { orderUuid: rec.found }); + const settled = await settleOrder( + existing.id, + rec.found, + existing.cartUuid, + goal, + ); + return { + outcome: settled.status, + orderUuid: rec.found, + ...(settled.errorMessage ? { error: settled.errorMessage } : {}), + note: "the lost submission had landed after all; this is its live status", + }; + } + await recordOutcome(existing.id, { + status: "failed", + errorMessage: "no order ever appeared in history", + }); + await setCartStatus(existing.cartUuid, "open"); + return { + outcome: "failed", + note: "nothing ever landed at DoorDash; the cart is open again, request a fresh approval when ready", + }; + } + } return { outcome: existing.status, orderUuid: existing.orderUuid, @@ -242,21 +302,26 @@ export const doordashSubmit = tool({ // and charged before the transport died. Only a structured error // from the CLI proves the order was refused. if (error instanceof dd.DdUncertainError) { - orderUuid = await reconcileFromHistory(snapshot.storeId, goal); - if (!orderUuid) { + const rec = await dd.findUnrecordedOrder( + snapshot.storeId, + await knownOrderUuids(), + goal, + ); + if ("found" in rec && rec.found) { + orderUuid = rec.found; + } else { await recordOutcome(orderRow.id, { status: "unknown", errorMessage: error.message, }); - // History says nothing landed, so the cart is intact by - // definition; leaving it sealed would strand it, which is - // what the first ceremony proved. - await setCartStatus(approval.cartUuid, "open"); + // The cart stays sealed: an immediate miss proves nothing, + // since creation can lag a timed-out submit and the history + // call itself can fail. The settle loop above releases it. return { outcome: "unknown", storeName: snapshot.storeName, error: error.message, - note: "the submission may or may not have reached DoorDash; never resubmit and never check out elsewhere until order history answers. Check the order history in a minute and reconcile honestly.", + note: "the submission may or may not have reached DoorDash; the cart stays sealed and nothing else may be ordered. Wait a minute, then call doordash-submit again with this same approval id: it settles against order history, adopting the order if it landed or reopening the cart if it never did.", }; } } else { @@ -279,23 +344,15 @@ export const doordashSubmit = tool({ let status = "pending"; let errorMessage: string | null = null; if (orderUuid) { - for (let attempt = 0; attempt < STATUS_POLLS; attempt++) { - const check = await dd.orderStatus(orderUuid, goal); - status = check.status; - errorMessage = check.error_message ?? null; - if (status !== "pending") break; - await new Promise((resolve) => setTimeout(resolve, STATUS_POLL_MS)); - } + ({ status, errorMessage } = await settleOrder( + orderRow.id, + orderUuid, + approval.cartUuid, + goal, + )); + } else { + await recordOutcome(orderRow.id, { status, errorMessage }); } - await recordOutcome(orderRow.id, { status, errorMessage }); - await setCartStatus( - approval.cartUuid, - status === "successful" - ? "placed" - : status === "pending" - ? "placing" - : "failed", - ); emitSpan({ conversationId: approval.conversationId, turn: turn?.turn ?? 0,