Skip to content

Fix #6466: make workflow checkpoint TypeId version-insensitive#6469

Open
saikir1994 wants to merge 1 commit into
microsoft:mainfrom
saikir1994:fix/typeid-version-insensitive-6466
Open

Fix #6466: make workflow checkpoint TypeId version-insensitive#6469
saikir1994 wants to merge 1 commit into
microsoft:mainfrom
saikir1994:fix/typeid-version-insensitive-6466

Conversation

@saikir1994

Copy link
Copy Markdown

Summary

Fixes #6466.

Workflow checkpoint restore failed with System.IO.InvalidDataException: The specified checkpoint is not compatible with the workflow associated with this runner. whenever the assembly version changed between the run that wrote a checkpoint and the run that restored it (i.e. after any package upgrade and redeploy), even when the workflow topology, executor IDs, and state shape were identical.

Root cause

TypeId identity was derived from Assembly.FullName, which embeds Version, Culture, and PublicKeyToken:

public bool IsMatch(Type type)
{
    if (AssemblyName == type.Assembly.FullName)   // version-sensitive
        return TypeName == type.FullName;
    return false;
}

TypeId is serialized into checkpoints and re-derived from the currently loaded assemblies on restore, so any version bump made the strings differ and invalidated every previously persisted checkpoint. The same AssemblyName comparison also backs TypeId.Equals/GetHashCode, which matters because TypeId is used as a dictionary key in MessageTypeTranslator, MessageRouter, and the executor yield-type set — so in-flight message type lookups would also fail across versions.

Changes

  • TypeId — Comparison and hashing are now based on the simple assembly name (version-, culture-, and public-key-token-independent) plus the type full name:
    • Added NormalizeAssemblyName (parses the simple name via System.Reflection.AssemblyName, with a safe fallback to the raw value if it can't be parsed) and a lazily-cached internal AssemblySimpleName.
    • Equals, GetHashCode, and IsMatch(Type) now use the simple assembly name.
    • The TypeId(Type) constructor, the public AssemblyName property, and ToString() are unchanged — the serialized checkpoint shape and public API are preserved, and legacy (version-qualified) checkpoints remain matchable via normalization.
  • WorkflowSession.TryGetRequestEnvelope — Fixes a latent second version-sensitivity where Type.GetType($"{TypeName}, {AssemblyName}") used the version-qualified assembly name (which can fail to resolve after an upgrade for strong-named assemblies). It now uses the simple name for robust resolution.

Approach / compatibility

This is the most conservative fix: the stored AssemblyName value, the serialized JSON shape, and the public AssemblyName property are all unchanged. Only the comparison/hash/resolution paths are normalized. As a result:

  • Existing checkpoints (full version-qualified names) continue to match.
  • New checkpoints have an identical shape — no schema change and no migration required.

Tests

  • Added TypeIdVersionMismatchTests (7 tests) covering:
    • IsMatch, Equals, and GetHashCode ignore assembly version.
    • Dictionary<TypeId, T> lookups succeed across version forms (legacy full name vs. current simple name).
    • Negative cases: different type name and different simple assembly name are not equal.
    • Malformed assembly names fall back to the raw value without throwing.
  • Full Microsoft.Agents.AI.Workflows.UnitTests suite passes: 652 passed, 0 failed (net10.0), including the existing JsonSerializationTests, checkpointing, and executor tests.

TypeId identity was derived from Assembly.FullName, which embeds Version, Culture, and PublicKeyToken. Any SDK version bump invalidated previously persisted checkpoints, causing restore to throw InvalidDataException.

Compare and hash TypeId using only the simple assembly name plus the type full name. The stored AssemblyName field, public property, and ToString output are unchanged, so the serialized checkpoint shape and public API are preserved and legacy checkpoints remain matchable.

Also fixes a latent second version-sensitivity in WorkflowSession.TryGetRequestEnvelope, where Type.GetType used the version-qualified assembly name; it now uses the simple name for robust resolution after upgrades.
Copilot AI review requested due to automatic review settings June 11, 2026 04:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR makes TypeId comparisons independent of assembly version (and related qualifiers) to ensure checkpoints remain compatible across SDK versions, and updates type resolution to use the assembly simple name.

Changes:

  • Normalize TypeId equality/hash/match behavior to ignore assembly version/culture/public key token.
  • Update WorkflowSession request envelope type resolution to use the normalized assembly simple name.
  • Add unit tests covering version-mismatch equality and dictionary lookup behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/TypeId.cs Adds assembly-name normalization and switches equality/hash/match to use the simple assembly name.
dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowSession.cs Uses the normalized assembly simple name for Type.GetType resolution.
dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TypeIdVersionMismatchTests.cs Adds tests validating version-independent TypeId equality/matching and dictionary lookup.

Comment on lines +35 to +45
private static string NormalizeAssemblyName(string assemblyName)
{
try
{
return new AssemblyName(assemblyName).Name ?? assemblyName;
}
catch (Exception)
{
return assemblyName;
}
}
}

return this.AssemblyName == other.AssemblyName && this.TypeName == other.TypeName;
return this.AssemblySimpleName == other.AssemblySimpleName && this.TypeName == other.TypeName;

/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(this.AssemblyName, this.TypeName);
public override int GetHashCode() => HashCode.Combine(this.AssemblySimpleName, this.TypeName);
Comment on lines +108 to 109
return this.AssemblySimpleName == type.Assembly.GetName().Name
&& this.TypeName == type.FullName;
Comment on lines +305 to 306
Type? concreteType = Type.GetType($"{requestType.TypeName}, {requestType.AssemblySimpleName}", throwOnError: false);
if (concreteType is null || !typeof(IExternalRequestEnvelope).IsAssignableFrom(concreteType))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: [Bug]: Workflow checkpoints are not restorable across SDK upgrades — TypeId uses Assembly.FullName (incl. version) for executor type matching

2 participants