@taskforcehq/taskforce 0.3.303 → 0.3.305

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 (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -148,6 +148,14 @@ export async function buildWorkspacePullFeed(input) {
148
148
  sessionId: String(session.id || '').trim()
149
149
  }))
150
150
  .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sessionId.length > 0);
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);
151
159
  const assetIndex = await contentSyncState.listWorkspaceSyncBinaryAssetIndex(workspaceId);
152
160
  const assetEntries = assetIndex
153
161
  .map((asset) => ({
@@ -190,6 +198,7 @@ export async function buildWorkspacePullFeed(input) {
190
198
  ...documentReviewCommentDeleteEntries,
191
199
  ...annotatedAttachmentSessionEntries,
192
200
  ...annotatedAttachmentSessionDeleteEntries,
201
+ ...taskEventEntries,
193
202
  ...assetEntries,
194
203
  ...assetDeleteEntries
195
204
  ];
@@ -238,16 +247,11 @@ export async function buildWorkspacePullFeed(input) {
238
247
  }
239
248
  }));
240
249
  const hydrationMs = Date.now() - hydrationStartedAtMs;
241
- const effectivePage = [];
242
- for (const entry of page) {
243
- if (entry.kind === 'document' && !hydratedDocuments.get(entry.path))
244
- break;
245
- if (entry.kind === 'asset' && !hydratedAssets.get(entry.path))
246
- break;
247
- effectivePage.push(entry);
248
- }
249
- const hasMore = afterCursor.length > effectivePage.length;
250
- const last = effectivePage.length > 0 ? effectivePage[effectivePage.length - 1] : null;
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;
251
255
  const nextCursor = encodeWorkspaceSyncCursor(last
252
256
  ? { u: last.watermark, id: String(last.sortId || '') }
253
257
  : cursor);
@@ -280,55 +284,57 @@ export async function buildWorkspacePullFeed(input) {
280
284
  ? [{ op: 'annotated-attachment-session-upsert', session: entry.session, watermark: entry.watermark }]
281
285
  : entry.kind === 'annotated-attachment-session-delete'
282
286
  ? [{ op: 'annotated-attachment-session-delete', sessionId: entry.sessionId, deletedAt: entry.watermark, watermark: entry.watermark }]
283
- : entry.kind === 'document'
284
- ? (() => {
285
- const payload = hydratedDocuments.get(entry.path);
286
- if (!payload)
287
- return [];
288
- return [{
289
- op: 'document-upsert',
290
- path: entry.path,
291
- updatedAt: entry.watermark,
292
- content: payload.content,
293
- assetId: payload.assetId,
294
- documentId: payload.documentId,
295
- referenceNumber: payload.referenceNumber,
296
- version: payload.version,
297
- taskId: payload.taskId,
298
- logicalName: payload.logicalName,
299
- caption: payload.caption,
300
- originalFilename: payload.originalFilename,
301
- linkRole: payload.linkRole === 'reference' ? 'reference' : 'attachment',
302
- watermark: entry.watermark
303
- }];
304
- })()
305
- : entry.kind === 'document-delete'
306
- ? [{ op: 'document-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }]
307
- : entry.kind === 'asset'
308
- ? (() => {
309
- const payload = hydratedAssets.get(entry.path);
310
- if (!payload)
311
- return [];
312
- return [{
313
- op: 'asset-upsert',
314
- path: entry.path,
315
- updatedAt: entry.watermark,
316
- contentBase64: payload.contentBase64,
317
- assetId: payload.assetId,
318
- kind: payload.kind,
319
- mimeType: payload.mimeType,
320
- referenceNumber: payload.referenceNumber,
321
- taskId: payload.taskId,
322
- logicalName: payload.logicalName,
323
- caption: payload.caption,
324
- originalFilename: payload.originalFilename,
325
- linkRole: payload.linkRole,
326
- watermark: entry.watermark
327
- }];
328
- })()
329
- : entry.kind === 'asset-delete'
330
- ? [{ op: 'asset-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }]
331
- : [{ op: 'upsert', task: entry.task, archived: Boolean(entry.archived), 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 }]));
332
338
  const encodeMs = Date.now() - encodeStartedAtMs;
333
339
  return {
334
340
  workspaceMembers,
@@ -338,6 +344,7 @@ export async function buildWorkspacePullFeed(input) {
338
344
  diagnostics: {
339
345
  totalEntries: all.length,
340
346
  pageEntries: page.length,
347
+ skippedHydrationEntries,
341
348
  documentIndexCount: documentIndex.length,
342
349
  assetIndexCount: assetIndex.length,
343
350
  indexMs,
@@ -29,6 +29,7 @@ function createCollaborationSyncStateMock() {
29
29
  listWorkspaceSyncDocumentReviewSessionsSnapshot: () => [],
30
30
  listWorkspaceSyncDocumentReviewCommentsSnapshot: () => [],
31
31
  listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot: () => [],
32
+ listWorkspaceSyncTaskEventsSnapshot: () => [],
32
33
  };
33
34
  }
34
35
  function encodeCursor(cursor) {
@@ -234,9 +235,22 @@ describe('workspacePullFeed', () => {
234
235
  expect(loadedDocs).toEqual(['docs/a.md']);
235
236
  expect(loadedAssets).toEqual(['workspaces/ws/assets/images/a.png']);
236
237
  });
237
- it('does not advance the cursor past a hydration miss', async () => {
238
+ it('skips hydration misses without blocking later feed progress', async () => {
238
239
  const core = createCoreMock();
239
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
+ ];
240
254
  content.listWorkspaceSyncDocumentIndex = async () => [
241
255
  {
242
256
  path: 'docs/a.md',
@@ -260,7 +274,7 @@ describe('workspacePullFeed', () => {
260
274
  core,
261
275
  workspaceId: 'workspace-4',
262
276
  cursor: null,
263
- pageSize: 2,
277
+ pageSize: 3,
264
278
  encodeWorkspaceSyncCursor: encodeCursor,
265
279
  contentSyncState: content,
266
280
  collaborationSyncState: createCollaborationSyncStateMock(),
@@ -270,9 +284,63 @@ describe('workspacePullFeed', () => {
270
284
  op: 'document-upsert',
271
285
  path: 'docs/a.md',
272
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' })
273
343
  })
274
344
  ]);
275
- expect(result.nextCursor).toBe('2026-04-10T10:00:00.000Z|doc:docs/a.md');
276
- expect(result.hasMore).toBe(true);
277
345
  });
278
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;
@@ -104,6 +118,8 @@ export type WorkspaceSyncChange = {
104
118
  } | {
105
119
  op: 'delete';
106
120
  taskId: string;
121
+ deletedAt?: string;
122
+ watermark?: string;
107
123
  } | {
108
124
  op: 'initiative-upsert';
109
125
  initiative: WorkspaceSyncInitiativeSnapshot;
@@ -177,6 +193,9 @@ export type WorkspaceSyncChange = {
177
193
  op: 'annotated-attachment-session-delete';
178
194
  sessionId: string;
179
195
  deletedAt?: string;
196
+ } | {
197
+ op: 'task-event-upsert';
198
+ event: WorkspaceSyncTaskEventSnapshot;
180
199
  };
181
200
  export interface WorkspaceSyncPayload {
182
201
  changes: WorkspaceSyncChange[];
@@ -203,6 +222,7 @@ export interface WorkspaceSyncPayload {
203
222
  currentAnnotatedAttachmentSessionIds: Set<string>;
204
223
  deleteAnnotatedAttachmentSessionIds: Set<string>;
205
224
  currentAnnotatedAttachmentSessionWatermarks: Map<string, string>;
225
+ currentTaskEventWatermarks: Map<string, string>;
206
226
  /** Watermarks for every task upsert included in this payload (id → updatedAt|createdAt). */
207
227
  pushedWatermarks: Map<string, string>;
208
228
  }
@@ -240,6 +260,7 @@ export declare function buildWorkspaceSyncPayloadModel(input: {
240
260
  archivedTasks: TaskItem[];
241
261
  lastPushedTaskIds: Set<string>;
242
262
  pendingDeletedTaskIds: Set<string>;
263
+ pendingDeletedTaskWatermarks?: Map<string, string>;
243
264
  initiatives?: InitiativeItem[];
244
265
  workstreams?: WorkstreamItem[];
245
266
  taxonomies?: unknown[];
@@ -250,6 +271,7 @@ export declare function buildWorkspaceSyncPayloadModel(input: {
250
271
  documentReviewSessions?: WorkspaceSyncDocumentReviewSessionSnapshot[];
251
272
  documentReviewComments?: WorkspaceSyncDocumentReviewCommentSnapshot[];
252
273
  annotatedAttachmentSessions?: WorkspaceSyncAnnotatedAttachmentSessionSnapshot[];
274
+ taskEvents?: WorkspaceSyncTaskEventSnapshot[];
253
275
  lastPushedInitiativeIds?: Set<string>;
254
276
  lastPushedInitiativeWatermarks?: Map<string, string>;
255
277
  lastPushedWorkstreamIds?: Set<string>;
@@ -266,6 +288,7 @@ export declare function buildWorkspaceSyncPayloadModel(input: {
266
288
  lastPushedDocumentReviewCommentWatermarks?: Map<string, string>;
267
289
  lastPushedAnnotatedAttachmentSessionIds?: Set<string>;
268
290
  lastPushedAnnotatedAttachmentSessionWatermarks?: Map<string, string>;
291
+ lastPushedTaskEventWatermarks?: Map<string, string>;
269
292
  /**
270
293
  * Watermark (updatedAt|createdAt) of each task as of its last successful push.
271
294
  * When provided, only tasks whose watermark differs from this map are included
@@ -289,4 +312,5 @@ export declare function buildWorkspaceSyncSignatureModel(input: {
289
312
  documentReviewSessions?: WorkspaceSyncDocumentReviewSessionSnapshot[];
290
313
  documentReviewComments?: WorkspaceSyncDocumentReviewCommentSnapshot[];
291
314
  annotatedAttachmentSessions?: WorkspaceSyncAnnotatedAttachmentSessionSnapshot[];
315
+ taskEvents?: WorkspaceSyncTaskEventSnapshot[];
292
316
  }): 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();
@@ -15,6 +25,9 @@ export function buildWorkspaceSyncPayloadModel(input) {
15
25
  const pendingDeleteIds = new Set(Array.from(input.pendingDeletedTaskIds)
16
26
  .map((taskId) => String(taskId || '').trim())
17
27
  .filter((taskId) => taskId.length > 0));
28
+ const pendingDeleteWatermarks = new Map(Array.from(input.pendingDeletedTaskWatermarks || new Map())
29
+ .map(([taskId, deletedAt]) => [String(taskId || '').trim(), String(deletedAt || '').trim()])
30
+ .filter(([taskId, deletedAt]) => taskId.length > 0 && Number.isFinite(Date.parse(deletedAt))));
18
31
  const lastWatermarks = input.lastPushedWatermarks;
19
32
  const isDirty = (task) => {
20
33
  if ((task?.referenceNumber === undefined || task?.referenceNumber === null) && Number.isFinite(Number(task?.localReferenceNumber))) {
@@ -64,12 +77,14 @@ export function buildWorkspaceSyncPayloadModel(input) {
64
77
  .filter((taskId) => !currentTaskIds.has(taskId))
65
78
  .map((taskId) => ({
66
79
  op: 'delete',
67
- taskId
80
+ taskId,
81
+ deletedAt: pendingDeleteWatermarks.get(taskId) || payloadBuiltAt
68
82
  }));
69
83
  const queuedDeleteChanges = Array.from(pendingDeleteIds)
70
84
  .map((taskId) => ({
71
85
  op: 'delete',
72
- taskId
86
+ taskId,
87
+ deletedAt: pendingDeleteWatermarks.get(taskId) || payloadBuiltAt
73
88
  }));
74
89
  const deleteTaskIds = new Set([
75
90
  ...deleteChanges.map((change) => String(change.taskId)),
@@ -175,6 +190,10 @@ export function buildWorkspaceSyncPayloadModel(input) {
175
190
  username: String(profile.username || '').trim(),
176
191
  icon: String(profile.icon || 'Bot').trim() || 'Bot',
177
192
  color: String(profile.color || '#8b5cf6').trim() || '#8b5cf6',
193
+ description: typeof profile.description === 'string' ? profile.description.trim() || null : null,
194
+ role: typeof profile.role === 'string' ? profile.role.trim() || null : null,
195
+ provider: typeof profile.provider === 'string' ? profile.provider.trim() || null : null,
196
+ model: typeof profile.model === 'string' ? profile.model.trim() || null : null,
178
197
  surfaceType: parseAiProfileSurfaceType(profile.surfaceType),
179
198
  providerMetadata: profile.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
180
199
  ? profile.providerMetadata
@@ -391,6 +410,40 @@ export function buildWorkspaceSyncPayloadModel(input) {
391
410
  return true;
392
411
  return previousAnnotatedAttachmentSessionWatermarks.get(change.session.id) !== change.session.updatedAt;
393
412
  });
413
+ const taskEvents = sortTaskEventsForSync(Array.isArray(input.taskEvents) ? input.taskEvents : []);
414
+ const currentTaskEventWatermarks = new Map(taskEvents
415
+ .map((event) => [String(event?.id || '').trim(), String(event?.createdAt || '').trim()])
416
+ .filter(([id]) => id.length > 0));
417
+ const previousTaskEventWatermarks = input.lastPushedTaskEventWatermarks;
418
+ const shouldSendAllTaskEvents = !previousTaskEventWatermarks || previousTaskEventWatermarks.size === 0;
419
+ const taskEventUpsertChanges = taskEvents
420
+ .map((event) => ({
421
+ op: 'task-event-upsert',
422
+ event: {
423
+ id: String(event.id || '').trim(),
424
+ taskId: String(event.taskId || '').trim(),
425
+ workspaceId: String(event.workspaceId || '').trim(),
426
+ action: String(event.action || '').trim(),
427
+ actor: String(event.actor || 'system').trim() || 'system',
428
+ actorType: event.actorType === 'ai' || event.actorType === 'human' || event.actorType === 'system'
429
+ ? event.actorType
430
+ : 'system',
431
+ details: event.details && typeof event.details === 'object' && !Array.isArray(event.details)
432
+ ? event.details
433
+ : {},
434
+ createdAt: String(event.createdAt || '').trim(),
435
+ }
436
+ }))
437
+ .filter((change) => (change.event.id.length > 0
438
+ && change.event.taskId.length > 0
439
+ && change.event.workspaceId.length > 0
440
+ && change.event.action.length > 0
441
+ && Number.isFinite(Date.parse(change.event.createdAt))))
442
+ .filter((change) => {
443
+ if (shouldSendAllTaskEvents)
444
+ return true;
445
+ return previousTaskEventWatermarks.get(change.event.id) !== change.event.createdAt;
446
+ });
394
447
  const changes = [
395
448
  ...initiativeUpsertChanges,
396
449
  ...workstreamUpsertChanges,
@@ -438,9 +491,14 @@ export function buildWorkspaceSyncPayloadModel(input) {
438
491
  sessionId,
439
492
  deletedAt: payloadBuiltAt
440
493
  })),
494
+ ...taskEventUpsertChanges,
441
495
  ...activeChanges,
442
496
  ...archivedChanges,
443
- ...Array.from(deleteTaskIds).map((taskId) => ({ op: 'delete', taskId }))
497
+ ...Array.from(deleteTaskIds).map((taskId) => ({
498
+ op: 'delete',
499
+ taskId,
500
+ deletedAt: pendingDeleteWatermarks.get(taskId) || payloadBuiltAt
501
+ }))
444
502
  ];
445
503
  return {
446
504
  changes,
@@ -467,6 +525,7 @@ export function buildWorkspaceSyncPayloadModel(input) {
467
525
  currentAnnotatedAttachmentSessionIds,
468
526
  deleteAnnotatedAttachmentSessionIds,
469
527
  currentAnnotatedAttachmentSessionWatermarks,
528
+ currentTaskEventWatermarks,
470
529
  pushedWatermarks
471
530
  };
472
531
  }
@@ -516,6 +575,21 @@ export function buildWorkspaceSyncSignatureModel(input) {
516
575
  .filter((profile) => profile.id.length > 0)
517
576
  .sort((a, b) => a.id.localeCompare(b.id))
518
577
  : [],
578
+ taskEvents: Array.isArray(input.taskEvents)
579
+ ? input.taskEvents
580
+ .map((event) => ({
581
+ id: String(event.id || '').trim(),
582
+ taskId: String(event.taskId || '').trim(),
583
+ action: String(event.action || '').trim(),
584
+ actor: String(event.actor || '').trim(),
585
+ actorType: event.actorType === 'ai' || event.actorType === 'human' || event.actorType === 'system'
586
+ ? event.actorType
587
+ : 'system',
588
+ createdAt: String(event.createdAt || '').trim()
589
+ }))
590
+ .filter((event) => event.id.length > 0 && event.taskId.length > 0)
591
+ .sort((a, b) => a.id.localeCompare(b.id))
592
+ : [],
519
593
  documents: Array.isArray(input.documents)
520
594
  ? input.documents
521
595
  .map((doc) => ({