Skip to content

Commit d95070f

Browse files
committed
fix(collab-doc): derive streaming baseVersion as contentUpdatedAt ?? updatedAt
Match the version line the seed/persist use so a legacy file with no content version still ships an ordered streaming snapshot instead of an unordered one.
1 parent 8a7bca4 commit d95070f

3 files changed

Lines changed: 32 additions & 7 deletions

File tree

apps/sim/lib/copilot/request/go/file-preview-adapter.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,23 @@ describe('processFilePreviewStreamEvent — live-doc streaming merge', () => {
162162
expect(second[2]?.baseVersion).toBe(BASE_VERSION_MS)
163163
})
164164

165+
it('falls back to updatedAt for baseVersion when the file has no content version', async () => {
166+
// A legacy file with no `contentUpdatedAt` — the base version must fall back to `updatedAt`, the SAME
167+
// line the relay's synced version is on, so the snapshot is still ordered (not shipped unordered).
168+
const UPDATED_AT_MS = 850_000
169+
peekFileIntentMock.mockResolvedValue({
170+
existingContent: 'Base.',
171+
fileRecord: { contentUpdatedAt: null, updatedAt: new Date(UPDATED_AT_MS) },
172+
})
173+
const intent = makeIntent({ operation: 'append', fileId: 'file-legacy', fileName: 'notes.md' })
174+
175+
await drive(editContentDelta('{"content":"Hello'), intent)
176+
await flushMicrotasks()
177+
178+
expect(mergeEditIntoLiveFileDocMock).toHaveBeenCalledTimes(1)
179+
expect(mergeEditIntoLiveFileDocMock.mock.calls[0][2]?.baseVersion).toBe(UPDATED_AT_MS)
180+
})
181+
165182
it('throttles merges: two deltas within LIVE_DOC_MERGE_THROTTLE_MS yield one merge', async () => {
166183
const intent = makeIntent({
167184
operation: 'append',

apps/sim/lib/copilot/request/go/file-preview-adapter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,11 @@ export async function processFilePreviewStreamEvent(input: {
619619
}
620620
)
621621
if (typeof intentBase?.existingContent === 'string') {
622-
const baseVersion = intentBase.fileRecord?.contentUpdatedAt?.getTime()
622+
// Same version line as the seed/persist (`contentUpdatedAt ?? updatedAt`), so the stream's
623+
// base is comparable to the relay's synced version even when the file has no content version.
624+
const baseVersion = (
625+
intentBase.fileRecord?.contentUpdatedAt ?? intentBase.fileRecord?.updatedAt
626+
)?.getTime()
623627
const seededSession: FilePreviewSession = {
624628
...currentPreview.session,
625629
baseContent: intentBase.existingContent,

apps/sim/lib/copilot/tools/server/files/file-preview.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,15 @@ function buildAppendPreview(existingContent: string, incomingContent: string): s
142142
* full-file replacement until the intent landed.
143143
*/
144144
/**
145-
* The base content a copilot edit is computed against, plus the durable version (`contentUpdatedAt`,
146-
* epoch ms) that content is at. The version is the stream's causal base: the relay drops a streaming
147-
* snapshot if a NEWER durable write landed than this, so a concurrent human edit is never clobbered.
148-
* `baseVersion` is undefined only for a legacy file with no recorded `contentUpdatedAt`.
145+
* The base content a copilot edit is computed against, plus the durable version (epoch ms) that content
146+
* is at. The version is the stream's causal base: the relay drops a streaming snapshot if a NEWER durable
147+
* write landed than this, so a concurrent human edit is never clobbered. Derived as
148+
* `contentUpdatedAt ?? updatedAt` — the SAME version line the seed/persist use — so it is directly
149+
* comparable to the relay's recorded synced version.
149150
*/
150151
export interface WorkspaceFilePreviewBase {
151152
text: string
152-
baseVersion: number | undefined
153+
baseVersion: number
153154
}
154155

155156
export async function loadWorkspaceFileTextForPreview(
@@ -160,7 +161,10 @@ export async function loadWorkspaceFileTextForPreview(
160161
const record = await getWorkspaceFile(workspaceId, fileId)
161162
if (!record) return undefined
162163
const buffer = await fetchWorkspaceFileBuffer(record)
163-
return { text: buffer.toString('utf-8'), baseVersion: record.contentUpdatedAt?.getTime() }
164+
return {
165+
text: buffer.toString('utf-8'),
166+
baseVersion: (record.contentUpdatedAt ?? record.updatedAt).getTime(),
167+
}
164168
} catch (error) {
165169
logger.warn('Failed to load workspace file text for preview', {
166170
workspaceId,

0 commit comments

Comments
 (0)