Context
v0.0.17, b9f9e99. In src/rebuild.ts:38-42:
const refResult = assignRefs(messages, {
existing: state.messageRefs,
nextIndex: highestUsedIndex(state.messageRefs) + 1,
});
let working: CompressionState = { ...state, messageRefs: refResult.map };
The spread { ...state } copies top-level fields, but blocks is an array reference — working.blocks === state.blocks. Only messageRefs is replaced (correctly, by a fresh refResult.map).
Why it (currently) does not bite
The loop body only ever consumes working via core.applyCompression({ state: working, ... }), and applyCompression opens with cloneState(input.state) (src/compress.ts:977-994) which deep-copies blocks. So the caller's state.blocks is not mutated today.
But rebuildCompressionState itself does not establish isolation for its own subsequent operations. If someone later adds a direct read/write of working.blocks (e.g. a post-pass that inspects blocks, or a deactivateBlock(working, …) call — note deactivateBlock returns a new state and does not mutate, so that specific case is safe, but other helpers like the block.active = false mutations inside applySingleRange would not be), they would alias-mutate the caller's state.
Suggested fix
Either:
- (preferred) Deep-clone explicitly — export
cloneState from src/compress.ts (currently module-private at line 977) and use let working = { ...cloneState(state), messageRefs: refResult.map };. Or inline the same deep copy.
- (minimal) Document the contract — add a comment at line 42 stating "
blocks shares the caller's array; safety relies on applyCompression deep-cloning before any mutation. Do not mutate working.blocks directly."
Severity
Low — latent only. No current bug because the only consumer is applyCompression, which clones. But this is exactly the shape of a future mutation-leak bug, and the deep-clone helper already exists in the codebase (just not exported).
Context
v0.0.17,
b9f9e99. Insrc/rebuild.ts:38-42:The spread
{ ...state }copies top-level fields, butblocksis an array reference —working.blocks === state.blocks. OnlymessageRefsis replaced (correctly, by a freshrefResult.map).Why it (currently) does not bite
The loop body only ever consumes
workingviacore.applyCompression({ state: working, ... }), andapplyCompressionopens withcloneState(input.state)(src/compress.ts:977-994) which deep-copiesblocks. So the caller'sstate.blocksis not mutated today.But
rebuildCompressionStateitself does not establish isolation for its own subsequent operations. If someone later adds a direct read/write ofworking.blocks(e.g. a post-pass that inspects blocks, or adeactivateBlock(working, …)call — notedeactivateBlockreturns a new state and does not mutate, so that specific case is safe, but other helpers like theblock.active = falsemutations insideapplySingleRangewould not be), they would alias-mutate the caller's state.Suggested fix
Either:
cloneStatefromsrc/compress.ts(currently module-private at line 977) and uselet working = { ...cloneState(state), messageRefs: refResult.map };. Or inline the same deep copy.blocksshares the caller's array; safety relies onapplyCompressiondeep-cloning before any mutation. Do not mutateworking.blocksdirectly."Severity
Low — latent only. No current bug because the only consumer is
applyCompression, which clones. But this is exactly the shape of a future mutation-leak bug, and the deep-clone helper already exists in the codebase (just not exported).