+## Summary
On Harper 5.2.6, initial-copy replication can silently drop every record from a table that has a surfaced scalar @computed attribute while advancing the copy cursor and reporting the link healthy.
The receiver logs:
TypeError: attribute.set is not a function
This is a data-convergence failure, not only a read error. The receive loop classifies the exception as decode-drop, skips the row, and continues to COPY_COMPLETE. Reconnecting does not redeliver rows after the cursor has advanced.
A contaminated record already at rest can also prevent the sender's copy walk from progressing, producing a close/retry loop at the same row.
Independent reproduction
This reproduction uses only Harper/Harper Pro and a minimal component; it does not depend on another application or production data.
- Start two Harper 5.2.6 nodes, A and B, with replication enabled and the same component deployed.
- Use this schema:
type ComputedReplicationRecord @table @export {
id: ID @primaryKey
source: String
derived: String @computed(from: "source") @indexed
}
- Before B subscribes, write multiple records on A:
PUT /ComputedReplicationRecord/one
Content-Type: application/json
{"id":"one","source":"value"}
- Connect B to A with
add_node and allow the initial copy/catch-up to finish.
- Compare source and receiver row counts, read
/ComputedReplicationRecord/one on B, and inspect receiver logs and cluster status.
- The same core failure can be exercised without networking by decoding/materializing an affected-release payload containing
{"id":"one","source":"value","derived":"value"} against the table's struct prototype.
Actual
- The receiver throws
TypeError: attribute.set is not a function while decoding the copy row.
- The row is skipped under the
decode-drop path.
- The copy cursor advances and the node can report connected/current despite missing rows.
- A table-sized copy can produce one large error log per dropped row.
Expected
- A stored or transported value colliding with a read-only computed attribute cannot prevent materialization.
- The computed resolver remains authoritative.
- Durable and transport encodings contain stored fields, not response-only computed projections.
- The affected payload decodes and applies successfully, so it never reaches Pro's established permanent-decode skip path.
Root cause
TableResource.updatedAttributes installs an accessor setter for every resolved attribute, but scalar computed attributes have no attribute.set. Harper's decoded record prototype also has a REST-oriented toJSON that adds surfaced scalar computed values. Any internal re-encode of a decoded record invokes that response projection before struct encoding.
Harper Pro initial copy performs such a re-encode on the sender. Core replication apply then re-encodes the decoded RecordObject into receiver storage, so a Pro-only sender fix would contaminate the receiver and fail on a later hop. Live audit-tail forwarding normally sends the original durable bytes and does not take the initial-copy re-encode path.
Acceptance criteria
- Tolerate affected-release payloads on the immutable decoded-record surface; signal the first collision per table/worker without per-row analytics or log flood.
- Keep mutable computed attributes explicitly read-only and preserve writable relationship setters.
- At the shared core durable-encoding boundary, suppress an affected table's response
toJSON so the real stored struct is encoded without a projection allocation; use a nesting-safe global symbol shared by duplicate module instances.
- In Pro's indexed residency/invalidation projection, substitute a simple computed field with its source attribute so receiver-side recomputation preserves search behavior.
- Preserve direct-write validation: clients still cannot assign
@computed attributes.
- Add a focused core regression that fails on the current base with the exact TypeError and passes with the fix.
- Prove durable encoding bypasses response projection while ordinary JSON output and no-computed table descriptor/bytes are unchanged; cover nested records, buffer grow/retry, variable/overflow structures, and typed values.
- Assert a forged transported computed value wins neither point reads nor computed-index lookups.
- Cover authentic old put and patch/merge payloads plus a local read-modify-write; assert primary and audit durable bytes omit the computed key.
- Add a Pro unit regression through initial-copy encode/decode, source-substituting indexed-partial encoding, and receiver durable re-encode, and mechanically demonstrate it fails on the Pro base revision.
- Add or extend a two-node integration test covering an authentic affected old payload and initial copy of multiple records with an indexed scalar
@computed attribute.
- The integration test verifies row-count parity, resolver-wins behavior, no decode-drop/error log, connected/current status, and a post-copy live update.
- Keep
decodeDropRecovery.test.mjs green unchanged; generic permanent-decode quarantine/retry policy is separate protocol work.
- Run the full required core and Pro unit/integration gates plus before/after cluster-ingest performance coverage.
Rollout and recovery
Upgrade source/sender nodes first so they can materialize contaminated stored rows and emit clean payloads, then upgrade receivers. A new receiver can tolerate an affected old sender when the sender can still materialize its source row; already-contaminated sender storage requires the sender upgrade first.
Because affected receivers may already have advanced past dropped rows or retained stale computed-index entries, force a full base copy and rebuild affected computed indices after both sides are fixed, then verify row-count and computed-index parity; reconnect alone is insufficient.
Version
Confirmed on 5.2.6. Target the v5.2 patch line as well as main.
Comment generated by kAIle (Codex GPT-5.6)
+## Summary
On Harper 5.2.6, initial-copy replication can silently drop every record from a table that has a surfaced scalar
@computedattribute while advancing the copy cursor and reporting the link healthy.The receiver logs:
This is a data-convergence failure, not only a read error. The receive loop classifies the exception as
decode-drop, skips the row, and continues toCOPY_COMPLETE. Reconnecting does not redeliver rows after the cursor has advanced.A contaminated record already at rest can also prevent the sender's copy walk from progressing, producing a close/retry loop at the same row.
Independent reproduction
This reproduction uses only Harper/Harper Pro and a minimal component; it does not depend on another application or production data.
add_nodeand allow the initial copy/catch-up to finish./ComputedReplicationRecord/oneon B, and inspect receiver logs and cluster status.{"id":"one","source":"value","derived":"value"}against the table's struct prototype.Actual
TypeError: attribute.set is not a functionwhile decoding the copy row.decode-droppath.Expected
Root cause
TableResource.updatedAttributesinstalls an accessor setter for every resolved attribute, but scalar computed attributes have noattribute.set. Harper's decoded record prototype also has a REST-orientedtoJSONthat adds surfaced scalar computed values. Any internal re-encode of a decoded record invokes that response projection before struct encoding.Harper Pro initial copy performs such a re-encode on the sender. Core replication apply then re-encodes the decoded
RecordObjectinto receiver storage, so a Pro-only sender fix would contaminate the receiver and fail on a later hop. Live audit-tail forwarding normally sends the original durable bytes and does not take the initial-copy re-encode path.Acceptance criteria
toJSONso the real stored struct is encoded without a projection allocation; use a nesting-safe global symbol shared by duplicate module instances.@computedattributes.@computedattribute.decodeDropRecovery.test.mjsgreen unchanged; generic permanent-decode quarantine/retry policy is separate protocol work.Rollout and recovery
Upgrade source/sender nodes first so they can materialize contaminated stored rows and emit clean payloads, then upgrade receivers. A new receiver can tolerate an affected old sender when the sender can still materialize its source row; already-contaminated sender storage requires the sender upgrade first.
Because affected receivers may already have advanced past dropped rows or retained stale computed-index entries, force a full base copy and rebuild affected computed indices after both sides are fixed, then verify row-count and computed-index parity; reconnect alone is insufficient.
Version
Confirmed on 5.2.6. Target the v5.2 patch line as well as main.
Comment generated by kAIle (Codex GPT-5.6)