fix: keep legacy auto memory without an id working after an untouched redeploy

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-09-15 19:47:24 +02:00
co-authored by Claude Opus 5
parent 091e1128a3
commit ad15cb740b
6 changed files with 49 additions and 4 deletions
+14 -1
View File
@@ -85,7 +85,7 @@ pub enum Memory {
Auto {
#[serde(default)]
context_length: usize,
#[serde(default)]
#[serde(default, deserialize_with = "deserialize_blank_as_none")]
memory_id: Option<Uuid>,
},
/// Written before the step's `messages` input, which it is equivalent to.
@@ -94,6 +94,19 @@ pub enum Memory {
},
}
// An editor form can leave `""` in a legacy baked id it never filled; it means no id rather than
// failing every run of the step.
fn deserialize_blank_as_none<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Uuid>, D::Error> {
match <Option<String> as serde::Deserialize>::deserialize(deserializer)? {
Some(id) if !id.trim().is_empty() => Uuid::parse_str(id.trim())
.map(Some)
.map_err(serde::de::Error::custom),
_ => Ok(None),
}
}
fn deserialize_present_messages<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Option<Vec<OpenAIMessage>>>, D::Error> {
@@ -1978,6 +1978,18 @@ mod tests {
None,
Resolved::Window(baked, 4),
),
(
"legacy auto with an empty baked id uses the run's",
json!({ "memory": { "kind": "auto", "context_length": 4, "memory_id": "" } }),
Some(run),
Resolved::Window(run, 4),
),
(
"legacy auto with an empty baked id and no run id is stateless",
json!({ "memory": { "kind": "auto", "context_length": 4, "memory_id": " " } }),
None,
Resolved::Stateless { noted: true },
),
(
"legacy auto without a length is off",
json!({ "memory": { "kind": "auto", "memory_id": baked } }),
@@ -115,5 +115,10 @@ describe('memoryPropertyFor', () => {
'auto'
])
expect(kinds({ kind: 'manual', messages: [] })).toEqual(['off', 'window', 'manual'])
const autoVariant = (value: unknown) => memoryPropertyFor(property, value).oneOf.at(-1)
expect(autoVariant({ kind: 'auto', context_length: 4 }).properties.memory_id).toBeUndefined()
expect(
autoVariant({ kind: 'auto', context_length: 4, memory_id: 'x' }).properties.memory_id
).toBeDefined()
})
})
@@ -213,8 +213,15 @@ export const LEGACY_MEMORY_VARIANTS: Record<string, any> = {
* value holds it. Otherwise the property itself is returned, which callers compare by identity to
* avoid rebuilding the step schema. */
export function memoryPropertyFor(property: any, value: any): any {
const legacy = value?.kind ? LEGACY_MEMORY_VARIANTS[value.kind] : undefined
let legacy = value?.kind ? LEGACY_MEMORY_VARIANTS[value.kind] : undefined
if (!legacy || !property?.oneOf) return property
// The form fills an empty string field with `''` when it opens, so the baked id field is only
// offered to a value saved with the key. Keyed on presence rather than content, or clearing the
// id to retype it would remove the field mid-edit.
if (value.kind === 'auto' && !('memory_id' in value)) {
const { memory_id: _, ...properties } = legacy.properties
legacy = { ...legacy, properties }
}
return { ...property, oneOf: [...property.oneOf, legacy] }
}
@@ -163,10 +163,10 @@ export function normalizeAgentHistory(
if (!inputTransforms) return
const memory = inputTransforms.memory
if (
chatInputEnabled &&
memory?.type === 'static' &&
memory.value?.kind === 'auto' &&
memory.value.memory_id
'memory_id' in memory.value &&
(chatInputEnabled || !String(memory.value.memory_id ?? '').trim())
) {
const { memory_id: _, ...policy } = memory.value
memory.value = policy
@@ -70,6 +70,14 @@ describe('normalizeAgentHistory', () => {
expect(transforms.memory.value).toEqual({ kind: 'auto', context_length: 10 })
})
it('drops an empty baked memory id, which names no memory', () => {
const transforms = {
memory: { type: 'static', value: { kind: 'auto', context_length: 10, memory_id: '' } }
}
normalizeAgentHistory(transforms, false)
expect(transforms.memory.value).toEqual({ kind: 'auto', context_length: 10 })
})
it('does not persist an empty static memory id or message list', () => {
const transforms: Record<string, any> = {
memory_id: { type: 'static', value: ' ' },