fix: /goal clear but /resume will get old goal
Browse files/goal clear 只清了内存中的 AppState.goal,但在 reAppendSessionMetadata 退出时把旧缓存 currentSessionGoal 又写回了
transcript。而且 loadTranscriptFile 是 last-wins 逻辑,一条旧的 goal entry 永远不会被清除。
需要两个修复:
1. /goal clear 时写入一条 goal 且带 cleared 标记,或写入 null 来覆盖
2. reAppendSessionMetadata 不要在 goal 已清除时重新写入
最简洁的方案:写入一条 goal: null 的 entry,加载时遇到 null 就视为无 goal
- src/utils/sessionStorage.ts +38 -6
src/utils/sessionStorage.ts
CHANGED
|
@@ -546,7 +546,9 @@ class Project {
|
|
| 546 |
currentSessionPrUrl: string | undefined
|
| 547 |
currentSessionPrRepository: string | undefined
|
| 548 |
// /goal state — re-appended on exit so --resume restores it
|
| 549 |
-
|
|
|
|
|
|
|
| 550 |
|
| 551 |
sessionFile: string | null = null
|
| 552 |
// Entries buffered while sessionFile is null. Flushed by materializeSessionFile
|
|
@@ -843,6 +845,21 @@ class Project {
|
|
| 843 |
...this.currentSessionGoal,
|
| 844 |
sessionId,
|
| 845 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
}
|
| 847 |
}
|
| 848 |
|
|
@@ -2951,12 +2968,27 @@ export function saveGoal(goal: GoalEntry): void {
|
|
| 2951 |
}
|
| 2952 |
|
| 2953 |
/**
|
| 2954 |
-
* Clear the
|
| 2955 |
-
*
|
|
|
|
| 2956 |
*/
|
| 2957 |
export function clearGoal(): void {
|
| 2958 |
const project = getProject()
|
| 2959 |
-
project.currentSessionGoal =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2960 |
}
|
| 2961 |
|
| 2962 |
/**
|
|
@@ -3655,7 +3687,7 @@ export async function loadTranscriptFile(
|
|
| 3655 |
prUrls.set(entry.sessionId, entry.prUrl)
|
| 3656 |
prRepositories.set(entry.sessionId, entry.prRepository)
|
| 3657 |
} else if (entry.type === 'goal' && entry.sessionId) {
|
| 3658 |
-
goal = entry
|
| 3659 |
}
|
| 3660 |
}
|
| 3661 |
}
|
|
@@ -3725,7 +3757,7 @@ export async function loadTranscriptFile(
|
|
| 3725 |
prUrls.set(entry.sessionId, entry.prUrl)
|
| 3726 |
prRepositories.set(entry.sessionId, entry.prRepository)
|
| 3727 |
} else if (entry.type === 'goal' && entry.sessionId) {
|
| 3728 |
-
goal = entry
|
| 3729 |
} else if (entry.type === 'file-history-snapshot') {
|
| 3730 |
fileHistorySnapshots.set(entry.messageId, entry)
|
| 3731 |
} else if (entry.type === 'attribution-snapshot') {
|
|
|
|
| 546 |
currentSessionPrUrl: string | undefined
|
| 547 |
currentSessionPrRepository: string | undefined
|
| 548 |
// /goal state — re-appended on exit so --resume restores it
|
| 549 |
+
// Tri-state: undefined = never touched, GoalEntry = active goal,
|
| 550 |
+
// null = cleared (write a tombstone so resume doesn't restore stale goal).
|
| 551 |
+
currentSessionGoal: GoalEntry | null | undefined
|
| 552 |
|
| 553 |
sessionFile: string | null = null
|
| 554 |
// Entries buffered while sessionFile is null. Flushed by materializeSessionFile
|
|
|
|
| 845 |
...this.currentSessionGoal,
|
| 846 |
sessionId,
|
| 847 |
})
|
| 848 |
+
} else if (this.currentSessionGoal === null) {
|
| 849 |
+
// Goal was explicitly cleared — write a tombstone so resume
|
| 850 |
+
// doesn't restore a stale goal from an earlier entry.
|
| 851 |
+
appendEntryToFile(this.sessionFile, {
|
| 852 |
+
type: 'goal',
|
| 853 |
+
id: '__cleared__',
|
| 854 |
+
objective: '',
|
| 855 |
+
status: 'achieved',
|
| 856 |
+
startedAt: 0,
|
| 857 |
+
startCostUSD: 0,
|
| 858 |
+
startTokensUsed: 0,
|
| 859 |
+
continuationCount: 0,
|
| 860 |
+
lastUpdatedAt: 0,
|
| 861 |
+
sessionId,
|
| 862 |
+
})
|
| 863 |
}
|
| 864 |
}
|
| 865 |
|
|
|
|
| 2968 |
}
|
| 2969 |
|
| 2970 |
/**
|
| 2971 |
+
* Clear the goal state and write a tombstone entry to the transcript.
|
| 2972 |
+
* Without the tombstone, a stale goal entry from earlier in the session
|
| 2973 |
+
* would be restored on --resume (last-wins, but no later entry to win).
|
| 2974 |
*/
|
| 2975 |
export function clearGoal(): void {
|
| 2976 |
const project = getProject()
|
| 2977 |
+
project.currentSessionGoal = null
|
| 2978 |
+
if (project.sessionFile) {
|
| 2979 |
+
appendEntryToFile(project.sessionFile, {
|
| 2980 |
+
type: 'goal',
|
| 2981 |
+
id: '__cleared__',
|
| 2982 |
+
objective: '',
|
| 2983 |
+
status: 'achieved',
|
| 2984 |
+
startedAt: 0,
|
| 2985 |
+
startCostUSD: 0,
|
| 2986 |
+
startTokensUsed: 0,
|
| 2987 |
+
continuationCount: 0,
|
| 2988 |
+
lastUpdatedAt: 0,
|
| 2989 |
+
sessionId: getSessionId(),
|
| 2990 |
+
})
|
| 2991 |
+
}
|
| 2992 |
}
|
| 2993 |
|
| 2994 |
/**
|
|
|
|
| 3687 |
prUrls.set(entry.sessionId, entry.prUrl)
|
| 3688 |
prRepositories.set(entry.sessionId, entry.prRepository)
|
| 3689 |
} else if (entry.type === 'goal' && entry.sessionId) {
|
| 3690 |
+
goal = entry.id === '__cleared__' ? undefined : entry
|
| 3691 |
}
|
| 3692 |
}
|
| 3693 |
}
|
|
|
|
| 3757 |
prUrls.set(entry.sessionId, entry.prUrl)
|
| 3758 |
prRepositories.set(entry.sessionId, entry.prRepository)
|
| 3759 |
} else if (entry.type === 'goal' && entry.sessionId) {
|
| 3760 |
+
goal = entry.id === '__cleared__' ? undefined : entry
|
| 3761 |
} else if (entry.type === 'file-history-snapshot') {
|
| 3762 |
fileHistorySnapshots.set(entry.messageId, entry)
|
| 3763 |
} else if (entry.type === 'attribution-snapshot') {
|