Skip to content

Recovered-session dedup drops a verbatim re-run: identical user message anywhere in history swallows the new turn #378

Description

@initializ-mk

Symptom

Re-running the same request in an existing (already-answered) execution does nothing — the agent replays its previous answer and never re-attempts tools. Field case: a delegated MCP (Atlassian) call that failed "not connected", then the user connected the account and re-sent the identical request — the execution stayed broken (the tool was never called again), while a fresh execution worked.

Proof (field log, next-SNAPSHOT-50cd5e3)

session recovered  task_id=tsk_6a6caa5e… messages=7
llm_call  llm_call_count=1   # prompt ENDS with the assistant's prior "Jira not connected" reply — NO new user turn appended
# no mcp_tool_call, no tool_exec — the tool was never re-attempted

The re-send's user message never reached the model, so it continued from its own last assistant turn and repeated the refusal.

Root cause — forge-core/runtime/loop.go

On a recovered session, the incoming user message is appended unless it dedups against the last user message anywhere in history:

if recovered {
    msgs := mem.Messages()
    lastUserIdx := -1
    for j := len(msgs) - 1; j >= 0; j-- {
        if msgs[j].Role == llm.RoleUser { lastUserIdx = j; break }   // last user ANYWHERE
    }
    if lastUserIdx < 0 || msgs[lastUserIdx].Content != newMsg.Content {
        mem.Append(newMsg)
    }
}

The comment scopes the intent correctly — "skip if the recovered session already ends with an identical user message (avoids duplicates when users retry after a premature loop exit)" — but the implementation compares against the last user turn regardless of position. When a prior identical request was already answered (the session ends in an assistant message), a verbatim re-run matches that earlier user turn and is dropped. The model is then re-run on the poisoned transcript with no new input.

A premature-exit duplicate — the case this guard is for — leaves the session ending in a user message (persisted, never answered). That's the only shape that should dedup.

Fix

Dedup only a trailing identical user message:

if n == 0 || msgs[n-1].Role != llm.RoleUser || msgs[n-1].Content != newMsg.Content {
    mem.Append(newMsg)
}

If the recovered session ends in an assistant turn, an identical incoming user message is a legitimate re-run and must be appended so the loop re-enters and re-attempts tools.

Test

  • Recovered session ending in an assistant turn + an identical incoming user message → the message IS appended (loop re-enters). Regression for this bug.
  • Recovered session ending in a user turn identical to the incoming message (premature-exit dup) → still skipped (no regression).
  • Recovered session ending in a user turn with DIFFERENT content → appended.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions