Skip to content

[Bug]: Recovered executions start without their base-branch map, so the commits panel shows repo history instead of task commits #3806

Description

@OS-Juho-Lahtinen

Summary

When an execution is re-created after the agent stops (rate-limit kill, crash, lazy recovery), the new agentctl instance is built without the per-repo base-branch map. Its workspace trackers fall back to the hardcoded {origin/main, origin/master} list, so for any repository whose integration branch is named something else the commits panel reports the branch's divergence from main rather than from its real base. On a task with zero commits of its own, the panel showed COMMITS (100) — the request cap, not a count — and the entries were unrelated history from three repositories.

The value corrects itself on a later refetch, so the panel silently alternates between right and wrong with no visible difference between the two.

Affected area

agentctl

Kandev version

0.94.0 (desktop app; source read at tag v0.94.0, behaviour also present on main)

Install mode

Other — packaged desktop app

Environment

macOS, Darwin 25.6.0, arm64. Multi-repo task, three repositories, all with base branch origin/develop. Worktree executor.

Kandev configuration

Three repositories attached to one task, each with base_branch = origin/develop and repository default_branch = develop. Claude ACP agent profile. No comparison targets set.

Task/session context

A multi-repo task started from a Jira ticket via the full launch path, whose agent was later terminated by a provider rate limit and whose execution was then re-created on demand.

Steps to reproduce

  1. Create a multi-repo task whose repositories use an integration branch not named main or master (e.g. develop), started from a Jira ticket or any full-launch path.
  2. Let the agent run, then let its process die — rate-limit kill, crash, or stop.
  3. Touch anything that needs the workspace (open the Changes panel, a terminal, a file). This re-creates the execution through GetOrEnsureExecution.
  4. Read the COMMITS count in the Changes panel while the new instance is coming up.

Reproducibility

Intermittent

Clean-state check

Not tried

Expected behavior

The commits panel counts commits on the task's branch relative to its configured base branch — zero when the branch has no commits of its own — on every execution, however it was created.

Actual behavior

The panel shows up to defaultLogLimit (100) commits of unrelated repository history. Observed on a task whose three repositories were each 0 commits ahead of origin/develop but 315 / 178 / 871 ahead of origin/main — 1,364 commits merged and truncated to 100. A later refetch (or a page reload) returns the correct 0.

Intermittency note: the bug depends on a fetch landing in the unseeded window, but it is guaranteed reachable on any task whose agent dies and is re-created. Clean-state note: the behaviour follows from execution-creation ordering, not stored workspace state.

Root cause

Two independent gaps, either of which produces it:

1. The recovery path never carries the map. prepareExecutionCreateRequest (manager_execution.go) builds the create request for every on-demand execution. It hydrates comparison targets:

comparisonTargets, err := comparisonTargetsFromMetadata(metadata)
…
ComparisonTargets: comparisonTargets,

but sets no BaseBranches — there is no baseBranchesFromMetadata equivalent and the field is never populated. The full launch path does carry it (buildMetadata → MetadataKeyBaseBranches → getMetadataStringMap in each executor), so a launched instance is born seeded and a recovered one is not.

2. Seeding happens after the instance is serving. waitForAgentctlReady (manager_startup.go):

err := client.WaitForReady(ctx, 60*time.Second)   // HTTP server proven up
if err != nil { … return }
execution.MarkAgentctlReady()                      // ← requests can be served from here
…
m.pushTaskBaseBranches(ctx, …)                     // ← map arrives only now
m.pushTaskComparisonTargets(ctx, …)

Anything that fetches between those two points gets trackers with no stored base branch.

Downstream, resolveBaseBranchWithReason falls through to branchDiffCandidates = integrationBranchRefs(true) over integrationBranchNames = {main, master}. develop isn't a candidate, and the repository's own DefaultBranch — which kandev stores and already reads in collectTaskBaseBranches — is never consulted. With no anchor at all, GetLog takes its open-ended path (-n<limit>) and mergeGitLogResults truncates to the same limit, producing a plausible-looking "100".

Prior art: #2270 added the post-ready push precisely because "LaunchRequest metadata only carries it on the full launch path". That fix is incomplete on both counts — the push runs after the gate, and the create-request path it backstops still omits the field.

Timeline from one occurrence

17:06:33  launching agent for prepared session     ← full launch, instance seeded
17:06:41  agent launched for prepared session
17:07:25–29  three branch switches                 ← worktrees checked out
17:32:47  handling agent stopped                   ← agent killed
17:33:14  creating execution for task session      ← ensure path, no BaseBranches
17:52:45  creating execution for task session      ← again

Suggested fix

In priority order:

  1. Populate BaseBranches in prepareExecutionCreateRequest, mirroring the ComparisonTargets hydration directly above it. Read it from info.Metadata when the environment persisted it, otherwise hydrate synchronously from m.baseBranchProvider(ctx, taskID) — the manager already holds that provider and already calls it in pushTaskBaseBranches. This makes a recovered execution born seeded, like a launched one. Without it, the other two only narrow the window.
  2. Move pushTaskBaseBranches / pushTaskComparisonTargets above execution.MarkAgentctlReady(). A pure reorder: client.WaitForReady has already proven the HTTP server answers, and the client is acquired identically in both places.
  3. Refuse to answer range queries before the map is known. config.BaseBranches already exists; add a seeded flag set at instance creation and by POST /workspace/base-branches, and have git log / status / cumulative-diff return the existing ready:false envelope until it is set. The frontend already retries that envelope (NOT_READY_RETRY_MS = 2000), so no UI change is needed. This makes the failure impossible to reintroduce silently.

Optional hardening, independent of the above: consult the repository's stored DefaultBranch before {main, master} in integrationBranchNames, so the fallback is correct for non-main shops when it genuinely is the right path.

Logs and artifacts

Timeline above is from the backend log of one occurrence. No errors are logged on this path — base-branch seeding only logs on failure, and none occurred; the wrong answer is produced by a successful request.

Last known good version

Unknown. The post-ready push dates to #2270; the create-request omission appears to predate it.

Extra context

Related: #2270 (added the post-ready push this extends). The truncation itself is also invisible: mergeGitLogResults slices to the limit with no flag, so a capped list renders identically to a real count — CumulativeDiffResult already carries TruncatedFilesCount for the analogous case.

Activity

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

Metadata

Metadata

Assignees

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