mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-11 17:10:38 -10:00
Root cause: a force-killed turn lost state in two ways. Sessions opened from a legacy transcript write the model-visible .jsonl only on the 30s autosave tick or at turn end, so a kill before the first tick dropped the prompt from the model's context even though the display ledger kept it. Event-log (v3) sessions record the prompt on admission, but streamed assistant text reaches the log only when the message completes, so a kill mid-stream lost the partial reply entirely. Decision (option A): two bounded changes, no change to the cache-stable prompt prefix. - The agent announces a new advisory checkpoint boundary right after the user message is admitted. A controller without an event store saves the legacy transcript there (one extra save per turn); stores that already hold the message ignore it, and a failed save only logs. - The controller accumulates the open stream's deltas and, on the existing 30s autosave tick, appends an optional stream/checkpoint event carrying the same local-only interrupted record a cancelled stream leaves. It holds commitMu so the checkpoint can never land after the commit of the message it describes. The projection keeps the newest checkpoint of the open turn and drops it on any later message commit or turn boundary; restart recovery upserts it before turn/end interrupted. A completed stream therefore never shows twice, and the record stays out of provider-visible history. Tests (internal/control/turn_kill_durability_test.go), each killing a child process with os.Exit and no TurnDone, then reopening: - TestKilledLegacyTurnKeepsItsPrompt fails on main-v2 with "prompt of the killed turn lost after restart"; passes with the fix. - TestKilledTurnKeepsItsPartialOutput fails on main-v2 with "partial output of the killed turn not durable after reopen: 0 rows"; passes with the fix, and the partial text stays out of ModelMessages. - TestCompletedStreamSupersedesItsCheckpoint and TestKilledAfterCommittedStreamShowsItOnce fail on main-v2 with "no partial-output checkpoint was written while the stream was open"; with the fix the completed reply appears exactly once, including after a kill between its commit and turn end. Disabling the supersede rule makes the latter fail with the checkpoint replacing the reply.
33 lines
974 B
Go
33 lines
974 B
Go
package control
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"reasonix/internal/agent"
|
|
)
|
|
|
|
// CheckpointSession implements agent.SessionCheckpointer. The boundary is
|
|
// intentionally semantic: ordinary todo, approval, assistant and turn-end
|
|
// events remain eligible for the write-behind batch.
|
|
func (c *Controller) CheckpointSession(ctx context.Context, boundary agent.SessionCheckpointBoundary) error {
|
|
switch boundary {
|
|
case agent.CheckpointBeforeModel, agent.CheckpointBeforeTopTool:
|
|
if _, err := c.flushSessionEvents(ctx); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
case agent.CheckpointUserAdmitted:
|
|
// An event store recorded the message when it was admitted; only a
|
|
// legacy transcript still holds it in memory alone.
|
|
if c.sessionEventStore() != nil || c.SessionPath() == "" {
|
|
return nil
|
|
}
|
|
if err := c.snapshot(false, false, false); err != nil {
|
|
slog.Warn("controller: save admitted user message", "err", err)
|
|
}
|
|
return nil
|
|
default:
|
|
return nil
|
|
}
|
|
}
|