Skip to content

fix(goal): resume blocked goal at before_agent_start to close stale-flag race - #2

Merged
code-yeongyu merged 1 commit into
mainfrom
fix/user-flag-leak
Jul 26, 2026
Merged

fix(goal): resume blocked goal at before_agent_start to close stale-flag race#2
code-yeongyu merged 1 commit into
mainfrom
fix/user-flag-leak

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Defect

src/goal/lifecycle.ts set nextAgentStartWasUserTriggered = true in the before_agent_start handler and consumed it only in agent_start. In the host (agent-session.ts), before_agent_start is emitted before the final provider admission check (_enforceFinalProviderAdmission), which can reject the run — so no agent_start follows, the flag survives, and the next run that bypasses before_agent_start (e.g. a goal-continuation / queued custom-message turn that starts the agent directly via agent.prompt) consumes the stale flag and wrongly resumes a blocked goal.

Fix (option a)

Move the blocked -> active resume into the before_agent_start handler itself (it fires only for real user prompts) and remove the flag entirely. agent_start becomes pure accounting init: it reads the goal, sees status: active (if just resumed), and begins accounting for the resumed turn. A continuation-style agent_start now has no resume path of its own and cannot trigger a stale resume.

A rejected user prompt now resumes the goal at before_agent_start (the user's expressed intent to resume); no accounting begins until agent_start, preserving the invariant that the first resumed turn is accounted.

This is the cleaner of the two options: option (b)'s generation-token binding is not implementable against this host's available extension events — BeforeAgentStartEvent carries no generation token, and no extension event fires between a rejected before_agent_start and a later continuation agent_start, so a flag set in before_agent_start cannot be deterministically invalidated before the continuation consumes it. Option (a) eliminates the race mechanism entirely.

Tests (TDD)

  • regression (resumes a blocked goal at before_agent_start even when admission rejects the run): a before_agent_start with no following agent_start must resume the blocked goal to active. Fails on the original flag-based code (which left it blocked waiting for agent_start), passes with this fix.
  • does not resume a blocked goal on a continuation-style agent_start with no preceding before_agent_start: a continuation-style agent_start must not resume a blocked goal on its own.
  • existing happy path (before_agent_start -> agent_start -> resume + first resumed turn accounted) still passes.

Evidence captured locally under .omo/evidence/pi-todo-goal-upgrade/ (task-f2a-red.txt, task-f2a-green.txt).

Gates

  • npx vitest run — 87 passed (9 files)
  • npm run check (tsgo + biome + no-excuse) — green (exit 0)

Summary by cubic

Fixes a race where a stale user-trigger flag could wrongly resume a blocked goal on continuation-only turns by moving the resume logic to before_agent_start and removing the flag. agent_start is now init-only, so rejected user runs no longer leak a resume into later turns.

  • Bug Fixes
    • Resume blocked -> active in before_agent_start; removed nextAgentStartWasUserTriggered.
    • agent_start only starts accounting when the goal is already active.
    • Rejected user prompts still resume the goal; continuation-style agent_start no longer resumes.

Written for commit 66224ec. Summary will update on new commits.

Review in cubic

…lag race

The host emits before_agent_start BEFORE the final provider admission
check, which can reject the run so no agent_start follows. The previous
design set a sticky nextAgentStartWasUserTriggered flag in
before_agent_start and consumed it in agent_start, so a rejected run
left the flag set. The next run that bypasses before_agent_start (e.g.
a goal-continuation / queued custom-message turn that starts the agent
directly via agent.prompt) consumed the stale flag and wrongly resumed
a blocked goal.

Move the blocked->active resume into before_agent_start itself (it
fires only for real user prompts) and remove the flag entirely.
agent_start becomes pure accounting init: it reads the goal, sees
status active (if just resumed), and begins accounting for the resumed
turn. A continuation-style agent_start now has no resume path of its
own and cannot trigger a stale resume.

A rejected user prompt now resumes the goal at before_agent_start (the
user's expressed intent to resume); no accounting begins until
agent_start, preserving the invariant that the first resumed turn is
accounted. This is the cleaner of the two options the fix considered;
option (b)'s generation-token binding is not implementable against
this host's available extension events (BeforeAgentStartEvent carries
no generation token, and no extension event fires between a rejected
before_agent_start and a later continuation agent_start).

Tests:
- regression: a before_agent_start with no following agent_start must
  resume the blocked goal to active (fails on the original flag-based
  code, which left it blocked waiting for agent_start).
- a continuation-style agent_start with no preceding before_agent_start
  must not resume a blocked goal on its own.
- existing happy path (before_agent_start -> agent_start -> resume +
  first resumed turn accounted) still passes.
@code-yeongyu
code-yeongyu merged commit 6937ce1 into main Jul 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant