@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
@@ -1,5 +1,6 @@
1
1
  import { serializeTaskForSync } from './taskSyncPayload.js';
2
2
  export async function buildWorkspacePullFeed(input) {
3
+ const buildStartedAtMs = Date.now();
3
4
  const { core, workspaceId, cursor, pageSize, encodeWorkspaceSyncCursor, contentSyncState, collaborationSyncState } = input;
4
5
  const syncCursor = cursor ? { watermark: cursor.u, id: cursor.id } : null;
5
6
  const dbFetchLimit = pageSize * 2 + 10;
@@ -62,13 +63,14 @@ export async function buildWorkspacePullFeed(input) {
62
63
  profile
63
64
  }))
64
65
  .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
65
- const documentEntries = (await contentSyncState.listWorkspaceSyncDocumentsSnapshot(core.getPaths().basePath, workspaceId))
66
+ const indexStartedAtMs = Date.now();
67
+ const documentIndex = await contentSyncState.listWorkspaceSyncDocumentIndex(core.getPaths().basePath, workspaceId);
68
+ const documentEntries = documentIndex
66
69
  .map((doc) => ({
67
70
  kind: 'document',
68
71
  watermark: String(doc.updatedAt || '').trim(),
69
72
  sortId: `doc:${String(doc.path || '').trim()}`,
70
73
  path: String(doc.path || '').trim(),
71
- content: String(doc.content || ''),
72
74
  assetId: doc.assetId,
73
75
  documentId: doc.documentId,
74
76
  referenceNumber: doc.referenceNumber,
@@ -146,13 +148,21 @@ export async function buildWorkspacePullFeed(input) {
146
148
  sessionId: String(session.id || '').trim()
147
149
  }))
148
150
  .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sessionId.length > 0);
149
- const assetEntries = (await contentSyncState.listWorkspaceSyncBinaryAssetsSnapshot(core.getPaths().basePath, workspaceId))
151
+ const taskEventEntries = collaborationSyncState.listWorkspaceSyncTaskEventsSnapshot(workspaceId)
152
+ .map((event) => ({
153
+ kind: 'task-event',
154
+ watermark: String(event.createdAt || '').trim(),
155
+ sortId: `task-event:${String(event.id || '').trim()}`,
156
+ event
157
+ }))
158
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.event.id.length > 0 && entry.event.taskId.length > 0);
159
+ const assetIndex = await contentSyncState.listWorkspaceSyncBinaryAssetIndex(workspaceId);
160
+ const assetEntries = assetIndex
150
161
  .map((asset) => ({
151
162
  kind: 'asset',
152
163
  watermark: String(asset.updatedAt || '').trim(),
153
164
  sortId: `asset:${String(asset.path || '').trim()}`,
154
165
  path: String(asset.path || '').trim(),
155
- contentBase64: String(asset.contentBase64 || ''),
156
166
  assetId: asset.assetId,
157
167
  kindValue: asset.kind,
158
168
  mimeType: asset.mimeType,
@@ -163,7 +173,7 @@ export async function buildWorkspacePullFeed(input) {
163
173
  originalFilename: asset.originalFilename,
164
174
  linkRole: asset.linkRole
165
175
  }))
166
- .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0 && entry.contentBase64.length > 0);
176
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0);
167
177
  const assetDeleteEntries = contentSyncState.listWorkspaceSyncBinaryAssetDeletesSnapshot(workspaceId)
168
178
  .map((asset) => ({
169
179
  kind: 'asset-delete',
@@ -188,6 +198,7 @@ export async function buildWorkspacePullFeed(input) {
188
198
  ...documentReviewCommentDeleteEntries,
189
199
  ...annotatedAttachmentSessionEntries,
190
200
  ...annotatedAttachmentSessionDeleteEntries,
201
+ ...taskEventEntries,
191
202
  ...assetEntries,
192
203
  ...assetDeleteEntries
193
204
  ];
@@ -206,11 +217,6 @@ export async function buildWorkspacePullFeed(input) {
206
217
  return String(a.sortId || '').localeCompare(String(b.sortId || ''));
207
218
  });
208
219
  const page = afterCursor.slice(0, pageSize);
209
- const hasMore = afterCursor.length > pageSize;
210
- const last = page.length > 0 ? page[page.length - 1] : null;
211
- const nextCursor = encodeWorkspaceSyncCursor(last
212
- ? { u: last.watermark, id: String(last.sortId || '') }
213
- : cursor);
214
220
  const workspaceMembers = core.listWorkspaceUsers(workspaceId).map((member) => ({
215
221
  userId: member.userId,
216
222
  workspaceId,
@@ -225,77 +231,126 @@ export async function buildWorkspacePullFeed(input) {
225
231
  updatedAt: member.updatedAt || null,
226
232
  emailVerifiedAt: member.emailVerifiedAt || null
227
233
  }));
228
- const changes = page.map((entry) => (entry.kind === 'delete'
229
- ? { op: 'delete', taskId: entry.taskId, deletedAt: entry.watermark, watermark: entry.watermark }
234
+ const indexMs = Date.now() - indexStartedAtMs;
235
+ const hydratedDocuments = new Map();
236
+ const hydratedAssets = new Map();
237
+ const hydrationStartedAtMs = Date.now();
238
+ await Promise.all(page.map(async (entry) => {
239
+ if (entry.kind === 'document') {
240
+ const payload = await contentSyncState.loadWorkspaceSyncDocumentPayload(core.getPaths().basePath, workspaceId, entry.path);
241
+ hydratedDocuments.set(entry.path, payload);
242
+ return;
243
+ }
244
+ if (entry.kind === 'asset') {
245
+ const payload = await contentSyncState.loadWorkspaceSyncBinaryAssetPayload(core.getPaths().basePath, workspaceId, entry.path);
246
+ hydratedAssets.set(entry.path, payload);
247
+ }
248
+ }));
249
+ const hydrationMs = Date.now() - hydrationStartedAtMs;
250
+ const skippedHydrationEntries = page.filter((entry) => ((entry.kind === 'document' && !hydratedDocuments.get(entry.path))
251
+ || (entry.kind === 'asset' && !hydratedAssets.get(entry.path)))).length;
252
+ const effectivePage = page;
253
+ const hasMore = afterCursor.length > page.length;
254
+ const last = page.length > 0 ? page[page.length - 1] : null;
255
+ const nextCursor = encodeWorkspaceSyncCursor(last
256
+ ? { u: last.watermark, id: String(last.sortId || '') }
257
+ : cursor);
258
+ const encodeStartedAtMs = Date.now();
259
+ const changes = effectivePage.flatMap((entry) => (entry.kind === 'delete'
260
+ ? [{ op: 'delete', taskId: entry.taskId, watermark: entry.watermark }]
230
261
  : entry.kind === 'taxonomy'
231
- ? {
232
- op: 'taxonomy-upsert',
233
- taxonomies: entry.taxonomies,
234
- ...(entry.taxonomyState ? { taxonomyState: entry.taxonomyState } : {}),
235
- updatedAt: entry.watermark,
236
- watermark: entry.watermark
237
- }
262
+ ? [{
263
+ op: 'taxonomy-upsert',
264
+ taxonomies: entry.taxonomies,
265
+ ...(entry.taxonomyState ? { taxonomyState: entry.taxonomyState } : {}),
266
+ updatedAt: entry.watermark,
267
+ watermark: entry.watermark
268
+ }]
238
269
  : entry.kind === 'initiative'
239
- ? { op: 'initiative-upsert', initiative: entry.initiative, updatedAt: entry.watermark, watermark: entry.watermark }
270
+ ? [{ op: 'initiative-upsert', initiative: entry.initiative, watermark: entry.watermark }]
240
271
  : entry.kind === 'workstream'
241
- ? { op: 'workstream-upsert', workstream: entry.workstream, updatedAt: entry.watermark, watermark: entry.watermark }
272
+ ? [{ op: 'workstream-upsert', workstream: entry.workstream, watermark: entry.watermark }]
242
273
  : entry.kind === 'ai-profile'
243
- ? { op: 'ai-profile-upsert', profile: entry.profile, updatedAt: entry.watermark, watermark: entry.watermark }
274
+ ? [{ op: 'ai-profile-upsert', profile: entry.profile, watermark: entry.watermark }]
244
275
  : entry.kind === 'document-review-session'
245
- ? { op: 'document-review-session-upsert', session: entry.session, updatedAt: entry.watermark, watermark: entry.watermark }
276
+ ? [{ op: 'document-review-session-upsert', session: entry.session, watermark: entry.watermark }]
246
277
  : entry.kind === 'document-review-session-delete'
247
- ? { op: 'document-review-session-delete', sessionId: entry.sessionId, deletedAt: entry.watermark, watermark: entry.watermark }
278
+ ? [{ op: 'document-review-session-delete', sessionId: entry.sessionId, deletedAt: entry.watermark, watermark: entry.watermark }]
248
279
  : entry.kind === 'document-review-comment'
249
- ? { op: 'document-review-comment-upsert', comment: entry.comment, updatedAt: entry.watermark, watermark: entry.watermark }
280
+ ? [{ op: 'document-review-comment-upsert', comment: entry.comment, watermark: entry.watermark }]
250
281
  : entry.kind === 'document-review-comment-delete'
251
- ? { op: 'document-review-comment-delete', commentId: entry.commentId, deletedAt: entry.watermark, watermark: entry.watermark }
282
+ ? [{ op: 'document-review-comment-delete', commentId: entry.commentId, deletedAt: entry.watermark, watermark: entry.watermark }]
252
283
  : entry.kind === 'annotated-attachment-session'
253
- ? { op: 'annotated-attachment-session-upsert', session: entry.session, updatedAt: entry.watermark, watermark: entry.watermark }
284
+ ? [{ op: 'annotated-attachment-session-upsert', session: entry.session, watermark: entry.watermark }]
254
285
  : entry.kind === 'annotated-attachment-session-delete'
255
- ? { op: 'annotated-attachment-session-delete', sessionId: entry.sessionId, deletedAt: entry.watermark, watermark: entry.watermark }
256
- : entry.kind === 'document'
257
- ? {
258
- op: 'document-upsert',
259
- path: entry.path,
260
- updatedAt: entry.watermark,
261
- content: entry.content,
262
- assetId: entry.assetId,
263
- documentId: entry.documentId,
264
- referenceNumber: entry.referenceNumber,
265
- version: entry.version,
266
- taskId: entry.taskId,
267
- logicalName: entry.logicalName,
268
- caption: entry.caption,
269
- originalFilename: entry.originalFilename,
270
- linkRole: entry.linkRole,
271
- watermark: entry.watermark
272
- }
273
- : entry.kind === 'document-delete'
274
- ? { op: 'document-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }
275
- : entry.kind === 'asset'
276
- ? {
277
- op: 'asset-upsert',
278
- path: entry.path,
279
- updatedAt: entry.watermark,
280
- contentBase64: entry.contentBase64,
281
- assetId: entry.assetId,
282
- kind: entry.kindValue,
283
- mimeType: entry.mimeType,
284
- referenceNumber: entry.referenceNumber,
285
- taskId: entry.taskId,
286
- logicalName: entry.logicalName,
287
- caption: entry.caption,
288
- originalFilename: entry.originalFilename,
289
- linkRole: entry.linkRole,
290
- watermark: entry.watermark
291
- }
292
- : entry.kind === 'asset-delete'
293
- ? { op: 'asset-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }
294
- : { op: 'upsert', task: entry.task, archived: Boolean(entry.archived), watermark: entry.watermark }));
286
+ ? [{ op: 'annotated-attachment-session-delete', sessionId: entry.sessionId, deletedAt: entry.watermark, watermark: entry.watermark }]
287
+ : entry.kind === 'task-event'
288
+ ? [{ op: 'task-event-upsert', event: entry.event, watermark: entry.watermark }]
289
+ : entry.kind === 'document'
290
+ ? (() => {
291
+ const payload = hydratedDocuments.get(entry.path);
292
+ if (!payload)
293
+ return [];
294
+ return [{
295
+ op: 'document-upsert',
296
+ path: entry.path,
297
+ updatedAt: entry.watermark,
298
+ content: payload.content,
299
+ assetId: payload.assetId,
300
+ documentId: payload.documentId,
301
+ referenceNumber: payload.referenceNumber,
302
+ version: payload.version,
303
+ taskId: payload.taskId,
304
+ logicalName: payload.logicalName,
305
+ caption: payload.caption,
306
+ originalFilename: payload.originalFilename,
307
+ linkRole: payload.linkRole === 'reference' ? 'reference' : 'attachment',
308
+ watermark: entry.watermark
309
+ }];
310
+ })()
311
+ : entry.kind === 'document-delete'
312
+ ? [{ op: 'document-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }]
313
+ : entry.kind === 'asset'
314
+ ? (() => {
315
+ const payload = hydratedAssets.get(entry.path);
316
+ if (!payload)
317
+ return [];
318
+ return [{
319
+ op: 'asset-upsert',
320
+ path: entry.path,
321
+ updatedAt: entry.watermark,
322
+ contentBase64: payload.contentBase64,
323
+ assetId: payload.assetId,
324
+ kind: payload.kind,
325
+ mimeType: payload.mimeType,
326
+ referenceNumber: payload.referenceNumber,
327
+ taskId: payload.taskId,
328
+ logicalName: payload.logicalName,
329
+ caption: payload.caption,
330
+ originalFilename: payload.originalFilename,
331
+ linkRole: payload.linkRole,
332
+ watermark: entry.watermark
333
+ }];
334
+ })()
335
+ : entry.kind === 'asset-delete'
336
+ ? [{ op: 'asset-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }]
337
+ : [{ op: 'upsert', task: entry.task, archived: Boolean(entry.archived), watermark: entry.watermark }]));
338
+ const encodeMs = Date.now() - encodeStartedAtMs;
295
339
  return {
296
340
  workspaceMembers,
297
341
  changes,
298
342
  nextCursor,
299
- hasMore
343
+ hasMore,
344
+ diagnostics: {
345
+ totalEntries: all.length,
346
+ pageEntries: page.length,
347
+ skippedHydrationEntries,
348
+ documentIndexCount: documentIndex.length,
349
+ assetIndexCount: assetIndex.length,
350
+ indexMs,
351
+ hydrationMs,
352
+ encodeMs,
353
+ buildMs: Date.now() - buildStartedAtMs
354
+ }
300
355
  };
301
356
  }
@@ -14,9 +14,13 @@ function createCoreMock() {
14
14
  }
15
15
  function createContentSyncStateMock() {
16
16
  return {
17
+ listWorkspaceSyncDocumentIndex: async () => [],
17
18
  listWorkspaceSyncDocumentsSnapshot: async () => [],
19
+ loadWorkspaceSyncDocumentPayload: async () => null,
18
20
  listWorkspaceSyncDocumentDeletesSnapshot: () => [],
21
+ listWorkspaceSyncBinaryAssetIndex: async () => [],
19
22
  listWorkspaceSyncBinaryAssetsSnapshot: async () => [],
23
+ loadWorkspaceSyncBinaryAssetPayload: async () => null,
20
24
  listWorkspaceSyncBinaryAssetDeletesSnapshot: () => [],
21
25
  };
22
26
  }
@@ -25,6 +29,7 @@ function createCollaborationSyncStateMock() {
25
29
  listWorkspaceSyncDocumentReviewSessionsSnapshot: () => [],
26
30
  listWorkspaceSyncDocumentReviewCommentsSnapshot: () => [],
27
31
  listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot: () => [],
32
+ listWorkspaceSyncTaskEventsSnapshot: () => [],
28
33
  };
29
34
  }
30
35
  function encodeCursor(cursor) {
@@ -163,4 +168,179 @@ describe('workspacePullFeed', () => {
163
168
  })
164
169
  ]);
165
170
  });
171
+ it('hydrates document and asset payloads only for entries included in the current page', async () => {
172
+ const core = createCoreMock();
173
+ const content = createContentSyncStateMock();
174
+ const loadedDocs = [];
175
+ const loadedAssets = [];
176
+ content.listWorkspaceSyncDocumentIndex = async () => [
177
+ {
178
+ path: 'docs/a.md',
179
+ updatedAt: '2026-04-10T10:00:00.000Z',
180
+ assetId: 'asset-doc-a',
181
+ },
182
+ {
183
+ path: 'docs/z.md',
184
+ updatedAt: '2026-04-12T10:00:00.000Z',
185
+ assetId: 'asset-doc-z',
186
+ }
187
+ ];
188
+ content.loadWorkspaceSyncDocumentPayload = async (_basePath, _workspaceId, relativePath) => {
189
+ loadedDocs.push(relativePath);
190
+ return {
191
+ path: relativePath,
192
+ updatedAt: relativePath === 'docs/a.md' ? '2026-04-10T10:00:00.000Z' : '2026-04-12T10:00:00.000Z',
193
+ content: `content:${relativePath}`
194
+ };
195
+ };
196
+ content.listWorkspaceSyncBinaryAssetIndex = async () => [
197
+ {
198
+ path: 'workspaces/ws/assets/images/a.png',
199
+ updatedAt: '2026-04-11T10:00:00.000Z',
200
+ assetId: 'asset-image-a',
201
+ kind: 'image',
202
+ mimeType: 'image/png'
203
+ }
204
+ ];
205
+ content.loadWorkspaceSyncBinaryAssetPayload = async (_basePath, _workspaceId, assetPath) => {
206
+ loadedAssets.push(assetPath);
207
+ return {
208
+ path: assetPath,
209
+ updatedAt: '2026-04-11T10:00:00.000Z',
210
+ contentBase64: 'Zm9v',
211
+ kind: 'image',
212
+ mimeType: 'image/png'
213
+ };
214
+ };
215
+ const result = await buildWorkspacePullFeed({
216
+ core,
217
+ workspaceId: 'workspace-3',
218
+ cursor: null,
219
+ pageSize: 2,
220
+ encodeWorkspaceSyncCursor: encodeCursor,
221
+ contentSyncState: content,
222
+ collaborationSyncState: createCollaborationSyncStateMock(),
223
+ });
224
+ expect(result.changes).toHaveLength(2);
225
+ expect(result.changes[0]).toEqual(expect.objectContaining({
226
+ op: 'document-upsert',
227
+ path: 'docs/a.md',
228
+ content: 'content:docs/a.md'
229
+ }));
230
+ expect(result.changes[1]).toEqual(expect.objectContaining({
231
+ op: 'asset-upsert',
232
+ path: 'workspaces/ws/assets/images/a.png',
233
+ contentBase64: 'Zm9v'
234
+ }));
235
+ expect(loadedDocs).toEqual(['docs/a.md']);
236
+ expect(loadedAssets).toEqual(['workspaces/ws/assets/images/a.png']);
237
+ });
238
+ it('skips hydration misses without blocking later feed progress', async () => {
239
+ const core = createCoreMock();
240
+ const content = createContentSyncStateMock();
241
+ core.listTasksForSync = () => [
242
+ {
243
+ archived: false,
244
+ task: {
245
+ id: 'task-after-miss',
246
+ title: 'Task after miss',
247
+ status: 'task',
248
+ category: 'general',
249
+ type: 'task',
250
+ createdAt: '2026-04-12T10:00:00.000Z',
251
+ }
252
+ }
253
+ ];
254
+ content.listWorkspaceSyncDocumentIndex = async () => [
255
+ {
256
+ path: 'docs/a.md',
257
+ updatedAt: '2026-04-10T10:00:00.000Z',
258
+ assetId: 'asset-doc-a',
259
+ },
260
+ {
261
+ path: 'docs/b.md',
262
+ updatedAt: '2026-04-11T10:00:00.000Z',
263
+ assetId: 'asset-doc-b',
264
+ }
265
+ ];
266
+ content.loadWorkspaceSyncDocumentPayload = async (_basePath, _workspaceId, relativePath) => (relativePath === 'docs/a.md'
267
+ ? {
268
+ path: relativePath,
269
+ updatedAt: '2026-04-10T10:00:00.000Z',
270
+ content: 'content:docs/a.md'
271
+ }
272
+ : null);
273
+ const result = await buildWorkspacePullFeed({
274
+ core,
275
+ workspaceId: 'workspace-4',
276
+ cursor: null,
277
+ pageSize: 3,
278
+ encodeWorkspaceSyncCursor: encodeCursor,
279
+ contentSyncState: content,
280
+ collaborationSyncState: createCollaborationSyncStateMock(),
281
+ });
282
+ expect(result.changes).toEqual([
283
+ expect.objectContaining({
284
+ op: 'document-upsert',
285
+ path: 'docs/a.md',
286
+ content: 'content:docs/a.md'
287
+ }),
288
+ expect.objectContaining({
289
+ op: 'upsert',
290
+ task: expect.objectContaining({
291
+ id: 'task-after-miss'
292
+ })
293
+ })
294
+ ]);
295
+ expect(result.nextCursor).toBe('2026-04-12T10:00:00.000Z|task-after-miss');
296
+ expect(result.hasMore).toBe(false);
297
+ expect(result.diagnostics.skippedHydrationEntries).toBe(1);
298
+ });
299
+ it('includes task-event upserts in the pull feed ordered by event watermark', async () => {
300
+ const core = createCoreMock();
301
+ const collaboration = createCollaborationSyncStateMock();
302
+ collaboration.listWorkspaceSyncTaskEventsSnapshot = () => [
303
+ {
304
+ id: 'task-event-2',
305
+ taskId: 'task-2',
306
+ workspaceId: 'workspace-5',
307
+ action: 'task-archived',
308
+ actor: 'ai-profile-2',
309
+ actorType: 'ai',
310
+ createdAt: '2026-04-12T10:06:00.000Z',
311
+ details: {}
312
+ },
313
+ {
314
+ id: 'task-event-1',
315
+ taskId: 'task-1',
316
+ workspaceId: 'workspace-5',
317
+ action: 'task-status-changed',
318
+ actor: 'ai-profile-1',
319
+ actorType: 'ai',
320
+ createdAt: '2026-04-12T10:05:00.000Z',
321
+ details: {}
322
+ }
323
+ ];
324
+ const result = await buildWorkspacePullFeed({
325
+ core,
326
+ workspaceId: 'workspace-5',
327
+ cursor: null,
328
+ pageSize: 5,
329
+ encodeWorkspaceSyncCursor: encodeCursor,
330
+ contentSyncState: createContentSyncStateMock(),
331
+ collaborationSyncState: collaboration,
332
+ });
333
+ expect(result.changes).toEqual([
334
+ expect.objectContaining({
335
+ op: 'task-event-upsert',
336
+ watermark: '2026-04-12T10:05:00.000Z',
337
+ event: expect.objectContaining({ id: 'task-event-1' })
338
+ }),
339
+ expect.objectContaining({
340
+ op: 'task-event-upsert',
341
+ watermark: '2026-04-12T10:06:00.000Z',
342
+ event: expect.objectContaining({ id: 'task-event-2' })
343
+ })
344
+ ]);
345
+ });
166
346
  });
@@ -1,10 +1,20 @@
1
- import type { InitiativeItem, TaskItem, WorkstreamItem } from '../types';
1
+ import type { InitiativeItem, TaskEvent, TaskItem, WorkstreamItem } from '../types';
2
2
  import type { DocumentReviewCommentRecord, DocumentReviewSessionStatus } from '../documentReviews/types.js';
3
3
  import type { AnnotatedAttachmentAnnotation } from '../annotatedAttachments/types.js';
4
4
  import type { WorkspaceTaxonomyState } from '../shared/taxonomyState.js';
5
5
  import { type AiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
6
6
  import { type TaskSyncPayload } from './taskSyncPayload.js';
7
7
  export type WorkspaceSyncTaskSnapshot = TaskSyncPayload;
8
+ export interface WorkspaceSyncTaskEventSnapshot {
9
+ id: string;
10
+ taskId: string;
11
+ workspaceId: string;
12
+ action: string;
13
+ actor: string;
14
+ actorType: 'system' | 'human' | 'ai';
15
+ details?: TaskEvent['details'];
16
+ createdAt: string;
17
+ }
8
18
  export interface WorkspaceSyncAiProfileSnapshot {
9
19
  id: string;
10
20
  workspaceId: string;
@@ -13,6 +23,10 @@ export interface WorkspaceSyncAiProfileSnapshot {
13
23
  username: string;
14
24
  icon: string;
15
25
  color: string;
26
+ description?: string | null;
27
+ role?: string | null;
28
+ provider?: string | null;
29
+ model?: string | null;
16
30
  surfaceType?: AiProfileSurfaceType | null;
17
31
  providerMetadata?: Record<string, unknown> | null;
18
32
  createdAt: string;
@@ -177,6 +191,9 @@ export type WorkspaceSyncChange = {
177
191
  op: 'annotated-attachment-session-delete';
178
192
  sessionId: string;
179
193
  deletedAt?: string;
194
+ } | {
195
+ op: 'task-event-upsert';
196
+ event: WorkspaceSyncTaskEventSnapshot;
180
197
  };
181
198
  export interface WorkspaceSyncPayload {
182
199
  changes: WorkspaceSyncChange[];
@@ -203,6 +220,7 @@ export interface WorkspaceSyncPayload {
203
220
  currentAnnotatedAttachmentSessionIds: Set<string>;
204
221
  deleteAnnotatedAttachmentSessionIds: Set<string>;
205
222
  currentAnnotatedAttachmentSessionWatermarks: Map<string, string>;
223
+ currentTaskEventWatermarks: Map<string, string>;
206
224
  /** Watermarks for every task upsert included in this payload (id → updatedAt|createdAt). */
207
225
  pushedWatermarks: Map<string, string>;
208
226
  }
@@ -250,6 +268,7 @@ export declare function buildWorkspaceSyncPayloadModel(input: {
250
268
  documentReviewSessions?: WorkspaceSyncDocumentReviewSessionSnapshot[];
251
269
  documentReviewComments?: WorkspaceSyncDocumentReviewCommentSnapshot[];
252
270
  annotatedAttachmentSessions?: WorkspaceSyncAnnotatedAttachmentSessionSnapshot[];
271
+ taskEvents?: WorkspaceSyncTaskEventSnapshot[];
253
272
  lastPushedInitiativeIds?: Set<string>;
254
273
  lastPushedInitiativeWatermarks?: Map<string, string>;
255
274
  lastPushedWorkstreamIds?: Set<string>;
@@ -266,6 +285,7 @@ export declare function buildWorkspaceSyncPayloadModel(input: {
266
285
  lastPushedDocumentReviewCommentWatermarks?: Map<string, string>;
267
286
  lastPushedAnnotatedAttachmentSessionIds?: Set<string>;
268
287
  lastPushedAnnotatedAttachmentSessionWatermarks?: Map<string, string>;
288
+ lastPushedTaskEventWatermarks?: Map<string, string>;
269
289
  /**
270
290
  * Watermark (updatedAt|createdAt) of each task as of its last successful push.
271
291
  * When provided, only tasks whose watermark differs from this map are included
@@ -289,4 +309,5 @@ export declare function buildWorkspaceSyncSignatureModel(input: {
289
309
  documentReviewSessions?: WorkspaceSyncDocumentReviewSessionSnapshot[];
290
310
  documentReviewComments?: WorkspaceSyncDocumentReviewCommentSnapshot[];
291
311
  annotatedAttachmentSessions?: WorkspaceSyncAnnotatedAttachmentSessionSnapshot[];
312
+ taskEvents?: WorkspaceSyncTaskEventSnapshot[];
292
313
  }): string;
@@ -3,6 +3,16 @@ import { serializeTaskForSync } from './taskSyncPayload.js';
3
3
  export function toWorkspaceSyncTaskSnapshot(task) {
4
4
  return serializeTaskForSync(task);
5
5
  }
6
+ function sortTaskEventsForSync(taskEvents) {
7
+ return [...taskEvents].sort((left, right) => {
8
+ const leftCreatedAt = String(left?.createdAt || '').trim();
9
+ const rightCreatedAt = String(right?.createdAt || '').trim();
10
+ const watermarkDelta = leftCreatedAt.localeCompare(rightCreatedAt);
11
+ if (watermarkDelta !== 0)
12
+ return watermarkDelta;
13
+ return String(left?.id || '').trim().localeCompare(String(right?.id || '').trim());
14
+ });
15
+ }
6
16
  function sortTasksForSync(tasks) {
7
17
  return [...tasks].sort((left, right) => {
8
18
  const leftId = String(left?.id || '').trim();
@@ -175,6 +185,10 @@ export function buildWorkspaceSyncPayloadModel(input) {
175
185
  username: String(profile.username || '').trim(),
176
186
  icon: String(profile.icon || 'Bot').trim() || 'Bot',
177
187
  color: String(profile.color || '#8b5cf6').trim() || '#8b5cf6',
188
+ description: typeof profile.description === 'string' ? profile.description.trim() || null : null,
189
+ role: typeof profile.role === 'string' ? profile.role.trim() || null : null,
190
+ provider: typeof profile.provider === 'string' ? profile.provider.trim() || null : null,
191
+ model: typeof profile.model === 'string' ? profile.model.trim() || null : null,
178
192
  surfaceType: parseAiProfileSurfaceType(profile.surfaceType),
179
193
  providerMetadata: profile.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
180
194
  ? profile.providerMetadata
@@ -391,6 +405,40 @@ export function buildWorkspaceSyncPayloadModel(input) {
391
405
  return true;
392
406
  return previousAnnotatedAttachmentSessionWatermarks.get(change.session.id) !== change.session.updatedAt;
393
407
  });
408
+ const taskEvents = sortTaskEventsForSync(Array.isArray(input.taskEvents) ? input.taskEvents : []);
409
+ const currentTaskEventWatermarks = new Map(taskEvents
410
+ .map((event) => [String(event?.id || '').trim(), String(event?.createdAt || '').trim()])
411
+ .filter(([id]) => id.length > 0));
412
+ const previousTaskEventWatermarks = input.lastPushedTaskEventWatermarks;
413
+ const shouldSendAllTaskEvents = !previousTaskEventWatermarks || previousTaskEventWatermarks.size === 0;
414
+ const taskEventUpsertChanges = taskEvents
415
+ .map((event) => ({
416
+ op: 'task-event-upsert',
417
+ event: {
418
+ id: String(event.id || '').trim(),
419
+ taskId: String(event.taskId || '').trim(),
420
+ workspaceId: String(event.workspaceId || '').trim(),
421
+ action: String(event.action || '').trim(),
422
+ actor: String(event.actor || 'system').trim() || 'system',
423
+ actorType: event.actorType === 'ai' || event.actorType === 'human' || event.actorType === 'system'
424
+ ? event.actorType
425
+ : 'system',
426
+ details: event.details && typeof event.details === 'object' && !Array.isArray(event.details)
427
+ ? event.details
428
+ : {},
429
+ createdAt: String(event.createdAt || '').trim(),
430
+ }
431
+ }))
432
+ .filter((change) => (change.event.id.length > 0
433
+ && change.event.taskId.length > 0
434
+ && change.event.workspaceId.length > 0
435
+ && change.event.action.length > 0
436
+ && Number.isFinite(Date.parse(change.event.createdAt))))
437
+ .filter((change) => {
438
+ if (shouldSendAllTaskEvents)
439
+ return true;
440
+ return previousTaskEventWatermarks.get(change.event.id) !== change.event.createdAt;
441
+ });
394
442
  const changes = [
395
443
  ...initiativeUpsertChanges,
396
444
  ...workstreamUpsertChanges,
@@ -438,6 +486,7 @@ export function buildWorkspaceSyncPayloadModel(input) {
438
486
  sessionId,
439
487
  deletedAt: payloadBuiltAt
440
488
  })),
489
+ ...taskEventUpsertChanges,
441
490
  ...activeChanges,
442
491
  ...archivedChanges,
443
492
  ...Array.from(deleteTaskIds).map((taskId) => ({ op: 'delete', taskId }))
@@ -467,6 +516,7 @@ export function buildWorkspaceSyncPayloadModel(input) {
467
516
  currentAnnotatedAttachmentSessionIds,
468
517
  deleteAnnotatedAttachmentSessionIds,
469
518
  currentAnnotatedAttachmentSessionWatermarks,
519
+ currentTaskEventWatermarks,
470
520
  pushedWatermarks
471
521
  };
472
522
  }
@@ -516,6 +566,21 @@ export function buildWorkspaceSyncSignatureModel(input) {
516
566
  .filter((profile) => profile.id.length > 0)
517
567
  .sort((a, b) => a.id.localeCompare(b.id))
518
568
  : [],
569
+ taskEvents: Array.isArray(input.taskEvents)
570
+ ? input.taskEvents
571
+ .map((event) => ({
572
+ id: String(event.id || '').trim(),
573
+ taskId: String(event.taskId || '').trim(),
574
+ action: String(event.action || '').trim(),
575
+ actor: String(event.actor || '').trim(),
576
+ actorType: event.actorType === 'ai' || event.actorType === 'human' || event.actorType === 'system'
577
+ ? event.actorType
578
+ : 'system',
579
+ createdAt: String(event.createdAt || '').trim()
580
+ }))
581
+ .filter((event) => event.id.length > 0 && event.taskId.length > 0)
582
+ .sort((a, b) => a.id.localeCompare(b.id))
583
+ : [],
519
584
  documents: Array.isArray(input.documents)
520
585
  ? input.documents
521
586
  .map((doc) => ({