refactor(coding-agent): wake RLM quiescence on activity changes instead of polling (#1859)
* fix(coding-agent): serialize post-compaction continuation on run settlement * refactor(coding-agent): wake RLM quiescence on activity changes instead of polling * fix(coding-agent): guard post-compaction continue against auto-compaction and stale runners - Publish _compactionOperation from _runAutoCompaction so the post-compaction continuation runner (and other waiters) serialize against auto/threshold/overflow/requested compaction, not just manual compact(). - Identity-guard the success path of the continuation runner so a cancelled runner cannot forget continuation messages that a replacement settlement still owns (busy/error paths already did). * fix(coding-agent): recheck refine apply before dispatching post-compaction continue A refine() call could enter its apply phase (which disconnects the agent event listener) between the runner's top-of-loop _waitForRefineIdle() and agent.continue(), losing the continuation's messages. Recheck _refineInFlight under the commit fence like pauses/_compactionOperation; the path from that recheck to agent.continue() is synchronous, mirroring the final refine barrier used by normal turn dispatch. * fix(coding-agent): release the continuation runner's idle wait on cancel A cancelled post-compaction continuation left its runner parked inside waitForIdle when the pump was suspended with queued actions: the wait was not raced against the settlement like its neighbors, so the runner re-registered a session-input checkpoint waiter on every notify. The leaked waiter kept hasPendingAdmissionWaiters true and blocked daemon passivation until an unrelated resume. waitForIdle's loop now takes an optional settlement: the suspended-queue park races the settlement promise, deregisters its waiter on loss, and the loop exits once the settlement is superseded. The runner uses the settlement-scoped variant; public waitForIdle is unchanged.
1- Removed the delay before continuing sessions after compaction.1- Wait for RLM session activity changes without zero-delay polling.1482 unmodified linesExpand all148314841485148614871487148814891490149111 unmodified lines15031504150515061507150715081509151015114 unmodified lines1516151715181519152015211522152315241524152515261527152815292689 unmodified linesExpand all1482 unmodified linesExpand all ); }); });
it("strong quiescence yields to a real gated child bash while interactive idle remains resolved", async () => { it("strong quiescence waits for a gated child bash activity change", async () => { const child = createSession({ rlmSessionDir: join(tempDir, "bash-active-child") }); const bashStarted = deferred<void>(); const bashCompletion = deferred<void>(); const operations: BashOperations = {11 unmodified lines expect(root.registerRlmChildSession("bash-active-child", child)).toBe(true); const originalHeadlessIdle = child.waitForHeadlessIdle.bind(child); let headlessIdleCalls = 0; vi.spyOn(child, "waitForHeadlessIdle").mockImplementation(async () => { if (++headlessIdleCalls > 100) throw new Error("RLM quiescence spun without yielding a macrotask"); headlessIdleCalls++; await originalHeadlessIdle(); });
const quiescence = root.waitForRlmQuiescence();4 unmodified lines ), sleep(20).then(() => "timer" as const), ]); expect(firstBoundary).toBe("timer"); expect(headlessIdleCalls).toBe(1);
bashCompletion.resolve(); await bash; await expect(quiescence).resolves.toBeUndefined(); expect(headlessIdleCalls).toBeLessThan(100); expect(headlessIdleCalls).toBe(2); });
it("rechecks parent self-activity after a child quiescence boundary", async () => { const child = createSession({ rlmSessionDir: join(tempDir, "boundary-active-child") });2689 unmodified linesExpand all