Found during the v0.8.4 release review (Chaos Gremlin persona). Deferred — rare concurrency edge, needs careful design + a deterministic test, >30 min.
Problem
#895 made getOrCreateTrace (worker.ts) async (it now awaits rehydrateFromFile). The within-stream for await loop is sequential, so same-stream reentrancy is not the issue. The issue is cross-handler: a setWorkspace RPC can run while the event loop is parked at await getOrCreateTrace(...). On a genuine workspace change, startEventStream calls eventStream.abort.abort() then sessionTraces.clear() synchronously. When the parked loop resumes, it runs Trace.setActive(trace) + sessionTraces.set(sessionID, trace) into the freshly-cleared map — resurrecting a Trace under a stream that's supposed to be dead (orphaned writer).
Impact
Bounded: only on a real workspace switch (rare). On-disk file content stays correct (keyed by sessionID). The defect is a leaked/duplicate in-memory Trace writing after its stream was aborted.
Proposal
After the await in getOrCreateTrace, re-check currency before committing to the map: if sessionTraces was cleared or the stream's abort signal fired while suspended, drop the freshly-built Trace (void trace.endTrace().catch(()=>{})) instead of inserting it.
Acceptance
- A
setWorkspace-driven clear that interleaves with a suspended getOrCreateTrace does not leave an entry in sessionTraces tied to the aborted stream.
- Deterministic test simulating the interleave (Dispatcher/spyOn, no
mock.module).
Refs: #895
Found during the v0.8.4 release review (Chaos Gremlin persona). Deferred — rare concurrency edge, needs careful design + a deterministic test, >30 min.
Problem
#895 made
getOrCreateTrace(worker.ts)async(it nowawaitsrehydrateFromFile). The within-streamfor awaitloop is sequential, so same-stream reentrancy is not the issue. The issue is cross-handler: asetWorkspaceRPC can run while the event loop is parked atawait getOrCreateTrace(...). On a genuine workspace change,startEventStreamcallseventStream.abort.abort()thensessionTraces.clear()synchronously. When the parked loop resumes, it runsTrace.setActive(trace)+sessionTraces.set(sessionID, trace)into the freshly-cleared map — resurrecting a Trace under a stream that's supposed to be dead (orphaned writer).Impact
Bounded: only on a real workspace switch (rare). On-disk file content stays correct (keyed by sessionID). The defect is a leaked/duplicate in-memory Trace writing after its stream was aborted.
Proposal
After the
awaitingetOrCreateTrace, re-check currency before committing to the map: ifsessionTraceswas cleared or the stream's abort signal fired while suspended, drop the freshly-built Trace (void trace.endTrace().catch(()=>{})) instead of inserting it.Acceptance
setWorkspace-driven clear that interleaves with a suspendedgetOrCreateTracedoes not leave an entry insessionTracestied to the aborted stream.mock.module).Refs: #895