fix: re-derive the turn number from the log at turn open

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.
This commit is contained in:
imccyu
2026-07-28 01:09:54 +08:00
parent 2b2840a6e1
commit b1bcb428a7
2 changed files with 10 additions and 2 deletions

View File

@@ -312,7 +312,11 @@ export class ReactLoopAgent implements Agent {
this.abort = controller this.abort = controller
this.acceptsNextStep = true this.acceptsNextStep = true
const signal = controller.signal 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 step = 0
let opened = false let opened = false
let reason: TurnEndReason = { kind: 'completed' } let reason: TurnEndReason = { kind: 'completed' }

View File

@@ -16,9 +16,13 @@ interface Harness {
readonly plugin: Awaited<ReturnType<Context['plugin']>> readonly plugin: Awaited<ReturnType<Context['plugin']>>
} }
/** 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 { 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('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. */ /** Build a live idle agent accepted by the exact-identity goal service. */