feat(tree): implement persisted commit metadata - #28063
Conversation
Adds an alpha API for attaching a JSON-serializable value to the commit produced by a transaction, replicating it to peers, and persisting it inline on commits in the EditManager summary. Introduces MessageFormatVersion.v7 and EditManagerFormatVersion.v7, gated behind FluidClientVersion.v3_0, so metadata is only written once every collaborating client understands it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Implements persisted, replicated commit metadata for SharedTree commits (keyed by RevisionTag) by introducing new v7 message/edit-manager formats gated behind FluidClientVersion.v3_0, wiring an in-memory PersistedCommitMetadataIndex through submit/process/stash/rollback/trim paths, and exposing an @alpha API to attach/read metadata.
Changes:
- Adds
PersistedCommitMetadataIndexand threads it throughSharedTreeCore+TreeCheckoutso metadata survives fork→merge and is pruned on trunk trimming. - Introduces
MessageFormatVersion.v7/EditManagerFormatVersion.v7(write-enabled atminVersionForCollab >= 3.0.0) and carries optionalpersistedMetadatathrough op + summary codecs. - Exposes alpha API surface (
RunTransactionParamsAlpha.persistedMetadata,UntypedTreeViewAlpha.getPersistedCommitMetadata,LocalChangeMetadata.revision,RevisionTagexport) with new tests and v3_0 snapshot baselines.
Reviewed changes
Copilot reviewed 72 out of 72 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| .changeset/persisted-commit-metadata.md | Release note for persisted commit metadata feature and usage example. |
| packages/dds/tree/src/codec/codec.ts | Adds FluidClientVersion.v3_0 gating for new v7 formats. |
| packages/dds/tree/src/core/rebase/types.ts | Exports RevisionTag as @alpha and adds LocalChangeMetadata.revision. |
| packages/dds/tree/src/entrypoints/alpha.ts | Re-exports RevisionTag on the alpha entrypoint. |
| packages/dds/tree/src/index.ts | Re-exports RevisionTag at package root. |
| packages/dds/tree/src/shared-tree/schematizingTreeView.ts | Plumbs getPersistedCommitMetadata through SchematizingSimpleTreeView. |
| packages/dds/tree/src/shared-tree/sharedTree.ts | Wires persisted-metadata index into checkout creation; maps v7 dependent format versions. |
| packages/dds/tree/src/shared-tree/treeCheckout.ts | Records transaction metadata against minted revisions; exposes getPersistedCommitMetadata; shares index across forks. |
| packages/dds/tree/src/shared-tree-core/editManager.ts | Exposes EditManager trimming events for metadata pruning. |
| packages/dds/tree/src/shared-tree-core/editManagerCodecs.ts | Adds v7 edit-manager codec entry and passes persisted-metadata index into codecs. |
| packages/dds/tree/src/shared-tree-core/editManagerCodecsCommons.ts | Encodes/decodes optional persistedMetadata via shared index (write at v7+ only). |
| packages/dds/tree/src/shared-tree-core/editManagerCodecsV1toV4.ts | Adds persisted-metadata option plumbing and gates writing by format version. |
| packages/dds/tree/src/shared-tree-core/editManagerCodecsVSharedBranches.ts | Enables persisted metadata in shared-branches codec path (always write for that experimental format). |
| packages/dds/tree/src/shared-tree-core/editManagerFormatCommons.ts | Adds persistedMetadata field to commit encodings; introduces EditManagerFormatVersion.v7. |
| packages/dds/tree/src/shared-tree-core/editManagerFormatV1toV4.ts | Extends edit-manager version union to include v7. |
| packages/dds/tree/src/shared-tree-core/index.ts | Re-exports PersistedCommitMetadataIndex and PersistedCommitMetadata. |
| packages/dds/tree/src/shared-tree-core/messageCodecV1ToV4.ts | Adds v7-aware encoding/decoding of persistedMetadata (write only at v7+). |
| packages/dds/tree/src/shared-tree-core/messageCodecVSharedBranches.ts | Adds encode/decode support for persistedMetadata in shared-branches messages. |
| packages/dds/tree/src/shared-tree-core/messageCodecs.ts | Registers v7 message codec gated behind FluidClientVersion.v3_0. |
| packages/dds/tree/src/shared-tree-core/messageFormat.ts | Introduces MessageFormatVersion.v7 and marks it supported. |
| packages/dds/tree/src/shared-tree-core/messageFormatV1ToV4.ts | Adds optional persistedMetadata field and includes v7 in version union. |
| packages/dds/tree/src/shared-tree-core/messageFormatVSharedBranches.ts | Adds optional persistedMetadata field to shared-branches message schema. |
| packages/dds/tree/src/shared-tree-core/messageTypes.ts | Adds persistedMetadata?: JsonCompatibleReadOnlyObject to CommitMessage. |
| packages/dds/tree/src/shared-tree-core/persistedCommitMetadata.ts | Adds PersistedCommitMetadataIndex (revision-keyed) and metadata type alias. |
| packages/dds/tree/src/shared-tree-core/sharedTreeCore.ts | Creates shared metadata index; prunes on trim; populates from ops/summaries; supports rollback/stash/resubmit paths. |
| packages/dds/tree/src/simple-tree/api/transactionTypes.ts | Adds RunTransactionParamsAlpha.persistedMetadata alpha API surface. |
| packages/dds/tree/src/simple-tree/api/tree.ts | Adds UntypedTreeViewAlpha.getPersistedCommitMetadata(revision) alpha API surface. |
| packages/dds/tree/src/test/shared-tree-core/edit-manager/editManagerCodecs.test.ts | Adds v7 to codec version test lists. |
| packages/dds/tree/src/test/shared-tree-core/messageCodec.spec.ts | Adds v7 to codec version test lists. |
| packages/dds/tree/src/test/shared-tree/persistedCommitMetadata.spec.ts | New spec exercising local readback, peer replication, rebasing, version gating, nested tx, rollback/no-op, fork/merge. |
| packages/dds/tree/src/test/snapshots/output/codec-tree/MinVersionForCollab.v3_0.json | New codec-tree snapshot reflecting v3_0 write versions. |
| packages/dds/tree/src/test/snapshots/output/op-format/v3_0/field change.json | New v3_0 op-format snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/op-format/v3_0/schema change.json | New v3_0 op-format snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/attachment-tree-final.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/competing-removes-index-0.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/competing-removes-index-1.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/competing-removes-index-2.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/competing-removes-index-3.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/complete-3x3-final.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/concurrent-inserts-tree2.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/concurrent-inserts-tree3.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/empty-root-final.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/has-handle-final.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/insert-and-remove-tree-0-after-insert.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/insert-and-remove-tree-0-final.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/insert-and-remove-tree-1-final.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/move-across-fields-tree-0-final.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/nested-sequence-change-final.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/optional-field-scenarios-final.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Compressed/v3_0/tree-with-identifier-field-final.json | New v3_0 compressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/attachment-tree-final.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/competing-removes-index-0.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/competing-removes-index-1.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/competing-removes-index-2.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/competing-removes-index-3.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/complete-3x3-final.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/concurrent-inserts-tree2.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/concurrent-inserts-tree3.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/empty-root-final.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/has-handle-final.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/insert-and-remove-tree-0-after-insert.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/insert-and-remove-tree-0-final.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/insert-and-remove-tree-1-final.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/move-across-fields-tree-0-final.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/nested-sequence-change-final.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/optional-field-scenarios-final.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/snapshots/output/summary/Uncompressed/v3_0/tree-with-identifier-field-final.json | New v3_0 uncompressed summary snapshot (format v7). |
| packages/dds/tree/src/test/shared-tree/summary-load-snapshots/singleTree-Compressed-v3_0-1.json | New summary-load baseline for compressed v3_0. |
| packages/dds/tree/src/test/shared-tree/summary-load-snapshots/singleTree-Uncompressed-v3_0-1.json | New summary-load baseline for uncompressed v3_0. |
| packages/dds/tree/src/test/utils.ts | Updates test checkout mock surface to include new API entrypoint. |
| packages/dds/tree/src/util/index.ts | Re-exports JsonCompatibleReadOnlyObjectSchema. |
| packages/dds/tree/src/util/utils.ts | Adds JsonCompatibleReadOnlyObjectSchema for object-only JSON-like validation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| --- | ||
| Persisted commit metadata | ||
|
|
||
| Applications can now attach an arbitrary JSON-serializable value to the commit produced by a transaction. The value is replicated to all collaborating clients and persisted in the document alongside the commit, and can be read back by revision. |
| * The value must be JSON-serializable, since it round-trips through the persisted format. It travels on every | ||
| * annotated op and occupies space in the summary for as long as its commit survives, so it should be kept small. |
| /** | ||
| * Looks up the {@link RunTransactionParamsAlpha.persistedMetadata | persisted metadata} associated with a commit. | ||
| * | ||
| * @param revision - The revision of the commit to look up. | ||
| * Revisions are provided to applications by the change events on this view. | ||
| * | ||
| * @returns The metadata that was supplied to the transaction which produced the commit, or `undefined` if the | ||
| * commit was not annotated, predates this feature, or has been trimmed from the trunk. | ||
| * | ||
| * @remarks | ||
| * Metadata shares the lifetime of the commit it is attached to: once the commit is trimmed from the trunk, | ||
| * the metadata goes with it. Every read path must therefore handle `undefined`. | ||
| */ |
| public getPersistedCommitMetadata(): never { | ||
| throw new Error( | ||
| "Method 'getPersistedCommitMetadata' not implemented in MockTreeCheckout.", | ||
| ); | ||
| } |
|
Hi! Thank you for opening this PR. Want me to review it? Based on the diff (16863 lines, 72 files), I've queued these reviewers:
How this works
|
|
VERDICT: REQUEST_CHANGES Persisted metadata can diverge between clients, is not discoverable for remote changes, and is retained after its owning fork is discarded; the public API reports and critical durability tests are also missing, and the client build is currently failing. Evidence and agent contextBlocking
Minimum change to approve: make metadata ownership and fork cleanup deterministic, provide a usable remote correlation path, add the critical persistence/lifecycle tests, regenerate API reports, and restore the client build.
Repo harnesses used
Review by Minions (Ripley — Lead / Explorer · gpt-5.6-sol) |
Bundle size comparisonBase commit: unresolved The PR's CI build failed — fix the build and the comment will update once the next run succeeds. |
|
Premature - ignore. |
Implements persisted commit metadata for SharedTree per the design in PR #28047: applications can attach a JSON-serializable value to the commit produced by a transaction, have it replicated to peers and persisted inline on commits in the
EditManagersummary, and read it back by revision. The metadata shares the lifetime of its commit, so it is dropped automatically when the commit is trimmed from the trunk — no separate GC policy.What changed
shared-tree-core): addsMessageFormatVersion.v7andEditManagerFormatVersion.v7, both gated behind a newFluidClientVersion.v3_0, plus an optionalpersistedMetadatafield on both message formats, onCommitBase/Commit/EncodedCommit, and onCommitMessage.encodeCommit/decodeCommitand both message codecs carry the field, writing it only at v7 or later.PersistedCommitMetadataIndexkeyed byRevisionTaglives onSharedTreeCoreand is shared by every checkout (including forks), so metadata recorded by a transaction on a fork is found when a merge submits the commit. It is populated/read acrosssubmitCommit,processMessagesCore,applyStashedOpandrollback(resubmit works becausesubmitCommitreads the index, not the decoded op), and pruned onancestryTrimmed. It is deliberately not stored onGraphCommit, becauserebaseBranchrebuilds commits as fresh object literals and would silently drop it.RunTransactionParamsAlpha.persistedMetadata,UntypedTreeViewAlpha.getPersistedCommitMetadata(revision),RevisionTagexported as@alpha, andLocalChangeMetadata.revisionso achangedevent can be correlated to its metadata. Metadata from nested transactions resolves to the outermost transaction; a transaction that produces no commit (empty body or rollback) discards its metadata without throwing.Validation
No validation could be run in this environment, and none is claimed. The sandbox has no package registry access and no
node_modules, so the toolchain could not be obtained:npx tsc --project ./tsconfig.json --noEmit(inpackages/dds/tree)node_modules; install blockednpm install -g typescript --offlinenpm error code ENOTCACHED(configured registry is an ADO feed returning 401; nothing cached)Invoke-WebRequest https://registry.npmjs.org/typescriptERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILUREcorepack pnpm --versionregistry.npmjs.org(same TLS failure)Consequently the branch has not been compiled, no test has been executed, and API reports were not regenerated. Before merging, a reviewer must run, from
packages/dds/tree:Required follow-up: API reports
api-report/*.api.mdare generated artifacts and per.claude/CLAUDE.mdmust never be hand-edited, so they are not included here and CI will flag them as stale. Regenerating should add exactly:FluidClientVersion.v3_0,RunTransactionParamsAlpha.persistedMetadata,UntypedTreeViewAlpha.getPersistedCommitMetadata,RevisionTag, andLocalChangeMetadata.revision(and the corresponding re-exports influid-framework).Snapshots
New snapshot files for the
v3_0client version were derived mechanically rather than generated, and should be verified withtest:snapshots:regen. Every one is byte-identical to itsv2_80counterpart except for the single EditManager/Message format version field changing from6to7— which is exactly the delta observed between the existingv2_74andv2_80snapshot sets (4→6). Existing snapshots are untouched, because the codec tree records the write version and olderminVersionForCollabvalues still write v6.Scope
Scope: 72 changed files, one concern. 39 of those are the mechanically derived
v3_0snapshot files described above, and 1 is a changeset — 32 files carry real change.The feature cannot be split. Landing the op codecs without the lifecycle wiring registers a format version with no producer; landing the summary persistence without the index leaves
encodeCommitwith nothing to read; landing the public API without either produces an alpha API that silently does nothing. Any intermediate slice would either be dead code or would advertise durability the document does not yet have.Review map
Read in this order:
shared-tree-core/persistedCommitMetadata.ts— the index and the rationale for keying by revision.shared-tree-core/sharedTreeCore.ts— all lifecycle wiring (submit / process / stash / rollback / trim).shared-tree-core/editManagerFormatCommons.ts,messageFormat*.ts,messageTypes.ts— the format additions.shared-tree-core/editManagerCodecsCommons.ts,messageCodec*.ts,*Codecs*.ts,codec/codec.ts,shared-tree/sharedTree.ts— codec plumbing and v7/v3_0registration.shared-tree/treeCheckout.ts— where a transaction's metadata is recorded. Note the non-obvious part: the transaction stack mints the squash commit's revision lazily on the first edit, andSharedTreeCoresubmits from the branch'sbeforeChangeduring apply, so the recording hook is the revision minter (mintTransactionRevisionTag) rather than a branch event.discardPendingPersistedMetadatacleans up the abort / no-commit cases.simple-tree/api/tree.ts,simple-tree/api/transactionTypes.ts,core/rebase/types.ts,index.ts,entrypoints/alpha.ts— the alpha surface.test/shared-tree/persistedCommitMetadata.spec.tsand the two version-list additions in existing codec tests — everything else is generated/mechanical.Alternatives considered: putting the metadata on
GraphCommit(loses data on the first rebase — see above), and a separately persisted index blob (needs its own GC policy and can drift from the trunk). A feature-flagged partial landing was rejected because the flag would beminVersionForCollabitself, which is already the gate this design uses.Test coverage
test/shared-tree/persistedCommitMetadata.spec.tscovers: local readback;undefinedfor un-annotated commits; peer replication; survival across rebase behind a concurrent remote commit; metadata dropped from the wire whenminVersionForCollabis too old; nested transactions resolving to the outermost; no-change and rolled-back transactions discarding metadata without throwing; and fork → merge → peer, including after the fork is disposed. The spec's summary round-trip, trailing-op,retainHistory, trimming, disconnect/resubmit and stashed-op scenarios are not yet covered by direct tests; the code paths are implemented but those cases need the asyncTestTreeProviderharness and should be added once the suite can actually be executed.Authored with Minions.