Repository navigation
The DoorDash skin: rendi's first non-analytics capability - #1
Conversation
A durable session lives forever in the Trigger environment it was born in; the row now records which. Prod lists prod-born conversations only, cross-environment opens render as read-only galleries in both directions, and archived pages no longer attach a session they could never resume.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 11cc6762b0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const key = process.env.TRIGGER_SECRET_KEY; | ||
| if (!key) throw new Error("TRIGGER_SECRET_KEY is not set"); |
There was a problem hiding this comment.
Seed the Trigger environment in persistence tests
When pnpm test runs in the checked-in CI environment, TRIGGER_SECRET_KEY is unset because the setup action only installs dependencies. Every pre-existing test in lib/rendi/harness/persistence.test.ts now reaches this throw through persistChatStart or reconcile, while the new test file's per-test environment changes are isolated and cannot initialize the existing suite, so the unit-test gate fails before its database assertions run. Set and restore a representative key in the persistence suite or inject the environment dependency.
AGENTS.md reference: AGENTS.md:L100-L107
Useful? React with 👍 / 👎.
| @@ -0,0 +1 @@ | |||
| ALTER TABLE "conversations" ADD COLUMN "trigger_env" text DEFAULT 'prod' NOT NULL; No newline at end of file | |||
There was a problem hiding this comment.
Preserve dev provenance when backfilling existing rows
On an existing production database, this migration labels every historical conversation as prod. The implementation being replaced explicitly classified conversations created before 2026-07-22T20:40:00Z as dev-born galleries, so those rows now pass isArchived in production and page.tsx attaches their dev-only session tokens, producing the 401 this change is meant to prevent. Backfill pre-cutover rows as dev before applying the prod default to future inserts.
Useful? React with 👍 / 👎.
A read-only first skin-slice: the dd-cli wrapper with schemas grounded in live captures (popularity fields stripped per the CLI's own embargo, unknown keys dropped so context stays lean), the doordash-browse tool with ten verbs, and one card with three faces: store tiles, an item spotlight, and quiet summaries. Search resolves location from the saved default address, their own doctrine. Money cannot move from here.
Both P1s were real; the first was already red in CI while passing locally on ambient environment. Unit tests now seed a representative dev key, and the pre-cutover provenance moves from a one-off into migration 0009 so any replay reconstructs birth environments instead of defaulting history to prod.
|
Both P1s were real, and the first was already red in CI while passing locally on ambient environment. Fixed in d242e75: unit tests seed a representative dev key (vitest.setup.unit.ts), and the pre-cutover provenance now lives in migration 0009 so replaying the migrations reconstructs birth environments instead of defaulting history to prod. |
Food is visual: thumbnails in the DoorDash cards grew a notch (stores 64px, spotlights 96px) and every one is now a zoom trigger opening the CDN original at natural aspect in a dialog, escape to dismiss. Composed from the installed dialog primitive; the spotlight story clicks it open and closes it.
Three tables land (dd_carts render snapshots, dd_approvals for the one-time codes, dd_orders keyed by approval so a re-fired turn can never charge twice), and the wrapper grows the full cart lifecycle: add-items with nested options, show, remove-item, delete, list, preview, promo. Shapes come from live captures of a real cart walked through every verb, which also proved the laws on the wire: append sums quantities, carts build at closed stores, closed previews say so honestly. Money is integer cents everywhere; dollars exist only at render.
The agent builds carts through doordash-cart (with the one-cart-per- store preflight as its opening move), and the user edits the same cart by touch: steppers, remove, tip presets defaulting to 10 percent at birth, delivery and pickup. Touches ride a non-model exec task, since the binary lives in the worker, and both hands move the cart through one shared physics: mutate at DoorDash, re-preview for honest money, upsert the snapshot the card paints from. The ladder renders their cents verbatim, strikethroughs included, and a closed store says so instead of pretending. Nothing here can charge anything; the approval machine is the next slice.
The agent holds the full tool surface, including submit; safety is structural, not a handicap. doordash-request-approval re-prices the cart, enforces the caps (order ceiling, daily budget, per-conversation and per-day request rates), freezes everything into a sha256 hash, and mails a six-digit code to the owner's inbox and nowhere else. The code is typed into the order card, never into the chat: the verify route checks it under a row lock with constant-time comparison, five wrong guesses kill the approval, and success wakes the agent through the session inbox. doordash-submit takes only the approval id: it consumes the row once (a re-fired turn reads the recorded outcome instead of charging twice), revalidates the live cart against the frozen hash, voids on any drift, and polls the order to a terminal state it reports honestly. DD_HIDE_PERSONAL=1 keeps addresses, cards, and history private when strangers hold gate codes. Approval and order cards render every face, reload-honest via a status route, with stories under the axe gate; fifteen pglite tests pin the machine's lifecycle. Proven live end to end tonight: two honest denials (closed store, no ASAP delivery), a real code email, a wrong guess counted, a cancel that reopened the cart, and not a cent moved.
The worker image gains the official dd-cli at deploy, pinned to v0.2.2 and checksum-verified against the published release; dev machines keep their own login-authenticated install. Env examples and both deploy guides teach the whole ritual: waitlisted account, dd-cli login locally, export-token into the worker env, the owner inbox, the caps, and DD_HIDE_PERSONAL for instances strangers can drive. Removing the four tool lines from agent.md removes the capability, so a clone without DoorDash access deploys clean.
|
@codex review |
Twenty guests on a challenge day would drain a hardcoded fifteen; the inbox budget is now DD_MAX_APPROVALS_PER_DAY with the same default.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 21acccbab8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const [orders] = await db | ||
| .select({ n: count() }) | ||
| .from(ddOrders) | ||
| .where(gt(ddOrders.createdAt, dayAgo)); | ||
| if ((orders?.n ?? 0) >= maxOrdersPerDay()) { |
There was a problem hiding this comment.
Reserve daily-order capacity atomically
When multiple approvals are requested before any corresponding order is submitted, every request observes the same ddOrders count and passes this check. The approvals can then be verified and submitted sequentially or concurrently, and neither consumeApproval nor the order insert rechecks or reserves capacity, so even DD_MAX_ORDERS_PER_DAY=1 permits two real orders. Reserve a slot atomically when issuing or consuming an approval.
Useful? React with 👍 / 👎.
| const liveHash = hashCart({ | ||
| items, | ||
| totalCents: liveTotal ?? -1, | ||
| tipCents: approval.tipCents, |
There was a problem hiding this comment.
Invalidate approval when the saved tip changes
If the user changes the tip in a cart card after receiving the approval email, opSetTip updates dd_carts.tip_cents, but revalidation hashes approval.tipCents rather than the current snapshot tip. A tip-only edit therefore passes the hash check and submission charges the previously approved tip, even while the UI shows the user's newer amount. Compare the snapshot tip and void the approval when it differs.
AGENTS.md reference: AGENTS.md:L17-L18
Useful? React with 👍 / 👎.
| await new Promise((resolve) => setTimeout(resolve, STATUS_POLL_MS)); | ||
| } | ||
| } | ||
| await recordOutcome(orderRow.id, { status, errorMessage }); |
There was a problem hiding this comment.
Reconcile orders that outlive the initial poll
When DoorDash remains pending through all eight polls, this permanently records pending and returns, but the already-consumed retry path only rereads that row and no task or exposed tool polls the order again. The cart and order card consequently stay in “placing” forever even after DoorDash reaches a terminal state. Schedule durable reconciliation or make the consumed retry path refresh pending orders.
AGENTS.md reference: AGENTS.md:L15-L18
Useful? React with 👍 / 👎.
| data: CartCardData; | ||
| exec?: typeof execOp; | ||
| }) { | ||
| const [cart, setCart] = useState<CartCardData>(data); |
There was a problem hiding this comment.
Hydrate cart cards from the live snapshot
On reload, and whenever another historical cart card edits the same UUID, this state is initialized from the frozen tool output in the transcript and never reads dd_carts. Each card can therefore display different quantities, fulfillment, tip, and totals, while a reload discards browser-side updates and restores historical numbers. Hydrate or subscribe by cart UUID so every rendered copy adopts the durable snapshot.
AGENTS.md reference: AGENTS.md:L15-L18
Useful? React with 👍 / 👎.
| if (run.status === "COMPLETED") { | ||
| const fresh = await getCartSnapshot(uuid); | ||
| return Response.json({ ok: true, cart: fresh ?? null }); |
There was a problem hiding this comment.
Deliver browser cart edits to the next agent turn
After a browser edit completes, the fresh cart is returned only to that HTTP caller. The task writes no append-only operation or unseen marker, while trigger/chat.ts injects only instrument and canvas readback, so the next agent turn still sees the historical cart tool output and can discuss or approve a stale quantity, tip, or fulfillment choice. Persist cart edits and inject their live state on the next turn.
AGENTS.md reference: AGENTS.md:L17-L18
Useful? React with 👍 / 👎.
| } | ||
| await new Promise((resolve) => setTimeout(resolve, POLL_MS)); | ||
| } | ||
| return Response.json({ error: "edit timed out" }, { status: 504 }); |
There was a problem hiding this comment.
Stop timed-out cart tasks from mutating later
When this 60-second deadline expires, the route returns a definitive failure but leaves the Trigger task running for up to 120 seconds, including while it waits in the global single-concurrency queue. The task can therefore mutate DoorDash and persist the cart after the UI has reported that the edit failed, causing an unexpected quantity, tip, or fulfillment change. Cancel the run on timeout or return a durable pending state that the card continues to reconcile.
AGENTS.md reference: AGENTS.md:L17-L18
Useful? React with 👍 / 👎.
| await recordOutcome(orderRow.id, { | ||
| status: "failed", | ||
| errorMessage: error instanceof Error ? error.message : "submit failed", | ||
| }); | ||
| await setCartStatus(approval.cartUuid, "failed"); | ||
| return { | ||
| outcome: "failed", | ||
| error: error instanceof Error ? error.message : "submit failed", | ||
| note: "never resubmit; the browser checkout is the fallback", |
There was a problem hiding this comment.
Preserve uncertainty when submission loses its response
A timeout or connection loss from orderSubmit does not prove DoorDash rejected the request, because the server may have accepted and charged it before the response was lost. This catch nevertheless records failed and recommends browser checkout, which can lead the user to place a duplicate order. Record an unknown/pending outcome and reconcile it through order history or status instead of presenting transport failure as rejection.
AGENTS.md reference: AGENTS.md:L76-L76
Useful? React with 👍 / 👎.
| default: | ||
| return { | ||
| title: "Placed the order", | ||
| summary: output.outcome ?? "", |
There was a problem hiding this comment.
Render not-found orders as unresolved
ddOrderStatus explicitly returns not_found, and the submit tool passes that value through as outcome, but it falls into this default branch and renders the title “Placed the order.” When DoorDash cannot find the submitted UUID, the user is therefore told the opposite of the actual result. Add an explicit unresolved or failed presentation for not_found.
AGENTS.md reference: AGENTS.md:L76-L76
Useful? React with 👍 / 👎.
| {output.message?.trim() || | ||
| `${summarize(verb, output)} · in hand`} |
There was a problem hiding this comment.
Render hidden personal data as private
With DD_HIDE_PERSONAL=1, the browse tool returns { private: true, note: "the owner keeps that private" }, but this card ignores both fields. Personal verbs are consequently rendered as “0 orders,” “0 saved,” or “0 on file,” falsely presenting hidden information as an empty account. Surface the privacy outcome rather than passing the payload through the ordinary count fallback.
AGENTS.md reference: AGENTS.md:L76-L76
Useful? React with 👍 / 👎.
| await sendApprovalEmail({ | ||
| code: approval.code, | ||
| storeName: priced.storeName, | ||
| itemsCount: priced.items.reduce((sum, line) => sum + line.quantity, 0), | ||
| totalDisplay: `$${(totalCents / 100).toFixed(2)}`, | ||
| destination: | ||
| priced.fulfillment === "pickup" ? "pickup" : "the saved address", | ||
| conversationTitle: conversation?.title ?? "a conversation", | ||
| minutes: 15, | ||
| approvalId: approval.id, | ||
| }); |
There was a problem hiding this comment.
Void approvals whose email delivery fails
If sendApprovalEmail throws, the tool exits before returning the code card or setting awaiting_code, but the newly created approval remains live and is still counted by both approval-rate queries. Repeated transient Resend failures can therefore exhaust the per-conversation and global limits even though no authorization code was delivered. Void the row on delivery failure or count only approvals whose email was accepted.
Useful? React with 👍 / 👎.
First slice of the Codex findings, the three that matter before real money moves. orderSubmit timeouts and transport deaths now surface as DdUncertainError; the submit tool answers them by reading order history (an order at this store our ledger has never seen is the one whose response was lost), adopting it into the normal status poll, or recording an honest unknown that tells everyone not to resubmit and not to check out elsewhere. A tip edited after the code email now voids the approval, since the hash binds the approval's own tip and could never see the snapshot move; the email promised any change voids. And not_found renders as what it is instead of falling through to the placed face.
|
Triage of the 19 findings, with thanks; this was a genuinely useful pass. Landed in
Landed in Queued next, before any shared/guest use of the instance: atomic daily-order reservation, blank-env caps parsing ( Rejected: the handwritten-migration finding misreads the repo law. Deferred with reasoning: cart-state readback into the next agent turn. Staleness cannot reach a charge because both money steps re-read the live cart (request-approval re-prices it; submit re-reads and re-hashes it), so this is conversational polish rather than a money-path defect. It rides the same follow-up as card hydration. |
rendi is an agent harness wearing analytics as its first skin. This PR grows the second skin: ordering food through the official DoorDash CLI, built slice by slice on one long-running branch.
The arc:
Ordering is personal-account only per the CLI's terms; the capability ships as configuration (own token, own approval inbox, spend caps) so any deployment can enable it or leave the tool line out of the agent definition.
Gates green per slice: lint, typecheck, tests, build.