Repository navigation
fix(core): keep one warming loop per session and ignore compaction - #61724
rekram1-node wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
The fix looks right. Each new test fails on dev and passes here: one loop per session, timed from the model the session last used, and no compaction hook. bun test test/plugin (566 pass) and tsc -b in packages/core are clean. I didn't run it in the TUI because the simulated model has no cache TTL to warm.
Public API: unchanged. The internal warming plugin just stops registering its compaction hook; config keys and the plugin API are the same.
Two things, both inline: the keep-alive now reaches FiberMap.run from inside the loop's own fiber, which is fragile, and the compaction test only checks that a hook isn't registered.
Related: #58163
| interval: settings.interval, | ||
| expires, | ||
| }) | ||
| yield* FiberMap.run( |
There was a problem hiding this comment.
The keep-alive itself goes through ctx.session.generate, which runs this generate hook inside the loop's own fiber. Today the keep-alive-prompt check above returns before we get here. If that check ever misses (another hook rewrites the last message, the prompt gets wrapped, etc.), FiberMap.run would interrupt the calling fiber, which is the loop itself. Every keep-alive would then be cancelled before it's sent, so warming would quietly stop working. Before this change, a miss only pushed the expiry out. A guard that doesn't depend on matching the prompt would make this robust, e.g. skipping the restart when the current fiber is the session's loop, or setting a flag around the keep-alive generate call.
| it.effect("ignores compaction, which runs on the compaction model", () => | ||
| Effect.gen(function* () { | ||
| const { warmed, turn, hooks } = yield* start | ||
| expect(hooks.has("compaction")).toBe(false) |
There was a problem hiding this comment.
This test only checks that a compaction hook isn't registered. The rest of it (an uncached turn followed by 10 minutes with no keep-alive) passes on dev too. It checks an implementation detail rather than the behaviour, so I'd drop it and keep the two loop tests.
There was a problem hiding this comment.
Both earlier points are addressed: keep-alives are marked in progress so the loop can't restart itself, and the compaction test that only checked hook registration is now a behaviour test. Tests and tsc -b in packages/core still pass (566 in test/plugin). The public API is unchanged.
One new point on the test mock (inline): the first test no longer fails on dev, so it stopped guarding the duplicate-loop bug.
| // Like the real one, a keep-alive runs the generate hook before it is sent, and takes a while, so overlapping | ||
| // loops would each send one. | ||
| generate: () => | ||
| turn("short").pipe( |
There was a problem hiding this comment.
Calling turn("short") from the mocked generate makes the first test ("keeps one loop per session…") pass on dev too. With messages: [], dev's prompt check never matches, so each keep-alive resets last for the session. The second loop then wakes, sees it's early, and goes back to sleep, which hides the duplicate keep-alive. Real keep-alives on dev do match the prompt, so this isn't what users see. Keeping the hook re-entry only in the new third test, or passing the keep-alive prompt as the last message here, would let the first test catch the bug again.
Problem
Since warming became per model (#60025, #60969):
The last one can start warming on a session whose model opted out, or stop it when the compaction model doesn't warm.
Change
FiberMapkeyed by session. Each turn restarts the session's loop, so there's only ever one, and it times from the current settings.Verification
test/plugin/warming.test.ts(3 tests, TestClock): all fail ondev, pass herebun test test/plugininpackages/core: 566 passbun run typecheckinpackages/core: no errorsAdded by the opencode bot (AI-generated).
Related: #58163