OPER-48
Fix `dispatch_retry_storm` body update — Prisma 6 validation regression
Body
# OPER-48 · Fix `dispatch_retry_storm` body update — Prisma 6 validation regression
## Problem
The `dispatch_retry_storm` sensor (state 1.3 in the taxonomy) fails on every reliability-loop tick with:
```
Invalid `prisma.task.update()` invocation:
Argument `body`: Invalid value provided. Expected String, provided Object.
```
Root cause: PR #134 (`feat(forge): reliability-loop cron + 5 seed sensors`) uses `body: { set: undefined }` as a no-op placeholder before a `$executeRawUnsafe` append. Prisma 6 validates operation shapes at call time and rejects `{ set: undefined }` on a required `String` field.
## Impact
- Sensor throws `error` on every 15-min tick since 2026-07-23.
- No tickets have been auto-bounced by the sensor — 1,357 failed dispatches on POR-422 / POR-67 this morning were parked manually because the sensor never fired.
- Retry storms on OPER-47 today (10 workflow_dispatch failures in <10 min) went undetected by this sensor for the same reason.
## Fix
File: `apps/web/src/lib/forge/sensor-dispatch-retry-storm.ts` (~line 96-104)
**Before**
```ts
await prisma.task.update({
where: { id: row.ticketId },
data: {
status: "BOUNCED",
updatedAt: ctx.now,
labels: { push: ["auto-bounced", "dispatch-retry-storm"] },
body: { set: undefined }, // rely on raw update below to concat
},
});
await prisma.$executeRawUnsafe(
`UPDATE "Task" SET body = COALESCE(body,'') || $1 WHERE id = $2`,
appended,
row.ticketId,
);
```
**After** — drop the no-op `body` line; the raw append is already the sole body writer.
```ts
await prisma.task.update({
where: { id: row.ticketId },
data: {
status: "BOUNCED",
updatedAt: ctx.now,
labels: { push: ["auto-bounced", "dispatch-retry-storm"] },
},
});
await prisma.$executeRawUnsafe(
`UPDATE "Task" SET body = COALESCE(body,'') || $1 WHERE id = $2`,
appended,
row.ticketId,
);
```
## Tests
File: `apps/web/src/lib/forge/__tests__/sensor-dispatch-retry-storm.test.ts`
Add a case that exercises the bounce path end-to-end with a real Prisma mock/spy:
- Seed a task with `body = "original body"`, 101 non_retriable failures in the last 3h.
- Run the sensor.
- Assert `prisma.task.update` was called WITHOUT a `body` key in `data`.
- Assert `prisma.$executeRawUnsafe` was called once with the append text.
- Assert the returned `SensorEvent` has `outcome: "healed"`, `autonomyDial: "auto"`, `sensorName: "dispatch_retry_storm"`.
If a matching test exists, extend it — do not duplicate.
## Verification
```bash
npx vitest run apps/web/src/lib/forge/__tests__/sensor-dispatch-retry-storm.test.ts
```
Must pass with the two assertions above.
## Out of scope
- Any change to `dispatch_retry_storm` thresholds (`THRESHOLD=100`, `WINDOW_HOURS=3`) — keep as-is.
- Any change to labels shape or `AlertHistory` writes.
- Pool tuning — tracked separately in OPER-49.
## Non-negotiables
- No new tables, no migration.
- No behavior change beyond removing the invalid `body` argument.
- Do not introduce `body: undefined` — that is also invalid. Simply omit the key.
- PR title: `fix(forge): drop invalid body no-op in dispatch_retry_storm sensor (OPER-48)`
- PR body must include `Ticket: OPER-48` trailer.
## Post-merge
Reliability loop will resume auto-bouncing storms within one 15-min tick. Verify by grepping `ReliabilityEvent` for `sensorName='dispatch_retry_storm' AND outcome!='error'` after next tick.
Attachments
Loading attachments…
Comments
Loading comments…