Skip to content

fix(agent): use unique IDs for host-advance todo_write events to prevent frontend panel merge#4132

Merged
esengine merged 1 commit into
esengine:main-v2from
JesonChou:fix/host-advance-id-collision
Jun 12, 2026
Merged

fix(agent): use unique IDs for host-advance todo_write events to prevent frontend panel merge#4132
esengine merged 1 commit into
esengine:main-v2from
JesonChou:fix/host-advance-id-collision

Conversation

@JesonChou

Copy link
Copy Markdown
Contributor

Closes #4105

Description

Problem

emitTodoState emits a synthetic todo_write event after each complete_step sign-off to refresh the frontend TodoPanel. Every event used the static tool ID "host-advance":

// Before — all sign-offs shared the same ID
t := event.Tool{ID: "host-advance", Name: "todo_write", ...}

The frontend (useController.ts) resolves tool items by ID: when it finds an existing item, it updates it in place instead of creating a new one. Multiple complete_step calls within a single turn therefore merged into one item, and the TodoPanel only rendered the first sign-off. Subsequent advances were silently dropped.

Reproduction (reported in #4105):

  1. Model sets up todo list: [A:in_progress, B:pending, C:pending]
  2. Model signs off A → host advances → emit host-advance → panel shows 1/3 ✓
  3. Model signs off B → host advances → emit host-advance → ID collision, merged → panel still 1/3 ✗
  4. Model signs off C → host advances → emit host-advance → ID collision, merged → panel still 1/3 ✗

Impact: the model follows the system prompt ("you don't need a separate todo_write") but the panel never catches up. The model's observed workaround is to manually re-send todo_write after every sign-off, adding turns and token overhead.

Design

Add a monotonic atomic.Int64 counter (hostAdvanceSeq) to the Agent struct, and pass the matched todo item index from advanceCanonicalTodo into emitTodoState to form a unique, self-documenting tool ID:

// After
id := fmt.Sprintf("host-advance-%d-%d", a.hostAdvanceSeq.Add(1), itemIndex)
Component Meaning Purpose
hostAdvanceSeq (prefix) Monotonic counter Guarantees uniqueness across turns
itemIndex (suffix) 1-based panel position Self-documenting for debugging

Examples:

host-advance-1-1  → 1st advance, panel item 1
host-advance-2-3  → 2nd advance, panel item 3
host-advance-3-2  → 3rd advance, panel item 2

Preserved (unchanged)

Related

Files changed

internal/agent/agent.go                  | 15 ++++++++++++---
internal/agent/complete_step_e2e_test.go |  2 +-
2 files changed, 13 insertions(+), 4 deletions(-)

Tests

All passing:

  • complete_step_e2e_test.go — 5 E2E tests (helper updated for prefix match)
  • canonical_todo_test.goTestAdvanceCanonicalTodoCompletesAndPromotes etc.
  • evidence_flow_test.go — 10 evidence flow tests
  • final_readiness_test.go — cross-turn gate behaviour
  • internal/evidence — receipt matching
  • internal/tool/builtin — complete_step / todo_write execution

…ite events

emitTodoState emitted every synthetic todo_write with the static ID
host-advance, so multiple complete_step calls within a turn produced
events that the frontend merged into a single item (matching by ID).
The todo panel only rendered the first sign-off update and dropped
subsequent advances.

Add a monotonic atomic.Int64 counter (hostAdvanceSeq) to the Agent and
pass the matched todo item index from advanceCanonicalTodo into
emitTodoState.  The tool ID is now host-advance-{seq}-{idx} — the
counter guarantees uniqueness across turns (no cross-turn ID reuse),
while the index maps directly to the task panel position for
debuggability (e.g. host-advance-3-2 means third advance, item 2).

The per-turn ledger, canonical todo state, and all guard mechanisms
are unchanged.  Update the hostAdvances test helper to accept any
host-advance-* prefix.
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development agent Core agent loop (internal/agent, internal/control) labels Jun 12, 2026
@esengine esengine merged commit 09fa4c0 into esengine:main-v2 Jun 12, 2026
14 checks passed
JesonChou added a commit to JesonChou/DeepSeek-Reasonix that referenced this pull request Jun 12, 2026
…arts

Builds on esengine#4132 (unique host-advance IDs) by fixing the second half of esengine#4105:
host-advance events were emitted in real time but never persisted, so the
TodoPanel regressed on tab switch or restart.

Add HistoryWithCanonicalTodos that injects the canonical task list into the
history response, and EmitCanonicalTodoState that pushes it via the live stream,
covering both history-load and cached-tab-switch paths.
JesonChou added a commit to JesonChou/DeepSeek-Reasonix that referenced this pull request Jun 12, 2026
…arts

Builds on esengine#4132 (unique host-advance IDs) by fixing the remaining half of
esengine#4105: host-advance events were emitted in real time but never persisted, so
the TodoPanel reverted on tab switch or restart.

Add HistoryWithCanonicalTodos that injects the canonical task list into the
history response, and EmitCanonicalTodoState that pushes it via the live
stream, covering both history-load and cached-tab-switch paths.

Closes esengine#4105.

Regression:
  internal/agent         PASS
  internal/agent/testutil PASS
  internal/control       PASS (1 pre-existing MCP env failure)
  internal/serve         PASS (1 pre-existing HTML lang failure)
  internal/evidence      PASS
  Full ./...             PASS (9 pre-existing env failures unchanged)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Core agent loop (internal/agent, internal/control) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: 代办任务更新不同步

2 participants