@taskforcehq/taskforce 0.3.303 → 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 (165) hide show
  1. package/dist/Taskforce.module.css +102 -5
  2. package/dist/TaskforceCore.js +514 -212
  3. package/dist/TaskforceCore.test.js +1172 -133
  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/TaskStatusActions.d.ts +6 -1
  21. package/dist/components/task/TaskStatusActions.js +8 -3
  22. package/dist/components/task/TaskStatusActions.test.js +9 -0
  23. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  24. package/dist/components/task/taskKanbanDropRules.js +187 -0
  25. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  26. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  27. package/dist/components/ui/Modal.d.ts +1 -1
  28. package/dist/components/ui/Modal.js +1 -0
  29. package/dist/components/views/AppShellHeader.js +5 -1
  30. package/dist/components/views/AppShellHeader.test.js +4 -0
  31. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  32. package/dist/components/views/PlanComparisonPage.js +4 -2
  33. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  34. package/dist/components/views/PlansPage.d.ts +5 -1
  35. package/dist/components/views/PlansPage.js +39 -12
  36. package/dist/components/views/PlansPage.test.js +59 -2
  37. package/dist/components/views/StandaloneLayout.js +113 -38
  38. package/dist/components/views/WidgetView.js +11 -2
  39. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  40. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  41. package/dist/core/PlanEntitlementService.d.ts +3 -1
  42. package/dist/core/PlanEntitlementService.js +61 -11
  43. package/dist/core/SignupMonetizationSettings.test.js +37 -2
  44. package/dist/core/TaskActivityService.d.ts +4 -1
  45. package/dist/core/TaskActivityService.js +45 -0
  46. package/dist/core/TaskAuditTimeline.test.js +52 -1
  47. package/dist/core/Taskforce.d.ts +15 -11
  48. package/dist/core/Taskforce.js +43 -2
  49. package/dist/core/WorkspacePlanMode.test.js +65 -0
  50. package/dist/documentReviews/service.d.ts +1 -1
  51. package/dist/documentReviews/types.d.ts +1 -1
  52. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  53. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  54. package/dist/hooks/sync/auth.d.ts +37 -0
  55. package/dist/hooks/sync/auth.js +61 -0
  56. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  57. package/dist/hooks/sync/bootstrap.js +52 -0
  58. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  59. package/dist/hooks/sync/orchestratorShared.js +209 -0
  60. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  61. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  62. package/dist/hooks/sync/realtime.d.ts +25 -0
  63. package/dist/hooks/sync/realtime.js +60 -0
  64. package/dist/hooks/sync/recovery.d.ts +69 -0
  65. package/dist/hooks/sync/recovery.js +118 -0
  66. package/dist/hooks/sync/transfers.d.ts +122 -0
  67. package/dist/hooks/sync/transfers.js +431 -0
  68. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  69. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  70. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  71. package/dist/hooks/ui/useResourceExport.js +5 -2
  72. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  73. package/dist/hooks/useRealtimeSync.js +2 -1
  74. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  75. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  76. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  77. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  78. package/dist/hooks/useSyncOrchestrator.js +314 -873
  79. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
  80. package/dist/hooks/useTaskMutations.js +45 -6
  81. package/dist/hooks/useTaskMutations.test.js +147 -0
  82. package/dist/hooks/useTaskforce.d.ts +8 -2
  83. package/dist/hooks/useTaskforce.js +285 -812
  84. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  85. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  86. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  87. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  88. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  89. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  90. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  91. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  92. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  93. package/dist/localization/locales/en-US.js +2 -2
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  95. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  96. package/dist/mcp/clientIntegrations.d.ts +12 -1
  97. package/dist/mcp/clientIntegrations.js +161 -6
  98. package/dist/mcp/clientIntegrations.test.js +76 -4
  99. package/dist/mcp/documentHelpers.d.ts +100 -0
  100. package/dist/mcp/documentHelpers.js +161 -0
  101. package/dist/mcp/runtime.js +174 -476
  102. package/dist/mcp/runtime.test.js +81 -8
  103. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  104. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  105. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  106. package/dist/mcp/taskAttachmentTypes.js +1 -0
  107. package/dist/plugins/vite.js +1 -1
  108. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  109. package/dist/runtime/appGateDefinitions.js +1 -1
  110. package/dist/server/index.d.ts +1 -0
  111. package/dist/server/index.js +46 -3
  112. package/dist/server/index.test.js +12 -1
  113. package/dist/server/routes/admin.js +13 -19
  114. package/dist/server/routes/auth.js +37 -2
  115. package/dist/server/routes/billing.js +18 -67
  116. package/dist/server/routes/billing.test.js +43 -0
  117. package/dist/server/routes/shared.d.ts +30 -0
  118. package/dist/server/routes/shared.js +26 -0
  119. package/dist/server/routes/sync.integration.test.js +20 -0
  120. package/dist/server/routes/sync.js +110 -237
  121. package/dist/server/routes/tasks.js +49 -7
  122. package/dist/server/routes.js +8 -19
  123. package/dist/server/routes.test.js +243 -5
  124. package/dist/shared/taskActor.d.ts +10 -0
  125. package/dist/shared/taskActor.js +1 -0
  126. package/dist/sync/collaborationSyncPayload.js +4 -0
  127. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  128. package/dist/sync/collaborationSyncState.d.ts +3 -1
  129. package/dist/sync/collaborationSyncState.js +19 -0
  130. package/dist/sync/contentSyncState.js +12 -10
  131. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  132. package/dist/sync/syncApplyHandlers.js +370 -8
  133. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  134. package/dist/sync/syncApplyHandlers.test.js +377 -0
  135. package/dist/sync/syncService.d.ts +8 -1
  136. package/dist/sync/syncService.js +36 -2
  137. package/dist/sync/syncService.test.js +48 -0
  138. package/dist/sync/workspacePullFeed.d.ts +1 -0
  139. package/dist/sync/workspacePullFeed.js +66 -59
  140. package/dist/sync/workspacePullFeed.test.js +72 -4
  141. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  142. package/dist/sync/workspaceSyncModel.js +65 -0
  143. package/dist/sync/workspaceSyncModel.test.js +119 -0
  144. package/dist/types.d.ts +2 -10
  145. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  146. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  147. package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  148. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
  149. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  150. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  151. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  152. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
  153. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  154. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  155. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
  156. package/dist/ui/index.html +4 -4
  157. package/dist/utils/taskActivity.js +4 -0
  158. package/dist/utils/taskActivity.test.js +25 -1
  159. package/package.json +3 -1
  160. package/scripts/playwright-auth.sh +4 -0
  161. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  162. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  163. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  164. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  165. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -0,0 +1,431 @@
1
+ import { applyLocalWorkspaceChanges } from '../../sync/cloudSyncApi';
2
+ import { isTransientWorkspaceSyncStatus, runWorkspacePullSyncService, runWorkspacePushSyncService } from '../../sync/syncService';
3
+ import { logSyncDebug, savePersistedSyncWatermarks, isValidWorkspaceKey } from './orchestratorShared';
4
+ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, currentWorkspaceId, workspaceSyncPhase, taxonomyStateFingerprint, isAuthenticated, checkAuthSession, resolveCloudAuthUrl, ensureCloudWorkspaceReadyForSync, ensureWorkspaceSyncReady, handleSyncAuthFailure, clearWorkspaceRetry, clearWorkspaceIssue, isWorkspaceRetryPending, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely, persistWorkspaceSyncPatchBestEffort, reportSyncEvent, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, reportWorkspaceWindowContention, refreshWorkspaceSyncMarkdownDocumentsSnapshot, refreshWorkspaceSyncAiProfilesSnapshot, refreshWorkspaceSyncAssetsSnapshot, refreshWorkspaceSyncDocumentReviewSessionsSnapshot, refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot, refreshLocalWorkspaceTaskCollections, buildWorkspaceSyncPayload, buildWorkspaceSyncSignature, seedWorkspaceSnapshotPushBaseline, shouldSuppressAttachBootstrapPush, recordSuppressedEventIds, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, workspaceLastPushedWatermarksRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, workspaceCaptureAttachBootstrapBaselineRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, bootstrapLastProgressAtRef, realtimeSuppressedEventQueueRef, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceSyncPendingChanges, setTasks, setArchivedTasks }) {
5
+ const pushWorkspaceChangesToCloud = async (signature, options) => {
6
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
7
+ return false;
8
+ if (!isValidWorkspaceKey(currentWorkspaceId))
9
+ return false;
10
+ if (workspaceRepairModeRef.current === true && options?.repairMode !== true) {
11
+ workspacePendingSignatureRef.current = signature;
12
+ return false;
13
+ }
14
+ if (workspaceSyncPhase === 'attach-cloud') {
15
+ workspacePendingSignatureRef.current = signature;
16
+ return false;
17
+ }
18
+ if (!isAuthenticated) {
19
+ const authed = await checkAuthSession();
20
+ if (!authed)
21
+ return false;
22
+ }
23
+ if (shouldSuppressAttachBootstrapPush(signature)) {
24
+ return false;
25
+ }
26
+ if (workspacePushInFlightRef.current || workspacePullInFlightRef.current) {
27
+ workspacePendingSignatureRef.current = signature;
28
+ return false;
29
+ }
30
+ const leaseAcquired = await acquireWorkspaceSyncLease(currentWorkspaceId, 'push');
31
+ if (!leaseAcquired) {
32
+ workspacePendingSignatureRef.current = signature;
33
+ reportWorkspaceWindowContention('push');
34
+ return false;
35
+ }
36
+ workspacePushInFlightRef.current = true;
37
+ setWorkspaceSyncBusy(true);
38
+ setUserGlobalSyncStatus('syncing');
39
+ setUserGlobalSyncError(null);
40
+ const pushStartedAtMs = Date.now();
41
+ const forceAllAiProfiles = options?.forceAllAiProfiles === true || workspaceForceFullAiProfilePushRef.current === true;
42
+ try {
43
+ await refreshWorkspaceSyncMarkdownDocumentsSnapshot();
44
+ await refreshWorkspaceSyncAiProfilesSnapshot();
45
+ await refreshWorkspaceSyncAssetsSnapshot();
46
+ await refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
47
+ const ready = await ensureWorkspaceSyncReady();
48
+ if (!ready)
49
+ return false;
50
+ const payload = buildWorkspaceSyncPayload({ forceAllAiProfiles });
51
+ setWorkspaceSyncPendingChanges(payload.changes.length);
52
+ const pushResult = await runWorkspacePushSyncService({
53
+ resolveCloudAuthUrl,
54
+ workspaceId: currentWorkspaceId,
55
+ payload,
56
+ ensureCloudWorkspaceReadyForSync,
57
+ repairMode: options?.repairMode === true || workspaceRepairModeRef.current === true
58
+ });
59
+ if (!pushResult.success) {
60
+ if (pushResult.status === 401) {
61
+ await handleSyncAuthFailure('push', 401);
62
+ return false;
63
+ }
64
+ let message = pushResult.error
65
+ ? `Workspace sync push failed (${pushResult.status || 0}): ${pushResult.error}`
66
+ : `Workspace sync push failed (${pushResult.status || 0})`;
67
+ if (pushResult.status === 413) {
68
+ message = 'Workspace sync failed because the payload is too large. This usually happens when one or more attachments exceed the project limit.';
69
+ }
70
+ setUserGlobalSyncStatus('error');
71
+ setUserGlobalSyncError(message);
72
+ setWorkspaceLastErrorMessage(message);
73
+ setWorkspaceLastErrorAt(new Date().toISOString());
74
+ reportSyncEvent(currentWorkspaceId, {
75
+ eventType: 'push',
76
+ status: 'error',
77
+ statusCode: pushResult.status,
78
+ errorMessage: message,
79
+ requestMs: pushResult.requestMs
80
+ });
81
+ workspacePendingSignatureRef.current = signature;
82
+ if (pushResult.transient || isTransientWorkspaceSyncStatus(pushResult.status)) {
83
+ scheduleWorkspaceSyncRetry(pushResult.retryAfterMs);
84
+ }
85
+ else if (workspaceSyncPhase === 'active') {
86
+ persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
87
+ }
88
+ return false;
89
+ }
90
+ workspaceLastPushedSignatureRef.current = signature;
91
+ workspaceLastPushedTaskIdsRef.current = pushResult.currentTaskIds;
92
+ workspaceLastPushedInitiativeIdsRef.current = new Set(pushResult.currentInitiativeIds);
93
+ workspaceLastPushedInitiativeWatermarksRef.current = new Map(pushResult.currentInitiativeWatermarks);
94
+ workspaceLastPushedWorkstreamIdsRef.current = new Set(pushResult.currentWorkstreamIds);
95
+ workspaceLastPushedWorkstreamWatermarksRef.current = new Map(pushResult.currentWorkstreamWatermarks);
96
+ workspaceLastPushedAiProfileIdsRef.current = new Set(pushResult.currentAiProfileIds);
97
+ workspaceLastPushedAiProfileWatermarksRef.current = new Map(pushResult.currentAiProfileWatermarks);
98
+ workspaceLastPushedDocumentPathsRef.current = new Set(pushResult.currentDocumentPaths);
99
+ workspaceLastPushedDocumentWatermarksRef.current = new Map(pushResult.currentDocumentWatermarks);
100
+ workspaceLastPushedAssetPathsRef.current = new Set(pushResult.currentAssetPaths);
101
+ workspaceLastPushedAssetWatermarksRef.current = new Map(pushResult.currentAssetWatermarks);
102
+ workspaceLastPushedDocumentReviewSessionIdsRef.current = new Set(pushResult.currentDocumentReviewSessionIds);
103
+ workspaceLastPushedDocumentReviewSessionWatermarksRef.current = new Map(pushResult.currentDocumentReviewSessionWatermarks);
104
+ workspaceLastPushedDocumentReviewCommentIdsRef.current = new Set(pushResult.currentDocumentReviewCommentIds);
105
+ workspaceLastPushedDocumentReviewCommentWatermarksRef.current = new Map(pushResult.currentDocumentReviewCommentWatermarks);
106
+ workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current = new Set(pushResult.currentAnnotatedAttachmentSessionIds);
107
+ workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map(pushResult.currentAnnotatedAttachmentSessionWatermarks);
108
+ workspaceLastPushedTaskEventWatermarksRef.current = new Map(pushResult.currentTaskEventWatermarks);
109
+ if (forceAllAiProfiles) {
110
+ workspaceForceFullAiProfilePushRef.current = false;
111
+ }
112
+ for (const [taskId, watermark] of pushResult.pushedWatermarks) {
113
+ workspaceLastPushedWatermarksRef.current.set(taskId, watermark);
114
+ }
115
+ if (pushResult.deleteTaskIds.size > 0) {
116
+ for (const taskId of pushResult.deleteTaskIds) {
117
+ workspaceDeletedTaskIdsRef.current.delete(taskId);
118
+ workspaceLastPushedWatermarksRef.current.delete(taskId);
119
+ }
120
+ }
121
+ savePersistedSyncWatermarks(currentWorkspaceId, {
122
+ taskWatermarks: workspaceLastPushedWatermarksRef.current,
123
+ initiativeWatermarks: workspaceLastPushedInitiativeWatermarksRef.current,
124
+ workstreamWatermarks: workspaceLastPushedWorkstreamWatermarksRef.current,
125
+ documentWatermarks: workspaceLastPushedDocumentWatermarksRef.current,
126
+ assetWatermarks: workspaceLastPushedAssetWatermarksRef.current,
127
+ documentReviewSessionWatermarks: workspaceLastPushedDocumentReviewSessionWatermarksRef.current,
128
+ documentReviewCommentWatermarks: workspaceLastPushedDocumentReviewCommentWatermarksRef.current,
129
+ annotatedAttachmentSessionWatermarks: workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current,
130
+ taskEventWatermarks: workspaceLastPushedTaskEventWatermarksRef.current,
131
+ taxonomyStateFingerprint
132
+ });
133
+ if (Array.isArray(pushResult.emittedEventIds) && pushResult.emittedEventIds.length > 0) {
134
+ recordSuppressedEventIds(pushResult.emittedEventIds, {
135
+ queueRef: realtimeSuppressedEventQueueRef,
136
+ maxQueueSize: 2048
137
+ });
138
+ }
139
+ if (Array.isArray(pushResult.appliedTaskUpserts) && pushResult.appliedTaskUpserts.length > 0) {
140
+ const applyResult = await applyLocalWorkspaceChanges(currentWorkspaceId, pushResult.appliedTaskUpserts.map((entry) => ({
141
+ op: 'upsert',
142
+ archived: entry.archived === true,
143
+ task: entry.task
144
+ })));
145
+ if (applyResult.ok) {
146
+ for (const entry of pushResult.appliedTaskUpserts) {
147
+ const taskId = String(entry?.task?.id || '').trim();
148
+ const watermark = String(entry?.task?.updatedAt || entry?.task?.createdAt || '').trim();
149
+ if (!taskId || !watermark)
150
+ continue;
151
+ workspaceLastPushedWatermarksRef.current.set(taskId, watermark);
152
+ }
153
+ try {
154
+ await refreshLocalWorkspaceTaskCollections();
155
+ }
156
+ catch {
157
+ logSyncDebug('push_local_task_refresh_failed', {
158
+ workspaceId: currentWorkspaceId,
159
+ taskCount: pushResult.appliedTaskUpserts.length
160
+ });
161
+ }
162
+ }
163
+ else {
164
+ logSyncDebug('push_local_task_apply_failed', {
165
+ workspaceId: currentWorkspaceId,
166
+ taskCount: pushResult.appliedTaskUpserts.length,
167
+ status: applyResult.status,
168
+ failures: applyResult.failures
169
+ });
170
+ }
171
+ }
172
+ clearWorkspaceRetry();
173
+ clearWorkspaceIssue();
174
+ const syncedAt = pushResult.syncedAt || new Date().toISOString();
175
+ workspaceLastPushedSignatureRef.current = buildWorkspaceSyncSignature();
176
+ setWorkspaceLastPushAt(syncedAt);
177
+ setWorkspaceSyncPendingChanges(0);
178
+ setUserGlobalSyncStatus('idle');
179
+ persistWorkspaceSyncPatchSafely({
180
+ phase: 'active',
181
+ lastPushAt: syncedAt,
182
+ lastSyncedAt: syncedAt,
183
+ lastErrorMessage: null
184
+ });
185
+ reportSyncEvent(currentWorkspaceId, {
186
+ eventType: workspaceSyncPhase === 'provision-local' ? 'bootstrap' : 'push',
187
+ status: 'success',
188
+ changeCount: pushResult.changeCount,
189
+ requestMs: pushResult.requestMs
190
+ });
191
+ return true;
192
+ }
193
+ catch (error) {
194
+ const message = 'Workspace sync push failed.';
195
+ setUserGlobalSyncStatus('error');
196
+ setUserGlobalSyncError(message);
197
+ setWorkspaceLastErrorMessage(message);
198
+ setWorkspaceLastErrorAt(new Date().toISOString());
199
+ workspacePendingSignatureRef.current = signature;
200
+ scheduleWorkspaceSyncRetry();
201
+ reportSyncEvent(currentWorkspaceId, {
202
+ eventType: 'push',
203
+ status: 'error',
204
+ errorMessage: `${message} ${String(error?.message || '').trim()}`.trim()
205
+ });
206
+ logSyncDebug('push_failed_exception', {
207
+ workspaceId: currentWorkspaceId,
208
+ elapsedMs: Date.now() - pushStartedAtMs
209
+ });
210
+ return false;
211
+ }
212
+ finally {
213
+ workspacePushInFlightRef.current = false;
214
+ setWorkspaceSyncBusy(workspacePullInFlightRef.current);
215
+ releaseWorkspaceSyncLease(currentWorkspaceId);
216
+ const pendingSignature = workspacePendingSignatureRef.current;
217
+ if (pendingSignature
218
+ && pendingSignature !== workspaceLastPushedSignatureRef.current
219
+ && !workspacePullInFlightRef.current) {
220
+ workspacePendingSignatureRef.current = '';
221
+ window.setTimeout(() => {
222
+ void pushWorkspaceChangesToCloud(pendingSignature);
223
+ }, 120);
224
+ }
225
+ }
226
+ };
227
+ const pullWorkspaceChangesFromCloud = async (options) => {
228
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
229
+ return false;
230
+ if (!isValidWorkspaceKey(currentWorkspaceId))
231
+ return false;
232
+ if (workspaceRepairModeRef.current === true && options?.repairMode !== true)
233
+ return false;
234
+ if (workspaceSyncPhase === 'provision-local')
235
+ return false;
236
+ if (isWorkspaceRetryPending())
237
+ return false;
238
+ if (!isAuthenticated) {
239
+ const authed = await checkAuthSession();
240
+ if (!authed)
241
+ return false;
242
+ }
243
+ if (workspacePullInFlightRef.current || workspacePushInFlightRef.current)
244
+ return false;
245
+ const leaseAcquired = await acquireWorkspaceSyncLease(currentWorkspaceId, 'pull');
246
+ if (!leaseAcquired) {
247
+ reportWorkspaceWindowContention('pull');
248
+ return false;
249
+ }
250
+ workspacePullInFlightRef.current = true;
251
+ setWorkspaceSyncBusy(true);
252
+ setUserGlobalSyncStatus('syncing');
253
+ setUserGlobalSyncError(null);
254
+ const pullStartedAtMs = Date.now();
255
+ try {
256
+ const ready = await ensureWorkspaceSyncReady();
257
+ if (!ready)
258
+ return false;
259
+ const bootstrap = workspaceSyncPhase === 'attach-cloud';
260
+ const cursor = bootstrap || options?.forceCursorNull === true ? null : workspacePullCursorRef.current;
261
+ const pullResult = await runWorkspacePullSyncService({
262
+ resolveCloudAuthUrl,
263
+ workspaceId: currentWorkspaceId,
264
+ cursor,
265
+ bootstrap,
266
+ ensureCloudWorkspaceReadyForSync,
267
+ maxPages: 20,
268
+ repairMode: options?.repairMode === true || workspaceRepairModeRef.current === true,
269
+ onPagePulled: () => { bootstrapLastProgressAtRef.current = Date.now(); },
270
+ onChangesApplied: () => { bootstrapLastProgressAtRef.current = Date.now(); }
271
+ });
272
+ if (!pullResult.success) {
273
+ if (pullResult.kind === 'pull_http' && pullResult.status === 401) {
274
+ await handleSyncAuthFailure('pull', 401);
275
+ return false;
276
+ }
277
+ if (pullResult.kind === 'apply_auth') {
278
+ await handleSyncAuthFailure('apply', 401);
279
+ return false;
280
+ }
281
+ let message = pullResult.kind === 'apply_payload' || pullResult.kind === 'apply_all_candidates' || pullResult.kind === 'exception'
282
+ ? String(pullResult.error || 'Workspace sync apply failed.')
283
+ : `Workspace sync pull failed (${pullResult.status || 0})`;
284
+ if (pullResult.status === 413) {
285
+ message = 'Local sync failed because the downloaded payload is too large. This usually happens when the workspace contains massive attachments.';
286
+ }
287
+ setUserGlobalSyncStatus('error');
288
+ setUserGlobalSyncError(message);
289
+ setWorkspaceLastErrorMessage(message);
290
+ setWorkspaceLastErrorAt(new Date().toISOString());
291
+ reportSyncEvent(currentWorkspaceId, {
292
+ eventType: pullResult.kind?.startsWith('apply') ? 'apply' : 'pull',
293
+ status: 'error',
294
+ statusCode: pullResult.status,
295
+ errorMessage: message,
296
+ requestMs: pullResult.pullRequestMs,
297
+ details: {
298
+ pages: pullResult.pages,
299
+ applyMs: pullResult.applyMs,
300
+ ...(pullResult.serverDiagnostics ? { serverPullDiagnostics: pullResult.serverDiagnostics } : {})
301
+ }
302
+ });
303
+ if (pullResult.transient || pullResult.hasTransientApplyFailure || isTransientWorkspaceSyncStatus(pullResult.status)) {
304
+ scheduleWorkspaceSyncRetry(pullResult.retryAfterMs);
305
+ }
306
+ else {
307
+ persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
308
+ }
309
+ return false;
310
+ }
311
+ if (Array.isArray(pullResult.emittedEventIds)) {
312
+ recordSuppressedEventIds(pullResult.emittedEventIds, { ttlMs: 60_000 });
313
+ }
314
+ if (pullResult.pulledDeleteTaskIds.size > 0) {
315
+ setTasks((prev) => prev.filter((task) => !pullResult.pulledDeleteTaskIds.has(String(task.id || ''))));
316
+ setArchivedTasks((prev) => prev.filter((task) => !pullResult.pulledDeleteTaskIds.has(String(task.id || ''))));
317
+ }
318
+ const decryptFailures = Array.isArray(pullResult.documentDecryptFailures)
319
+ ? pullResult.documentDecryptFailures.filter((value) => String(value || '').trim().length > 0)
320
+ : [];
321
+ if (decryptFailures.length > 0) {
322
+ logSyncDebug('pull_document_decrypt_failures', {
323
+ workspaceId: currentWorkspaceId,
324
+ failureCount: decryptFailures.length
325
+ });
326
+ }
327
+ workspacePullCursorRef.current = pullResult.cursor || null;
328
+ if (pullResult.appliedChanges > 0
329
+ && (pullResult.pulledActiveUpserts
330
+ || pullResult.pulledArchivedUpserts
331
+ || pullResult.pulledTaskEventUpserts)) {
332
+ try {
333
+ await refreshLocalWorkspaceTaskCollections();
334
+ }
335
+ catch {
336
+ logSyncDebug('pull_local_refresh_failed', {
337
+ workspaceId: currentWorkspaceId,
338
+ appliedChanges: pullResult.appliedChanges
339
+ });
340
+ }
341
+ }
342
+ clearWorkspaceRetry();
343
+ clearWorkspaceIssue();
344
+ const syncedAt = pullResult.syncedAt || new Date().toISOString();
345
+ setWorkspaceLastPullAt(syncedAt);
346
+ setWorkspaceSyncPendingChanges(0);
347
+ setUserGlobalSyncStatus('idle');
348
+ if (workspaceSyncPhase === 'attach-cloud') {
349
+ workspaceCaptureAttachBootstrapBaselineRef.current = true;
350
+ workspacePendingSignatureRef.current = '';
351
+ }
352
+ await persistWorkspaceSyncPatchBestEffort({
353
+ phase: 'active',
354
+ pullCursor: workspacePullCursorRef.current,
355
+ lastPullAt: syncedAt,
356
+ lastSyncedAt: syncedAt,
357
+ lastErrorMessage: null
358
+ });
359
+ reportSyncEvent(currentWorkspaceId, {
360
+ eventType: workspaceSyncPhase === 'attach-cloud' ? 'bootstrap' : 'pull',
361
+ status: 'success',
362
+ changeCount: pullResult.appliedChanges,
363
+ requestMs: pullResult.pullRequestMs,
364
+ details: {
365
+ pages: pullResult.pages,
366
+ applyMs: pullResult.applyMs,
367
+ ...(pullResult.serverDiagnostics ? { serverPullDiagnostics: pullResult.serverDiagnostics } : {})
368
+ }
369
+ });
370
+ if (workspaceSyncPhase === 'attach-cloud') {
371
+ await refreshWorkspaceSyncMarkdownDocumentsSnapshot();
372
+ await refreshWorkspaceSyncAssetsSnapshot();
373
+ await refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
374
+ await refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot();
375
+ seedWorkspaceSnapshotPushBaseline();
376
+ return true;
377
+ }
378
+ if (!options?.forceCursorNull && !bootstrap && !workspaceStartupFullReconcileRanRef.current) {
379
+ workspaceStartupFullReconcileRanRef.current = true;
380
+ window.setTimeout(() => {
381
+ if (workspaceFullReconcileInFlightRef.current)
382
+ return;
383
+ workspaceFullReconcileInFlightRef.current = true;
384
+ void pullWorkspaceChangesFromCloud({ forceCursorNull: true }).finally(() => {
385
+ workspaceFullReconcileInFlightRef.current = false;
386
+ });
387
+ }, 250);
388
+ }
389
+ const pendingSignature = workspacePendingSignatureRef.current || buildWorkspaceSyncSignature();
390
+ if (pendingSignature && pendingSignature !== workspaceLastPushedSignatureRef.current) {
391
+ workspacePendingSignatureRef.current = '';
392
+ window.setTimeout(() => {
393
+ void pushWorkspaceChangesToCloud(pendingSignature);
394
+ }, 120);
395
+ }
396
+ return true;
397
+ }
398
+ catch (error) {
399
+ const detail = String(error?.message || '').trim();
400
+ const message = detail
401
+ ? `Workspace sync pull failed. ${detail}`
402
+ : 'Workspace sync pull failed.';
403
+ setUserGlobalSyncStatus('error');
404
+ setUserGlobalSyncError(message);
405
+ setWorkspaceLastErrorMessage(message);
406
+ setWorkspaceLastErrorAt(new Date().toISOString());
407
+ scheduleWorkspaceSyncRetry();
408
+ reportSyncEvent(currentWorkspaceId, {
409
+ eventType: 'pull',
410
+ status: 'error',
411
+ errorMessage: message,
412
+ requestMs: Date.now() - pullStartedAtMs
413
+ });
414
+ logSyncDebug('pull_failed_exception', {
415
+ workspaceId: currentWorkspaceId,
416
+ elapsedMs: Date.now() - pullStartedAtMs,
417
+ error: detail || null
418
+ });
419
+ return false;
420
+ }
421
+ finally {
422
+ workspacePullInFlightRef.current = false;
423
+ setWorkspaceSyncBusy(workspacePushInFlightRef.current);
424
+ releaseWorkspaceSyncLease(currentWorkspaceId);
425
+ }
426
+ };
427
+ return {
428
+ pushWorkspaceChangesToCloud,
429
+ pullWorkspaceChangesFromCloud
430
+ };
431
+ }
@@ -0,0 +1,16 @@
1
+ import { type MutableRefObject } from 'react';
2
+ interface UseDataVersionRefreshArgs {
3
+ isOpen: boolean;
4
+ authBlocked: boolean;
5
+ authRequiredForApi: boolean;
6
+ isAuthenticated: boolean;
7
+ canCallProtectedApi: boolean;
8
+ shouldGateProtectedApiCalls: boolean;
9
+ authSessionResolvedRef: MutableRefObject<boolean>;
10
+ authRequiredForApiRef: MutableRefObject<boolean>;
11
+ isAuthenticatedRef: MutableRefObject<boolean>;
12
+ dataVersionRef: MutableRefObject<number | null>;
13
+ refreshTaskCollectionsForDataVersion: () => Promise<void>;
14
+ }
15
+ export declare function useDataVersionRefresh({ isOpen, authBlocked, authRequiredForApi, isAuthenticated, canCallProtectedApi, shouldGateProtectedApiCalls, authSessionResolvedRef, authRequiredForApiRef, isAuthenticatedRef, dataVersionRef, refreshTaskCollectionsForDataVersion }: UseDataVersionRefreshArgs): void;
16
+ export {};
@@ -0,0 +1,73 @@
1
+ import { useEffect } from 'react';
2
+ import { shouldSkipDataVersionPolling, shouldSkipDataVersionTick } from '../auth/useAuthGuardPolicy';
3
+ export function useDataVersionRefresh({ isOpen, authBlocked, authRequiredForApi, isAuthenticated, canCallProtectedApi, shouldGateProtectedApiCalls, authSessionResolvedRef, authRequiredForApiRef, isAuthenticatedRef, dataVersionRef, refreshTaskCollectionsForDataVersion }) {
4
+ useEffect(() => {
5
+ if (typeof window === 'undefined' || typeof document === 'undefined')
6
+ return;
7
+ if (shouldSkipDataVersionPolling({
8
+ isOpen,
9
+ authBlocked,
10
+ authRequiredForApi,
11
+ isAuthenticated,
12
+ canCallProtectedApi
13
+ }))
14
+ return;
15
+ let cancelled = false;
16
+ const checkDataVersion = async () => {
17
+ if (cancelled || document.visibilityState !== 'visible')
18
+ return;
19
+ if (shouldSkipDataVersionTick({
20
+ shouldGateProtectedApiCalls,
21
+ authSessionResolved: authSessionResolvedRef.current,
22
+ authRequiredForApi: authRequiredForApiRef.current,
23
+ isAuthenticated: isAuthenticatedRef.current
24
+ }))
25
+ return;
26
+ try {
27
+ const res = await fetch('/api/taskforce/data-version');
28
+ if (!res.ok)
29
+ return;
30
+ const data = await res.json();
31
+ const version = Number(data?.dataVersion);
32
+ if (!Number.isFinite(version))
33
+ return;
34
+ if (dataVersionRef.current === null) {
35
+ dataVersionRef.current = version;
36
+ return;
37
+ }
38
+ if (version !== dataVersionRef.current) {
39
+ dataVersionRef.current = version;
40
+ await refreshTaskCollectionsForDataVersion();
41
+ }
42
+ }
43
+ catch {
44
+ // best effort only
45
+ }
46
+ };
47
+ const onVisibilityChange = () => {
48
+ if (document.visibilityState === 'visible') {
49
+ void checkDataVersion();
50
+ }
51
+ };
52
+ const intervalId = window.setInterval(() => { void checkDataVersion(); }, 3000);
53
+ document.addEventListener('visibilitychange', onVisibilityChange);
54
+ void checkDataVersion();
55
+ return () => {
56
+ cancelled = true;
57
+ window.clearInterval(intervalId);
58
+ document.removeEventListener('visibilitychange', onVisibilityChange);
59
+ };
60
+ }, [
61
+ isOpen,
62
+ authBlocked,
63
+ authRequiredForApi,
64
+ isAuthenticated,
65
+ canCallProtectedApi,
66
+ shouldGateProtectedApiCalls,
67
+ authSessionResolvedRef,
68
+ authRequiredForApiRef,
69
+ isAuthenticatedRef,
70
+ dataVersionRef,
71
+ refreshTaskCollectionsForDataVersion
72
+ ]);
73
+ }
@@ -1,8 +1,10 @@
1
1
  import type { Environment } from '../../types';
2
2
  interface UseResourceExportInput {
3
3
  storagePath: string;
4
+ shouldDeferProtectedApiCalls: boolean;
5
+ shouldBlockProtectedApiCalls: boolean;
4
6
  }
5
- export declare function useResourceExport({ storagePath }: UseResourceExportInput): {
7
+ export declare function useResourceExport({ storagePath, shouldDeferProtectedApiCalls, shouldBlockProtectedApiCalls }: UseResourceExportInput): {
6
8
  exportEnvironment: string;
7
9
  setExportEnvironment: import("react").Dispatch<import("react").SetStateAction<string>>;
8
10
  availableEnvironments: Environment[];
@@ -1,11 +1,12 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
- export function useResourceExport({ storagePath }) {
2
+ export function useResourceExport({ storagePath, shouldDeferProtectedApiCalls, shouldBlockProtectedApiCalls }) {
3
3
  const [exportEnvironment, setExportEnvironment] = useState('antigravity');
4
4
  const [availableEnvironments, setAvailableEnvironments] = useState([]);
5
5
  const [exportWorkflowsPath, setExportWorkflowsPath] = useState('.agent/workflows');
6
6
  const [exportingResource, setExportingResource] = useState(null);
7
7
  const [exportResult, setExportResult] = useState(null);
8
8
  const exportResultTimerRef = useRef(null);
9
+ const shouldSkipProtectedApiCalls = shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls;
9
10
  useEffect(() => {
10
11
  const env = availableEnvironments.find((item) => item.id === exportEnvironment);
11
12
  if (env) {
@@ -23,13 +24,15 @@ export function useResourceExport({ storagePath }) {
23
24
  }, 5000);
24
25
  }, []);
25
26
  const loadAvailableEnvironments = useCallback(async () => {
27
+ if (shouldSkipProtectedApiCalls)
28
+ return;
26
29
  const envRes = await fetch('/api/taskforce/environments');
27
30
  if (!envRes.ok)
28
31
  return;
29
32
  const envData = await envRes.json().catch(() => ({}));
30
33
  if (envData.environments)
31
34
  setAvailableEnvironments(envData.environments);
32
- }, []);
35
+ }, [shouldSkipProtectedApiCalls]);
33
36
  const handleExportWorkflows = useCallback(async (environment, selected) => {
34
37
  setExportingResource('workflows');
35
38
  setExportResult(null);
@@ -2,8 +2,9 @@ import { useCallback, useState } from 'react';
2
2
  export function useWorkflowTemplates({ shouldDeferProtectedApiCalls, shouldBlockProtectedApiCalls }) {
3
3
  const [availableWorkflows, setAvailableWorkflows] = useState([]);
4
4
  const [initiativeTemplates, setInitiativeTemplates] = useState([]);
5
+ const shouldSkipProtectedApiCalls = shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls;
5
6
  const fetchWorkflows = useCallback(async () => {
6
- if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
7
+ if (shouldSkipProtectedApiCalls)
7
8
  return;
8
9
  try {
9
10
  const res = await fetch('/api/taskforce/workflow-templates');
@@ -15,8 +16,10 @@ export function useWorkflowTemplates({ shouldDeferProtectedApiCalls, shouldBlock
15
16
  catch {
16
17
  // Ignore error
17
18
  }
18
- }, [shouldDeferProtectedApiCalls, shouldBlockProtectedApiCalls]);
19
+ }, [shouldSkipProtectedApiCalls]);
19
20
  const fetchInitiativeTemplates = useCallback(async () => {
21
+ if (shouldSkipProtectedApiCalls)
22
+ return [];
20
23
  try {
21
24
  const res = await fetch('/api/taskforce/initiative-templates');
22
25
  if (!res.ok)
@@ -29,9 +32,9 @@ export function useWorkflowTemplates({ shouldDeferProtectedApiCalls, shouldBlock
29
32
  catch {
30
33
  return [];
31
34
  }
32
- }, []);
35
+ }, [shouldSkipProtectedApiCalls]);
33
36
  const fetchWorkflowTemplate = useCallback(async (name) => {
34
- if (!name)
37
+ if (!name || shouldSkipProtectedApiCalls)
35
38
  return null;
36
39
  const paths = [
37
40
  `/api/taskforce/workflow-template/${encodeURIComponent(name)}`,
@@ -51,8 +54,10 @@ export function useWorkflowTemplates({ shouldDeferProtectedApiCalls, shouldBlock
51
54
  catch {
52
55
  return null;
53
56
  }
54
- }, []);
57
+ }, [shouldSkipProtectedApiCalls]);
55
58
  const fetchWorkflowOverrideNames = useCallback(async () => {
59
+ if (shouldSkipProtectedApiCalls)
60
+ return [];
56
61
  try {
57
62
  const res = await fetch('/api/taskforce/workflow-editor/overrides');
58
63
  if (!res.ok)
@@ -63,7 +68,7 @@ export function useWorkflowTemplates({ shouldDeferProtectedApiCalls, shouldBlock
63
68
  catch {
64
69
  return [];
65
70
  }
66
- }, []);
71
+ }, [shouldSkipProtectedApiCalls]);
67
72
  const saveWorkflowTemplateDraft = useCallback(async (name, draft) => {
68
73
  try {
69
74
  const res = await fetch('/api/taskforce/workflow-editor/save', {
@@ -168,7 +168,8 @@ export function useRealtimeSync(input) {
168
168
  if (event.type === 'taskforce:update') {
169
169
  queueSignal({
170
170
  type: 'taskforce:update',
171
- workspaceId: normalizedWorkspaceId
171
+ workspaceId: normalizedWorkspaceId,
172
+ eventId: typeof event.eventId === 'string' ? event.eventId.trim() : undefined
172
173
  });
173
174
  return;
174
175
  }
@@ -10,6 +10,10 @@ describe('normalizeWorkspaceSyncAiProfileSnapshotEntry', () => {
10
10
  username: 'codex',
11
11
  icon: 'Bot',
12
12
  color: '#0ea5e9',
13
+ description: 'Terminal-first coding agent',
14
+ role: 'Implementer',
15
+ provider: 'OpenAI',
16
+ model: 'gpt-5.4',
13
17
  surface_type: 'coding_tool',
14
18
  provider_metadata: { source: 'test' },
15
19
  created_at: '2026-03-15T18:00:00.000Z',
@@ -24,6 +28,10 @@ describe('normalizeWorkspaceSyncAiProfileSnapshotEntry', () => {
24
28
  username: 'codex',
25
29
  icon: 'Bot',
26
30
  color: '#0ea5e9',
31
+ description: 'Terminal-first coding agent',
32
+ role: 'Implementer',
33
+ provider: 'OpenAI',
34
+ model: 'gpt-5.4',
27
35
  surfaceType: 'coding_tool',
28
36
  providerMetadata: { source: 'test' },
29
37
  createdAt: '2026-03-15T18:00:00.000Z',