feat(agent): versioned ConversationState serialization contract#66
Conversation
…idate createdAt/updatedAt in deserializer
|
Both findings addressed:
Suite: 30 files / 321 tests, typecheck + biome green. |
There was a problem hiding this comment.
Perry's Review
Verdict: 🔁 Needs changes
CI lint is red — Biome flags 4 formatting issues across conversation-state.ts and the new test file. Everything else (typecheck, unit tests, e2e) is green.
Findings
Blocker: lint failure (CI red)
biome check src tests fails with 2 errors (4 formatting violations):
conversation-state.ts:90—UnsupportedStateVersionErrorconstructor params should be multi-line per Biome's print width rule.conversation-state.ts:200—throw new UnsupportedStateVersionError(version, [CONVERSATION_STATE_VERSION])should be multi-line.conversation-state-serialization.test.ts:27—it("...")should use single quotes (it('...')).conversation-state-serialization.test.ts:145—toEqual([1])should be multi-line.
Fix: run Biome's auto-formatter on the agent package (biome check --write against src and tests).
Suggestion: version check ordering in deserializeConversationState
The version check runs after createdAt/updatedAt validation. A version-2 blob with missing timestamps throws InvalidStateError instead of the more informative UnsupportedStateVersionError. Moving the version check before the field validations would give consumers a clearer signal when they encounter a future-version blob, regardless of whether the other fields happen to be valid.
What's good
- Clean, well-documented serialization contract. The
version?: numberoptional field with "absence means v1" semantics is the right backwards-compat strategy. UnsupportedStateVersionErrorwith{found, supported}metadata is well-designed for programmatic error handling.- Test coverage is thorough: round-trip, legacy fixture, version-mismatch, malformed shapes, and version injection all covered.
- The
describeTypehelper produces actionable error messages.
Devin review notes
Devin flagged two issues that are both false positives in the current code:
- "JSDoc stolen by inserted constant" —
CONVERSATION_STATE_VERSIONhas its own JSDoc, andcreateInitialState's JSDoc sits directly above the function. Association is intact. - "Missing createdAt/updatedAt validation" — the code at lines 181-189 validates both fields. Devin likely reviewed an earlier revision.
| ); | ||
| } | ||
|
|
||
| const version = obj['version']; |
There was a problem hiding this comment.
💡 Suggestion: The version check runs after createdAt/updatedAt validation. A version-2 blob with missing timestamps would throw InvalidStateError instead of the more informative UnsupportedStateVersionError. Consider moving the version check before the field validations so unsupported-version blobs fail fast with the correct error type.
| const version = obj['version']; | |
| const version = obj['version']; | |
| if (version !== undefined && version !== CONVERSATION_STATE_VERSION) { | |
| if (typeof version !== 'number') { | |
| throw new InvalidStateError( | |
| `ConversationState field "version" must be a number when present (got ${describeType(version)})`, | |
| ); | |
| } | |
| throw new UnsupportedStateVersionError(version, [ | |
| CONVERSATION_STATE_VERSION, | |
| ]); | |
| } |
This block would move above the id / messages / status / createdAt / updatedAt checks.
▶ Prompt for agents
In deserializeConversationState (packages/agent/src/lib/conversation-state.ts), move the version check block (currently at line 193) to before the id/messages/status/createdAt/updatedAt field validation checks. This ensures that blobs with unsupported versions throw UnsupportedStateVersionError before any field-level validation runs.
There was a problem hiding this comment.
Done in 00d21a5 — version check now runs before the structural field validations, with a comment explaining why. Added a test: a version-2 blob with a completely different shape (conversationId/history instead of v1 fields) still throws UnsupportedStateVersionError.
| readonly found: number; | ||
| readonly supported: readonly number[]; | ||
|
|
||
| constructor(found: number, supported: readonly number[] = [CONVERSATION_STATE_VERSION]) { |
There was a problem hiding this comment.
🔴 Blocker — lint failure: Biome wants this constructor split across multiple lines (print width exceeded). CI lint is red because of this and 3 other formatting issues. Run Biome's auto-formatter (biome check --write) on the agent package to auto-fix all 4 violations.
▶ Prompt for agents
Run Biome's auto-formatter (biome check --write) on the agent package in the typescript-agent repo to fix the 4 Biome formatting violations: (1) UnsupportedStateVersionError constructor at conversation-state.ts:90 should be multi-line, (2) throw new UnsupportedStateVersionError at conversation-state.ts:200 should be multi-line, (3) it("...") at conversation-state-serialization.test.ts:27 should use single quotes, (4) toEqual([1]) at conversation-state-serialization.test.ts:145 should be multi-line. Commit and push the formatting fix.
There was a problem hiding this comment.
Fixed in fbbfb80 (biome check --write on src+tests); lint is green as of that commit.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…pdateState - deserializeConversationState now rejects unsupported versions before v1 field checks, so future-version blobs with changed shapes throw UnsupportedStateVersionError instead of a misleading InvalidStateError - updateState Omit list now includes 'version' alongside id/createdAt/updatedAt - new test: reshaped version-2 blob still gets the version error
Problem
ConversationStateround-trips throughJSON.stringify/parse(how every durable-store consumer uses it — serverless cold-start resume, SQLite state stores), but there were no helpers, no version field, and no forward-compat guarantee onmessagesitem shapes. Fine within one SDK version; unsafe for long-lived production stores. (agent-monorepo wikillm FRICTION #4; the #1 committed core item in the feature-parity plan.)Change
version?: 1onConversationState(optional so legacy blobs stay assignable — absence means v1);createInitialStatestampsversion: 1/conversation-statesubpath):serializeConversationState(state)— JSON withversionguaranteed presentdeserializeConversationState(json)— parse + shape validation;undefined/1accepted and normalized; other versions throwUnsupportedStateVersionError({found, supported}); malformed shape throwsInvalidStateErrornaming the fieldCONVERSATION_STATE_VERSIONconstantdeserializeConversationStateon version bump; consumers treat the JSON as opaqueTests
6 new cases: fresh + rich round-trips (incl.
awaiting_client_tools+pendingToolCallsfrom #64), version-less legacy fixture loads,version: 2fails loudly, malformed shapes fail loudly, serialize injects version. Suite: 30 files / 321 tests green.Changeset: minor. Plan: agent-monorepo
planning/sdk-fix-plan.mdPR-6 (sequenced after #64 as planned).