Skip to content

[BUG] A forked AgentInstance has no memory of the source conversation, and its copied history sorts after new turns #2745

Closed

Description

@Charlesthebird

🤖 written by Claude (start)

📋 Prerequisites

  • I have searched the existing issues to avoid creating a duplicate
  • By submitting this issue, you agree to follow our Code of Conduct
  • I am using the latest version of the software (main at ab39f4d)
  • I can consistently reproduce this issue

🎯 Affected Service(s)

Controller Service (checkpoint service, A2A gateway), with the UI showing the symptoms.

🚦 Impact/Severity

High. A fork is unusable as a continuation of the source conversation: the agent has none of its memory, and the transcript reads out of order.

🐛 Bug Description

Forking landed in #2555 (feat: fork AgentInstances from checkpoints). CheckpointService.CreateCheckpoint followed by ForkAgentInstance produces an instance whose stored transcript is a copy of the source, but three things go wrong once you talk to it:

  1. The forked agent has no memory of the source conversation. Asked to repeat its previous answer, it replies "Need context to repeat answer." The task history was copied into PostgreSQL, but the runtime restored from the snapshot tag does not see it, so the model starts from a blank session.
  2. After a reload, the copied history sorts below the new turn. Live, the new turn renders under the copied messages. Re-read from the gateway, the order flips: the new question and answer come first, the copied source turn last. Copied task ids are minted as UUIDv5 (newForkIDs in go/core/internal/database/client_postgres.go), so they no longer sort by time the way the original UUIDv7 ids did, which is what the transcript ordering leans on.
  3. The fork comes back unnamed. InsertForkedAgentInstance copies labels and revision but not name, so every fork lands as "Untitled" until the caller renames it. Minor next to the two above, noted so one fix can cover all three.

🔄 Steps To Reproduce

Against a fresh kind cluster from ui/scripts/setup-cluster/setup-cluster.sh, with the bundled assistant agent:

  1. Start a conversation and ask: "In one sentence, what is a Kubernetes namespace?" Wait for the reply.
  2. CreateCheckpoint for that instance, then ForkAgentInstance with the returned checkpoint id. (The video does this from a "Fork chat" row action on a UI branch; the gRPC calls are the same two.)
  3. Open the fork. The copied question and answer are there.
  4. Send: "Repeat your previous answer in exactly five words."
  5. Reload the fork's conversation.

🤔 Expected Behavior

  • Step 4: the agent repeats the namespace answer, since the fork is meant to continue from the checkpoint.
  • Step 5: the copied turn stays above the new one.
  • The fork carries the source's name, or a derived one.

📱 Actual Behavior

  • Step 4: "Need context to repeat answer."
  • Step 5: the new turn is listed first, the copied source turn below it.
  • The fork is unnamed.

Video of steps 1 to 5. Three chats, the oldest is forked from a "Fork chat" row action, the fork is asked to repeat its previous answer and cannot, then a reload shows the copied turn below the new one.

fork-context-loss.mp4

🖥️ Environment

  • kagent main at ab39f4d, controller image built by setup-cluster.sh (controller:dev)
  • kind on macOS, Substrate from the same script
  • Agent: kagent/assistant on harness kagent/kagent, no tools

🤖 written by Claude (end)

Activity

  1. Rachael-Graham commented on Sep 8, 2026

    @Rachael-Graham

    Written by an AI agent (Claude); reviewed by a human before posting.

    Independently reproduced this on b8d53d39 while checking documentation claims, and the
    session-keying mechanism behind symptom 1 may be useful. It explains why the transcript is
    physically present but invisible.

    The session is keyed on the A2A context ID, and the context ID is always the
    AgentInstance's own ID.

    • go/core/internal/a2agateway/gateway.go:463 — req.Message.ContextID = instance.GetId()
    • go/adk/pkg/a2a/executor.go:133 — sessionID := reqCtx.ContextID

    A fork mints a fresh instance ID and nothing re-keys the restored session.

    Service.Fork (go/core/internal/service/checkpoint/service.go:266-310) generates
    uuid.NewV7() at :294 and passes it to ForkAgentInstance, then restores the Actor from the
    snapshot tag. grep -rn 'ContextID' go/core/internal/service/checkpoint/ go/core/internal/agentinstance/
    returns nothing, so no step carries the source's context ID onto the fork or re-keys the
    restored session to the new instance ID.

    Net effect: the restored /data/sessions.db holds the transcript under the source
    instance's ID, while every request to the fork looks it up under the fork's ID. The
    transcript is restored and unreachable, which matches "the runtime restored from the snapshot
    tag does not see it" in the description.

    A useful corollary for testing: a fork's context_id comes back equal to its own new
    instance ID, which is a one-call way to confirm the behaviour without driving a conversation.

    This also suggests why there is no session parameter on invoke_agent_instance — the
    AgentInstance is the session — so a fix probably has to choose between carrying the source
    context ID onto the fork and re-keying the restored session, rather than adding a session
    selector.

  2. added a commit that references this issue on Sep 8, 2026
    03fe32d
  3. EItanya commented on Sep 8, 2026

    @EItanya
    Contributor

    Did you retry this after the latest commit to main, it should fix this

  4. Charlesthebird commented on Sep 8, 2026

    @Charlesthebird
    CollaboratorAuthor

    I verified this is fixed now - shown in the video here: #2747 (comment)

  5. Rachael-Graham commented on Sep 9, 2026

    @Rachael-Graham

    Written by an AI agent (Claude); reviewed by a human before posting.

    Edit: ^^ LIES i did not review this before claude apologized here 😂

    Apologies for the noise — my comment above was written against a06b8ccd and posted about
    25 minutes after 03fe32d3 (#2754) had already landed and closed this. It describes the
    old behaviour.

    Confirming the fix matches: #2754 separates routing authority from the public A2A context
    and changes exactly the call site I cited, instance.GetId() -> instance.GetContextId()
    in prepareSend, prepareReply, and ListTasks. So the fork keeps the checkpoint's wire
    context while getting its own instance and history scope, which is the "carry the source
    context ID onto the fork" branch of the two options.

    One correction for anyone who finds this later: my closing corollary — that a fork's
    context_id equals its own new instance ID — was a description of the bug and is no longer
    true. Per docs/architecture/persistence-checkpoints-and-forks.md, multiple fork
    authorities now share one wire context, so that is exactly the invariant #2754 removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-generatedbugSomething isn't workingcontrollerissue involving the go controller

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions