diff --git a/server/src/__tests__/activity-log-retention.test.ts b/server/src/__tests__/activity-log-retention.test.ts index b435d90778a..a6a36f7f1ff 100644 --- a/server/src/__tests__/activity-log-retention.test.ts +++ b/server/src/__tests__/activity-log-retention.test.ts @@ -38,6 +38,7 @@ describe("capActivityDetails", () => { }); expect(capped?._paperclipOriginalBytes).toBeGreaterThan(MAX_ACTIVITY_DETAILS_BYTES); expect(capped?._paperclipPreview).toEqual(expect.stringContaining("start-")); + expect(/[\uD800-\uDBFF]$/.test(String(capped?._paperclipPreview))).toBe(false); expect(Buffer.byteLength(JSON.stringify(capped), "utf8")).toBeLessThanOrEqual( MAX_ACTIVITY_DETAILS_BYTES, ); @@ -161,7 +162,7 @@ describeEmbeddedPostgres("activity log retention", () => { action: "test.oversized", entityType: "company", entityId: companyId, - details: { output: "x".repeat(MAX_ACTIVITY_DETAILS_BYTES * 2) }, + details: { output: "😀".repeat(MAX_ACTIVITY_DETAILS_BYTES) }, }); const row = await db diff --git a/server/src/index.ts b/server/src/index.ts index f312dd9d135..083626115b9 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -827,7 +827,7 @@ export async function startServer(): Promise { let prepareHotRestartShutdown: ((signal: "SIGINT" | "SIGTERM") => Promise<{ skipDrain: boolean }>) | null = null; let heartbeatSchedulerStopped = false; let heartbeatSchedulerInterval: ReturnType | null = null; - const stopActivityLogRetention = startActivityLogRetention(db as any); + const stopActivityLogRetention = startActivityLogRetention(db); const heartbeatSchedulerInFlight = new Set>(); const trackHeartbeatSchedulerWork = (work: Promise) => { let tracked: Promise; diff --git a/server/src/services/activity-log.ts b/server/src/services/activity-log.ts index 63a53603b58..7340156f42e 100644 --- a/server/src/services/activity-log.ts +++ b/server/src/services/activity-log.ts @@ -99,7 +99,15 @@ export function capActivityDetails( } } - return buildMarker(serialized.slice(0, low)); + const finalCodeUnit = serialized.charCodeAt(low - 1); + const nextCodeUnit = serialized.charCodeAt(low); + const splitsSurrogatePair = finalCodeUnit >= 0xd800 + && finalCodeUnit <= 0xdbff + && nextCodeUnit >= 0xdc00 + && nextCodeUnit <= 0xdfff; + const previewEnd = splitsSurrogatePair ? low - 1 : low; + + return buildMarker(serialized.slice(0, previewEnd)); } export async function resolveResponsibleUserIdForActivity(db: Db, input: LogActivityInput) { diff --git a/ui/src/context/CompanyContext.test.tsx b/ui/src/context/CompanyContext.test.tsx index 43beef5b533..799f56d2345 100644 --- a/ui/src/context/CompanyContext.test.tsx +++ b/ui/src/context/CompanyContext.test.tsx @@ -39,6 +39,7 @@ function makeCompany(id: string): Company { budgetMonthlyCents: 0, spentMonthlyCents: 0, attachmentMaxBytes: 10 * 1024 * 1024, + activityLogRetentionDays: null, defaultResponsibleUserId: null, requireBoardApprovalForNewAgents: false, feedbackDataSharingEnabled: false, diff --git a/ui/storybook/fixtures/paperclipData.ts b/ui/storybook/fixtures/paperclipData.ts index 5f6bae12977..f739e81794a 100644 --- a/ui/storybook/fixtures/paperclipData.ts +++ b/ui/storybook/fixtures/paperclipData.ts @@ -43,6 +43,7 @@ export const storybookCompanies: Company[] = [ budgetMonthlyCents: 250_000, spentMonthlyCents: 67_500, attachmentMaxBytes: 10 * 1024 * 1024, + activityLogRetentionDays: null, defaultResponsibleUserId: "user-board", requireBoardApprovalForNewAgents: true, feedbackDataSharingEnabled: true, @@ -67,6 +68,7 @@ export const storybookCompanies: Company[] = [ budgetMonthlyCents: 180_000, spentMonthlyCents: 39_500, attachmentMaxBytes: 10 * 1024 * 1024, + activityLogRetentionDays: null, defaultResponsibleUserId: "user-board", requireBoardApprovalForNewAgents: false, feedbackDataSharingEnabled: false, @@ -91,6 +93,7 @@ export const storybookCompanies: Company[] = [ budgetMonthlyCents: 90_000, spentMonthlyCents: 91_200, attachmentMaxBytes: 10 * 1024 * 1024, + activityLogRetentionDays: null, defaultResponsibleUserId: "user-board", requireBoardApprovalForNewAgents: true, feedbackDataSharingEnabled: false,