refactor(training-agent): adopt @adcp/client 5.1 state-store helpers#2283
Merged
Conversation
Closes #2269. - serializeSession / deserializeSession replaced by structuredSerialize / structuredDeserialize from @adcp/client/server. Disk format is now the SDK canonical form (tagged __adcpType envelopes for Map/Date) instead of our hand-rolled Object.fromEntries + ISO-string approach. - MAX_SESSION_JSON_BYTES manual check in flushDirtySessions removed. PostgresStateStore.put / InMemoryStateStore.put enforce DEFAULT_MAX_DOCUMENT_BYTES (5 MB) at the boundary and throw StateError('PAYLOAD_TOO_LARGE') automatically. - RESERVED_KEYS prototype-pollution filter removed. structuredDeserialize walks tagged envelopes, not Object.entries on untrusted JSONB, and the SDK's validateId / validateCollection protects the store boundary. Migration 411 clears `training_sessions` rows on deploy because the serialization format changed. Training sessions are sandbox state with a 1-hour TTL — losing them is equivalent to a machine restart. Net: server/src/training-agent/state.ts goes from 384 → 356 lines. Tests: 302/302 training-agent + 587/587 tests/ suite pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applying feedback from code + security review on #2283: - Document the security invariant around RESERVED_KEYS removal: structured- Deserialize DOES walk untrusted JSONB, so prototype pollution is closed at the INPUT boundary (handlers never spread raw request payloads into session state) — not by the SDK. Comment warns maintainers to keep that invariant if adding new handlers. - Document the Map-field maintenance invariant on deserializeSession: each Map in SessionState needs an explicit `asMap(...)` override, or it will hydrate as a raw envelope object. - Lift the "products dropped from persistence" intent onto the call site where the override happens (code-reviewer Should Fix #2). - Add explicit disk-format test that verifies: - Maps serialize as { __adcpType: 'Map', entries: [...] } - Dates serialize as { __adcpType: 'Date', value: ISO } - Round-trip hydrates back to real Map/Date instances Pins the wire format against future SDK regressions. 303/303 training-agent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #2263. Now that @adcp/client 5.1 landed (via #2277), retire our hand-rolled state-store helpers in favor of the SDK equivalents.
Closes #2269.
What changed
serializeSession/deserializeSession→ SDKstructuredSerialize/structuredDeserialize. Disk format is now the SDK canonical form — tagged__adcpTypeenvelopes for Map/Date instead of ourObject.fromEntries+ ISO-string hand-roll. Net ~40 fewer lines of type-coercion code.flushDirtySessions.PostgresStateStore.put/InMemoryStateStore.putenforceDEFAULT_MAX_DOCUMENT_BYTESat the boundary and throwStateError('PAYLOAD_TOO_LARGE')automatically.RESERVED_KEYSprototype-pollution filter: removed.structuredDeserializewalks tagged envelopes, notObject.entrieson untrusted JSONB, and SDK key validation (validateId/validateCollection) protects the store boundary.Migration
server/src/db/migrations/411_clear_training_sessions.sql:The serialization format changed, so pre-existing sessions won't deserialize cleanly with the new code. Clearing is safe:
training_sessionsrowsOther collections in
adcp_state(if any are ever added) are unaffected.Kept (out of scope for this PR)
sessionKeyFromArgs+safeKeystay — they're coupled to this repo'sAccountRef/BrandRefshapes and provide the training-specific "collapse toopen:default" UX on malformed input. Would naturally retire when we adoptcreateAdcpServer'sresolveSessionKeyhook, which is a larger migration.store.scoped(sessionKey)is a per-entity pattern; adopting it would require rewriting every handler to usectx.storecollections individually. Tracked as a future refactor.Test plan
npm run typecheck— cleannpx vitest run server/tests/unit/training-agent.test.ts— 302 / 302 pass (including read-only-no-flush, snapshot round-trip, DNS-case-insensitive session keys)npx vitest run --dir tests/— 587 / 587 passnpm start/ standalone — passestest-agent.adcontextprotocol.org(no regression), migration 411 runs, fresh sessions in new formatLine count
server/src/training-agent/state.ts: 384 → 356 lines (-28 net, 70 deletions / 42 additions — slightly less than the "-90" Adopt @adcp/client 5.1 state-store helpers once upstream #554 lands #2269 estimated becausesessionKeyFromArgs+safeKeywere kept)🤖 Generated with Claude Code