fix(web): record which preset a session actually runs
The creation header names the preset a session STARTED with and is frozen, which is correct — it is a creation fact. Switching is legal only while a session is blank, and that looked like enough: no history exists yet. It is not, because the switch's effect outlives the blank window. The user switches, then sends the first message; every turn from there runs under the new composition while the header still names the old one. The session is then locked around a misrecorded preset, and resume reads the header to rebuild it — composing one preset's tools over a history another produced, which is exactly the replay the blank-only lock exists to prevent, reached by another route. A picker showed `standard` for a session running `core-web`. A switch is now an `agent-preset/selected` event appended after the swap commits, and `resolveSessionPreset()` (last selection, else the header) is what every reconstruction reads: the summary, resume, the conflict guard, and the fork introduced one layer down.
This commit is contained in:
54
packages/preset/agent-presets/src/session.ts
Normal file
54
packages/preset/agent-presets/src/session.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* The session-log record of which preset a session actually runs.
|
||||
*
|
||||
* The creation header names the preset a session STARTED with, and it is
|
||||
* deep-frozen because that is a creation fact. A session may still change
|
||||
* preset while it is blank, and the effect of that change outlives the blank
|
||||
* window: the first turn — and every turn after it — runs under the newly
|
||||
* mounted composition. Recording the change is what keeps the log honest, and
|
||||
* it is required outright by the repo's model-visible ⟺ logged rule, since the
|
||||
* preset decides the tool schemas and prompt sections the model sees.
|
||||
*
|
||||
* Reconstruction reads {@link resolveSessionPreset}, never the header alone.
|
||||
* @module @deepseek-ai/dsh-agent-presets/session
|
||||
*/
|
||||
|
||||
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
|
||||
declare module '@deepseek-ai/dsh-session' {
|
||||
interface SessionEventMap {
|
||||
/**
|
||||
* The session's agent preset was chosen after creation, while the session
|
||||
* was still blank. Log-only: it records the composition later turns ran
|
||||
* under, so a resumed or forked session rebuilds the same one instead of
|
||||
* the header's creation-time value.
|
||||
*/
|
||||
'agent-preset/selected': { agentPreset: string }
|
||||
}
|
||||
}
|
||||
|
||||
/** The minimum a caller must supply to resolve a session's preset. */
|
||||
export interface PresetBearingSession {
|
||||
/** The session's creation header. */
|
||||
readonly header: SessionHeader
|
||||
/** The session's event log, oldest first. */
|
||||
readonly events: readonly SessionEvent[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The preset a session actually runs, newest selection winning.
|
||||
*
|
||||
* The header supplies the creation-time value; every later selection is a
|
||||
* logged event, so the last one is the answer. Reading the header alone
|
||||
* rebuilds a switched session under the composition it was created with, not
|
||||
* the one its history was produced under.
|
||||
* @param session - the session's header and event log.
|
||||
* @returns the preset id, or `undefined` when the deployment composes none.
|
||||
*/
|
||||
export function resolveSessionPreset(session: PresetBearingSession): string | undefined {
|
||||
for (let index = session.events.length - 1; index >= 0; index -= 1) {
|
||||
const event = session.events[index]
|
||||
if (event?.type === 'agent-preset/selected') return event.data.agentPreset
|
||||
}
|
||||
return session.header.agentPreset
|
||||
}
|
||||
Reference in New Issue
Block a user