From e804362a94764e81af5cd72a879d03559b9602e0 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 7 Nov 2026 19:57:24 -0600 Subject: [PATCH] fix(documents): say whether an exact revision read is still current An agent asked to read the current plan fetched the revision it had just restored by id and reported it as the latest, missing a newer edit by another person. The exact-revision read now carries the document's latestRevisionId, latestRevisionNumber and isLatest, and its API description points to the current-document read. Co-Authored-By: Claude Opus 5.5 --- packages/shared/src/index.ts | 1 + packages/shared/src/types/index.ts | 1 + packages/shared/src/types/issue.ts | 7 +++++++ server/src/__tests__/documents-service.test.ts | 3 +++ server/src/routes/openapi.ts | 2 +- server/src/services/documents.ts | 4 +++- ui/src/api/issues.ts | 3 ++- 7 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 4f351647499..f1c8a7de168 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1220,6 +1220,7 @@ export type { IssueDocument, IssueDocumentSummary, DocumentRevision, + ExactDocumentRevision, DocumentFormat, LegacyPlanDocument, IssueAttachment, diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 884c44fd39c..4aa18840240 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -820,6 +820,7 @@ export type { IssueDocument, IssueDocumentSummary, DocumentRevision, + ExactDocumentRevision, DocumentFormat, LegacyPlanDocument, IssueAncestor, diff --git a/packages/shared/src/types/issue.ts b/packages/shared/src/types/issue.ts index f5a8eaa5493..0d598234344 100644 --- a/packages/shared/src/types/issue.ts +++ b/packages/shared/src/types/issue.ts @@ -214,6 +214,13 @@ export interface DocumentRevision { createdAt: Date; } +/** One exact revision plus the document's current revision, so a pinned read never passes as the latest. */ +export interface ExactDocumentRevision extends DocumentRevision { + latestRevisionId: string | null; + latestRevisionNumber: number; + isLatest: boolean; +} + export interface LegacyPlanDocument { key: "plan"; body: string; diff --git a/server/src/__tests__/documents-service.test.ts b/server/src/__tests__/documents-service.test.ts index bab96aba79f..37909508ec7 100644 --- a/server/src/__tests__/documents-service.test.ts +++ b/server/src/__tests__/documents-service.test.ts @@ -109,6 +109,9 @@ describeEmbeddedPostgres("documentService system issue documents", () => { const pinned = await svc.getIssueDocumentRevision(issueId, "plan", original!.latestRevisionId!); expect(pinned?.body).toBe("# Plan"); expect(pinned?.id).toBe(original!.latestRevisionId); + const current = await svc.getIssueDocumentByKey(issueId, "plan"); + expect(pinned).toMatchObject({ isLatest: false, latestRevisionId: current!.latestRevisionId, latestRevisionNumber: 2 }); + expect(await svc.getIssueDocumentRevision(issueId, "plan", current!.latestRevisionId!)).toMatchObject({ isLatest: true, revisionNumber: 2, body: "# Newer plan" }); expect(await svc.getIssueDocumentRevision(issueId, "other-document", original!.latestRevisionId!)).toBeNull(); expect(await svc.getIssueDocumentRevision(randomUUID(), "plan", original!.latestRevisionId!)).toBeNull(); expect(await svc.getIssueDocumentRevision(issueId, "plan", randomUUID())).toBeNull(); diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index 4399bb9ed40..b52c6cf4842 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -4624,7 +4624,7 @@ registry.registerPath({ tags: ["issues"], summary: "Get an exact issue document revision", description: - "Available to board users and agents with current issue read access. Returns the specified revision only when its document is linked to this issue under the given key. Document keys are trimmed and lowercased before validation. This read does not restore or modify the document.", + "Available to board users and agents with current issue read access. Returns the specified revision only when its document is linked to this issue under the given key. Document keys are trimmed and lowercased before validation. The response also carries the document's current latestRevisionId and latestRevisionNumber, and isLatest says whether this revision is still current; to read the current text, use GET /api/issues/{id}/documents/{key}. This read does not restore or modify the document.", request: { params: z.object({ id: z.string(), diff --git a/server/src/services/documents.ts b/server/src/services/documents.ts index 7c81d075354..3b4ffe9ee71 100644 --- a/server/src/services/documents.ts +++ b/server/src/services/documents.ts @@ -181,11 +181,13 @@ export function documentService(db: Db) { format: documentRevisions.format, body: documentRevisions.body, changeSummary: documentRevisions.changeSummary, createdByAgentId: documentRevisions.createdByAgentId, createdByUserId: documentRevisions.createdByUserId, createdByRunId: documentRevisions.createdByRunId, createdAt: documentRevisions.createdAt, + latestRevisionId: documents.latestRevisionId, latestRevisionNumber: documents.latestRevisionNumber, }).from(issueDocuments) .innerJoin(documents, eq(issueDocuments.documentId, documents.id)) .innerJoin(documentRevisions, and(eq(documentRevisions.documentId, documents.id), eq(documentRevisions.companyId, documents.companyId))) .where(and(eq(issueDocuments.issueId, issueId), eq(issueDocuments.key, key), eq(documentRevisions.id, revisionId))).limit(1); - return revision ?? null; + // An exact read also states whether it is still current, so a reader cannot report an older revision as the latest one. + return revision ? { ...revision, isLatest: revision.latestRevisionId === revision.id } : null; }, listIssueDocumentRevisions: async (issueId: string, rawKey: string) => { diff --git a/ui/src/api/issues.ts b/ui/src/api/issues.ts index 1e5a094076f..cfa16a500db 100644 --- a/ui/src/api/issues.ts +++ b/ui/src/api/issues.ts @@ -11,6 +11,7 @@ import type { InboxMineSnapshot, CreateIssueTreeHold, DocumentRevision, + ExactDocumentRevision, FeedbackTargetType, FeedbackTrace, FeedbackVote, @@ -745,7 +746,7 @@ export const issuesApi = { `/issues/${id}/documents/${encodeURIComponent(key)}/revisions`, ), getDocumentRevision: (id: string, key: string, revisionId: string) => - api.get(`/issues/${id}/documents/${encodeURIComponent(key)}/revisions/${encodeURIComponent(revisionId)}`), + api.get(`/issues/${id}/documents/${encodeURIComponent(key)}/revisions/${encodeURIComponent(revisionId)}`), restoreDocumentRevision: (id: string, key: string, revisionId: string) => api.post( `/issues/${id}/documents/${encodeURIComponent(key)}/revisions/${revisionId}/restore`,