You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Large thread decodes validate every known timeline item twice #18856
Decoding large thread snapshots and history pages validates every known timeline item twice.
ForwardCompatibleUnionArray (packages/contracts/src/baseSchemas.ts) and projectedTurnItemArray (packages/contracts/src/orchestrationV2.ts) decode each element through ForwardCompatibleUnion, drop unknown members, and then decodeTo(Schema.Array(Schema.toType(...))). That second step validates and rebuilds every surviving element again. Both turnItems and projected timeline rows pay for it. A large ordinary turn stays whole in bounded snapshots, so a turn with thousands of items pays it on every open and page.
Measured on 511d20c in Node, 6,000 known items, alternating old and new codecs in one process, 5 warmups and 15 samples each:
Decode
Current
Single validation
Item array
68.4 ms
47.4 ms
OrchestrationV2ThreadHistoryPage
89.5 ms
61.5 ms
A counting field check confirms that each known member is validated twice today. vp run test:perf:v2-wire passes either way: it checks payload sizes, not decode CPU.
Why this is an issue and not a PR
A working single-pass version exists on saphid:t3code/sweep/contracts-single-pass-items (148 contract tests pass, identical output in 20 old/new edge cases, wire suite before / after). It needs a custom Schema.declareConstructor that skips revalidation by comparing transformation identity, and it relies on Effect Schema's internal ~constructor hook to keep make/is and class constructors working. That is more machinery than a ~20 ms win justifies if Effect's internals change. Two smaller directions may exist:
An Effect Schema primitive for "decode an array and drop elements matching a predicate" without a typed second pass (possibly an upstream Effect request).
Accept unknown members in the decoded type (ReadonlyArray<Known | UnknownUnionMember>) and filter at the few read sites, which removes the second schema pass entirely.
Found during an agent-driven audit (GPT-6.1 Sol, high), with the reference implementation by GPT-6.1 Sol (high) and review by Claude Opus 5.5 (high) in T3 Code. Filed by Claude Opus 5.5 in Claude Code via T3 Code on behalf of Alex Southwell.
Thanks for the detailed write-up and measurements. Confirmed on main: ForwardCompatibleUnionArray (baseSchemas.ts) and projectedTurnItemArray (orchestrationV2.ts) decode each element through ForwardCompatibleUnion, then decodeTo(Schema.Array(Schema.toType(...))) validates the surviving known members a second time. Nothing requires that second pass for forward compatibility. It's a side effect of narrowing the decoded type so unknown members get filtered out.
Still, this is a performance improvement, not a bug. Output is correct, and the cost you measured is about 20–28 ms on a 6,000-item thread, which is an extreme case. As you say yourself, the single-pass branch depends on Effect Schema internals (~constructor, a custom declareConstructor), and that's more risk than the win is worth. So I'm closing this as not planned and tracking decode cost under the perf tracker #9661.
If you want to pursue it, we'd welcome a PR for your second option: keep Known | UnknownUnionMember in the decoded type and filter at the read sites. That removes the second pass without touching Effect internals. Please include the same before/after numbers. An upstream Effect primitive for "decode and drop" would also be worth raising with Effect.
Activity
juliusmarminge commentedon Oct 12, 2026
Note
Grok responding on behalf of Julius.
Thanks for the detailed write-up and measurements. Confirmed on main:
ForwardCompatibleUnionArray(baseSchemas.ts) andprojectedTurnItemArray(orchestrationV2.ts) decode each element throughForwardCompatibleUnion, thendecodeTo(Schema.Array(Schema.toType(...)))validates the surviving known members a second time. Nothing requires that second pass for forward compatibility. It's a side effect of narrowing the decoded type so unknown members get filtered out.Still, this is a performance improvement, not a bug. Output is correct, and the cost you measured is about 20–28 ms on a 6,000-item thread, which is an extreme case. As you say yourself, the single-pass branch depends on Effect Schema internals (
~constructor, a customdeclareConstructor), and that's more risk than the win is worth. So I'm closing this as not planned and tracking decode cost under the perf tracker #9661.If you want to pursue it, we'd welcome a PR for your second option: keep
Known | UnknownUnionMemberin the decoded type and filter at the read sites. That removes the second pass without touching Effect internals. Please include the same before/after numbers. An upstream Effect primitive for "decode and drop" would also be worth raising with Effect.