@taskforcehq/taskforce 0.3.327 → 0.3.328

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 (187) hide show
  1. package/dist/Taskforce.module.css +24 -53
  2. package/dist/components/activity/EntityActivityTimeline.js +15 -5
  3. package/dist/components/context/AttachmentPreviewHost.js +1 -1
  4. package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
  5. package/dist/components/context/ContextAttachmentManager.js +116 -20
  6. package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
  7. package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
  8. package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
  9. package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
  10. package/dist/components/features/planning/PlanningModule.js +26 -9
  11. package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
  12. package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
  13. package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
  14. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  15. package/dist/components/task/TaskActionHeader.js +2 -2
  16. package/dist/components/task/TaskCard.js +37 -4
  17. package/dist/components/task/TaskContextUpload.d.ts +3 -0
  18. package/dist/components/task/TaskContextUpload.js +2 -2
  19. package/dist/components/task/TaskForm.d.ts +4 -0
  20. package/dist/components/task/TaskForm.js +32 -7
  21. package/dist/components/task/TaskImageReviews.js +16 -16
  22. package/dist/components/task/TaskStatusActions.js +11 -2
  23. package/dist/components/task/TaskWorkstreamPicker.js +2 -1
  24. package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
  25. package/dist/components/ui/DocumentTypeIcon.js +8 -0
  26. package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
  27. package/dist/components/ui/TaxonomyDropdown.js +30 -3
  28. package/dist/components/views/StandaloneLayout.js +62 -9
  29. package/dist/components/views/WidgetView.js +8 -5
  30. package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
  31. package/dist/components/views/panels/PlanningDrawer.js +48 -44
  32. package/dist/config/envSchema.js +1 -0
  33. package/dist/config/localServiceCloudTarget.d.ts +10 -0
  34. package/dist/config/localServiceCloudTarget.js +76 -0
  35. package/dist/core/AiProfileService.d.ts +1 -0
  36. package/dist/core/AiProfileService.js +2 -1
  37. package/dist/core/AttachmentLinkService.js +7 -4
  38. package/dist/core/DeletedTaskLifecycleService.d.ts +4 -0
  39. package/dist/core/DeletedTaskLifecycleService.js +21 -0
  40. package/dist/core/McpTokenService.d.ts +4 -0
  41. package/dist/core/McpTokenService.js +52 -0
  42. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +5 -0
  43. package/dist/core/TaskAttachmentLinksDurableMutationService.js +20 -11
  44. package/dist/core/Taskforce.d.ts +63 -1
  45. package/dist/core/Taskforce.js +558 -73
  46. package/dist/core/types.d.ts +2 -1
  47. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
  48. package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
  49. package/dist/documentReviews/service.d.ts +4 -0
  50. package/dist/documentReviews/service.js +141 -46
  51. package/dist/hooks/tasks/useTaskFormActions.d.ts +3 -0
  52. package/dist/hooks/tasks/useTaskFormActions.js +33 -7
  53. package/dist/hooks/useTaskforce.d.ts +59 -50
  54. package/dist/hooks/useTaskforce.js +103 -1
  55. package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
  56. package/dist/mcp/canonicalAssetHelpers.js +39 -5
  57. package/dist/mcp/collaborationRegistrar.js +15 -13
  58. package/dist/mcp/documentAssetRegistrar.js +16 -9
  59. package/dist/mcp/localServiceProxy.js +4 -1
  60. package/dist/mcp/runtime.d.ts +16 -0
  61. package/dist/mcp/runtime.js +566 -171
  62. package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
  63. package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
  64. package/dist/mcp/taskPlanningRegistrar.js +42 -34
  65. package/dist/mcp/toolRegistry.js +2 -2
  66. package/dist/migrations/taskSchemaMigrations.js +35 -0
  67. package/dist/runtime/appGateDefinitions.d.ts +2 -2
  68. package/dist/runtime/appGateDefinitions.js +2 -2
  69. package/dist/server/appAccess.d.ts +12 -0
  70. package/dist/server/appAccess.js +21 -0
  71. package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
  72. package/dist/server/authorizedWorkspaceAccess.js +40 -0
  73. package/dist/server/cloudCollectionRead.d.ts +25 -0
  74. package/dist/server/cloudCollectionRead.js +120 -0
  75. package/dist/server/cloudCollectionReadPool.d.ts +52 -0
  76. package/dist/server/cloudCollectionReadPool.js +140 -0
  77. package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
  78. package/dist/server/cloudCollectionReadWorker.js +36 -0
  79. package/dist/server/index.d.ts +3 -11
  80. package/dist/server/index.js +53 -40
  81. package/dist/server/localServiceLease.d.ts +3 -0
  82. package/dist/server/localServiceLease.js +9 -1
  83. package/dist/server/routes/documents.d.ts +5 -2
  84. package/dist/server/routes/documents.js +160 -13
  85. package/dist/server/routes/durableTaskHttpRouters.d.ts +2 -0
  86. package/dist/server/routes/durableTaskHttpRouters.js +2 -2
  87. package/dist/server/routes/localService.js +2 -0
  88. package/dist/server/routes/primitives.d.ts +2 -0
  89. package/dist/server/routes/shared.d.ts +24 -0
  90. package/dist/server/routes/shared.js +114 -18
  91. package/dist/server/routes/sync.js +22 -0
  92. package/dist/server/routes/syncV3FeedRoutes.js +4 -2
  93. package/dist/server/routes/syncV3LocalCaptureRoutes.js +20 -2
  94. package/dist/server/routes/syncV3OperationRoutes.js +17 -2
  95. package/dist/server/routes/tasks.js +199 -89
  96. package/dist/server/routes.js +35 -7
  97. package/dist/service/localServiceCli.js +36 -25
  98. package/dist/service/localServiceClientLifecycle.js +67 -6
  99. package/dist/shared/planningIdentity.d.ts +20 -0
  100. package/dist/shared/planningIdentity.js +79 -0
  101. package/dist/storage/postgresAdapter.js +4 -0
  102. package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
  103. package/dist/storage/postgresRequestDiagnostics.js +24 -0
  104. package/dist/storage/workspaceAssetStore.d.ts +32 -0
  105. package/dist/storage/workspaceAssetStore.js +70 -0
  106. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +16 -0
  107. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +70 -22
  108. package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
  109. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +5 -0
  110. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
  111. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +15 -0
  112. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +26 -3
  113. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
  114. package/dist/sync/syncApplyHandlers.d.ts +3 -1
  115. package/dist/sync/syncApplyHandlers.js +29 -0
  116. package/dist/sync/syncService.js +9 -0
  117. package/dist/sync/taskSyncPayload.d.ts +1 -0
  118. package/dist/sync/taskSyncPayload.js +6 -0
  119. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +1 -0
  120. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +21 -3
  121. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +17 -4
  122. package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
  123. package/dist/sync/v3/localOutboxDispatcher.js +5 -1
  124. package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
  125. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +1 -0
  126. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +15 -3
  127. package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
  128. package/dist/sync/v3/syncDurabilityStore.d.ts +8 -0
  129. package/dist/sync/v3/syncDurabilityStore.js +171 -33
  130. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +1 -0
  131. package/dist/sync/v3/taskAttachmentLinksMutationService.js +18 -2
  132. package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
  133. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +1 -9
  134. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +1 -0
  135. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +7 -1
  136. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +15 -3
  137. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +4 -0
  138. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +29 -6
  139. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +1 -0
  140. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +29 -7
  141. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +62 -7
  142. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +1 -0
  143. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
  144. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
  145. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
  146. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
  147. package/dist/sync/workspacePullFeed.d.ts +6 -1
  148. package/dist/sync/workspacePullFeed.js +172 -161
  149. package/dist/sync/workspaceSyncModel.d.ts +11 -0
  150. package/dist/sync/workspaceSyncModel.js +27 -2
  151. package/dist/types.d.ts +8 -0
  152. package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-MZs7lVED.js} +1 -1
  153. package/dist/ui/assets/{AiProfilesModule-DPxQeF9N.js → AiProfilesModule-C3i0LOxe.js} +1 -1
  154. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-DHNYaNg6.js → AnnotatedAttachmentWorkspace-DVZk5Tln.js} +1 -1
  155. package/dist/ui/assets/AssetTraySortControl-Kf1pelSy.js +1 -0
  156. package/dist/ui/assets/ContextAttachmentManager-B8boOs6Q.js +3 -0
  157. package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-DXH4XUsB.js} +4 -4
  158. package/dist/ui/assets/EntityActivityTimeline-BtSTq0ON.js +1 -0
  159. package/dist/ui/assets/PlanningModule-BPVyEa-f.js +1 -0
  160. package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
  161. package/dist/ui/assets/{PlansPage-BVH_NfgT.js → PlansPage-ChqgxALc.js} +1 -1
  162. package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +1 -0
  163. package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
  164. package/dist/ui/assets/{TaskSettings-xVsxSa0g.js → TaskSettings-g4ECKNOz.js} +1 -1
  165. package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BazdUwxZ.js} +1 -1
  166. package/dist/ui/assets/WorkflowManagerModule-DWFspC-o.js +1 -0
  167. package/dist/ui/assets/index-BUplSsv_.js +7 -0
  168. package/dist/ui/assets/index-gz2EXuoK.css +1 -0
  169. package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-BkFLXavV.js} +1 -1
  170. package/dist/ui/index.html +3 -3
  171. package/dist/utils/constants.d.ts +2 -2
  172. package/dist/utils/constants.js +1 -1
  173. package/dist/utils/contextAttachmentUpload.d.ts +2 -0
  174. package/dist/utils/contextAttachmentUpload.js +7 -0
  175. package/dist/utils/contextFiles.d.ts +1 -1
  176. package/dist/utils/contextFiles.js +4 -4
  177. package/package.json +6 -1
  178. package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
  179. package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
  180. package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
  181. package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
  182. package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
  183. package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
  184. package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
  185. package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
  186. package/dist/ui/assets/index-BzJlVnNS.css +0 -1
  187. package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
@@ -3,11 +3,13 @@ import * as path from 'path';
3
3
  import * as os from 'os';
4
4
  import { createHash, pbkdf2Sync, randomBytes, timingSafeEqual } from 'crypto';
5
5
  import { fileURLToPath } from 'url';
6
+ import { assertPlanningIdentityColor, assertPlanningIdentityIcon, } from '../shared/planningIdentity.js';
6
7
  import { createDatabaseAdapter } from '../storage/createDatabaseAdapter.js';
7
8
  import { assertLocalDataOwnerPermit, } from '../server/localDataOwnerPermit.js';
8
9
  import { resolveLocalDatabaseIdentity } from '../server/localDatabaseIdentity.js';
9
10
  import { compareWorkspaceSyncStateRevisions, normalizeWorkspaceSyncStateRevision, } from '../sync/workspaceSyncState.js';
10
11
  import { WorkspaceSyncDurabilityStore } from '../sync/v3/syncDurabilityStore.js';
12
+ import { workspaceSyncV3RepairEntityReadBatchSize } from '../sync/v3/workspaceSyncV3RepairSqlBatching.js';
11
13
  import { isTaskMetadataActivityEventId } from '../sync/v3/taskMetadataActivityIdentity.js';
12
14
  import { isTaskLifecycleActivityEventId } from '../sync/v3/taskLifecycleActivityIdentity.js';
13
15
  import { resolveV3OwnedTaskLifecycleIds, resolveV3OwnedTaskMetadataIds, resolveV3OwnedTaskTaxonomyIds, } from '../sync/v3/taskMetadataCoexistence.js';
@@ -23,7 +25,7 @@ import { archiveAiProfileService, bindAiProfileConnectionService, getAiProfileBy
23
25
  import { syncCanonicalAttachmentLinksForTargetService, syncCanonicalTaskAttachmentLinksService, } from './AttachmentLinkService.js';
24
26
  import { listTaskAttachmentLinksForDurableSync as listTaskAttachmentLinksForDurableSyncService, listWorkspaceTaskAttachmentLinksForDurableSync as listWorkspaceTaskAttachmentLinksForDurableSyncService, normalizeTaskAttachmentDescriptorsForDurableSync as normalizeTaskAttachmentDescriptorsForDurableSyncService, resolveTaskAttachmentLinksForDurableSync as resolveTaskAttachmentLinksForDurableSyncService, } from './TaskAttachmentLinksDurableMutationService.js';
25
27
  import { EntityEventIdentityCollisionError, addEntityEventService, addTaskEventService, buildEntityActivityService, buildTaskActivityService, getEntityEventForSyncService, getTaskEventForSyncService, listEntityEventsInternalService, listWorkspaceEntityEventsForSyncService, listTaskEventsInternalService, listWorkspaceTaskEventsForSyncService, replaceEntityEventsService, replaceTaskCommentsService, replaceTaskEventsService, upsertEntityEventForSyncService, deleteEntityEventForSyncService, upsertTaskEventForSyncService, } from './TaskActivityService.js';
26
- import { addDeletedTaskRecordService, getDeletedTaskService, listDeletedTasksService, mapDeletedTaskRowService, normalizeTaskSnapshotForRestoreService, normalizeDeletedTaskRestoreLifecycleService, purgeExpiredDeletedTasksService, } from './DeletedTaskLifecycleService.js';
28
+ import { addDeletedTaskRecordService, getDeletedTaskService, listDeletedTasksService, listDeletedTasksReadOnlyService, mapDeletedTaskRowService, normalizeTaskSnapshotForRestoreService, normalizeDeletedTaskRestoreLifecycleService, purgeExpiredDeletedTasksService, } from './DeletedTaskLifecycleService.js';
27
29
  import { DELETED_TASK_RETENTION_DAYS } from '../shared/deletedTaskRetention.js';
28
30
  import { applyWorkspaceSyncSnapshotService, claimPushIdempotencyService, completePushIdempotencyClaimService, deleteDocumentWatermarkService, deleteDocumentWatermarkStrictService, getDocumentWatermarkService, listTasksForSyncService, listWorkspaceSyncDeletesSinceService, readPushIdempotencyService, releasePushIdempotencyClaimService, renewPushIdempotencyClaimService, recordSyncEventDurablyService, recordSyncEventService, upsertDocumentWatermarkService, upsertDocumentWatermarkStrictService, writePushIdempotencyService, } from './SyncReconciliationService.js';
29
31
  import { getGlobalSettingsService, hasPersistedGlobalSettingsService, readGlobalConfigService, updateGlobalSettingsService, writeGlobalConfigService, } from './GlobalSettingsService.js';
@@ -1375,9 +1377,13 @@ export class TaskforceCore {
1375
1377
  // Initialize Database Adapter (SQLite default; Postgres scaffolded)
1376
1378
  const provider = config.database?.provider || 'sqlite';
1377
1379
  const connectionString = config.database?.connectionString || process.env.TASKFORCE_DATABASE_URL;
1380
+ const isCloudReadWorker = config.initializationMode === 'cloud-read-worker';
1378
1381
  if (provider === 'postgres' && !connectionString) {
1379
1382
  throw new Error('[Taskforce] Postgres provider requires a connection string (TASKFORCE_DATABASE_URL).');
1380
1383
  }
1384
+ if (isCloudReadWorker && (this.runtimeMode !== 'cloud' || provider !== 'postgres')) {
1385
+ throw new Error('[Taskforce] Cloud read-worker initialization requires cloud runtime with PostgreSQL.');
1386
+ }
1381
1387
  if (this.runtimeMode === 'local') {
1382
1388
  assertLocalDataOwnerPermit({
1383
1389
  permit: config.localDataOwnerPermit,
@@ -1391,8 +1397,10 @@ export class TaskforceCore {
1391
1397
  databaseIdentityKind: provider === 'sqlite' ? 'path' : 'opaque',
1392
1398
  });
1393
1399
  }
1394
- this.ensureDirectories();
1395
- this.migrateLegacyGlobalSettingsDbIfNeeded();
1400
+ if (!isCloudReadWorker) {
1401
+ this.ensureDirectories();
1402
+ this.migrateLegacyGlobalSettingsDbIfNeeded();
1403
+ }
1396
1404
  this.db = createDatabaseAdapter({
1397
1405
  provider,
1398
1406
  sqliteDbPath: this.dbPath,
@@ -1428,53 +1436,59 @@ export class TaskforceCore {
1428
1436
  throw new Error(`[Taskforce] Failed to initialize global settings DB at ${this.globalSettingsDbPath}: ${String(error?.message || error)}`);
1429
1437
  }
1430
1438
  }
1431
- this.initializeSchema();
1432
- ensureTaskApproachRemoved(this.db);
1433
- ensureTenantSchema(this.db);
1434
- ensureWorkspaceSchema(this.db);
1435
- ensureWorkspaceSyncDurabilitySchema(this.db);
1436
- if (this.runtimeMode === 'local') {
1437
- ensureLocalSyncCoordinatorSchema(this.db);
1438
- }
1439
- ensureScheduleTaskSchema(this.db);
1440
- ensureWorkstreamTaskOrderSchema(this.db);
1441
- ensureAssigneeTaskSchema(this.db);
1442
- ensureTaskReferenceSchema(this.db);
1443
- ensurePlanningReferenceSchema(this.db);
1444
- ensureTaskAssetsSchema(this.db);
1445
- ensureWorkspaceAssetsSchema(this.db);
1446
- backfillPlanningAttachmentAssetIdentities(this.db, this.tenantId);
1447
- ensureDocumentReferenceSchema(this.db);
1448
- ensureDocumentReviewSessionsSchema(this.db);
1449
- ensureAnnotatedAttachmentSessionsSchema(this.db);
1450
- ensureTaskforceAgentsSchema(this.db);
1451
- ensureAgentRolesSchema(this.db);
1452
- ensureTaskforceAgentSkillsSchema(this.db);
1453
- ensureWorkflowSchema(this.db);
1454
- ensureBillingSchema(this.db);
1455
- ensureEntitlementsSchema(this.db);
1456
- ensureAuthIdentitySchema(this.db);
1457
- this.performMigrationIfNeeded();
1458
- repairTaskUpdatedAtOnlySyncDrift({
1459
- db: this.db,
1460
- tenantId: this.tenantId,
1461
- loadTask: (taskId, workspaceId) => this.getTask(taskId, workspaceId),
1462
- });
1439
+ if (!isCloudReadWorker) {
1440
+ this.initializeSchema();
1441
+ ensureTaskApproachRemoved(this.db);
1442
+ ensureTenantSchema(this.db);
1443
+ ensureWorkspaceSchema(this.db);
1444
+ ensureWorkspaceSyncDurabilitySchema(this.db);
1445
+ if (this.runtimeMode === 'local') {
1446
+ ensureLocalSyncCoordinatorSchema(this.db);
1447
+ }
1448
+ ensureScheduleTaskSchema(this.db);
1449
+ ensureWorkstreamTaskOrderSchema(this.db);
1450
+ ensureAssigneeTaskSchema(this.db);
1451
+ ensureTaskReferenceSchema(this.db);
1452
+ ensurePlanningReferenceSchema(this.db);
1453
+ ensureTaskAssetsSchema(this.db);
1454
+ ensureWorkspaceAssetsSchema(this.db);
1455
+ backfillPlanningAttachmentAssetIdentities(this.db, this.tenantId);
1456
+ ensureDocumentReferenceSchema(this.db);
1457
+ ensureDocumentReviewSessionsSchema(this.db);
1458
+ ensureAnnotatedAttachmentSessionsSchema(this.db);
1459
+ ensureTaskforceAgentsSchema(this.db);
1460
+ ensureAgentRolesSchema(this.db);
1461
+ ensureTaskforceAgentSkillsSchema(this.db);
1462
+ ensureWorkflowSchema(this.db);
1463
+ ensureBillingSchema(this.db);
1464
+ ensureEntitlementsSchema(this.db);
1465
+ ensureAuthIdentitySchema(this.db);
1466
+ this.performMigrationIfNeeded();
1467
+ repairTaskUpdatedAtOnlySyncDrift({
1468
+ db: this.db,
1469
+ tenantId: this.tenantId,
1470
+ loadTask: (taskId, workspaceId) => this.getTask(taskId, workspaceId),
1471
+ });
1472
+ }
1463
1473
  this.planEntitlementService = new PlanEntitlementService(this.db, this.tenantId, {
1464
1474
  getWorkspaceOwnerUserId: (wid) => this.getWorkspaceOwnerUserId(wid),
1465
1475
  readGlobalConfig: () => this.readGlobalConfig(),
1466
1476
  writeGlobalConfig: (config) => this.writeGlobalConfig(config),
1467
1477
  });
1468
- this.planEntitlementService.repairPlanCatalogDefaultsIfNeeded();
1469
- this.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
1470
- this.planEntitlementService.backfillAccountEntitlements();
1478
+ if (!isCloudReadWorker) {
1479
+ this.planEntitlementService.repairPlanCatalogDefaultsIfNeeded();
1480
+ this.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
1481
+ this.planEntitlementService.backfillAccountEntitlements();
1482
+ }
1471
1483
  this.authTokenService = new AuthTokenService(this.db, this.tenantId);
1472
1484
  this.identityService = new AuthIdentityService(this.db, this.tenantId);
1473
1485
  this.authTokenService.setIdentityService(this.identityService);
1474
1486
  this.mcpTokenService = new McpTokenService(this.db, this.tenantId, {
1475
1487
  getAccountAccessStatus: (userId) => this.getAccountAccessStatus(userId),
1476
1488
  });
1477
- purgeExpiredDeletedTasksService(this.getDeletedTaskLifecycleServiceDeps());
1489
+ if (!isCloudReadWorker) {
1490
+ purgeExpiredDeletedTasksService(this.getDeletedTaskLifecycleServiceDeps());
1491
+ }
1478
1492
  }
1479
1493
  close() {
1480
1494
  if (this.globalSettingsDb && this.globalSettingsDb.open) {
@@ -1751,6 +1765,7 @@ export class TaskforceCore {
1751
1765
  order_in_day INTEGER,
1752
1766
  workstream_order INTEGER,
1753
1767
  workstream_id TEXT,
1768
+ card_cover_asset_id TEXT,
1754
1769
  FOREIGN KEY(workstream_id) REFERENCES workstreams(id) ON DELETE SET NULL
1755
1770
  );
1756
1771
 
@@ -1832,6 +1847,20 @@ export class TaskforceCore {
1832
1847
  FOREIGN KEY(initiative_id) REFERENCES initiatives(id) ON DELETE SET NULL
1833
1848
  );
1834
1849
 
1850
+ CREATE TABLE IF NOT EXISTS planning_entity_identities (
1851
+ tenant_id TEXT NOT NULL DEFAULT 'default',
1852
+ workspace_id TEXT NOT NULL DEFAULT 'default',
1853
+ entity_type TEXT NOT NULL,
1854
+ entity_id TEXT NOT NULL,
1855
+ icon TEXT,
1856
+ color TEXT,
1857
+ updated_at TEXT NOT NULL,
1858
+ PRIMARY KEY (tenant_id, workspace_id, entity_type, entity_id)
1859
+ );
1860
+
1861
+ CREATE INDEX IF NOT EXISTS idx_planning_entity_identities_updated
1862
+ ON planning_entity_identities (tenant_id, workspace_id, updated_at, entity_type, entity_id);
1863
+
1835
1864
  CREATE TABLE IF NOT EXISTS workspace_workstream_reference_sequences (
1836
1865
  tenant_id TEXT NOT NULL DEFAULT 'default',
1837
1866
  workspace_id TEXT NOT NULL DEFAULT 'default',
@@ -2222,6 +2251,9 @@ export class TaskforceCore {
2222
2251
  if (!taskColumns.some((c) => c.name === 'workstream_id')) {
2223
2252
  this.db.exec(`ALTER TABLE tasks ADD COLUMN workstream_id TEXT`);
2224
2253
  }
2254
+ if (!taskColumns.some((c) => c.name === 'card_cover_asset_id')) {
2255
+ this.db.exec(`ALTER TABLE tasks ADD COLUMN card_cover_asset_id TEXT`);
2256
+ }
2225
2257
  const initiativeColumns = this.db.prepare(`PRAGMA table_info(initiatives)`).all();
2226
2258
  if (!initiativeColumns.some((c) => c.name === 'comments_json')) {
2227
2259
  this.db.exec(`ALTER TABLE initiatives ADD COLUMN comments_json TEXT`);
@@ -2258,6 +2290,12 @@ export class TaskforceCore {
2258
2290
  catch {
2259
2291
  // Best-effort schema repair for existing deployments.
2260
2292
  }
2293
+ try {
2294
+ this.db.exec(`ALTER TABLE tasks ADD COLUMN IF NOT EXISTS card_cover_asset_id TEXT`);
2295
+ }
2296
+ catch {
2297
+ // Best-effort schema repair for existing deployments.
2298
+ }
2261
2299
  try {
2262
2300
  this.db.exec(`ALTER TABLE initiatives ADD COLUMN IF NOT EXISTS comments_json TEXT`);
2263
2301
  this.db.exec(`ALTER TABLE initiatives ADD COLUMN IF NOT EXISTS attachments_json TEXT`);
@@ -3679,7 +3717,7 @@ export class TaskforceCore {
3679
3717
  const originalFilename = row.original_filename ? String(row.original_filename) : path.basename(storageKey);
3680
3718
  const assetKind = String(canonicalAsset?.kind || row.kind || '').trim();
3681
3719
  const isCanonicalDocument = Boolean(canonicalAsset && isValidCanonicalDocumentAsset(canonicalAsset));
3682
- const displayName = isCanonicalDocument
3720
+ const caption = isCanonicalDocument
3683
3721
  ? getCanonicalDocumentName({
3684
3722
  logicalName: canonicalAsset?.logicalName || (row.logical_name ? String(row.logical_name) : null),
3685
3723
  originalFilename,
@@ -3687,11 +3725,7 @@ export class TaskforceCore {
3687
3725
  assetId: String(row.asset_id || '').trim() || null
3688
3726
  })
3689
3727
  : (canonicalAsset?.logicalName || (row.logical_name ? String(row.logical_name) : originalFilename));
3690
- const caption = isCanonicalDocument
3691
- ? displayName
3692
- : (row.display_name
3693
- ? String(row.display_name)
3694
- : displayName);
3728
+ const displayName = row.display_name ? String(row.display_name) : originalFilename;
3695
3729
  const referenceNumber = typeof canonicalAsset?.referenceNumber === 'number'
3696
3730
  ? canonicalAsset.referenceNumber
3697
3731
  : (Number.isFinite(Number(row.reference_number))
@@ -3825,6 +3859,9 @@ export class TaskforceCore {
3825
3859
  orderInDay: this.sanitizeOrderInDay(row.order_in_day),
3826
3860
  workstreamOrder: this.sanitizeOrderInDay(row.workstream_order),
3827
3861
  workstreamId: this.sanitizeWorkstreamId(row.workstream_id),
3862
+ cardCoverAssetId: typeof row.card_cover_asset_id === 'string' && row.card_cover_asset_id.trim()
3863
+ ? row.card_cover_asset_id.trim()
3864
+ : null,
3828
3865
  taxonomies: JSON.parse(row.taxonomies || '{}'),
3829
3866
  createdBy: this.normalizeCreatedBy(row.created_by, workspaceId, { preserveGenericAi: true }),
3830
3867
  createdByActor: this.resolveWorkspaceActor(workspaceId, row.created_by),
@@ -3857,6 +3894,11 @@ export class TaskforceCore {
3857
3894
  title: String(row.title || '').trim() || 'Untitled Initiative',
3858
3895
  description: typeof row.description === 'string' ? row.description : null,
3859
3896
  ownerId: typeof row.owner_id === 'string' && row.owner_id.trim().length > 0 ? row.owner_id.trim() : null,
3897
+ icon: typeof row.identity_icon === 'string' && row.identity_icon.trim().length > 0 ? row.identity_icon.trim() : null,
3898
+ color: typeof row.identity_color === 'string' && row.identity_color.trim().length > 0 ? row.identity_color.trim() : null,
3899
+ identityUpdatedAt: typeof row.identity_updated_at === 'string' && row.identity_updated_at.trim().length > 0
3900
+ ? row.identity_updated_at.trim()
3901
+ : null,
3860
3902
  createdAt: String(row.created_at || '').trim() || new Date().toISOString(),
3861
3903
  updatedAt: typeof row.updated_at === 'string' && row.updated_at.trim().length > 0 ? row.updated_at.trim() : undefined,
3862
3904
  order: row.sort_order == null ? null : Number(row.sort_order),
@@ -3890,6 +3932,11 @@ export class TaskforceCore {
3890
3932
  title: String(row.title || '').trim() || 'Untitled Workstream',
3891
3933
  description: typeof row.description === 'string' ? row.description : null,
3892
3934
  ownerId: typeof row.owner_id === 'string' && row.owner_id.trim().length > 0 ? row.owner_id.trim() : null,
3935
+ icon: typeof row.identity_icon === 'string' && row.identity_icon.trim().length > 0 ? row.identity_icon.trim() : null,
3936
+ color: typeof row.identity_color === 'string' && row.identity_color.trim().length > 0 ? row.identity_color.trim() : null,
3937
+ identityUpdatedAt: typeof row.identity_updated_at === 'string' && row.identity_updated_at.trim().length > 0
3938
+ ? row.identity_updated_at.trim()
3939
+ : null,
3893
3940
  createdAt: String(row.created_at || '').trim() || new Date().toISOString(),
3894
3941
  updatedAt: typeof row.updated_at === 'string' && row.updated_at.trim().length > 0 ? row.updated_at.trim() : undefined,
3895
3942
  order: row.sort_order == null ? null : Number(row.sort_order),
@@ -3945,9 +3992,15 @@ export class TaskforceCore {
3945
3992
  return null;
3946
3993
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
3947
3994
  const row = this.db.prepare(`
3948
- SELECT *
3949
- FROM initiatives
3950
- WHERE tenant_id = ? AND workspace_id = ? AND id = ?
3995
+ SELECT i.*, identity.icon AS identity_icon, identity.color AS identity_color,
3996
+ identity.updated_at AS identity_updated_at
3997
+ FROM initiatives i
3998
+ LEFT JOIN planning_entity_identities identity
3999
+ ON identity.tenant_id = i.tenant_id
4000
+ AND identity.workspace_id = i.workspace_id
4001
+ AND identity.entity_type = 'initiative'
4002
+ AND identity.entity_id = i.id
4003
+ WHERE i.tenant_id = ? AND i.workspace_id = ? AND i.id = ?
3951
4004
  LIMIT 1
3952
4005
  `).get(this.tenantId, normalizedWorkspaceId, normalizedId);
3953
4006
  if (!row)
@@ -3957,10 +4010,16 @@ export class TaskforceCore {
3957
4010
  listInitiatives(workspaceId) {
3958
4011
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
3959
4012
  const rows = this.db.prepare(`
3960
- SELECT *
3961
- FROM initiatives
3962
- WHERE tenant_id = ? AND workspace_id = ?
3963
- ORDER BY COALESCE(sort_order, 2147483647), created_at ASC
4013
+ SELECT i.*, identity.icon AS identity_icon, identity.color AS identity_color,
4014
+ identity.updated_at AS identity_updated_at
4015
+ FROM initiatives i
4016
+ LEFT JOIN planning_entity_identities identity
4017
+ ON identity.tenant_id = i.tenant_id
4018
+ AND identity.workspace_id = i.workspace_id
4019
+ AND identity.entity_type = 'initiative'
4020
+ AND identity.entity_id = i.id
4021
+ WHERE i.tenant_id = ? AND i.workspace_id = ?
4022
+ ORDER BY COALESCE(i.sort_order, 2147483647), i.created_at ASC
3964
4023
  `).all(this.tenantId, normalizedWorkspaceId);
3965
4024
  return rows.map((row) => this.rowToInitiative(row));
3966
4025
  }
@@ -3968,10 +4027,16 @@ export class TaskforceCore {
3968
4027
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
3969
4028
  const safeLimit = Math.max(1, Math.min(10000, Math.floor(limit)));
3970
4029
  const rows = this.db.prepare(`
3971
- SELECT *
3972
- FROM initiatives
3973
- WHERE tenant_id = ? AND workspace_id = ? AND id > ?
3974
- ORDER BY id ASC
4030
+ SELECT i.*, identity.icon AS identity_icon, identity.color AS identity_color,
4031
+ identity.updated_at AS identity_updated_at
4032
+ FROM initiatives i
4033
+ LEFT JOIN planning_entity_identities identity
4034
+ ON identity.tenant_id = i.tenant_id
4035
+ AND identity.workspace_id = i.workspace_id
4036
+ AND identity.entity_type = 'initiative'
4037
+ AND identity.entity_id = i.id
4038
+ WHERE i.tenant_id = ? AND i.workspace_id = ? AND i.id > ?
4039
+ ORDER BY i.id ASC
3975
4040
  LIMIT ?
3976
4041
  `).all(this.tenantId, normalizedWorkspaceId, String(afterId || ''), safeLimit);
3977
4042
  return rows.map((row) => this.rowToInitiative(row));
@@ -4234,9 +4299,15 @@ export class TaskforceCore {
4234
4299
  return null;
4235
4300
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4236
4301
  const row = this.db.prepare(`
4237
- SELECT *
4238
- FROM workstreams
4239
- WHERE tenant_id = ? AND workspace_id = ? AND id = ?
4302
+ SELECT w.*, identity.icon AS identity_icon, identity.color AS identity_color,
4303
+ identity.updated_at AS identity_updated_at
4304
+ FROM workstreams w
4305
+ LEFT JOIN planning_entity_identities identity
4306
+ ON identity.tenant_id = w.tenant_id
4307
+ AND identity.workspace_id = w.workspace_id
4308
+ AND identity.entity_type = 'workstream'
4309
+ AND identity.entity_id = w.id
4310
+ WHERE w.tenant_id = ? AND w.workspace_id = ? AND w.id = ?
4240
4311
  LIMIT 1
4241
4312
  `).get(this.tenantId, normalizedWorkspaceId, normalizedId);
4242
4313
  if (!row)
@@ -4246,10 +4317,16 @@ export class TaskforceCore {
4246
4317
  listWorkstreams(workspaceId) {
4247
4318
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4248
4319
  const rows = this.db.prepare(`
4249
- SELECT *
4250
- FROM workstreams
4251
- WHERE tenant_id = ? AND workspace_id = ?
4252
- ORDER BY COALESCE(sort_order, 2147483647), created_at ASC
4320
+ SELECT w.*, identity.icon AS identity_icon, identity.color AS identity_color,
4321
+ identity.updated_at AS identity_updated_at
4322
+ FROM workstreams w
4323
+ LEFT JOIN planning_entity_identities identity
4324
+ ON identity.tenant_id = w.tenant_id
4325
+ AND identity.workspace_id = w.workspace_id
4326
+ AND identity.entity_type = 'workstream'
4327
+ AND identity.entity_id = w.id
4328
+ WHERE w.tenant_id = ? AND w.workspace_id = ?
4329
+ ORDER BY COALESCE(w.sort_order, 2147483647), w.created_at ASC
4253
4330
  `).all(this.tenantId, normalizedWorkspaceId);
4254
4331
  return rows.map((row) => this.rowToWorkstream(row));
4255
4332
  }
@@ -4257,14 +4334,157 @@ export class TaskforceCore {
4257
4334
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4258
4335
  const safeLimit = Math.max(1, Math.min(10000, Math.floor(limit)));
4259
4336
  const rows = this.db.prepare(`
4260
- SELECT *
4261
- FROM workstreams
4262
- WHERE tenant_id = ? AND workspace_id = ? AND id > ?
4263
- ORDER BY id ASC
4337
+ SELECT w.*, identity.icon AS identity_icon, identity.color AS identity_color,
4338
+ identity.updated_at AS identity_updated_at
4339
+ FROM workstreams w
4340
+ LEFT JOIN planning_entity_identities identity
4341
+ ON identity.tenant_id = w.tenant_id
4342
+ AND identity.workspace_id = w.workspace_id
4343
+ AND identity.entity_type = 'workstream'
4344
+ AND identity.entity_id = w.id
4345
+ WHERE w.tenant_id = ? AND w.workspace_id = ? AND w.id > ?
4346
+ ORDER BY w.id ASC
4264
4347
  LIMIT ?
4265
4348
  `).all(this.tenantId, normalizedWorkspaceId, String(afterId || ''), safeLimit);
4266
4349
  return rows.map((row) => this.rowToWorkstream(row));
4267
4350
  }
4351
+ listPlanningEntityIdentities(workspaceId) {
4352
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4353
+ const rows = this.db.prepare(`
4354
+ SELECT entity_type, entity_id, icon, color, updated_at
4355
+ FROM planning_entity_identities
4356
+ WHERE tenant_id = ? AND workspace_id = ?
4357
+ ORDER BY updated_at ASC, entity_type ASC, entity_id ASC
4358
+ `).all(this.tenantId, normalizedWorkspaceId);
4359
+ return rows.map((row) => ({
4360
+ entityType: row.entity_type === 'workstream' ? 'workstream' : 'initiative',
4361
+ entityId: String(row.entity_id || '').trim(),
4362
+ icon: assertPlanningIdentityIcon(row.icon),
4363
+ color: assertPlanningIdentityColor(row.color),
4364
+ updatedAt: String(row.updated_at || '').trim(),
4365
+ }));
4366
+ }
4367
+ writePlanningEntityIdentity(identity, workspaceId) {
4368
+ const existing = this.db.prepare(`
4369
+ SELECT icon, color, updated_at
4370
+ FROM planning_entity_identities
4371
+ WHERE tenant_id = ? AND workspace_id = ? AND entity_type = ? AND entity_id = ?
4372
+ LIMIT 1
4373
+ `).get(this.tenantId, workspaceId, identity.entityType, identity.entityId);
4374
+ if (existing) {
4375
+ this.db.prepare(`
4376
+ UPDATE planning_entity_identities
4377
+ SET icon = ?, color = ?, updated_at = ?
4378
+ WHERE tenant_id = ? AND workspace_id = ? AND entity_type = ? AND entity_id = ?
4379
+ `).run(identity.icon, identity.color, identity.updatedAt, this.tenantId, workspaceId, identity.entityType, identity.entityId);
4380
+ }
4381
+ else {
4382
+ this.db.prepare(`
4383
+ INSERT INTO planning_entity_identities (
4384
+ tenant_id, workspace_id, entity_type, entity_id, icon, color, updated_at
4385
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
4386
+ `).run(this.tenantId, workspaceId, identity.entityType, identity.entityId, identity.icon, identity.color, identity.updatedAt);
4387
+ }
4388
+ return identity;
4389
+ }
4390
+ updatePlanningEntityIdentity(entityType, entityId, patch, workspaceId) {
4391
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4392
+ const normalizedEntityId = String(entityId || '').trim();
4393
+ const entity = entityType === 'initiative'
4394
+ ? this.getInitiative(normalizedEntityId, normalizedWorkspaceId)
4395
+ : this.getWorkstream(normalizedEntityId, normalizedWorkspaceId);
4396
+ if (!entity) {
4397
+ throw new TaskforceRuleError(`${entityType === 'initiative' ? 'Initiative' : 'Workstream'} not found: ${normalizedEntityId}`, 404, entityType === 'initiative' ? 'INITIATIVE_NOT_FOUND' : 'WORKSTREAM_NOT_FOUND');
4398
+ }
4399
+ if (entity.isArchived) {
4400
+ throw new TaskforceRuleError('Restore the planning entity before changing its visual identity.', 409, 'PLANNING_IDENTITY_ARCHIVED');
4401
+ }
4402
+ if (!Object.prototype.hasOwnProperty.call(patch, 'icon')
4403
+ && !Object.prototype.hasOwnProperty.call(patch, 'color')) {
4404
+ throw new TaskforceRuleError('Planning identity update must include icon or color.', 400, 'PLANNING_IDENTITY_INVALID');
4405
+ }
4406
+ let icon;
4407
+ let color;
4408
+ try {
4409
+ icon = Object.prototype.hasOwnProperty.call(patch, 'icon')
4410
+ ? assertPlanningIdentityIcon(patch.icon)
4411
+ : assertPlanningIdentityIcon(entity.icon);
4412
+ color = Object.prototype.hasOwnProperty.call(patch, 'color')
4413
+ ? assertPlanningIdentityColor(patch.color)
4414
+ : assertPlanningIdentityColor(entity.color);
4415
+ }
4416
+ catch (error) {
4417
+ throw new TaskforceRuleError(error instanceof Error ? error.message : 'Planning identity is invalid.', 400, 'PLANNING_IDENTITY_INVALID');
4418
+ }
4419
+ this.writePlanningEntityIdentity({
4420
+ entityType,
4421
+ entityId: normalizedEntityId,
4422
+ icon,
4423
+ color,
4424
+ updatedAt: new Date().toISOString(),
4425
+ }, normalizedWorkspaceId);
4426
+ this.emitRealtimeMutation({
4427
+ workspaceId: normalizedWorkspaceId,
4428
+ entityType,
4429
+ changeType: 'identity-updated',
4430
+ entityIds: [normalizedEntityId],
4431
+ affectedCollections: ['planning'],
4432
+ });
4433
+ return entityType === 'initiative'
4434
+ ? this.getInitiative(normalizedEntityId, normalizedWorkspaceId)
4435
+ : this.getWorkstream(normalizedEntityId, normalizedWorkspaceId);
4436
+ }
4437
+ applyPlanningEntityIdentityForSync(identity, workspaceId) {
4438
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId || undefined);
4439
+ const entityType = identity.entityType === 'workstream' ? 'workstream' : 'initiative';
4440
+ const entityId = String(identity.entityId || '').trim();
4441
+ const updatedAt = String(identity.updatedAt || '').trim();
4442
+ if (!entityId || !Number.isFinite(Date.parse(updatedAt))) {
4443
+ throw new TaskforceRuleError('Planning identity sync payload is invalid.', 400, 'PLANNING_IDENTITY_INVALID');
4444
+ }
4445
+ const entityExists = entityType === 'initiative'
4446
+ ? this.initiativeExistsForSync(entityId, normalizedWorkspaceId)
4447
+ : this.workstreamExistsForSync(entityId, normalizedWorkspaceId);
4448
+ if (!entityExists)
4449
+ return null;
4450
+ const incoming = {
4451
+ entityType,
4452
+ entityId,
4453
+ icon: assertPlanningIdentityIcon(identity.icon),
4454
+ color: assertPlanningIdentityColor(identity.color),
4455
+ updatedAt: new Date(Date.parse(updatedAt)).toISOString(),
4456
+ };
4457
+ const existing = this.db.prepare(`
4458
+ SELECT icon, color, updated_at
4459
+ FROM planning_entity_identities
4460
+ WHERE tenant_id = ? AND workspace_id = ? AND entity_type = ? AND entity_id = ?
4461
+ LIMIT 1
4462
+ `).get(this.tenantId, normalizedWorkspaceId, entityType, entityId);
4463
+ if (existing) {
4464
+ const existingUpdatedAt = String(existing.updated_at || '').trim();
4465
+ const timeComparison = incoming.updatedAt.localeCompare(existingUpdatedAt);
4466
+ const incomingTieBreak = `${incoming.icon || ''}\u0000${incoming.color || ''}`;
4467
+ const existingTieBreak = `${String(existing.icon || '')}\u0000${String(existing.color || '')}`;
4468
+ if (timeComparison < 0 || (timeComparison === 0 && incomingTieBreak <= existingTieBreak)) {
4469
+ return {
4470
+ entityType,
4471
+ entityId,
4472
+ icon: assertPlanningIdentityIcon(existing.icon),
4473
+ color: assertPlanningIdentityColor(existing.color),
4474
+ updatedAt: existingUpdatedAt,
4475
+ };
4476
+ }
4477
+ }
4478
+ const applied = this.writePlanningEntityIdentity(incoming, normalizedWorkspaceId);
4479
+ this.emitRealtimeMutation({
4480
+ workspaceId: normalizedWorkspaceId,
4481
+ entityType,
4482
+ changeType: 'identity-updated',
4483
+ entityIds: [entityId],
4484
+ affectedCollections: ['planning'],
4485
+ });
4486
+ return applied;
4487
+ }
4268
4488
  listPlanningBootstrap(workspaceId, diagnostics) {
4269
4489
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4270
4490
  const startedAt = Date.now();
@@ -5302,6 +5522,10 @@ export class TaskforceCore {
5302
5522
  }
5303
5523
  return apply(durableDeleteOwnedKeys);
5304
5524
  })(),
5525
+ listDurableDeleteOwnedTaskIds: (workspaceId) => new Set(new WorkspaceSyncDurabilityStore(this.db)
5526
+ .listTaskMetadataOwnership({ tenantId: this.tenantId, workspaceId })
5527
+ .filter((ownership) => ownership.activeDeleteOperation || ownership.deleteOwned)
5528
+ .map((ownership) => ownership.taskId)),
5305
5529
  };
5306
5530
  }
5307
5531
  getSyncReconciliationServiceDeps() {
@@ -5360,9 +5584,11 @@ export class TaskforceCore {
5360
5584
  const clauses = [
5361
5585
  'tenant_id = ?',
5362
5586
  'workspace_id = ?',
5363
- 'is_archived = 0',
5364
5587
  ];
5365
5588
  const params = [this.tenantId, normalizedWorkspaceId];
5589
+ if (!filters.includeArchived) {
5590
+ clauses.push('is_archived = 0');
5591
+ }
5366
5592
  if (filters.category) {
5367
5593
  clauses.push('category = ?');
5368
5594
  params.push(filters.category);
@@ -5467,6 +5693,114 @@ export class TaskforceCore {
5467
5693
  };
5468
5694
  });
5469
5695
  }
5696
+ listTaskCountsByWorkstream(workspaceId, workstreamId) {
5697
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
5698
+ const clauses = [
5699
+ 'tenant_id = ?',
5700
+ 'workspace_id = ?',
5701
+ 'workstream_id IS NOT NULL',
5702
+ "TRIM(workstream_id) <> ''",
5703
+ ];
5704
+ const params = [this.tenantId, normalizedWorkspaceId];
5705
+ const normalizedWorkstreamId = this.sanitizeWorkstreamId(workstreamId);
5706
+ if (normalizedWorkstreamId) {
5707
+ clauses.push('workstream_id = ?');
5708
+ params.push(normalizedWorkstreamId);
5709
+ }
5710
+ const rows = this.db.prepare(`
5711
+ SELECT
5712
+ workstream_id,
5713
+ COUNT(*) AS total,
5714
+ SUM(CASE WHEN is_archived = 0 THEN 1 ELSE 0 END) AS active,
5715
+ SUM(CASE WHEN is_archived = 1 THEN 1 ELSE 0 END) AS archived
5716
+ FROM tasks
5717
+ WHERE ${clauses.join(' AND ')}
5718
+ GROUP BY workstream_id
5719
+ `).all(...params);
5720
+ return rows.map((row) => ({
5721
+ workstreamId: String(row.workstream_id || '').trim(),
5722
+ total: Number(row.total || 0),
5723
+ active: Number(row.active || 0),
5724
+ archived: Number(row.archived || 0),
5725
+ }));
5726
+ }
5727
+ listTaskReferenceSummaries(taskIds, workspaceId) {
5728
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
5729
+ const normalizedIds = Array.from(new Set(taskIds.map((id) => String(id || '').trim()).filter(Boolean)));
5730
+ if (normalizedIds.length === 0)
5731
+ return [];
5732
+ const placeholders = normalizedIds.map(() => '?').join(', ');
5733
+ const rows = this.db.prepare(`
5734
+ SELECT id, reference_number, local_reference_number, title, status
5735
+ FROM tasks
5736
+ WHERE tenant_id = ?
5737
+ AND workspace_id = ?
5738
+ AND id IN (${placeholders})
5739
+ `).all(this.tenantId, normalizedWorkspaceId, ...normalizedIds);
5740
+ return rows.map((row) => {
5741
+ const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
5742
+ const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
5743
+ return {
5744
+ id: String(row.id || '').trim(),
5745
+ referenceNumber,
5746
+ localReferenceNumber,
5747
+ referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
5748
+ title: String(row.title || ''),
5749
+ status: this.normalizeStatusValue(row.status),
5750
+ };
5751
+ });
5752
+ }
5753
+ searchTaskSummaries(query, scope = 'all', workspaceId) {
5754
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
5755
+ const normalizedQuery = String(query || '').trim();
5756
+ if (!normalizedQuery)
5757
+ return [];
5758
+ const exactReferenceNumber = parseTaskReference(normalizedQuery);
5759
+ const exactLocalReferenceNumber = parseLocalTaskReference(normalizedQuery);
5760
+ const term = `%${normalizedQuery}%`;
5761
+ const clauses = [
5762
+ 'tenant_id = ?',
5763
+ 'workspace_id = ?',
5764
+ '(title LIKE ? OR description LIKE ? OR id LIKE ? OR reference_number = ? OR local_reference_number = ?)',
5765
+ ];
5766
+ const params = [
5767
+ this.tenantId,
5768
+ normalizedWorkspaceId,
5769
+ term,
5770
+ term,
5771
+ term,
5772
+ exactReferenceNumber || -1,
5773
+ exactLocalReferenceNumber || -1,
5774
+ ];
5775
+ if (scope === 'active')
5776
+ clauses.push('is_archived = 0');
5777
+ if (scope === 'archive')
5778
+ clauses.push('is_archived = 1');
5779
+ const rows = this.db.prepare(`
5780
+ SELECT id, reference_number, local_reference_number, title, status, assignee, category, priority
5781
+ FROM tasks
5782
+ WHERE ${clauses.join(' AND ')}
5783
+ ORDER BY
5784
+ CASE WHEN id = ? OR reference_number = ? OR local_reference_number = ? THEN 0 ELSE 1 END,
5785
+ created_at DESC
5786
+ LIMIT 50
5787
+ `).all(...params, normalizedQuery, exactReferenceNumber || -1, exactLocalReferenceNumber || -1);
5788
+ return rows.map((row) => {
5789
+ const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
5790
+ const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
5791
+ return {
5792
+ id: String(row.id || '').trim(),
5793
+ referenceNumber,
5794
+ localReferenceNumber,
5795
+ referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
5796
+ title: String(row.title || ''),
5797
+ status: this.normalizeStatusValue(row.status),
5798
+ assignee: String(row.assignee || '').trim() || null,
5799
+ category: String(row.category || ''),
5800
+ priority: this.normalizePriorityValue(row.priority),
5801
+ };
5802
+ });
5803
+ }
5470
5804
  getDataVersion() {
5471
5805
  if (this.db.provider === 'sqlite') {
5472
5806
  const version = this.db.pragma('data_version', { simple: true });
@@ -5511,6 +5845,9 @@ export class TaskforceCore {
5511
5845
  listDeletedTasks(workspaceId) {
5512
5846
  return listDeletedTasksService(this.getDeletedTaskLifecycleServiceDeps(), workspaceId);
5513
5847
  }
5848
+ listDeletedTasksReadOnly(workspaceId) {
5849
+ return listDeletedTasksReadOnlyService(this.getDeletedTaskLifecycleServiceDeps(), workspaceId);
5850
+ }
5514
5851
  getDeletedTask(id, workspaceId) {
5515
5852
  return getDeletedTaskService(this.getDeletedTaskLifecycleServiceDeps(), id, workspaceId);
5516
5853
  }
@@ -5594,11 +5931,13 @@ export class TaskforceCore {
5594
5931
  const deletedStatsSql = this.db.provider === 'postgres'
5595
5932
  ? `SELECT count(*) as total,
5596
5933
  sum(case when task_snapshot_json::jsonb ->> 'status' = 'done' then 1 else 0 end) as done_count,
5597
- sum(case when task_snapshot_json::jsonb ->> 'status' = 'cancelled' then 1 else 0 end) as cancelled_count
5934
+ sum(case when task_snapshot_json::jsonb ->> 'status' = 'cancelled' then 1 else 0 end) as cancelled_count,
5935
+ sum(case when coalesce(task_snapshot_json::jsonb ->> 'status', '') not in ('done', 'cancelled') then 1 else 0 end) as other_count
5598
5936
  FROM deleted_tasks WHERE tenant_id = ? AND workspace_id = ?`
5599
5937
  : `SELECT count(*) as total,
5600
5938
  sum(case when json_extract(task_snapshot_json, '$.status') = 'done' then 1 else 0 end) as done_count,
5601
- sum(case when json_extract(task_snapshot_json, '$.status') = 'cancelled' then 1 else 0 end) as cancelled_count
5939
+ sum(case when json_extract(task_snapshot_json, '$.status') = 'cancelled' then 1 else 0 end) as cancelled_count,
5940
+ sum(case when coalesce(json_extract(task_snapshot_json, '$.status'), '') not in ('done', 'cancelled') then 1 else 0 end) as other_count
5602
5941
  FROM deleted_tasks WHERE tenant_id = ? AND workspace_id = ?`;
5603
5942
  const deletedStats = this.db.prepare(deletedStatsSql).get(this.tenantId, normalizedWorkspaceId);
5604
5943
  return {
@@ -5612,6 +5951,7 @@ export class TaskforceCore {
5612
5951
  total: Number(deletedStats.total || 0),
5613
5952
  doneCount: Number(deletedStats.done_count || 0),
5614
5953
  cancelledCount: Number(deletedStats.cancelled_count || 0),
5954
+ otherCount: Number(deletedStats.other_count || 0),
5615
5955
  retentionDays: DELETED_TASK_RETENTION_DAYS
5616
5956
  }
5617
5957
  };
@@ -7835,6 +8175,9 @@ export class TaskforceCore {
7835
8175
  bindMcpOAuthConnectorGrantAiProfile(input) {
7836
8176
  return this.mcpTokenService.bindMcpOAuthConnectorGrantAiProfile(input);
7837
8177
  }
8178
+ listMcpOAuthRestorableAiProfileIds(input) {
8179
+ return this.mcpTokenService.listMcpOAuthRestorableAiProfileIds(input);
8180
+ }
7838
8181
  recordMcpOAuthConnectorGrantUsage(input) {
7839
8182
  return this.mcpTokenService.recordMcpOAuthConnectorGrantUsage(input);
7840
8183
  }
@@ -8566,6 +8909,12 @@ export class TaskforceCore {
8566
8909
  return inferredAssignee || 'unassigned';
8567
8910
  })();
8568
8911
  const normalizedAttachments = normalizedInput.attachments ?? [];
8912
+ const requestedCardCoverAssetId = typeof normalizedInput.cardCoverAssetId === 'string'
8913
+ ? normalizedInput.cardCoverAssetId.trim()
8914
+ : null;
8915
+ if (requestedCardCoverAssetId && !normalizedAttachments.some((item) => (typeof item !== 'string' && item.assetId === requestedCardCoverAssetId && item.linkRole === 'image'))) {
8916
+ throw new TaskforceRuleError('Task card cover must reference an image already attached to this task.', 400, 'TASK_CARD_COVER_INVALID');
8917
+ }
8569
8918
  const normalizedComments = this.normalizeIncomingComments(normalizedInput.comments, normalizedWorkspaceId);
8570
8919
  const normalizedDescription = this.normalizeGeneratedDescription(normalizedInput.description, createdBySignal);
8571
8920
  const workstreamId = this.sanitizeWorkstreamId(normalizedInput.workstreamId);
@@ -8598,6 +8947,7 @@ export class TaskforceCore {
8598
8947
  ? this.nextWorkstreamTaskOrder(workstreamId, normalizedWorkspaceId)
8599
8948
  : null,
8600
8949
  workstreamId,
8950
+ cardCoverAssetId: requestedCardCoverAssetId,
8601
8951
  checklistItems: Array.isArray(normalizedInput.checklistItems)
8602
8952
  ? normalizedInput.checklistItems.map((item, index) => ({
8603
8953
  id: String(item?.id || this.generateEntityId('checklist')),
@@ -8628,7 +8978,7 @@ export class TaskforceCore {
8628
8978
  local_reference_number,
8629
8979
  created_at, updated_at, is_archived, taxonomies, created_by, assignee,
8630
8980
  scheduled_date, due_date, scheduled_week_key, order_in_day, workstream_order,
8631
- workstream_id
8981
+ workstream_id, card_cover_asset_id
8632
8982
  ) VALUES (
8633
8983
  @id, @tenantId, @title, @description, @status, @priority, @complexity, @type, @category,
8634
8984
  @workspaceId,
@@ -8636,7 +8986,7 @@ export class TaskforceCore {
8636
8986
  @localReferenceNumber,
8637
8987
  @createdAt, @updatedAt, 0, @taxonomies, @createdBy, @assignee,
8638
8988
  @scheduledDate, @dueDate, @scheduledWeekKey, @orderInDay, @workstreamOrder,
8639
- @workstreamId
8989
+ @workstreamId, @cardCoverAssetId
8640
8990
  )
8641
8991
  `);
8642
8992
  stmt.run({
@@ -9422,6 +9772,23 @@ export class TaskforceCore {
9422
9772
  if (Object.prototype.hasOwnProperty.call(patch, 'workstreamOrder')) {
9423
9773
  patch.workstreamOrder = this.sanitizeOrderInDay(patch.workstreamOrder);
9424
9774
  }
9775
+ if (Object.prototype.hasOwnProperty.call(patch, 'cardCoverAssetId')) {
9776
+ const requestedCoverAssetId = typeof patch.cardCoverAssetId === 'string'
9777
+ ? patch.cardCoverAssetId.trim()
9778
+ : null;
9779
+ if (requestedCoverAssetId) {
9780
+ const attachment = (patch.attachments || existing.attachments || []).find((item) => (typeof item !== 'string' && item.assetId === requestedCoverAssetId && item.linkRole === 'image'));
9781
+ if (!attachment) {
9782
+ throw new TaskforceRuleError('Task card cover must reference an image already attached to this task.', 400, 'TASK_CARD_COVER_INVALID');
9783
+ }
9784
+ }
9785
+ patch.cardCoverAssetId = requestedCoverAssetId;
9786
+ }
9787
+ else if (Object.prototype.hasOwnProperty.call(patch, 'attachments') && existing.cardCoverAssetId) {
9788
+ const coverStillAttached = (patch.attachments || []).some((item) => (typeof item !== 'string' && item.assetId === existing.cardCoverAssetId && item.linkRole === 'image'));
9789
+ if (!coverStillAttached)
9790
+ patch.cardCoverAssetId = null;
9791
+ }
9425
9792
  const hasScheduledDatePatch = Object.prototype.hasOwnProperty.call(patch, 'scheduledDate');
9426
9793
  const hasScheduledWeekKeyPatch = Object.prototype.hasOwnProperty.call(patch, 'scheduledWeekKey');
9427
9794
  const hasOrderInDayPatch = Object.prototype.hasOwnProperty.call(patch, 'orderInDay');
@@ -9486,7 +9853,7 @@ export class TaskforceCore {
9486
9853
  const mappableFields = [
9487
9854
  'title', 'description', 'status', 'completedAt', 'priority', 'complexity', 'type', 'category',
9488
9855
  'canceledReason', 'createdBy', 'assignee', 'scheduledDate', 'dueDate',
9489
- 'scheduledWeekKey', 'orderInDay', 'workstreamOrder', 'workstreamId', 'referenceNumber', 'localReferenceNumber'
9856
+ 'scheduledWeekKey', 'orderInDay', 'workstreamOrder', 'workstreamId', 'cardCoverAssetId', 'referenceNumber', 'localReferenceNumber'
9490
9857
  ];
9491
9858
  const propertyToColumn = {
9492
9859
  canceledReason: 'canceled_reason',
@@ -9500,6 +9867,7 @@ export class TaskforceCore {
9500
9867
  orderInDay: 'order_in_day',
9501
9868
  workstreamOrder: 'workstream_order',
9502
9869
  workstreamId: 'workstream_id',
9870
+ cardCoverAssetId: 'card_cover_asset_id',
9503
9871
  };
9504
9872
  mappableFields.forEach(f => {
9505
9873
  if (patch[f] !== undefined) {
@@ -10398,6 +10766,123 @@ export class TaskforceCore {
10398
10766
  listTasksForSync(workspaceId, cursor, limit) {
10399
10767
  return listTasksForSyncService(this.getSyncReconciliationServiceDeps(), workspaceId, cursor, limit);
10400
10768
  }
10769
+ /**
10770
+ * Full task inventory for authoritative repair without the per-task UI
10771
+ * hydration performed by rowToTask. Repair serializes task roots, comments,
10772
+ * and checklist items separately; task activity, actor decoration, and
10773
+ * resolved attachment bodies are not part of that canonical inventory.
10774
+ */
10775
+ listTasksForAuthoritativeRepair(workspaceId) {
10776
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
10777
+ const rows = this.db.prepare(`
10778
+ SELECT *
10779
+ FROM tasks
10780
+ WHERE tenant_id = ? AND workspace_id = ?
10781
+ ORDER BY COALESCE(updated_at, created_at) ASC, id ASC
10782
+ `).all(this.tenantId, normalizedWorkspaceId);
10783
+ if (rows.length === 0)
10784
+ return [];
10785
+ const commentsByTaskId = new Map();
10786
+ const checklistByTaskId = new Map();
10787
+ const taskIds = rows.map((row) => String(row.id || '').trim()).filter(Boolean);
10788
+ const batchSize = workspaceSyncV3RepairEntityReadBatchSize(this.db);
10789
+ const normalizedCommentAuthor = new Map();
10790
+ for (let offset = 0; offset < taskIds.length; offset += batchSize) {
10791
+ const batch = taskIds.slice(offset, offset + batchSize);
10792
+ const placeholders = batch.map(() => '?').join(', ');
10793
+ const commentRows = this.db.prepare(`
10794
+ SELECT id, task_id, author, submitted_author, text, timestamp
10795
+ FROM comments
10796
+ WHERE tenant_id = ? AND workspace_id = ? AND task_id IN (${placeholders})
10797
+ ORDER BY task_id ASC, timestamp ASC
10798
+ `).all(this.tenantId, normalizedWorkspaceId, ...batch);
10799
+ for (const comment of commentRows) {
10800
+ const taskId = String(comment.task_id || '').trim();
10801
+ const rawAuthor = String(comment.author || 'user');
10802
+ let author = normalizedCommentAuthor.get(rawAuthor);
10803
+ if (!author) {
10804
+ author = this.normalizeCommentAuthor(rawAuthor, normalizedWorkspaceId);
10805
+ normalizedCommentAuthor.set(rawAuthor, author);
10806
+ }
10807
+ const items = commentsByTaskId.get(taskId) || [];
10808
+ items.push({
10809
+ id: String(comment.id || '').trim(),
10810
+ author,
10811
+ submittedAuthor: String(comment.submitted_author || comment.author || 'user').trim() || 'user',
10812
+ text: String(comment.text || ''),
10813
+ timestamp: String(comment.timestamp || ''),
10814
+ });
10815
+ commentsByTaskId.set(taskId, items);
10816
+ }
10817
+ const checklistRows = this.db.prepare(`
10818
+ SELECT id, task_id, title, is_completed, sort_order, created_at, updated_at
10819
+ FROM checklist_items
10820
+ WHERE tenant_id = ? AND workspace_id = ? AND task_id IN (${placeholders})
10821
+ ORDER BY task_id ASC, sort_order ASC, created_at ASC
10822
+ `).all(this.tenantId, normalizedWorkspaceId, ...batch);
10823
+ for (const checklistRow of checklistRows) {
10824
+ const item = this.rowToChecklistItem(checklistRow);
10825
+ const items = checklistByTaskId.get(item.taskId) || [];
10826
+ items.push(item);
10827
+ checklistByTaskId.set(item.taskId, items);
10828
+ }
10829
+ }
10830
+ const sanitizedAssigneeByValue = new Map();
10831
+ const normalizedCreatedBy = new Map();
10832
+ return rows.map((row) => {
10833
+ const taskId = String(row.id || '').trim();
10834
+ const rawAssignee = String(row.assignee || '');
10835
+ let sanitizedAssignee = sanitizedAssigneeByValue.get(rawAssignee);
10836
+ if (sanitizedAssignee === undefined) {
10837
+ sanitizedAssignee = this.sanitizeAssignee(row.assignee, normalizedWorkspaceId);
10838
+ sanitizedAssigneeByValue.set(rawAssignee, sanitizedAssignee);
10839
+ }
10840
+ const assignee = this.repairTaskAssignee(taskId, normalizedWorkspaceId, row.assignee, sanitizedAssignee) ? sanitizedAssignee : String(row.assignee || '').trim() || 'unassigned';
10841
+ const rawCreatedBy = String(row.created_by || '');
10842
+ if (!normalizedCreatedBy.has(rawCreatedBy)) {
10843
+ normalizedCreatedBy.set(rawCreatedBy, this.normalizeCreatedBy(row.created_by, normalizedWorkspaceId, { preserveGenericAi: true }));
10844
+ }
10845
+ const scheduledDate = this.sanitizeDateOnly(row.scheduled_date);
10846
+ const dueDate = this.sanitizeDateOnly(row.due_date);
10847
+ const scheduledWeekKey = row.scheduled_week_key
10848
+ || (scheduledDate ? this.dateOnlyToIsoWeekKey(scheduledDate) : null);
10849
+ const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
10850
+ const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
10851
+ return {
10852
+ id: taskId,
10853
+ referenceNumber,
10854
+ localReferenceNumber,
10855
+ referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
10856
+ title: row.title,
10857
+ description: row.description,
10858
+ status: this.normalizeStatusValue(row.status),
10859
+ priority: this.normalizePriorityValue(row.priority),
10860
+ complexity: row.complexity,
10861
+ type: row.type,
10862
+ category: row.category,
10863
+ createdAt: row.created_at,
10864
+ updatedAt: row.updated_at,
10865
+ completedAt: row.completed_at,
10866
+ canceledReason: row.canceled_reason,
10867
+ isArchived: row.is_archived === 1,
10868
+ scheduledDate,
10869
+ dueDate,
10870
+ scheduledWeekKey,
10871
+ orderInDay: this.sanitizeOrderInDay(row.order_in_day),
10872
+ workstreamOrder: this.sanitizeOrderInDay(row.workstream_order),
10873
+ workstreamId: this.sanitizeWorkstreamId(row.workstream_id),
10874
+ cardCoverAssetId: typeof row.card_cover_asset_id === 'string' && row.card_cover_asset_id.trim()
10875
+ ? row.card_cover_asset_id.trim()
10876
+ : null,
10877
+ taxonomies: JSON.parse(row.taxonomies || '{}'),
10878
+ createdBy: normalizedCreatedBy.get(rawCreatedBy),
10879
+ assignee,
10880
+ comments: commentsByTaskId.get(taskId) || [],
10881
+ attachments: [],
10882
+ checklistItems: checklistByTaskId.get(taskId) || [],
10883
+ };
10884
+ });
10885
+ }
10401
10886
  listWorkspaceSyncDeletesSince(workspaceId, cursor, limit) {
10402
10887
  return listWorkspaceSyncDeletesSinceService(this.getSyncReconciliationServiceDeps(), workspaceId, cursor, limit);
10403
10888
  }