Skip to content

[BUG] Dev session reported completed from the idle-poll shortcut while the agent window is still alive #48

Description

@dracic

Description

GenericAdapter.wait_for_completion (src/automator/adapters/generic.py) can report a dev session completed from the idle-polling shortcut while the native agent window is still alive. Once an idle-grace window is armed, each silent tick does a non-blocking result-artifact check and returns completed the moment the artifact exists (generic.py:238-245, and again at grace expiry, generic.py:260-267) — neither path confirms the window has actually exited (alive is computed at generic.py:221 but only gates the crash branch). The stall fallback shares the hole: _final (generic.py:361-362) upgrades to completed on artifact presence alone.

Three aggravating facts (verified on current main):

  1. The premature completed kills the live agent. AgentAdapter.run (src/automator/adapters/base.py:91-96) runs finally: self.kill(handle) — the moment the wait returns, the window is killed. So this is not just a race with post-session reads: the agent is terminated mid-turn, before its remaining commits/cleanup/transcript flush.
  2. The artifact check runs before the pane-activity re-arm (generic.py:238 vs generic.py:251), so even a session that is visibly streaming output — demonstrably working — is declared complete and killed on the next idle tick.
  3. Re-driven specs widen the race to most of the session. devcontract.find_result_artifact (devcontract.py:193-211) qualifies any spec-*.md carrying an ## Auto Run Result heading with mtime ≥ launch. Escalation re-arm (reset_spec_status) rewrites only the frontmatter status: and leaves the stale ## Auto Run Result section in the file, so on a re-drive the spec qualifies as "terminal" from the agent's first mid-session save — not just in the final minutes.

The engine then enters post-session handling (spec-frontmatter reconcile, sprint sync, verify_dev, usage tally) against files the killed-or-still-writing agent left half-finished; a mis-read there feeds the destructive auto-rollback path.

Surfaced while prototyping the native-Windows multiplexer backend (slower ConPTY timing widens the race), but the bug is platform-neutral — the same interleaving exists on POSIX.

Steps to reproduce

Timing race, so conceptual repro:

  1. Start a dev session; let it end a turn without a terminal artifact (awaiting a long test, or an early turn-end) → the result-less Stop arms the idle-grace window (generic.py:322).
  2. Let the stall nudge re-invoke it (generic.py:278); the agent resumes working in a new turn while stall_deadline stays armed.
  3. Mid-turn the agent appends ## Auto Run Result to the spec (a tool call, typically before final prose/cleanup) — or, on a re-driven escalated story, simply saves any edit to a spec that still carries the stale section.
  4. The next idle tick finds the artifact → wait_for_completion returns completed, run()'s finally kills the still-working window, and the engine post-processes half-written state.

Expected behavior

Stop events and window-death are the only authoritative completion signals. A terminal artifact observed while the window is still alive is advisory: note it, keep waiting, and complete only once the Stop lands or the window dies. The stall fallback in _final should likewise accept a result only when the window is no longer alive.

Actual behavior

The idle-tick and grace-expiry paths return completed on artifact presence alone; the session window — possibly mid-turn and streaming — is then killed by run()'s finally, and the engine post-processes (and can roll back) a half-written attempt.

Proposed resolution

  • Gate the idle/grace terminal-artifact shortcuts on not self._window_alive(handle); while the window is alive they only re-arm/annotate. (The normal Stop-path completion — artifact + Stop with the window alive — is untouched; artifacts are written by tool calls inside turns, so a legitimate artifact cannot first appear after the final Stop beyond the existing 15 s result grace.)
  • _final gains an accept_result flag set to "window not alive" for the stall fallback, so a stall verdict can't silently upgrade to completed under a live window.
  • Consider stripping the stale ## Auto Run Result section when a spec is re-armed for re-drive, so a re-driven spec can't qualify as terminal from its first save.
  • Keep Stop + window-death as the authoritative paths; add focused tests: an idle-tick result under a live window must not complete the session, and a grace-expiry result under a live window must not complete it.

Which area is this for?

CLI adapters & profiles

bmad-auto Version

0.7.12

Which coding CLI are you using?

Claude (claude)

Operating System

Other (platform-neutral; observed timing widens on Windows/ConPTY, same race on Linux/macOS)

Confirm

  • I've searched for existing issues
  • I'm using the latest version

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions