ai: persist agent-history + per-message attribution to localStorage
Reloading the page mid-conversation used to keep the messages (via
LIVE_KEY) but lose the two agent-tracking maps, so:
- row signatures snapped back to whoever was currently active
- the next turn after reload didn't include the PRIOR HAND-OFF
block, even though the transcript clearly had multiple personas
Both maps are now stored alongside the live snapshot:
AGENTS_KEY crema.ai.agent-history set of Agent
MSG_AGENTS_KEY crema.ai.message-agents index -> Agent
Stored as JSON arrays since Maps don't serialize. Hydrated on mount
via useState lazy initializer, persisted on every change via two
useEffects, cleared in lockstep with LIVE_KEY when the operator
hits Clear conversation.
Reload mid-thread now reads identically to the pre-reload state:
- atlas» turns 1-3 stay attributed to atlas
- pythia» turn 4 stays attributed to pythia
- next turn after reload still carries the hand-off note
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,65 @@ function saveLive(msgs: LLMMessage[]) {
|
|||||||
// Quota exceeded or similar — silently drop persistence.
|
// Quota exceeded or similar — silently drop persistence.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Per-message agent attribution + the set of agents that have produced
|
||||||
|
* turns in the current conversation. Persisted alongside LIVE_KEY so a
|
||||||
|
* reload mid-thread preserves both who-said-what and the hand-off note
|
||||||
|
* the next turn carries.
|
||||||
|
*
|
||||||
|
* Stored as plain JSON shapes (Maps don't serialize):
|
||||||
|
* AGENTS_KEY: Array<[agentId, Agent]> ← agentHistory
|
||||||
|
* MSG_AGENTS_KEY: Array<[index, Agent]> ← messageAgents
|
||||||
|
*/
|
||||||
|
const AGENTS_KEY = "crema.ai.agent-history"
|
||||||
|
const MSG_AGENTS_KEY = "crema.ai.message-agents"
|
||||||
|
|
||||||
|
function loadAgentHistory(): Map<string, Agent> {
|
||||||
|
if (typeof window === "undefined") return new Map()
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(AGENTS_KEY)
|
||||||
|
if (!raw) return new Map()
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
if (Array.isArray(parsed)) return new Map(parsed as [string, Agent][])
|
||||||
|
} catch {}
|
||||||
|
return new Map()
|
||||||
|
}
|
||||||
|
function saveAgentHistory(m: Map<string, Agent>) {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
if (m.size === 0) {
|
||||||
|
localStorage.removeItem(AGENTS_KEY)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(AGENTS_KEY, JSON.stringify([...m.entries()]))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMessageAgents(): Map<number, Agent> {
|
||||||
|
if (typeof window === "undefined") return new Map()
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(MSG_AGENTS_KEY)
|
||||||
|
if (!raw) return new Map()
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
if (Array.isArray(parsed)) return new Map(parsed as [number, Agent][])
|
||||||
|
} catch {}
|
||||||
|
return new Map()
|
||||||
|
}
|
||||||
|
function saveMessageAgents(m: Map<number, Agent>) {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
if (m.size === 0) {
|
||||||
|
localStorage.removeItem(MSG_AGENTS_KEY)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(MSG_AGENTS_KEY, JSON.stringify([...m.entries()]))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
function clearAgentMaps() {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
localStorage.removeItem(AGENTS_KEY)
|
||||||
|
localStorage.removeItem(MSG_AGENTS_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
function clearLive() {
|
function clearLive() {
|
||||||
if (typeof window === "undefined") return
|
if (typeof window === "undefined") return
|
||||||
localStorage.removeItem(LIVE_KEY)
|
localStorage.removeItem(LIVE_KEY)
|
||||||
@@ -369,19 +428,29 @@ function ChatSurface({
|
|||||||
// the new agent knows it's stepping into a transcript started by another
|
// the new agent knows it's stepping into a transcript started by another
|
||||||
// persona — without that note it answers as if it produced every prior
|
// persona — without that note it answers as if it produced every prior
|
||||||
// turn itself, which is jarring.
|
// turn itself, which is jarring.
|
||||||
|
// Both maps are seeded from localStorage so a reload mid-thread keeps
|
||||||
|
// attribution + hand-off context intact. Persisted on every change via
|
||||||
|
// the effect lower down, cleared together with the live snapshot.
|
||||||
const [agentHistory, setAgentHistory] = useState<Map<string, Agent>>(
|
const [agentHistory, setAgentHistory] = useState<Map<string, Agent>>(
|
||||||
() => new Map(),
|
() => loadAgentHistory(),
|
||||||
)
|
)
|
||||||
const prevAgentRef = useRef<Agent | undefined>(activeAgent)
|
const prevAgentRef = useRef<Agent | undefined>(activeAgent)
|
||||||
|
|
||||||
// Per-message agent attribution: which agent produced the assistant
|
// Per-message agent attribution: which agent produced the assistant
|
||||||
// message at each index. Populated when a turn finishes streaming, used
|
// message at each index. Populated when a turn finishes streaming, used
|
||||||
// in MessageRow to show the right name in the signature line. Survives
|
// in MessageRow to show the right name in the signature line.
|
||||||
// until the conversation is cleared.
|
|
||||||
const [messageAgents, setMessageAgents] = useState<Map<number, Agent>>(
|
const [messageAgents, setMessageAgents] = useState<Map<number, Agent>>(
|
||||||
() => new Map(),
|
() => loadMessageAgents(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Persist whenever either map changes.
|
||||||
|
useEffect(() => {
|
||||||
|
saveAgentHistory(agentHistory)
|
||||||
|
}, [agentHistory])
|
||||||
|
useEffect(() => {
|
||||||
|
saveMessageAgents(messageAgents)
|
||||||
|
}, [messageAgents])
|
||||||
|
|
||||||
// Hand-off prompt — only emitted when this conversation has been touched
|
// Hand-off prompt — only emitted when this conversation has been touched
|
||||||
// by more than one agent. Lists prior personas the new agent might see in
|
// by more than one agent. Lists prior personas the new agent might see in
|
||||||
// the transcript.
|
// the transcript.
|
||||||
@@ -439,6 +508,7 @@ function ChatSurface({
|
|||||||
const resetAndClear = useCallback(() => {
|
const resetAndClear = useCallback(() => {
|
||||||
initialLive.current = []
|
initialLive.current = []
|
||||||
clearLive()
|
clearLive()
|
||||||
|
clearAgentMaps()
|
||||||
setMessages([])
|
setMessages([])
|
||||||
setAgentHistory(new Map())
|
setAgentHistory(new Map())
|
||||||
setMessageAgents(new Map())
|
setMessageAgents(new Map())
|
||||||
|
|||||||
Reference in New Issue
Block a user