mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(compact): decide step-alignment from surface tool-pairing, fire compaction pre-step (CBR-001)
Codex round 1 CBR-001: a head-anchored compaction checkpoint was mis-classified by the log-position step-alignment scan, so a second auto-compaction over a checkpoint-headed surface silently failed. Root cause: `isStepAlignedStart/End` scanned the LOG by seq, but a `replace` op lands a checkpoint at a high log seq whose SURFACE position is the head — its log neighbours (the open step's assistant/message) are not its surface neighbours, so the forward scan wrongly reported mid-step. Fix, per the agreed direction: - Replace the two log-position predicates with one surface-anchored helper `isToolPairingBalanced(nodes, events, beforeSeq)` in `dsh-session` (renamed step-boundary.ts → tool-pairing.ts). A cut is balanced when no unanswered tool-call precedes it on the surface; a region is collapsible iff both edges are balanced cuts. The open-tail and free-node cases fall out of the same counter. It also throws on a corrupt surface (a tool/result with no matching call). - Move compaction off the in-step seam to a new "pre-step" seam fired after turn/start and before step/start, so a compaction's log-only compact/* records and its replacement node land cleanly OUTSIDE any step (the honest structure crash-safety relies on). Renamed the event agent/pre-request → agent/pre-step and switched its dispatch from parallel → serial (listeners mutate the surface as a side effect; serial isolates them so concurrent appends can't interleave). Extended the catalog generator to accept @mode serial. Regression coverage: a real-loop test driving an auto-compaction asserts the landed checkpoint is a balanced cut on both sides; unit tests pin the checkpoint case, the mid-step injection case, multi-call steps, and the corrupt-surface guard. Proven red on the old log-position logic.
This commit is contained in:
@@ -73,7 +73,8 @@ export abstract class CompactService extends Service {
|
||||
* Non-surface context injected downstream (into the request `messages` by a
|
||||
* later listener) is out of this accounting by construction.
|
||||
* - **Head-anchored, best-effort.** Auto-compaction consolidates from the
|
||||
* surface HEAD up to a step-aligned cutoff, so a prior head checkpoint is
|
||||
* surface HEAD up to a balanced tool-pairing cutoff, so a prior head
|
||||
* checkpoint is
|
||||
* re-summarized into one fresh checkpoint (the surface holds at most one
|
||||
* auto-generated checkpoint, always at the head). It is best-effort over
|
||||
* CLOSED steps: when the only compactable content left is an un-splittable
|
||||
@@ -106,15 +107,15 @@ export abstract class CompactService extends Service {
|
||||
* summarizes their content and appends a replacement surface node. Used by the
|
||||
* (future) `/compact` tool and internally by {@link compactIfNeeded}.
|
||||
*
|
||||
* The region MUST contain whole steps — `start` and `end` must each sit on a
|
||||
* step boundary (the first / last surface node of a step) or on a node that
|
||||
* belongs to no step (a pre-step user message, inter-step steering, or an
|
||||
* injection context message). A boundary that falls INSIDE a step would split
|
||||
* that step's `assistant/message` tool-calls from their `tool/result`s, leaving
|
||||
* the rehydrated transcript with a dangling tool-call or an orphaned
|
||||
* tool-result that every provider rejects. An `end` inside an open (unclosed)
|
||||
* tail step is likewise invalid — its tool-calls have no results yet.
|
||||
* `dsh-session` exports `isStepAlignedStart` / `isStepAlignedEnd` for this check.
|
||||
* The region MUST NOT split a step's `assistant/message` tool-calls from their
|
||||
* `tool/result`s, leaving the rehydrated transcript with a dangling tool-call
|
||||
* or an orphaned tool-result that every provider rejects. A region is safe iff
|
||||
* both its edges are balanced cuts on the surface: the cut before `start` and
|
||||
* the cut after `end` each have no unanswered tool-call before them. A node
|
||||
* that belongs to no step (a pre-step user message, inter-step steering, or an
|
||||
* injection context message) is a balanced (free) boundary; an `end` inside an
|
||||
* open (unclosed) tail step is invalid — its tool-calls have no results yet.
|
||||
* `dsh-session` exports `isToolPairingBalanced` for this check.
|
||||
*
|
||||
* @param session - the session whose surface is mutated.
|
||||
* @param start - inclusive seq of the first surface node to compact.
|
||||
@@ -128,8 +129,8 @@ export abstract class CompactService extends Service {
|
||||
* valid surface nodes, if `start` is positioned after `end` on the surface
|
||||
* (the range is a surface-POSITION span, not a numeric seq interval — a
|
||||
* prior replace can leave the surface non-monotonic in seq order), or if
|
||||
* either boundary is not step-aligned (would split a step's tool-call/result
|
||||
* pair).
|
||||
* either boundary is not a balanced tool-pairing cut (would split a step's
|
||||
* tool-call/result pair).
|
||||
*/
|
||||
abstract compactRegion(
|
||||
session: Session,
|
||||
|
||||
Reference in New Issue
Block a user