Skip to content

Add server timestamps to version mark resolution APIs - #28037

Open
Mark Fields (markfields) wants to merge 15 commits into
microsoft:mainfrom
markfields:markfields-version-mark-timestamps
Open

Add server timestamps to version mark resolution APIs#28037
Mark Fields (markfields) wants to merge 15 commits into
microsoft:mainfrom
markfields:markfields-version-mark-timestamps

Conversation

@markfields

@markfields Mark Fields (markfields) commented Aug 21, 2026

Copy link
Copy Markdown
Member

Description

Version mark consumers need the server timestamp associated with a resolved mark, not only its sequence number. This change carries the final op's timestamp through each resolution path:

  • Resolved values from sealAndCaptureVersionMark() and resolve() include timestamp.
  • onBatchSequenced() supplies the timestamp as its third listener argument when an incoming local batch resolves a pending mark.
  • Live and historical resolution paths retain the timestamp alongside the batch sequence number.

API reports, type compatibility baselines, and a legacy API changeset are included.

Breaking Changes

None - the changes are all backwards compatible.

Reviewer Guidance

The review process is outlined in the pull request guidelines.

Please verify that each API receives the timestamp from the exact op represented by its resolved sequence number, particularly the final op of grouped and piecemeal batches.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 21, 2026 06:56
@github-actions github-actions Bot added area: tools area: runtime Runtime related issues area: repo Repo related work area: website public api change Changes to a public API changeset-present base: main PRs targeted against main branch labels Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Hi! Thank you for opening this PR. Want me to review it?

Based on the diff (446 lines, 10 files), I've queued these reviewers:

  • Correctness — logic errors, race conditions, lifecycle issues
  • Security — vulnerabilities, secret exposure, injection
  • API Compatibility — breaking changes, release tags, type design
  • Performance — algorithmic regressions, memory leaks
  • Testing — coverage gaps, hollow tests

How this works

  • Adjust the reviewer set by ticking/unticking boxes above. Reviewer toggles alone don't trigger anything.

  • Tick Start review below to dispatch the review fleet.

  • After review finishes, tick Start review again to request another run — it auto-resets after each dispatch.

  • This comment updates as new commits land; your reviewer selections are preserved.

  • Start review

@markfields
Mark Fields (markfields) marked this pull request as draft August 21, 2026 07:01

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

This PR extends the container-runtime’s legacy/beta version mark resolution surface area to propagate the server timestamp alongside the resolved sequence number, ensuring consumers can associate a resolved mark with the exact server-ticketed op time.

Changes:

  • Add timestamp to resolved results for ResolveResult and VersionMarkCapture, and thread it through live + historical resolution paths.
  • Extend IVersionMarkResolver.onBatchSequenced() to provide (batchId, sequenceNumber, timestamp) and carry the timestamp through inbound batch processing.
  • Update runtime wiring, API reports, type-compat baselines, tests, and publish a legacy changeset noting the breaking API change.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/runtime/container-runtime/src/versionMarks/versionMarkResolver.ts Adds timestamp to resolved types, tracks {sequenceNumber, timestamp} per batch, and extends listener + runtime hooks.
packages/runtime/container-runtime/src/versionMarks/inboundBatch.ts Carries the last op’s timestamp when deriving sequenced batch completion updates.
packages/runtime/container-runtime/src/test/versionMarks/versionMarkResolver.spec.ts Updates unit tests to validate timestamp propagation across capture/resolve/listener paths.
packages/runtime/container-runtime/src/test/versionMarks/inboundBatch.spec.ts Updates helper tests to validate timestamp extraction from inbound messages.
packages/runtime/container-runtime/src/test/types/validateContainerRuntimePrevious.generated.ts Marks expected forward-compat breaks for updated type aliases.
packages/runtime/container-runtime/src/test/containerRuntime.spec.ts Updates runtime-level test expectations to include resolved timestamp.
packages/runtime/container-runtime/src/containerRuntime.ts Wires deltaManager.lastMessage?.timestamp into resolver hooks and forwards inbound timestamps into processInboundBatch.
packages/runtime/container-runtime/package.json Updates type validation “broken” config for the changed public type aliases.
packages/runtime/container-runtime/api-report/container-runtime.legacy.beta.api.md Updates generated API report to reflect new timestamp-bearing signatures/types.
packages/runtime/container-runtime/api-report/container-runtime.legacy.alpha.api.md Updates generated API report to reflect new timestamp-bearing signatures/types.
.changeset/lazy-teams-tan.md Adds a minor legacy changeset describing the timestamp additions and callback signature change.
Suppressed comments (1)

packages/runtime/container-runtime/src/versionMarks/versionMarkResolver.ts:84

  • The @returns docs state the resolved path always includes a "last processed op timestamp", but the returned timestamp can be undefined per the VersionMarkCapture type. Document that explicitly so consumers know to handle it.
	 * @returns The pending batch identity and exclusive sequence number lower bound, or the current sequence number
	 * and last processed op timestamp when there is no pending local batch.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/runtime/container-runtime/src/versionMarks/versionMarkResolver.ts Outdated
Comment thread packages/runtime/container-runtime/src/versionMarks/inboundBatch.ts Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread packages/runtime/container-runtime/src/versionMarks/versionMarkResolver.ts Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fleet Review — In progress

Running reviewers: correctness, security, api-compatibility

View run

@sonalideshpandemsft Sonali Deshpande (sonalideshpandemsft) added the release-blocking Must be addressed before we cut and publish the next release label Aug 21, 2026
@markfields
Mark Fields (markfields) marked this pull request as ready for review August 24, 2026 21:27
…resolved sequenceNumber bug in sealAndCaptureVersionMark

Clarify that the resolved capture's timestamp can be undefined not only for
back-compat with previously stored captures, but also when no op has been
processed yet (e.g. a freshly loaded container). Also fix a bug where the
resolved branch returned sequenceNumberLowerBound (out of scope, a
different method`'s parameter) instead of referenceSequenceNumber, which
contradicted existing unit test expectations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0add8302-8442-4f31-8596-1d0d7bcebf4b
The doc comments described only the last op's sequence number, but the sequenced field/return value also carries the last op's server timestamp. Update the comments to match.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0add8302-8442-4f31-8596-1d0d7bcebf4b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@anthony-murphy

Tony Murphy (anthony-murphy) commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Deep Review is feeling its way through the dark.

  • ✅ Mechanical Review
  • ✅ Historical / Contextual Review
  • ✅ Solution / Problem Space Review
  • 🪸 Adversarial Filter — identifying the colony…
  • ⬜ Synthesis
  • ⬜ Comments

@anthony-murphy

Copy link
Copy Markdown
Contributor

Deep Review: Update on the 0xd34 assert concern from the prior review (commit 8f8438a). On e584c59 the container hard-fault reading is resolved — the (sequenceNumber, timestamp) pair is immutable per sequenced op (both read from the same final ISequencedDocumentMessage), so a tolerated duplicate carries the same timestamp and cannot convert into a hard-fault. What remains is the stale assert message (it names only sequence-number remapping though the dedup key is now the (batchId, sequenceNumber, timestamp) triple) plus one open question for a DuplicateBatchDetector owner. Both are now tracked inline on the assert line.

* @param sequenceNumberLowerBound - The inclusive lower bound for the historical op search.
* @returns The resolved sequence number, or a result indicating that the batch remains pending or can
* no longer be resolved.
* @param sequenceNumberLowerBound - The exclusive lower bound for the historical op search.

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.

Deep Review: This JSDoc now describes sequenceNumberLowerBound as exclusive — line 85 (@returns ...exclusive sequence number lower bound...) and line 96 (@param sequenceNumberLowerBound - The exclusive lower bound...) — but line 92 in the same block still says inclusive, and the implementation is inclusive. sealAndCaptureVersionMark returns sequenceNumberLowerBound: referenceSequenceNumber + 1 with the comment "Store that as an inclusive lower bound so resolve() scans directly from it," and resolveFromHistory fetches from sequenceNumberLowerBound directly (commented "(inclusive)"). DEV.md and the container-runtime test both reaffirm an inclusive lower bound, and the PR's own edited test moves resolve("targetBatch", 11)resolve("targetBatch", 10) and expects fetchMessages to start at exactly 10 — an exclusive bound would skip the boundary op.

Revert the two "exclusive" edits (lines 85 and 96) back to "inclusive" so the public @beta/@legacy JSDoc matches the implementation, line 92, DEV.md, and the edited test.

* unacked local batch, or a `resolved` capture (`sequenceNumber` + the last processed op's server
* `timestamp`) when there is no in-flight local work. The timestamp property is optional both for
* compatibility with previously stored captures and because no op may have been processed yet (e.g.
* on a freshly loaded container), in which case it is `undefined`.

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.

Deep Review: The sealAndCaptureVersionMark JSDoc says the timestamp is undefined when "no op may have been processed yet (e.g. on a freshly loaded container)." But the PR's own test "captures the summary timestamp after load before any new ops arrive" (containerRuntime.spec.ts:2894-2938) shows it retrieves the timestamp (123456) from summary metadata on a freshly loaded container, because getCurrentReferenceTimestampMs() falls back to messageAtLastSummary?.timestamp (containerRuntime.ts:4477-4480). It is undefined only when neither a last message nor a last-summary message exists.

Separately, DEV.md (line 681, also lines 45/58) documents the hook as getCurrentTimestamp -> deltaManager.lastMessage?.timestamp, omitting the ?? messageAtLastSummary?.timestamp fallback the code actually wires.

Correct the JSDoc to reflect that a freshly loaded container does obtain a timestamp via summary metadata, and update DEV.md to state the hook is getCurrentReferenceTimestampMs() (lastMessage?.timestamp ?? messageAtLastSummary?.timestamp). While in DEV.md, add a one-sentence rationale that reusing getCurrentReferenceTimestampMs() keeps the seq/timestamp pairing aligned at load — on load messageAtLastSummary.sequenceNumber === initialSequenceNumber, so with no new ops the fallback timestamp and the reported sequence number come from the same op.

@anthony-murphy

Copy link
Copy Markdown
Contributor

packages/runtime/container-runtime/src/versionMarks/versionMarkResolver.ts:1025

Deep Review: Following up on the 0xd34 concern from the prior review. The early-return dedup is now existingResolution?.sequenceNumber === sequenceNumber && existingResolution.timestamp === timestamp, and the following assert(existingResolution === undefined, 0xd34 ...) fires when that early-return is skipped — so a (batchId, sequenceNumber) re-observed with a differing timestamp now reaches the assert, whose message still names only sequence-number remapping.

The hard-fault reading is resolved: the (sequenceNumber, timestamp) pair is immutable per sequenced op (both read from the same final ISequencedDocumentMessage — inboundBatch.ts:47-57, 64-74), so a legitimate re-delivery carries the same timestamp and the timestamp branch can't trip. What remains is stale text: update the 0xd34 assert message to name the timestamp-mismatch case now that the dedup key is the (batchId, sequenceNumber, timestamp) triple.

Open question for a DuplicateBatchDetector owner: if any redelivery path (resubmit, replay, or a batch surfaced by both the live and history paths) could present an identical (batchId, sequenceNumber) with a differing timestamp, narrow the dedup guard back to sequenceNumber alone rather than asserting; if not, the message update is sufficient.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

🔗 Found some broken links! 💔

Run a link check locally to find them. See Checking for Broken Links for more information.

linkcheck output

$ start-server-and-test "npm run serve -- --host 127.0.0.1 --no-open" http://127.0.0.1:3000 check-links
1: starting server using command "npm run serve -- --host 127.0.0.1 --no-open"
and when url "[ 'http://127.0.0.1:3000' ]" is responding with HTTP status code 200
running tests using command "npm run check-links"


> fluid-framework-website@0.0.0 serve
> docusaurus serve --host 127.0.0.1 --no-open

[SUCCESS] Serving "build" directory at: http://127.0.0.1:3000/

> fluid-framework-website@0.0.0 check-links
> linkcheck http://127.0.0.1:3000 --skip-file skipped-urls.txt

Crawling...

http://127.0.0.1:3000/docs/data-structures/tree/schema-evolution/feature-flag-schema-upgrades
- (72:12) 'isStaged..' => http://127.0.0.1:3000/docs/api/fluid-framework/treeviewalpha-interface#isstagedupgradeenabled-methodsignature (HTTP 200 but missing anchor)


Stats:
  338443 links
    2041 destination URLs
    2297 URLs ignored
       1 warnings
       0 errors

Error: Command failed with exit code 1: npm run check-links
    at makeError (/home/runner/work/FluidFramework/FluidFramework/website/node_modules/.pnpm/execa@5.1.1/node_modules/execa/lib/error.js:60:11)
    at handlePromise (/home/runner/work/FluidFramework/FluidFramework/website/node_modules/.pnpm/execa@5.1.1/node_modules/execa/index.js:118:26)
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5) {
  shortMessage: 'Command failed with exit code 1: npm run check-links',
  command: 'npm run check-links',
  escapedCommand: '"npm run check-links"',
  exitCode: 1,
  signal: undefined,
  signalDescription: undefined,
  stdout: undefined,
  stderr: undefined,
  failed: true,
  timedOut: false,
  isCanceled: false,
  killed: false
}
[ELIFECYCLE] Command failed with exit code 1.

@github-actions

Copy link
Copy Markdown
Contributor

Bundle size comparison

Base commit: unresolved
Head commit: ff6808da36e5ad6029944597e28136670a1d1cd9

⚠️ Comparison unavailable.

The PR's CI build failed — fix the build and the comment will update once the next run succeeds.

@anthony-murphy

Copy link
Copy Markdown
Contributor

Deep Review

Reviewed commit ff6808d on 2026-08-24.

Readiness: 8/10 — ALMOST READY

Not signed off, but close. The resolved op's server timestamp is threaded additively through the live/session, inbound, and historical resolution paths, and the @legacy @beta surface stays optional and source-compatible. Both prior-run inline doc threads are now fixed in code (the exclusive/inclusive JSDoc contradiction reverted to "inclusive"; the freshly-loaded-container timestamp docstring corrected; plus a real referenceSequenceNumber return-value bug fixed). No correctness blockers remain — what's left is a cluster of four contained polish/hardening items: a changeset disclosure gap, a stale assert message, an optional-listener-parameter decision, and a capture-pairing question. Fixing them moves this to READY.

Path to Ready

  • Resolve inline threads
  • Add a sentence to .changeset/lazy-teams-tan.md and the PR description disclosing the internal assert→tolerate behavior change — the resolver no longer faults on a re-broadcasted no-explicit-batch single-op batch that remaps to a new sequence number; it keeps the first-landed resolution.
  • Update the stale 0xd34 assert message to name the timestamp-mismatch case now that the dedup key is the (batchId, sequenceNumber, timestamp) triple — or restore a non-fatal telemetry/debug-only assert when an incoming resolution disagrees with the stored sequenceNumber (detail in this note).

Context for Reviewers

For human reviewer
  • Domain-expert judgment on the widened dedup path — an owner of DuplicateBatchDetector should confirm whether any redelivery path (resubmit, replay, or a batch surfaced by both the live and history paths) can present an identical (batchId, sequenceNumber) with a differing timestamp, or a same-batchId→different-sequenceNumber corruption that slips past the upstream detector. This drives whether the 0xd34 item is a message fix or a guard change.
  • Beta contract owners — lindsnguyen (original resolver author) and sonalideshpandemsft (beta-promotion owner) should confirm the optional-timestamp compatibility story matches external Bohemia/Loop consumption expectations, given this extends the resolved shape ~6 days after beta promotion.
  • Cross-module load-time invariant (cannot be assessed by the pipeline) — the capture-path pairing depends on messageAtLastSummary corresponding to deltaManager.initialSequenceNumber at load across module boundaries; confirm this holds on all load/recovery paths, not just the tested fresh-load case.
Review history (2 prior reviews)
  • e584c59 2026-08-24 · 7/100xd34 hard-fault reading resolved; four contained doc/polish items remain
  • 8f8438a 2026-08-21 · 5/100xd34 assert flagged as a gating hard-fault concern requiring domain-expert sign-off

onBatchSequenced(listener: (batchId: string, sequenceNumber: number) => void): () => void;
onBatchSequenced(
listener: (batchId: string, sequenceNumber: number, timestamp?: number) => void,
): () => void;

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.

Deep Review: The @legacy @beta listener signature types timestamp as optional — listener: (batchId: string, sequenceNumber: number, timestamp?: number) => void — but the sole runtime caller always supplies a concrete value: processInboundBatch(batchId, sequenceNumber, timestamp: number) takes a required timestamp and invokes listener(batchId, sequenceNumber, timestamp). The doc comment itself describes the listener firing with "the final op's server timestamp," implying no optionality.

Since the only producer never omits the argument, tightening the parameter to timestamp: number removes defensive dead code with no runtime change. If the optionality is intentional (source-compatibility with existing two-arg listeners), keep it and note that rationale at the declaration so the width is a documented decision rather than an apparent oversight on a release-blocking public surface.

Suggested change
): () => void;
onBatchSequenced(
listener: (batchId: string, sequenceNumber: number, timestamp: number) => void,
): () => void;

const fetchOps = (context as IContainerContextInternal).fetchOps;
this.versionMarkResolverInternal = new VersionMarkResolver({
getCurrentSequenceNumber: () => this.deltaManager.lastSequenceNumber,
getCurrentTimestamp: () => this.getCurrentReferenceTimestampMs(),

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.

Deep Review: The two capture hooks read different DeltaManager members — getCurrentSequenceNumber: () => this.deltaManager.lastSequenceNumber (line 1945) and getCurrentTimestamp: () => this.getCurrentReferenceTimestampMs() (line 1946), where getCurrentReferenceTimestampMs() returns this.deltaManager.lastMessage?.timestamp ?? this.messageAtLastSummary?.timestamp. sealAndCaptureVersionMark() pairs these two independently-read values into one { sequenceNumber, timestamp } capture.

#22508's stated motivation was that lastSequenceNumber can advance ahead of the last surfaced message, so the pairing is a caller discipline rather than a structural guarantee. Load-time consistency is verified (messageAtLastSummary?.sequenceNumber vs initialSequenceNumber) and the added test covers the load/summary fallback, but the mid-batch skew case is not covered, and the PR's own Reviewer Guidance asks to confirm each API receives the timestamp from the exact op represented by its resolved sequence number.

Confirm no synchronous window exists where deltaManager.lastSequenceNumber refers to an op whose timestamp is not lastMessage.timestamp at capture time (validate across fullBatch vs piecemeal inbound shapes). If the invariant holds, optionally harden by deriving both values from a single combined hook returning { sequenceNumber, timestamp }, making the pairing atomic by construction rather than a documented caller invariant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: repo Repo related work area: runtime Runtime related issues area: tools area: website base: main PRs targeted against main branch changeset-present deep-review public api change Changes to a public API release-blocking Must be addressed before we cut and publish the next release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants