From b1bcb428a76a96ec890f25b38c1de18dfc2854c1 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:09:54 +0800 Subject: [PATCH] fix: re-derive the turn number from the log at turn open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An out-of-band zero-step turn (durable command lifecycle on an idle log) advances the log's turn numbering behind ReactLoopAgent's cached lastTurn, so the next real turn reused a stale number and tripped the session invariant (turn/start expected N, got 1) — hanging the TUI after any idle slash command. The log is the numbering authority: take max(cached, logged) + 1 at open. The command-goal stub's inject helper also gains the one-shot injection turn wrap the real agent performs, restoring turn enclosure in its log assertions. --- packages/core/agent-loop/src/agent.ts | 6 +++++- packages/goal/command-goal/tests/command-goal.spec.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 2d25185733..26e4fb6f45 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -312,7 +312,11 @@ export class ReactLoopAgent implements Agent { this.abort = controller this.acceptsNextStep = true const signal = controller.signal - const turn = this.lastTurn + 1 + // The log is the turn-number authority: out-of-band zero-step turns + // (command lifecycle on an idle log) advance it behind this cached + // counter, so re-derive the successor at open instead of trusting it. + const loggedLast = this.session.events.findLast(event => event.type === 'turn/start')?.data.turn ?? 0 + const turn = Math.max(this.lastTurn, loggedLast) + 1 let step = 0 let opened = false let reason: TurnEndReason = { kind: 'completed' } diff --git a/packages/goal/command-goal/tests/command-goal.spec.ts b/packages/goal/command-goal/tests/command-goal.spec.ts index d77c64a089..2bf06c3f3e 100644 --- a/packages/goal/command-goal/tests/command-goal.spec.ts +++ b/packages/goal/command-goal/tests/command-goal.spec.ts @@ -16,9 +16,13 @@ interface Harness { readonly plugin: Awaited> } -/** Append one idle injection using the public Agent contract. */ +/** Append one idle injection using the public Agent contract (idle inject wraps in a one-shot injection turn, per turn enclosure). */ function appendInjection(session: Session, input: UserMessageData): void { + const lastStart = session.events.findLast(event => event.type === 'turn/start') + const turn = (lastStart?.data.turn ?? 0) + 1 + session.append('turn/start', { turn, trigger: { kind: 'injection', source: input.source } }) session.append('user/message', input, { surfaceOp: 'append' }) + session.append('turn/end', { turn, reason: { kind: 'completed' } }) } /** Build a live idle agent accepted by the exact-identity goal service. */