@taskforcehq/taskforce 0.3.302 → 0.3.304

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -0,0 +1,209 @@
1
+ import { parseAiProfileSurfaceType } from '../../shared/aiProfileSurfaceType.js';
2
+ export const USER_GLOBAL_SYNC_META_KEY = 'taskforce.userGlobalSyncMeta.v1';
3
+ export const SYNC_DEBUG_LOG_KEY = 'taskforce.syncDebug.v1';
4
+ export const WORKSPACE_BOOTSTRAP_TIMEOUT_MS = 120_000;
5
+ export const WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX = 'taskforce.syncWatermarks.v3';
6
+ export const WORKSPACE_FULL_RECONCILE_INTERVAL_MS = 5 * 60 * 1000;
7
+ export const WORKSPACE_CONCURRENT_WINDOW_MESSAGE = 'Another local window is already syncing this workspace. Wait a few seconds, or press Repair in that window.';
8
+ export function isValidWorkspaceKey(workspaceId) {
9
+ const workspaceKey = String(workspaceId || '').trim();
10
+ return workspaceKey.length > 0 && workspaceKey.toLowerCase() !== 'default';
11
+ }
12
+ function isSyncDebugLoggingEnabled() {
13
+ if (typeof window === 'undefined')
14
+ return false;
15
+ try {
16
+ const raw = String(window.localStorage.getItem(SYNC_DEBUG_LOG_KEY) || '').trim().toLowerCase();
17
+ return raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes';
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ export function logSyncDebug(event, details) {
24
+ if (!isSyncDebugLoggingEnabled())
25
+ return;
26
+ const payload = details && typeof details === 'object' ? details : {};
27
+ console.info('[Taskforce Sync]', event, payload);
28
+ }
29
+ export function normalizeWorkspaceSyncAiProfileSnapshotEntry(entry) {
30
+ const metadata = entry?.providerMetadata ?? entry?.provider_metadata ?? null;
31
+ return {
32
+ id: String(entry?.id || '').trim(),
33
+ workspaceId: String(entry?.workspaceId || entry?.workspace_id || '').trim(),
34
+ profileToken: String(entry?.profileToken || entry?.profile_token || '').trim(),
35
+ name: String(entry?.name || '').trim(),
36
+ username: String(entry?.username || '').trim(),
37
+ icon: String(entry?.icon || 'Bot').trim() || 'Bot',
38
+ color: String(entry?.color || '#8b5cf6').trim() || '#8b5cf6',
39
+ description: typeof entry?.description === 'string' && entry.description.trim().length > 0
40
+ ? entry.description.trim()
41
+ : null,
42
+ role: typeof entry?.role === 'string' && entry.role.trim().length > 0
43
+ ? entry.role.trim()
44
+ : null,
45
+ provider: typeof entry?.provider === 'string' && entry.provider.trim().length > 0
46
+ ? entry.provider.trim()
47
+ : null,
48
+ model: typeof entry?.model === 'string' && entry.model.trim().length > 0
49
+ ? entry.model.trim()
50
+ : null,
51
+ surfaceType: parseAiProfileSurfaceType(entry?.surfaceType ?? entry?.surface_type),
52
+ providerMetadata: metadata && typeof metadata === 'object' && !Array.isArray(metadata)
53
+ ? metadata
54
+ : null,
55
+ createdAt: String(entry?.createdAt || entry?.created_at || '').trim(),
56
+ updatedAt: String(entry?.updatedAt || entry?.updated_at || entry?.createdAt || entry?.created_at || '').trim(),
57
+ lastActiveAt: typeof entry?.lastActiveAt === 'string' && entry.lastActiveAt.trim().length > 0
58
+ ? entry.lastActiveAt.trim()
59
+ : typeof entry?.last_active_at === 'string' && entry.last_active_at.trim().length > 0
60
+ ? entry.last_active_at.trim()
61
+ : null
62
+ };
63
+ }
64
+ export function readUserGlobalSyncMeta() {
65
+ if (typeof window === 'undefined')
66
+ return {};
67
+ try {
68
+ const raw = window.localStorage.getItem(USER_GLOBAL_SYNC_META_KEY);
69
+ if (!raw)
70
+ return {};
71
+ const parsed = JSON.parse(raw);
72
+ return parsed && typeof parsed === 'object'
73
+ ? parsed
74
+ : {};
75
+ }
76
+ catch {
77
+ return {};
78
+ }
79
+ }
80
+ export function writeUserGlobalSyncMeta(next) {
81
+ if (typeof window === 'undefined')
82
+ return;
83
+ try {
84
+ window.localStorage.setItem(USER_GLOBAL_SYNC_META_KEY, JSON.stringify(next));
85
+ }
86
+ catch {
87
+ // best-effort only
88
+ }
89
+ }
90
+ export function buildTaxonomyStateFingerprint(taxonomyState) {
91
+ try {
92
+ return JSON.stringify(taxonomyState && typeof taxonomyState === 'object' ? taxonomyState : null);
93
+ }
94
+ catch {
95
+ return 'null';
96
+ }
97
+ }
98
+ export function flattenDocumentReviewComments(sessions) {
99
+ return sessions.flatMap((session) => {
100
+ const sessionId = String(session?.id || '').trim();
101
+ if (!sessionId || !Array.isArray(session?.comments))
102
+ return [];
103
+ return session.comments
104
+ .map((comment) => ({
105
+ id: String(comment?.id || '').trim(),
106
+ sessionId,
107
+ body: String(comment?.body || ''),
108
+ anchor: comment?.anchor ?? null,
109
+ order: Number.isFinite(Number(comment?.order)) ? Math.max(0, Math.floor(Number(comment.order))) : 0,
110
+ authorActorId: typeof comment?.authorActorId === 'string' && comment.authorActorId.trim().length > 0 ? comment.authorActorId.trim() : null,
111
+ createdAt: String(comment?.createdAt || '').trim(),
112
+ updatedAt: String(comment?.updatedAt || comment?.createdAt || '').trim(),
113
+ deletedAt: typeof comment?.deletedAt === 'string' && comment.deletedAt.trim().length > 0 ? comment.deletedAt.trim() : null,
114
+ }))
115
+ .filter((comment) => comment.id.length > 0 && comment.sessionId.length > 0);
116
+ });
117
+ }
118
+ export function flattenTaskEvents(tasks, archivedTasks) {
119
+ const deduped = new Map();
120
+ for (const task of [...(tasks || []), ...(archivedTasks || [])]) {
121
+ const taskEvents = Array.isArray(task?.taskEvents) ? task.taskEvents : [];
122
+ for (const event of taskEvents) {
123
+ const id = String(event?.id || '').trim();
124
+ const taskId = String(event?.taskId || task?.id || '').trim();
125
+ const workspaceId = String(event?.workspaceId || '').trim();
126
+ const createdAt = String(event?.createdAt || '').trim();
127
+ if (!id || !taskId || !workspaceId || !createdAt)
128
+ continue;
129
+ const nextEvent = {
130
+ id,
131
+ taskId,
132
+ workspaceId,
133
+ action: String(event?.action || '').trim(),
134
+ actor: String(event?.actor || 'system').trim() || 'system',
135
+ actorType: event?.actorType === 'ai' || event?.actorType === 'human' || event?.actorType === 'system'
136
+ ? event.actorType
137
+ : 'system',
138
+ details: event?.details,
139
+ createdAt,
140
+ };
141
+ const existing = deduped.get(id);
142
+ if (!existing || nextEvent.createdAt > existing.createdAt) {
143
+ deduped.set(id, nextEvent);
144
+ }
145
+ }
146
+ }
147
+ return Array.from(deduped.values()).sort((left, right) => {
148
+ if (left.createdAt !== right.createdAt)
149
+ return left.createdAt.localeCompare(right.createdAt);
150
+ return left.id.localeCompare(right.id);
151
+ });
152
+ }
153
+ export function readPersistedSyncWatermarks(workspaceId, taxonomyStateFingerprint) {
154
+ if (typeof window === 'undefined')
155
+ return null;
156
+ try {
157
+ const key = `${WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX}.${workspaceId}`;
158
+ const raw = window.localStorage.getItem(key);
159
+ if (!raw)
160
+ return null;
161
+ const parsed = JSON.parse(raw);
162
+ if (!parsed || (parsed.v !== 2 && parsed.v !== 3 && parsed.v !== 4 && parsed.v !== 5 && parsed.v !== 6 && parsed.v !== 7 && parsed.v !== 8))
163
+ return null;
164
+ const savedTaxonomyStateFingerprint = typeof parsed.taxonomyStateFingerprint === 'string'
165
+ ? parsed.taxonomyStateFingerprint
166
+ : '';
167
+ const taskWatermarks = (!taxonomyStateFingerprint || !savedTaxonomyStateFingerprint || savedTaxonomyStateFingerprint === taxonomyStateFingerprint)
168
+ ? new Map(Array.isArray(parsed.taskWatermarks) ? parsed.taskWatermarks : [])
169
+ : new Map();
170
+ return {
171
+ taskWatermarks,
172
+ initiativeWatermarks: new Map(Array.isArray(parsed.initiativeWatermarks) ? parsed.initiativeWatermarks : []),
173
+ workstreamWatermarks: new Map(Array.isArray(parsed.workstreamWatermarks) ? parsed.workstreamWatermarks : []),
174
+ documentWatermarks: new Map(Array.isArray(parsed.documentWatermarks) ? parsed.documentWatermarks : []),
175
+ assetWatermarks: new Map(Array.isArray(parsed.assetWatermarks) ? parsed.assetWatermarks : []),
176
+ documentReviewSessionWatermarks: new Map(Array.isArray(parsed.documentReviewSessionWatermarks) ? parsed.documentReviewSessionWatermarks : []),
177
+ documentReviewCommentWatermarks: new Map(Array.isArray(parsed.documentReviewCommentWatermarks) ? parsed.documentReviewCommentWatermarks : []),
178
+ annotatedAttachmentSessionWatermarks: new Map(Array.isArray(parsed.annotatedAttachmentSessionWatermarks) ? parsed.annotatedAttachmentSessionWatermarks : []),
179
+ taskEventWatermarks: new Map(Array.isArray(parsed.taskEventWatermarks) ? parsed.taskEventWatermarks : []),
180
+ };
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ }
186
+ export function savePersistedSyncWatermarks(workspaceId, data) {
187
+ if (typeof window === 'undefined')
188
+ return;
189
+ try {
190
+ const key = `${WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX}.${workspaceId}`;
191
+ window.localStorage.setItem(key, JSON.stringify({
192
+ v: 8,
193
+ savedAt: new Date().toISOString(),
194
+ taskWatermarks: Array.from(data.taskWatermarks.entries()),
195
+ initiativeWatermarks: Array.from(data.initiativeWatermarks.entries()),
196
+ workstreamWatermarks: Array.from(data.workstreamWatermarks.entries()),
197
+ documentWatermarks: Array.from(data.documentWatermarks.entries()),
198
+ assetWatermarks: Array.from(data.assetWatermarks.entries()),
199
+ documentReviewSessionWatermarks: Array.from(data.documentReviewSessionWatermarks.entries()),
200
+ documentReviewCommentWatermarks: Array.from(data.documentReviewCommentWatermarks.entries()),
201
+ annotatedAttachmentSessionWatermarks: Array.from(data.annotatedAttachmentSessionWatermarks.entries()),
202
+ taskEventWatermarks: Array.from(data.taskEventWatermarks.entries()),
203
+ taxonomyStateFingerprint: typeof data.taxonomyStateFingerprint === 'string' ? data.taxonomyStateFingerprint : ''
204
+ }));
205
+ }
206
+ catch {
207
+ // localStorage may be full or unavailable; non-fatal
208
+ }
209
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { readPersistedSyncWatermarks, savePersistedSyncWatermarks, WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX, } from './orchestratorShared';
3
+ describe('orchestratorShared sync watermarks', () => {
4
+ const workspaceId = 'workspace-local-active';
5
+ beforeEach(() => {
6
+ localStorage.clear();
7
+ });
8
+ it('keeps persisted sync watermarks available across long idle periods', () => {
9
+ const key = `${WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX}.${workspaceId}`;
10
+ localStorage.setItem(key, JSON.stringify({
11
+ v: 8,
12
+ savedAt: '2026-04-14T10:00:00.000Z',
13
+ taskWatermarks: [['task-1', '2026-04-14T09:00:00.000Z']],
14
+ initiativeWatermarks: [['initiative-1', '2026-04-14T09:00:00.000Z']],
15
+ workstreamWatermarks: [],
16
+ documentWatermarks: [],
17
+ assetWatermarks: [],
18
+ documentReviewSessionWatermarks: [],
19
+ documentReviewCommentWatermarks: [],
20
+ annotatedAttachmentSessionWatermarks: [],
21
+ taskEventWatermarks: [['event-1', '2026-04-14T09:30:00.000Z']],
22
+ taxonomyStateFingerprint: ''
23
+ }));
24
+ const persisted = readPersistedSyncWatermarks(workspaceId, '');
25
+ expect(persisted).not.toBeNull();
26
+ expect(persisted?.taskWatermarks.get('task-1')).toBe('2026-04-14T09:00:00.000Z');
27
+ expect(persisted?.initiativeWatermarks.get('initiative-1')).toBe('2026-04-14T09:00:00.000Z');
28
+ expect(persisted?.taskEventWatermarks.get('event-1')).toBe('2026-04-14T09:30:00.000Z');
29
+ });
30
+ it('still clears task watermarks when the taxonomy fingerprint changes', () => {
31
+ savePersistedSyncWatermarks(workspaceId, {
32
+ taskWatermarks: new Map([['task-1', '2026-04-14T09:00:00.000Z']]),
33
+ initiativeWatermarks: new Map([['initiative-1', '2026-04-14T09:00:00.000Z']]),
34
+ workstreamWatermarks: new Map(),
35
+ documentWatermarks: new Map(),
36
+ assetWatermarks: new Map(),
37
+ documentReviewSessionWatermarks: new Map(),
38
+ documentReviewCommentWatermarks: new Map(),
39
+ annotatedAttachmentSessionWatermarks: new Map(),
40
+ taskEventWatermarks: new Map([['event-1', '2026-04-14T09:30:00.000Z']]),
41
+ taxonomyStateFingerprint: 'fingerprint-a'
42
+ });
43
+ const persisted = readPersistedSyncWatermarks(workspaceId, 'fingerprint-b');
44
+ expect(persisted).not.toBeNull();
45
+ expect(persisted?.taskWatermarks.size).toBe(0);
46
+ expect(persisted?.initiativeWatermarks.get('initiative-1')).toBe('2026-04-14T09:00:00.000Z');
47
+ expect(persisted?.taskEventWatermarks.get('event-1')).toBe('2026-04-14T09:30:00.000Z');
48
+ });
49
+ });
@@ -0,0 +1,25 @@
1
+ import type { MutableRefObject } from 'react';
2
+ import type { WorkspaceSyncPhase } from '../../sync/workspaceSyncState';
3
+ import type { RealtimeSyncSignal } from '../useRealtimeSync';
4
+ interface CreateRealtimeSyncBridgeArgs {
5
+ cloudAuthConfigured: boolean;
6
+ runtimeMode: 'local' | 'cloud';
7
+ workspaceCloudSyncEnabled: boolean;
8
+ workspaceSyncPhase: WorkspaceSyncPhase;
9
+ workspacePullInFlightRef: MutableRefObject<boolean>;
10
+ realtimePullDebounceRef: MutableRefObject<number | null>;
11
+ realtimeSuppressedEventIdsRef: MutableRefObject<Set<string>>;
12
+ clearWorkspaceRetry: () => void;
13
+ isWorkspaceRetryPending: () => boolean;
14
+ pullWorkspaceChangesFromCloud: () => Promise<boolean>;
15
+ }
16
+ export declare function createRealtimeSyncBridge({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, workspaceSyncPhase, workspacePullInFlightRef, realtimePullDebounceRef, realtimeSuppressedEventIdsRef, clearWorkspaceRetry, isWorkspaceRetryPending, pullWorkspaceChangesFromCloud }: CreateRealtimeSyncBridgeArgs): {
17
+ handleRealtimeSignal: (signal: RealtimeSyncSignal) => void;
18
+ recordSuppressedEventIds: (eventIds: Iterable<string>, options?: {
19
+ queueRef?: MutableRefObject<string[]>;
20
+ maxQueueSize?: number;
21
+ ttlMs?: number;
22
+ }) => void;
23
+ clearRealtimePullDebounce: () => void;
24
+ };
25
+ export {};
@@ -0,0 +1,60 @@
1
+ export function createRealtimeSyncBridge({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, workspaceSyncPhase, workspacePullInFlightRef, realtimePullDebounceRef, realtimeSuppressedEventIdsRef, clearWorkspaceRetry, isWorkspaceRetryPending, pullWorkspaceChangesFromCloud }) {
2
+ const handleRealtimeSignal = (signal) => {
3
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
4
+ return;
5
+ if (workspaceSyncPhase === 'provision-local')
6
+ return;
7
+ const eventId = String(signal?.eventId || '').trim();
8
+ if (eventId && realtimeSuppressedEventIdsRef.current.delete(eventId))
9
+ return;
10
+ if (workspacePullInFlightRef.current)
11
+ return;
12
+ if (isWorkspaceRetryPending()) {
13
+ clearWorkspaceRetry();
14
+ }
15
+ if (realtimePullDebounceRef.current !== null) {
16
+ window.clearTimeout(realtimePullDebounceRef.current);
17
+ }
18
+ realtimePullDebounceRef.current = window.setTimeout(() => {
19
+ realtimePullDebounceRef.current = null;
20
+ void pullWorkspaceChangesFromCloud();
21
+ }, 180);
22
+ };
23
+ const recordSuppressedEventIds = (eventIds, options) => {
24
+ const queueRef = options?.queueRef;
25
+ const maxQueueSize = Number.isFinite(options?.maxQueueSize) ? Math.max(1, Number(options?.maxQueueSize)) : 2048;
26
+ const ttlMs = Number.isFinite(options?.ttlMs) ? Math.max(0, Number(options?.ttlMs)) : null;
27
+ for (const rawEventId of eventIds) {
28
+ const eventId = String(rawEventId || '').trim();
29
+ if (!eventId || realtimeSuppressedEventIdsRef.current.has(eventId))
30
+ continue;
31
+ realtimeSuppressedEventIdsRef.current.add(eventId);
32
+ if (queueRef) {
33
+ queueRef.current.push(eventId);
34
+ }
35
+ if (ttlMs !== null) {
36
+ window.setTimeout(() => {
37
+ realtimeSuppressedEventIdsRef.current.delete(eventId);
38
+ }, ttlMs);
39
+ }
40
+ }
41
+ if (queueRef) {
42
+ while (queueRef.current.length > maxQueueSize) {
43
+ const stale = queueRef.current.shift();
44
+ if (stale)
45
+ realtimeSuppressedEventIdsRef.current.delete(stale);
46
+ }
47
+ }
48
+ };
49
+ const clearRealtimePullDebounce = () => {
50
+ if (realtimePullDebounceRef.current !== null) {
51
+ window.clearTimeout(realtimePullDebounceRef.current);
52
+ realtimePullDebounceRef.current = null;
53
+ }
54
+ };
55
+ return {
56
+ handleRealtimeSignal,
57
+ recordSuppressedEventIds,
58
+ clearRealtimePullDebounce
59
+ };
60
+ }
@@ -0,0 +1,69 @@
1
+ import type { MutableRefObject } from 'react';
2
+ import type { WorkspaceSyncPhase } from '../../sync/workspaceSyncState';
3
+ interface CreateSyncRecoveryCoordinatorArgs {
4
+ currentWorkspaceId: string;
5
+ taxonomyStateFingerprint: string;
6
+ workspaceCloudSyncEnabled: boolean;
7
+ workspaceSyncPhase: WorkspaceSyncPhase;
8
+ workspaceLastPullAt: string | null;
9
+ lastBootstrapPhaseRef: MutableRefObject<WorkspaceSyncPhase>;
10
+ workspaceRepairModeRef: MutableRefObject<boolean>;
11
+ workspaceForceFullAiProfilePushRef: MutableRefObject<boolean>;
12
+ workspaceStartupRepairRanRef: MutableRefObject<boolean>;
13
+ workspaceStartupFullReconcileRanRef: MutableRefObject<boolean>;
14
+ workspaceFullReconcileInFlightRef: MutableRefObject<boolean>;
15
+ workspacePullCursorRef: MutableRefObject<string | null>;
16
+ workspaceAttachBootstrapBaselineSignatureRef: MutableRefObject<string>;
17
+ workspaceCaptureAttachBootstrapBaselineRef: MutableRefObject<boolean>;
18
+ workspacePendingSignatureRef: MutableRefObject<string>;
19
+ workspaceLastPushedSignatureRef: MutableRefObject<string>;
20
+ workspaceLastPushedWatermarksRef: MutableRefObject<Map<string, string>>;
21
+ workspaceLastPushedInitiativeIdsRef: MutableRefObject<Set<string>>;
22
+ workspaceLastPushedInitiativeWatermarksRef: MutableRefObject<Map<string, string>>;
23
+ workspaceLastPushedWorkstreamIdsRef: MutableRefObject<Set<string>>;
24
+ workspaceLastPushedWorkstreamWatermarksRef: MutableRefObject<Map<string, string>>;
25
+ workspaceLastPushedAiProfileIdsRef: MutableRefObject<Set<string>>;
26
+ workspaceLastPushedAiProfileWatermarksRef: MutableRefObject<Map<string, string>>;
27
+ workspaceLastPushedDocumentPathsRef: MutableRefObject<Set<string>>;
28
+ workspaceLastPushedDocumentWatermarksRef: MutableRefObject<Map<string, string>>;
29
+ workspaceLastPushedAssetPathsRef: MutableRefObject<Set<string>>;
30
+ workspaceLastPushedAssetWatermarksRef: MutableRefObject<Map<string, string>>;
31
+ workspaceLastPushedDocumentReviewSessionIdsRef: MutableRefObject<Set<string>>;
32
+ workspaceLastPushedDocumentReviewSessionWatermarksRef: MutableRefObject<Map<string, string>>;
33
+ workspaceLastPushedDocumentReviewCommentIdsRef: MutableRefObject<Set<string>>;
34
+ workspaceLastPushedDocumentReviewCommentWatermarksRef: MutableRefObject<Map<string, string>>;
35
+ workspaceLastPushedAnnotatedAttachmentSessionIdsRef: MutableRefObject<Set<string>>;
36
+ workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef: MutableRefObject<Map<string, string>>;
37
+ workspaceLastPushedTaskEventWatermarksRef: MutableRefObject<Map<string, string>>;
38
+ bootstrapPhaseStartedAtRef: MutableRefObject<number | null>;
39
+ bootstrapLastProgressAtRef: MutableRefObject<number | null>;
40
+ acquireWorkspaceSyncLease: (workspaceId: string, owner: 'pull' | 'push' | 'repair') => Promise<boolean>;
41
+ releaseWorkspaceSyncLease: (workspaceId: string) => void;
42
+ forceClearWorkspaceSyncLease: (workspaceId: string) => void;
43
+ clearWorkspaceRetry: () => void;
44
+ clearWorkspaceIssue: () => void;
45
+ reportWorkspaceWindowContention: (operation: 'pull' | 'push' | 'repair') => void;
46
+ persistWorkspaceSyncPatch: (patch: {
47
+ phase: WorkspaceSyncPhase;
48
+ pullCursor: null;
49
+ lastErrorMessage: null;
50
+ }) => Promise<unknown>;
51
+ buildWorkspaceSyncSignature: () => string;
52
+ pullWorkspaceChangesFromCloud: (options?: {
53
+ repairMode?: boolean;
54
+ forceCursorNull?: boolean;
55
+ }) => Promise<boolean>;
56
+ pushWorkspaceChangesToCloud: (signature: string, options?: {
57
+ forceAllAiProfiles?: boolean;
58
+ repairMode?: boolean;
59
+ }) => Promise<boolean>;
60
+ cloudAuthConfigured: boolean;
61
+ runtimeMode: 'local' | 'cloud';
62
+ isAuthenticated: boolean;
63
+ checkAuthSession: () => Promise<boolean>;
64
+ }
65
+ export declare function createSyncRecoveryCoordinator({ currentWorkspaceId, taxonomyStateFingerprint, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceLastPullAt, lastBootstrapPhaseRef, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspaceStartupRepairRanRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, workspacePullCursorRef, workspaceAttachBootstrapBaselineSignatureRef, workspaceCaptureAttachBootstrapBaselineRef, workspacePendingSignatureRef, workspaceLastPushedSignatureRef, workspaceLastPushedWatermarksRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, bootstrapPhaseStartedAtRef, bootstrapLastProgressAtRef, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, forceClearWorkspaceSyncLease, clearWorkspaceRetry, clearWorkspaceIssue, reportWorkspaceWindowContention, persistWorkspaceSyncPatch, buildWorkspaceSyncSignature, pullWorkspaceChangesFromCloud, pushWorkspaceChangesToCloud, cloudAuthConfigured, runtimeMode, isAuthenticated, checkAuthSession }: CreateSyncRecoveryCoordinatorArgs): {
66
+ retryWorkspaceCloudSync: () => Promise<boolean>;
67
+ resetWorkspaceSyncCursorAndPull: () => Promise<void>;
68
+ };
69
+ export {};
@@ -0,0 +1,118 @@
1
+ import { resolveRepairPhase } from './bootstrap';
2
+ import { savePersistedSyncWatermarks } from './orchestratorShared';
3
+ export function createSyncRecoveryCoordinator({ currentWorkspaceId, taxonomyStateFingerprint, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceLastPullAt, lastBootstrapPhaseRef, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspaceStartupRepairRanRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, workspacePullCursorRef, workspaceAttachBootstrapBaselineSignatureRef, workspaceCaptureAttachBootstrapBaselineRef, workspacePendingSignatureRef, workspaceLastPushedSignatureRef, workspaceLastPushedWatermarksRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, bootstrapPhaseStartedAtRef, bootstrapLastProgressAtRef, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, forceClearWorkspaceSyncLease, clearWorkspaceRetry, clearWorkspaceIssue, reportWorkspaceWindowContention, persistWorkspaceSyncPatch, buildWorkspaceSyncSignature, pullWorkspaceChangesFromCloud, pushWorkspaceChangesToCloud, cloudAuthConfigured, runtimeMode, isAuthenticated, checkAuthSession }) {
4
+ const retryWorkspaceCloudSync = async () => {
5
+ clearWorkspaceRetry();
6
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
7
+ return false;
8
+ if (!isAuthenticated) {
9
+ const authed = await checkAuthSession();
10
+ if (!authed)
11
+ return false;
12
+ }
13
+ workspaceForceFullAiProfilePushRef.current = true;
14
+ if (workspaceSyncPhase === 'provision-local') {
15
+ const signature = buildWorkspaceSyncSignature();
16
+ if (!signature)
17
+ return false;
18
+ return pushWorkspaceChangesToCloud(signature, { forceAllAiProfiles: true });
19
+ }
20
+ const pulled = await pullWorkspaceChangesFromCloud();
21
+ if (workspaceSyncPhase === 'attach-cloud')
22
+ return pulled;
23
+ const signature = buildWorkspaceSyncSignature();
24
+ if (signature) {
25
+ workspacePendingSignatureRef.current = signature;
26
+ const pushed = await pushWorkspaceChangesToCloud(signature, {
27
+ forceAllAiProfiles: workspaceForceFullAiProfilePushRef.current === true
28
+ });
29
+ return pulled || pushed;
30
+ }
31
+ return pulled;
32
+ };
33
+ const resetWorkspaceSyncCursorAndPull = async () => {
34
+ const leaseAcquired = await acquireWorkspaceSyncLease(currentWorkspaceId, 'repair');
35
+ if (!leaseAcquired) {
36
+ forceClearWorkspaceSyncLease(currentWorkspaceId);
37
+ const reclaimed = await acquireWorkspaceSyncLease(currentWorkspaceId, 'repair');
38
+ if (!reclaimed) {
39
+ reportWorkspaceWindowContention('repair');
40
+ return;
41
+ }
42
+ }
43
+ clearWorkspaceRetry();
44
+ clearWorkspaceIssue();
45
+ bootstrapPhaseStartedAtRef.current = null;
46
+ bootstrapLastProgressAtRef.current = null;
47
+ workspacePullCursorRef.current = null;
48
+ workspaceAttachBootstrapBaselineSignatureRef.current = '';
49
+ workspaceCaptureAttachBootstrapBaselineRef.current = false;
50
+ workspacePendingSignatureRef.current = '';
51
+ workspaceLastPushedSignatureRef.current = '';
52
+ workspaceLastPushedWatermarksRef.current = new Map();
53
+ workspaceLastPushedInitiativeIdsRef.current = new Set();
54
+ workspaceLastPushedInitiativeWatermarksRef.current = new Map();
55
+ workspaceLastPushedWorkstreamIdsRef.current = new Set();
56
+ workspaceLastPushedWorkstreamWatermarksRef.current = new Map();
57
+ workspaceLastPushedAiProfileIdsRef.current = new Set();
58
+ workspaceLastPushedAiProfileWatermarksRef.current = new Map();
59
+ workspaceLastPushedDocumentPathsRef.current = new Set();
60
+ workspaceLastPushedDocumentWatermarksRef.current = new Map();
61
+ workspaceLastPushedAssetPathsRef.current = new Set();
62
+ workspaceLastPushedAssetWatermarksRef.current = new Map();
63
+ workspaceLastPushedDocumentReviewSessionIdsRef.current = new Set();
64
+ workspaceLastPushedDocumentReviewSessionWatermarksRef.current = new Map();
65
+ workspaceLastPushedDocumentReviewCommentIdsRef.current = new Set();
66
+ workspaceLastPushedDocumentReviewCommentWatermarksRef.current = new Map();
67
+ workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current = new Set();
68
+ workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map();
69
+ workspaceLastPushedTaskEventWatermarksRef.current = new Map();
70
+ workspaceForceFullAiProfilePushRef.current = false;
71
+ workspaceStartupRepairRanRef.current = false;
72
+ workspaceStartupFullReconcileRanRef.current = false;
73
+ workspaceFullReconcileInFlightRef.current = false;
74
+ savePersistedSyncWatermarks(currentWorkspaceId, {
75
+ taskWatermarks: new Map(),
76
+ initiativeWatermarks: new Map(),
77
+ workstreamWatermarks: new Map(),
78
+ documentWatermarks: new Map(),
79
+ assetWatermarks: new Map(),
80
+ documentReviewSessionWatermarks: new Map(),
81
+ documentReviewCommentWatermarks: new Map(),
82
+ annotatedAttachmentSessionWatermarks: new Map(),
83
+ taskEventWatermarks: new Map(),
84
+ taxonomyStateFingerprint
85
+ });
86
+ try {
87
+ if (!workspaceCloudSyncEnabled)
88
+ return;
89
+ workspaceRepairModeRef.current = true;
90
+ const repairPhase = resolveRepairPhase({
91
+ workspaceSyncPhase,
92
+ lastBootstrapPhase: lastBootstrapPhaseRef.current,
93
+ workspaceLastPullAt
94
+ });
95
+ await persistWorkspaceSyncPatch({
96
+ phase: repairPhase,
97
+ pullCursor: null,
98
+ lastErrorMessage: null
99
+ });
100
+ if (repairPhase === 'provision-local') {
101
+ const signature = buildWorkspaceSyncSignature();
102
+ if (!signature)
103
+ return;
104
+ await pushWorkspaceChangesToCloud(signature, { forceAllAiProfiles: true, repairMode: true });
105
+ return;
106
+ }
107
+ await pullWorkspaceChangesFromCloud({ repairMode: true });
108
+ }
109
+ finally {
110
+ workspaceRepairModeRef.current = false;
111
+ releaseWorkspaceSyncLease(currentWorkspaceId);
112
+ }
113
+ };
114
+ return {
115
+ retryWorkspaceCloudSync,
116
+ resetWorkspaceSyncCursorAndPull
117
+ };
118
+ }