fix(run-engine): createCancelledRun normalises snapshot.tags

cjson encodes empty Lua tables as `{}`, not `[]`. When the drainer pops a
buffered run that was cancelled with no tags ever set, snapshot.tags is
an empty object, and `.length === 0` evaluates to undefined → the empty
object falls through into Prisma's `runTags:` field. Prisma interprets
a plain object on a scalar-list field as a relation update operation
(`{ set: [...] }`) and rejects with `Argument 'set' is missing`. The
drainer treats this as a terminal failure and marks the buffer entry
FAILED, so the PG row never lands.

Defensive normalisation: only pass `runTags: snapshot.tags` when it's
actually an array with content; pass undefined otherwise.

Found while running the Phase F challenge suite cancel scenario.
This commit is contained in:
Dan Sutton
2026-05-21 11:14:22 +01:00
parent fd891563ae
commit eef33e5bdb
@@ -511,7 +511,15 @@ export class RunEngine {
workerQueue: snapshot.workerQueue,
isTest: snapshot.isTest,
taskEventStore: snapshot.taskEventStore,
runTags: snapshot.tags.length === 0 ? undefined : snapshot.tags,
// Defensive: the snapshot comes from a cjson-encoded buffer
// payload, where empty Lua tables encode as `{}` not `[]`. If
// the drainer pops a buffered run with no tags, snapshot.tags
// will be an empty object, which Prisma misreads as a relation
// update op. Normalise to a real array (or undefined for the
// empty case).
runTags: Array.isArray(snapshot.tags) && snapshot.tags.length > 0
? snapshot.tags
: undefined,
oneTimeUseToken: snapshot.oneTimeUseToken,
parentTaskRunId: snapshot.parentTaskRunId,
rootTaskRunId: snapshot.rootTaskRunId,