fix(control): clear the active goal on session rotation#6114
Conversation
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.
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.
|
Valid catch — fixed in bc1add0. Verified every link of the reported chain: typed Fix takes the "session sidecar as source of truth" shape rather than intercepting slash strings in
Also audited the remaining Verification: full desktop suite green (36s), |
Summary
/new和/clear会重置执行器会话、planner、guardian、checkpoints——唯独不重置 goal 状态机。运行中 goal 的文本(连同 autoresearch runtime 块)会继续注入新会话的首轮,静默地引导一段用户已明确重置的对话。语义决定:两个入口都清空 goal(新会话 = 白纸),同时保证旧会话可完整找回:
NewSession/ClearSession在rebindCheckpoints把 goal-state 路径移到新会话之后调用ClearGoal()——清空后的 stopped 状态落在新路径的 sidecar 上(desktop 的 profile 恢复读到 stopped 即视为无 goal),而旧会话的 sidecar 在轮换前已持久化、保持 running goal 不动——/resume旧会话时restoreRunningFromState照常恢复它的 goal。这一层覆盖所有前端(打字/new、CLI、serve、bot、desktop)。App.NewSession/ClearSession清tab.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 同属会话轮换清理线。