fix(execution-history): tolerate runs stored before the actor fields - #23
Conversation
RunRow's actorId/actorLabel/actorKind were NullOr (key required), but runs are JSON documents and rows written before #20 have no such key at all — so the HTTP response encoder rejected the newest page ("Missing key at runs[0].actorId") and the entire runs list 400'd with an empty body. Make the keys optional with a decoding default of null: the decoded type stays `string | null` (always present, so every reader still treats it as required) while legacy docs decode AND encode unchanged. This also immunizes the collection against the next field added the same way. Covered by a legacy-doc encode/decode regression test.
Greptile SummaryThis PR fixes a schema-evolution bug where the three actor fields (
Confidence Score: 5/5Safe to merge — the change is minimal, targeted, and directly addresses the 400 regression with no data migration required. The fix makes the three actor fields optional at the wire level while keeping the decoded TypeScript type as string | null (always present), so no readers need updating. The regression test directly encodes a legacy-shaped document — the exact path that was 400ing — confirming the failure mode is resolved. The round-trip test for attributed runs confirms existing populated values are unaffected. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant Store as KV Store
participant Encoder as Response Encoder (RunRow)
participant Client
note over Store: Legacy doc (no actorId/actorLabel/actorKind keys)
Store->>Encoder: raw JSON doc
alt Before fix (Schema.NullOr - key required)
Encoder-->>Client: 400 Missing key at runs[0].actorId
else After fix (Schema.optional + decoding default null)
Encoder->>Encoder: absent key treated as optional, no error
Encoder-->>Client: 200 with actorId absent
end
note over Store: New doc with actorId set
Store->>Encoder: raw JSON doc with actor keys
Encoder-->>Client: 200 with actorId present
Reviews (2): Last reviewed commit: "test(execution-history): assert actor-ke..." | Re-trigger Greptile |
…e comment Strengthen the legacy-doc regression test to assert the encoder leaves absent actor keys absent (not fabricated) and carries present keys through encode unchanged — so a regression in how the optional actor keys encode is caught, not just a bare executionId check. Also drops a stale `optionalWith` comment.
…23) ## The bug The runs list returns an **empty-body 400** for everyone. Root cause is a schema/data-evolution bug from #20, not anything resource-related: `RunRow.actorId/actorLabel/actorKind` were `Schema.NullOr(...)` — present-but-nullable, which means **the key must exist**. Runs are stored as JSON documents, and every run written *before* #20 has a `data` doc with no such key. The store passes stored docs straight to the HTTP response encoder, which rejects the newest row with `Missing key at ["runs"][0]["actorId"]`. A response-encode failure emits a bare 400 (no body, no content-type) — the empty-body signature. Since no executions have happened since the actor store deployed, 100% of stored runs are legacy and every request fails. ## The fix Make the three actor keys optional with a **decoding default of null**: ```ts actorId: Schema.optional(Schema.NullOr(Schema.String)).pipe( Schema.withDecodingDefaultType(Effect.succeed(null)), ) ``` - **Decoded type stays `string | null`** (always present) — every reader still treats the field as required; it just defaults to `null` for pre-actor runs. - **Wire/storage tolerates an absent key** — legacy docs decode *and* encode unchanged. - Immunizes the collection against the next field added the same way. No data migration, no prod writes — deploy-forward and the page is back with all run history intact. ## Verification - New regression test round-trips a legacy-shaped doc (decode → null; **encode with the keys absent succeeds** — the exact failure). - execution-history suite 20/20, typecheck 41/41, lint + format clean. (Build/Deploy-preview checks fail on the fork's missing R2 secrets, as on prior PRs — unrelated.)
…23) ## The bug The runs list returns an **empty-body 400** for everyone. Root cause is a schema/data-evolution bug from #20, not anything resource-related: `RunRow.actorId/actorLabel/actorKind` were `Schema.NullOr(...)` — present-but-nullable, which means **the key must exist**. Runs are stored as JSON documents, and every run written *before* #20 has a `data` doc with no such key. The store passes stored docs straight to the HTTP response encoder, which rejects the newest row with `Missing key at ["runs"][0]["actorId"]`. A response-encode failure emits a bare 400 (no body, no content-type) — the empty-body signature. Since no executions have happened since the actor store deployed, 100% of stored runs are legacy and every request fails. ## The fix Make the three actor keys optional with a **decoding default of null**: ```ts actorId: Schema.optional(Schema.NullOr(Schema.String)).pipe( Schema.withDecodingDefaultType(Effect.succeed(null)), ) ``` - **Decoded type stays `string | null`** (always present) — every reader still treats the field as required; it just defaults to `null` for pre-actor runs. - **Wire/storage tolerates an absent key** — legacy docs decode *and* encode unchanged. - Immunizes the collection against the next field added the same way. No data migration, no prod writes — deploy-forward and the page is back with all run history intact. ## Verification - New regression test round-trips a legacy-shaped doc (decode → null; **encode with the keys absent succeeds** — the exact failure). - execution-history suite 20/20, typecheck 41/41, lint + format clean. (Build/Deploy-preview checks fail on the fork's missing R2 secrets, as on prior PRs — unrelated.)
The bug
The runs list returns an empty-body 400 for everyone. Root cause is a schema/data-evolution bug from #20, not anything resource-related:
RunRow.actorId/actorLabel/actorKindwereSchema.NullOr(...)— present-but-nullable, which means the key must exist. Runs are stored as JSON documents, and every run written before #20 has adatadoc with no such key. The store passes stored docs straight to the HTTP response encoder, which rejects the newest row withMissing key at ["runs"][0]["actorId"]. A response-encode failure emits a bare 400 (no body, no content-type) — the empty-body signature. Since no executions have happened since the actor store deployed, 100% of stored runs are legacy and every request fails.The fix
Make the three actor keys optional with a decoding default of null:
string | null(always present) — every reader still treats the field as required; it just defaults tonullfor pre-actor runs.No data migration, no prod writes — deploy-forward and the page is back with all run history intact.
Verification
(Build/Deploy-preview checks fail on the fork's missing R2 secrets, as on prior PRs — unrelated.)