Skip to content

fix(control): clear the active goal on session rotation#6114

Merged
SivanCola merged 2 commits into
esengine:main-v2from
SivanCola:fix/new-session-resets-goal
Jul 6, 2026
Merged

fix(control): clear the active goal on session rotation#6114
SivanCola merged 2 commits into
esengine:main-v2from
SivanCola:fix/new-session-resets-goal

Conversation

@SivanCola

Copy link
Copy Markdown
Collaborator

Summary

/new/clear 会重置执行器会话、planner、guardian、checkpoints——唯独不重置 goal 状态机。运行中 goal 的文本(连同 autoresearch runtime 块)会继续注入新会话的首轮,静默地引导一段用户已明确重置的对话。

语义决定:两个入口都清空 goal(新会话 = 白纸),同时保证旧会话可完整找回:

  • Controller 层NewSession/ClearSessionrebindCheckpoints 把 goal-state 路径移到新会话之后调用 ClearGoal()——清空后的 stopped 状态落在新路径的 sidecar 上(desktop 的 profile 恢复读到 stopped 即视为无 goal),而旧会话的 sidecar 在轮换前已持久化、保持 running goal 不动——/resume 旧会话时 restoreRunningFromState 照常恢复它的 goal。这一层覆盖所有前端(打字 /new、CLI、serve、bot、desktop)。
  • Desktop 层:tab 的持久化 goal 副本跟随控制器——App.NewSession/ClearSessiontab.goal(否则下次 rebuild/重启会经 SetGoal(tab.goal) 把旧 goal 复活进新会话);clearActiveSessionRuntime(运行中 /clear 的重建路径)不再把已销毁对话的 goal 种进替换控制器。模型/effort/设置切换的 rebuild 路径保留 goal——那些是同会话重建,语义不同,未动。

Verification

  • TestSessionRotationClearsActiveGoal 钉死四条性质:/new 与 /clear 后 Goal() 为空且 Compose 不含 <active-goal> 注入;旧会话 sidecar 保留 running goal;新会话 sidecar 不携带旧 goal。反向验证:去掉两处 ClearGoal() 后测试失败。
  • 全套绿:internal/control/cli/serve/bot/acp + desktop(39s);go vet 干净。

Cache impact

Cache-impact: none - goal rides turn injection (input.go Compose), never the system prompt or tool schema; this only stops an unwanted injection on fresh sessions.
Cache-guard: not applicable (no cache-sensitive files); the new rotation test covers the change.
System-prompt-review: N/A

Context: /new 语义触发的后续(#5699 排查时发现的遗留项),与 #6077 的 rotation gate 同属会话轮换清理线。

NewSession and ClearSession reset the executor session, planner,
guardian, and checkpoints — but not the goal machine. A running goal's
text (plus its autoresearch runtime block) kept injecting into the
fresh session's first turns, silently steering a conversation the user
had explicitly reset.

Controller: both rotations now ClearGoal() after rebindCheckpoints has
moved the goal-state path to the fresh session, so the cleared
(stopped) state lands on the NEW path while the old session's sidecar —
persisted before the rotation — keeps its running goal for /resume.
This covers every frontend (typed /new, CLI, serve, bot, desktop).

Desktop: the tab's persisted goal copy follows the controller —
App.NewSession and ClearSession clear tab.goal (otherwise the next
rebuild or restart would re-seed the old goal via SetGoal(tab.goal)),
and clearActiveSessionRuntime no longer seeds the destroyed
conversation's goal into the replacement controller. The model/effort/
settings rebuild paths keep the goal: those are same-session rebuilds.

TestSessionRotationClearsActiveGoal pins all four properties: goal
empty and no <active-goal> injection after /new and /clear, the old
session's sidecar keeps the running goal, and the new session's sidecar
does not carry it. Verified it fails without the ClearGoal calls.
@SivanCola SivanCola requested a review from esengine as a code owner July 6, 2026 15:18
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) agent Core agent loop (internal/agent, internal/control) labels Jul 6, 2026
Review catch: a typed /new or /clear rotates the session through the
controller slash path and never reaches App.NewSession/ClearSession, so
desktop-tabs.json can keep the pre-rotation goal. On the next restart,
restoreOrBuildTabs read entry.Goal unfiltered and the tab build seeded
it into the rotated session via SetGoal(buildGoal) — resurrecting the
goal the rotation had cleared. (The goal-state filter I pointed at in
the PR description only runs on the runtime-profile path, not startup
restore.)

Restore now validates entry.Goal through runningTabSessionGoal: the
rotation writes a stopped goal-state sidecar onto the fresh path, so a
restart reads stopped → no goal; a tab still pointing at the old
session reads running → goal preserved (resume semantics); a session
without a sidecar keeps the persisted goal (legacy).

TestRestoredTabGoalFollowsSessionGoalState pins the cross-layer
contract with a real controller rotation: rotated path → cleared, old
path → preserved, no sidecar → fallback.
@SivanCola

Copy link
Copy Markdown
Collaborator Author

Valid catch — fixed in bc1add0.

Verified every link of the reported chain: typed /new//clear goes SubmitToTab → controller slash dispatch and never reaches App.NewSession/ClearSession, so desktop-tabs.json keeps the pre-rotation goal; startup restore read entry.Goal unfiltered (app.go) and the tab build seeded it via ctrl.SetGoal(buildGoal) (tabs.go). And the reviewer is right that my PR description over-claimed: the runningTabSessionGoal filter I pointed at only runs on the runtime-profile path (runningTabSessionProfile), not startup restore.

Fix takes the "session sidecar as source of truth" shape rather than intercepting slash strings in SubmitToTab: restore now validates the persisted goal through runningTabSessionGoal(entry.SessionPath, entry.Goal). This leans on exactly what the controller half of this PR guarantees — rotation writes a stopped goal-state sidecar onto the fresh path — so the three restart cases resolve correctly:

  • tab pointing at the rotated session → sidecar says stopped → goal cleared (the reported leak);
  • tab pointing at the old session → sidecar says running → goal preserved (resume semantics);
  • session without a sidecar → persisted goal kept (legacy sessions predating the sidecar).

TestRestoredTabGoalFollowsSessionGoalState pins the cross-layer contract with a real controller rotation (not a hand-written sidecar), covering all three cases.

Also audited the remaining tab.goal consumers for the same stale-copy shape: the rebuild paths (model/effort/settings) snapshot via tabRuntimeSnapshot, which already prefers ctrl.Goal() over tab.goal when a controller is live, so they self-correct after a typed rotation; startTabControllerBuild's buildGoal := tab.goal is now always downstream of the filtered restore value.

Verification: full desktop suite green (36s), go vet clean; controller suites unchanged since your review.

@SivanCola SivanCola merged commit 4facd0d into esengine:main-v2 Jul 6, 2026
14 checks passed
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) desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant