@taskforcehq/taskforce 0.3.306 → 0.3.307
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.
- package/README.md +0 -2
- package/dist/Taskforce.module.css +317 -27
- package/dist/TaskforceCore.js +1 -0
- package/dist/TaskforceCore.test.js +178 -0
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.js +15 -3
- package/dist/components/features/AgentsModule.d.ts +5 -1
- package/dist/components/features/AgentsModule.js +142 -39
- package/dist/components/features/AgentsModule.test.js +260 -11
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +3 -1
- package/dist/components/features/DocumentViewer.js +6 -5
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +20 -3
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +6 -2
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +14 -64
- package/dist/components/task/TaskKanban.js +0 -8
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.js +9 -1
- package/dist/components/views/PlanComparisonPage.test.js +19 -0
- package/dist/components/views/PlansPage.js +1 -1
- package/dist/components/views/StandaloneLayout.js +256 -69
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
- package/dist/components/views/panels/PlanningDrawer.js +259 -51
- package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
- package/dist/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +7 -0
- package/dist/core/AiProfileService.js +460 -35
- package/dist/core/AiProfiles.test.js +648 -2
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +58 -13
- package/dist/core/GlobalSettingsService.js +12 -0
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- package/dist/core/PlanningEntities.test.js +366 -4
- package/dist/core/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +190 -26
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +775 -71
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/types.d.ts +7 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
- package/dist/hooks/sync/orchestratorShared.js +61 -1
- package/dist/hooks/sync/recovery.js +8 -0
- package/dist/hooks/sync/recovery.test.js +5 -5
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +36 -9
- package/dist/hooks/sync/transfers.test.js +534 -2
- package/dist/hooks/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.js +50 -5
- package/dist/hooks/useTaskforce.d.ts +85 -50
- package/dist/hooks/useTaskforce.js +44 -14
- package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
- package/dist/hooks/useWorkspaceSyncController.js +7 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
- package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +113 -7
- package/dist/mcp/runtime.d.ts +1 -0
- package/dist/mcp/runtime.js +969 -122
- package/dist/mcp/runtime.test.js +1083 -11
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.js +21 -0
- package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
- package/dist/server/index.js +4 -0
- package/dist/server/index.test.js +6 -1
- package/dist/server/routes/admin.js +187 -9
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +11 -0
- package/dist/server/routes/auth.js +189 -27
- package/dist/server/routes/authSupport.d.ts +4 -0
- package/dist/server/routes/authSupport.js +12 -1
- package/dist/server/routes/billing.js +11 -0
- package/dist/server/routes/billing.test.js +3 -0
- package/dist/server/routes/documents.js +10 -0
- package/dist/server/routes/shared.js +0 -10
- package/dist/server/routes/sync.integration.test.js +221 -0
- package/dist/server/routes/sync.js +13 -3
- package/dist/server/routes/syncSnapshotRoutes.js +2 -0
- package/dist/server/routes/tasks.js +12 -9
- package/dist/server/routes.js +87 -8
- package/dist/server/routes.test.js +890 -22
- package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- package/dist/storage/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- package/dist/sync/collaborationSyncPayload.js +2 -0
- package/dist/sync/collaborationSyncPayload.test.js +1 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +5 -1
- package/dist/sync/contentSyncState.js +231 -37
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +4 -4
- package/dist/sync/syncApplyHandlers.js +89 -31
- package/dist/sync/syncApplyHandlers.test.js +184 -4
- package/dist/sync/syncResourceAdapters.d.ts +2 -0
- package/dist/sync/syncResourceAdapters.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +13 -1
- package/dist/sync/syncService.test.js +39 -0
- package/dist/sync/workspacePullFeed.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +129 -70
- package/dist/sync/workspacePullFeed.test.js +49 -0
- package/dist/sync/workspaceSyncModel.d.ts +28 -6
- package/dist/sync/workspaceSyncModel.js +159 -45
- package/dist/sync/workspaceSyncModel.test.js +269 -3
- package/dist/types.d.ts +33 -2
- package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
- package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-D4XseTwW.js} +1 -1
- package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
- package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
- package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
- package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-oOA9bcdK.js} +4 -4
- package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
- package/dist/ui/assets/index-607WPo_X.js +4 -0
- package/dist/ui/assets/index-CYyodXsg.css +1 -0
- package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +6 -0
- package/dist/utils/assignees.js +8 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -0
- package/dist/utils/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- package/dist/utils/taskActivity.d.ts +7 -1
- package/dist/utils/taskActivity.js +63 -1
- package/dist/utils/taskActivity.test.js +23 -1
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- package/package.json +1 -1
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
- package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
- package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
- package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
- package/dist/ui/assets/index-C6k75jr8.js +0 -6
package/dist/core/Taskforce.js
CHANGED
|
@@ -8,9 +8,9 @@ import { createDatabaseAdapter } from '../storage/createDatabaseAdapter.js';
|
|
|
8
8
|
import { WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
|
|
9
9
|
import { AuthTokenService } from './AuthTokenService.js';
|
|
10
10
|
import { AuthIdentityService } from './AuthIdentityService.js';
|
|
11
|
-
import { archiveAiProfileService, getAiProfileByTokenService, listAiProfilesService, mergeAiProfilesService, resolveAiProfileService, updateAiProfileAvatarService, upsertAiProfileForSyncService, } from './AiProfileService.js';
|
|
12
|
-
import { syncCanonicalTaskAttachmentLinksService, } from './AttachmentLinkService.js';
|
|
13
|
-
import { addTaskEventService, buildTaskActivityService, listTaskEventsInternalService,
|
|
11
|
+
import { archiveAiProfileService, getAiProfileByTokenService, listAiProfilesService, mergeAiProfilesService, resolveAiProfileService, updateAiProfileAvatarService, updateAiProfileSurfaceTypeService, upsertAiProfileForSyncService, } from './AiProfileService.js';
|
|
12
|
+
import { syncCanonicalAttachmentLinksForTargetService, syncCanonicalTaskAttachmentLinksService, } from './AttachmentLinkService.js';
|
|
13
|
+
import { addEntityEventService, addTaskEventService, buildEntityActivityService, buildTaskActivityService, getEntityEventForSyncService, getTaskEventForSyncService, listEntityEventsInternalService, listWorkspaceEntityEventsForSyncService, listTaskEventsInternalService, listWorkspaceTaskEventsForSyncService, replaceEntityEventsService, replaceTaskCommentsService, replaceTaskEventsService, upsertEntityEventForSyncService, upsertTaskEventForSyncService, } from './TaskActivityService.js';
|
|
14
14
|
import { addDeletedTaskRecordService, getDeletedTaskService, listDeletedTasksService, mapDeletedTaskRowService, normalizeTaskSnapshotForRestoreService, pruneDeletedTasksService, purgeExpiredDeletedTasksService, } from './DeletedTaskLifecycleService.js';
|
|
15
15
|
import { applyWorkspaceSyncSnapshotService, deleteDocumentWatermarkService, getDocumentWatermarkService, listTasksForSyncService, listWorkspaceSyncDeletesSinceService, readPushIdempotencyService, recordSyncEventService, upsertDocumentWatermarkService, writePushIdempotencyService, } from './SyncReconciliationService.js';
|
|
16
16
|
import { getGlobalSettingsService, hasPersistedGlobalSettingsService, readGlobalConfigService, updateGlobalSettingsService, writeGlobalConfigService, } from './GlobalSettingsService.js';
|
|
@@ -268,6 +268,15 @@ export class TaskforceCore {
|
|
|
268
268
|
const normalized = String(value || '').trim().toLowerCase();
|
|
269
269
|
return normalized === 'user' || normalized === 'human';
|
|
270
270
|
}
|
|
271
|
+
isStableActorReference(value) {
|
|
272
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
273
|
+
if (!normalized)
|
|
274
|
+
return false;
|
|
275
|
+
return normalized === 'user'
|
|
276
|
+
|| normalized === 'system'
|
|
277
|
+
|| normalized.startsWith('user-')
|
|
278
|
+
|| normalized.startsWith('ai-profile-');
|
|
279
|
+
}
|
|
271
280
|
normalizeAiProfileName(name) {
|
|
272
281
|
const normalized = String(name || '').trim();
|
|
273
282
|
if (!normalized) {
|
|
@@ -375,6 +384,8 @@ export class TaskforceCore {
|
|
|
375
384
|
color: this.normalizeAiProfileColor(row.color),
|
|
376
385
|
avatarUrl: typeof row.avatar_url === 'string' && row.avatar_url.trim().length > 0 ? row.avatar_url.trim() : null,
|
|
377
386
|
avatarSourceUrl: typeof row.avatar_source_url === 'string' && row.avatar_source_url.trim().length > 0 ? row.avatar_source_url.trim() : null,
|
|
387
|
+
avatarRevision: Number.isFinite(Number(row.avatar_revision)) ? Math.max(0, Math.floor(Number(row.avatar_revision))) : 0,
|
|
388
|
+
avatarUpdatedAt: typeof row.avatar_updated_at === 'string' && row.avatar_updated_at.trim().length > 0 ? row.avatar_updated_at.trim() : null,
|
|
378
389
|
description: typeof row.description === 'string' && row.description.trim().length > 0 ? row.description.trim() : null,
|
|
379
390
|
role: typeof row.role === 'string' && row.role.trim().length > 0 ? row.role.trim() : null,
|
|
380
391
|
provider: typeof row.provider === 'string' && row.provider.trim().length > 0 ? row.provider.trim() : null,
|
|
@@ -404,7 +415,6 @@ export class TaskforceCore {
|
|
|
404
415
|
color: profile.color,
|
|
405
416
|
isNamed: true,
|
|
406
417
|
meta: {
|
|
407
|
-
profileToken: profile.profileToken,
|
|
408
418
|
workspaceId: profile.workspaceId,
|
|
409
419
|
avatarUrl: profile.avatarUrl ?? null,
|
|
410
420
|
avatarSourceUrl: profile.avatarSourceUrl ?? null,
|
|
@@ -423,6 +433,59 @@ export class TaskforceCore {
|
|
|
423
433
|
}
|
|
424
434
|
};
|
|
425
435
|
}
|
|
436
|
+
resolveCanonicalAiProfileId(rowById, profileId) {
|
|
437
|
+
let currentId = String(profileId || '').trim();
|
|
438
|
+
const visited = new Set();
|
|
439
|
+
while (currentId) {
|
|
440
|
+
if (visited.has(currentId))
|
|
441
|
+
break;
|
|
442
|
+
visited.add(currentId);
|
|
443
|
+
const row = rowById.get(currentId);
|
|
444
|
+
const mergedIntoProfileId = String(row?.merged_into_profile_id || '').trim();
|
|
445
|
+
if (!mergedIntoProfileId || mergedIntoProfileId === currentId)
|
|
446
|
+
break;
|
|
447
|
+
currentId = mergedIntoProfileId;
|
|
448
|
+
}
|
|
449
|
+
return currentId;
|
|
450
|
+
}
|
|
451
|
+
findCanonicalAiProfileIdsByAlias(workspaceId, actorRef) {
|
|
452
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
453
|
+
const rawActorRef = String(actorRef || '').trim();
|
|
454
|
+
if (!rawActorRef)
|
|
455
|
+
return [];
|
|
456
|
+
const matches = this.db.prepare(`
|
|
457
|
+
SELECT *
|
|
458
|
+
FROM ai_profiles
|
|
459
|
+
WHERE tenant_id = ?
|
|
460
|
+
AND workspace_id = ?
|
|
461
|
+
AND (
|
|
462
|
+
LOWER(name) = LOWER(?)
|
|
463
|
+
OR LOWER(username) = LOWER(?)
|
|
464
|
+
)
|
|
465
|
+
ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
|
|
466
|
+
`).all(this.tenantId, normalizedWorkspaceId, rawActorRef, rawActorRef);
|
|
467
|
+
const rowById = new Map(matches
|
|
468
|
+
.map((row) => [String(row.id || '').trim(), row])
|
|
469
|
+
.filter(([id]) => Boolean(id)));
|
|
470
|
+
return [...new Set(matches
|
|
471
|
+
.map((row) => this.resolveCanonicalAiProfileId(rowById, String(row.id || '').trim()))
|
|
472
|
+
.filter(Boolean))];
|
|
473
|
+
}
|
|
474
|
+
resolveUniqueAiProfileActorByAlias(workspaceId, actorRef) {
|
|
475
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
476
|
+
const uniqueCanonicalIds = this.findCanonicalAiProfileIdsByAlias(normalizedWorkspaceId, actorRef);
|
|
477
|
+
if (uniqueCanonicalIds.length !== 1)
|
|
478
|
+
return null;
|
|
479
|
+
const resolvedRow = this.db.prepare(`
|
|
480
|
+
SELECT *
|
|
481
|
+
FROM ai_profiles
|
|
482
|
+
WHERE tenant_id = ?
|
|
483
|
+
AND workspace_id = ?
|
|
484
|
+
AND id = ?
|
|
485
|
+
LIMIT 1
|
|
486
|
+
`).get(this.tenantId, normalizedWorkspaceId, uniqueCanonicalIds[0]);
|
|
487
|
+
return resolvedRow ? this.aiProfileToActor(this.aiProfileRowToRecord(resolvedRow)) : null;
|
|
488
|
+
}
|
|
426
489
|
canonicalizeActorReference(workspaceId, value, fallback) {
|
|
427
490
|
const raw = String(value || '').trim();
|
|
428
491
|
if (!raw)
|
|
@@ -531,8 +594,21 @@ export class TaskforceCore {
|
|
|
531
594
|
normalized.fsPath = raw.fsPath.trim();
|
|
532
595
|
if (typeof raw.caption === 'string' && raw.caption.trim())
|
|
533
596
|
normalized.caption = raw.caption.trim();
|
|
597
|
+
if (typeof raw.displayName === 'string' && raw.displayName.trim())
|
|
598
|
+
normalized.displayName = raw.displayName.trim();
|
|
599
|
+
if (typeof raw.originalFilename === 'string' && raw.originalFilename.trim())
|
|
600
|
+
normalized.originalFilename = raw.originalFilename.trim();
|
|
534
601
|
if (typeof raw.timestamp === 'string' && raw.timestamp.trim())
|
|
535
602
|
normalized.timestamp = raw.timestamp.trim();
|
|
603
|
+
if (typeof raw.assetId === 'string' && raw.assetId.trim())
|
|
604
|
+
normalized.assetId = raw.assetId.trim();
|
|
605
|
+
const referenceNumber = this.sanitizeTaskReferenceNumber(raw.referenceNumber);
|
|
606
|
+
if (referenceNumber !== null)
|
|
607
|
+
normalized.referenceNumber = referenceNumber;
|
|
608
|
+
if (typeof raw.referenceLabel === 'string' && raw.referenceLabel.trim())
|
|
609
|
+
normalized.referenceLabel = raw.referenceLabel.trim();
|
|
610
|
+
if (typeof raw.taskId === 'string' && raw.taskId.trim())
|
|
611
|
+
normalized.taskId = raw.taskId.trim();
|
|
536
612
|
return normalized;
|
|
537
613
|
})
|
|
538
614
|
.filter((attachment) => attachment !== null);
|
|
@@ -552,15 +628,27 @@ export class TaskforceCore {
|
|
|
552
628
|
actorProfile
|
|
553
629
|
};
|
|
554
630
|
}
|
|
631
|
+
listEntityEventsInternal(entityType, entityId, workspaceId) {
|
|
632
|
+
return listEntityEventsInternalService(this.getTaskActivityServiceDeps(), entityType, entityId, workspaceId);
|
|
633
|
+
}
|
|
555
634
|
listTaskEventsInternal(taskId, workspaceId) {
|
|
556
635
|
return listTaskEventsInternalService(this.getTaskActivityServiceDeps(), taskId, workspaceId);
|
|
557
636
|
}
|
|
637
|
+
buildEntityActivity(comments, entityEvents, hiddenActions) {
|
|
638
|
+
return buildEntityActivityService(comments, entityEvents, hiddenActions?.length ? { hiddenActions } : undefined);
|
|
639
|
+
}
|
|
558
640
|
buildTaskActivity(comments, taskEvents) {
|
|
559
641
|
return buildTaskActivityService(comments, taskEvents);
|
|
560
642
|
}
|
|
643
|
+
addEntityEvent(input) {
|
|
644
|
+
addEntityEventService(this.getTaskActivityServiceDeps(), input);
|
|
645
|
+
}
|
|
561
646
|
addTaskEvent(input) {
|
|
562
647
|
addTaskEventService(this.getTaskActivityServiceDeps(), input);
|
|
563
648
|
}
|
|
649
|
+
replaceEntityEvents(entityType, entityId, workspaceId, entityEvents) {
|
|
650
|
+
replaceEntityEventsService(this.getTaskActivityServiceDeps(), entityType, entityId, workspaceId, entityEvents);
|
|
651
|
+
}
|
|
564
652
|
replaceTaskEvents(taskId, workspaceId, taskEvents) {
|
|
565
653
|
replaceTaskEventsService(this.getTaskActivityServiceDeps(), taskId, workspaceId, taskEvents);
|
|
566
654
|
}
|
|
@@ -1066,6 +1154,65 @@ export class TaskforceCore {
|
|
|
1066
1154
|
}
|
|
1067
1155
|
return (existingValue ?? null) === (nextValue ?? null);
|
|
1068
1156
|
}
|
|
1157
|
+
buildPlanningEntityEventChanges(existing, next, fields) {
|
|
1158
|
+
const changes = {};
|
|
1159
|
+
for (const field of fields) {
|
|
1160
|
+
const previousValue = existing[field] ?? null;
|
|
1161
|
+
const nextValue = next[field] ?? null;
|
|
1162
|
+
if (JSON.stringify(previousValue) === JSON.stringify(nextValue))
|
|
1163
|
+
continue;
|
|
1164
|
+
changes[field] = { from: previousValue, to: nextValue };
|
|
1165
|
+
}
|
|
1166
|
+
return changes;
|
|
1167
|
+
}
|
|
1168
|
+
getPlanningEntityEventAction(entityType, changes) {
|
|
1169
|
+
const fields = Object.keys(changes);
|
|
1170
|
+
if (fields.length === 1 && fields[0] === 'attachments') {
|
|
1171
|
+
return `${entityType}-attachments-changed`;
|
|
1172
|
+
}
|
|
1173
|
+
if (entityType === 'workstream' && fields.length === 1 && fields[0] === 'initiativeId') {
|
|
1174
|
+
return 'workstream-relationship-changed';
|
|
1175
|
+
}
|
|
1176
|
+
return `${entityType}-updated`;
|
|
1177
|
+
}
|
|
1178
|
+
addWorkstreamTaskMembershipEvents(input) {
|
|
1179
|
+
const previousWorkstreamId = this.sanitizeWorkstreamId(input.previousWorkstreamId);
|
|
1180
|
+
const nextWorkstreamId = this.sanitizeWorkstreamId(input.nextWorkstreamId);
|
|
1181
|
+
if (previousWorkstreamId === nextWorkstreamId)
|
|
1182
|
+
return;
|
|
1183
|
+
const details = {
|
|
1184
|
+
taskId: input.task.id,
|
|
1185
|
+
taskTitle: input.task.title,
|
|
1186
|
+
changes: {
|
|
1187
|
+
workstreamId: {
|
|
1188
|
+
from: previousWorkstreamId ?? null,
|
|
1189
|
+
to: nextWorkstreamId ?? null,
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
if (previousWorkstreamId) {
|
|
1194
|
+
this.addEntityEvent({
|
|
1195
|
+
entityType: 'workstream',
|
|
1196
|
+
entityId: previousWorkstreamId,
|
|
1197
|
+
workspaceId: input.workspaceId,
|
|
1198
|
+
action: 'workstream-task-removed',
|
|
1199
|
+
actor: input.actor,
|
|
1200
|
+
createdAt: input.createdAt,
|
|
1201
|
+
details,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
if (nextWorkstreamId) {
|
|
1205
|
+
this.addEntityEvent({
|
|
1206
|
+
entityType: 'workstream',
|
|
1207
|
+
entityId: nextWorkstreamId,
|
|
1208
|
+
workspaceId: input.workspaceId,
|
|
1209
|
+
action: 'workstream-task-added',
|
|
1210
|
+
actor: input.actor,
|
|
1211
|
+
createdAt: input.createdAt,
|
|
1212
|
+
details,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1069
1216
|
assertWorkstreamLinkAllowed(workstreamId, workspaceId) {
|
|
1070
1217
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
1071
1218
|
if (!workstreamId)
|
|
@@ -1470,6 +1617,19 @@ export class TaskforceCore {
|
|
|
1470
1617
|
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
|
|
1471
1618
|
);
|
|
1472
1619
|
|
|
1620
|
+
CREATE TABLE IF NOT EXISTS entity_events (
|
|
1621
|
+
id TEXT PRIMARY KEY,
|
|
1622
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
1623
|
+
workspace_id TEXT NOT NULL DEFAULT 'default',
|
|
1624
|
+
entity_type TEXT NOT NULL,
|
|
1625
|
+
entity_id TEXT NOT NULL,
|
|
1626
|
+
action TEXT NOT NULL,
|
|
1627
|
+
actor TEXT NOT NULL,
|
|
1628
|
+
actor_type TEXT NOT NULL,
|
|
1629
|
+
details_json TEXT,
|
|
1630
|
+
created_at TEXT NOT NULL
|
|
1631
|
+
);
|
|
1632
|
+
|
|
1473
1633
|
CREATE TABLE IF NOT EXISTS deleted_tasks (
|
|
1474
1634
|
id TEXT PRIMARY KEY,
|
|
1475
1635
|
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
@@ -1598,6 +1758,9 @@ export class TaskforceCore {
|
|
|
1598
1758
|
token_prefix TEXT NOT NULL,
|
|
1599
1759
|
token_hash TEXT NOT NULL,
|
|
1600
1760
|
status TEXT NOT NULL DEFAULT 'active',
|
|
1761
|
+
ai_profile_id TEXT,
|
|
1762
|
+
ai_profile_token TEXT,
|
|
1763
|
+
ai_profile_name TEXT,
|
|
1601
1764
|
last_used_at TEXT,
|
|
1602
1765
|
revoked_at TEXT,
|
|
1603
1766
|
revoked_by_user_id TEXT,
|
|
@@ -1656,6 +1819,8 @@ export class TaskforceCore {
|
|
|
1656
1819
|
color TEXT NOT NULL,
|
|
1657
1820
|
avatar_url TEXT,
|
|
1658
1821
|
avatar_source_url TEXT,
|
|
1822
|
+
avatar_revision INTEGER NOT NULL DEFAULT 0,
|
|
1823
|
+
avatar_updated_at TEXT,
|
|
1659
1824
|
description TEXT,
|
|
1660
1825
|
role TEXT,
|
|
1661
1826
|
provider TEXT,
|
|
@@ -1682,6 +1847,8 @@ export class TaskforceCore {
|
|
|
1682
1847
|
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_workspace_created ON admin_audit_logs(workspace_id, created_at);
|
|
1683
1848
|
CREATE INDEX IF NOT EXISTS idx_task_events_task_created ON task_events(tenant_id, workspace_id, task_id, created_at);
|
|
1684
1849
|
CREATE INDEX IF NOT EXISTS idx_task_events_workspace_created ON task_events(tenant_id, workspace_id, created_at);
|
|
1850
|
+
CREATE INDEX IF NOT EXISTS idx_entity_events_entity_created ON entity_events(tenant_id, workspace_id, entity_type, entity_id, created_at);
|
|
1851
|
+
CREATE INDEX IF NOT EXISTS idx_entity_events_workspace_created ON entity_events(tenant_id, workspace_id, created_at);
|
|
1685
1852
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_deleted_tasks_workspace_task ON deleted_tasks(tenant_id, workspace_id, task_id);
|
|
1686
1853
|
CREATE INDEX IF NOT EXISTS idx_deleted_tasks_workspace_deleted ON deleted_tasks(tenant_id, workspace_id, deleted_at);
|
|
1687
1854
|
CREATE INDEX IF NOT EXISTS idx_mcp_audit_logs_workspace_created ON mcp_audit_logs(tenant_id, workspace_id, created_at);
|
|
@@ -1786,6 +1953,25 @@ export class TaskforceCore {
|
|
|
1786
1953
|
// Best-effort schema repair for older deployments.
|
|
1787
1954
|
}
|
|
1788
1955
|
}
|
|
1956
|
+
if (this.db.provider === 'sqlite') {
|
|
1957
|
+
this.db.exec(`
|
|
1958
|
+
INSERT OR IGNORE INTO entity_events (
|
|
1959
|
+
id, tenant_id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
|
|
1960
|
+
)
|
|
1961
|
+
SELECT id, tenant_id, workspace_id, 'task', task_id, action, actor, actor_type, details_json, created_at
|
|
1962
|
+
FROM task_events
|
|
1963
|
+
`);
|
|
1964
|
+
}
|
|
1965
|
+
else {
|
|
1966
|
+
this.db.exec(`
|
|
1967
|
+
INSERT INTO entity_events (
|
|
1968
|
+
id, tenant_id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
|
|
1969
|
+
)
|
|
1970
|
+
SELECT id, tenant_id, workspace_id, 'task', task_id, action, actor, actor_type, details_json, created_at
|
|
1971
|
+
FROM task_events
|
|
1972
|
+
ON CONFLICT (id) DO NOTHING
|
|
1973
|
+
`);
|
|
1974
|
+
}
|
|
1789
1975
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_workstream ON tasks(tenant_id, workspace_id, workstream_id)`);
|
|
1790
1976
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(tenant_id, workspace_id, assignee, is_archived)`);
|
|
1791
1977
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_workstreams_initiative ON workstreams(tenant_id, workspace_id, initiative_id)`);
|
|
@@ -1806,6 +1992,28 @@ export class TaskforceCore {
|
|
|
1806
1992
|
}
|
|
1807
1993
|
}
|
|
1808
1994
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_auth_tokens_workspace_purpose ON auth_tokens(workspace_id, purpose)`);
|
|
1995
|
+
if (this.db.provider === 'sqlite') {
|
|
1996
|
+
const mcpOauthGrantColumns = this.db.prepare(`PRAGMA table_info(mcp_oauth_connector_grants)`).all();
|
|
1997
|
+
if (!mcpOauthGrantColumns.some((c) => c.name === 'ai_profile_id')) {
|
|
1998
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN ai_profile_id TEXT`);
|
|
1999
|
+
}
|
|
2000
|
+
if (!mcpOauthGrantColumns.some((c) => c.name === 'ai_profile_token')) {
|
|
2001
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN ai_profile_token TEXT`);
|
|
2002
|
+
}
|
|
2003
|
+
if (!mcpOauthGrantColumns.some((c) => c.name === 'ai_profile_name')) {
|
|
2004
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN ai_profile_name TEXT`);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
else {
|
|
2008
|
+
try {
|
|
2009
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN IF NOT EXISTS ai_profile_id TEXT`);
|
|
2010
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN IF NOT EXISTS ai_profile_token TEXT`);
|
|
2011
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN IF NOT EXISTS ai_profile_name TEXT`);
|
|
2012
|
+
}
|
|
2013
|
+
catch {
|
|
2014
|
+
// Best-effort schema repair for existing deployments.
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
1809
2017
|
if (this.db.provider === 'sqlite') {
|
|
1810
2018
|
const mcpAuditColumns = this.db.prepare(`PRAGMA table_info(mcp_audit_logs)`).all();
|
|
1811
2019
|
if (!mcpAuditColumns.some((c) => c.name === 'request_id')) {
|
|
@@ -1906,6 +2114,8 @@ export class TaskforceCore {
|
|
|
1906
2114
|
{ name: 'color', ddl: `ALTER TABLE ai_profiles ADD COLUMN color TEXT NOT NULL DEFAULT '#64748b'` },
|
|
1907
2115
|
{ name: 'avatar_url', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_url TEXT` },
|
|
1908
2116
|
{ name: 'avatar_source_url', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_source_url TEXT` },
|
|
2117
|
+
{ name: 'avatar_revision', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_revision INTEGER NOT NULL DEFAULT 0` },
|
|
2118
|
+
{ name: 'avatar_updated_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_updated_at TEXT` },
|
|
1909
2119
|
{ name: 'description', ddl: `ALTER TABLE ai_profiles ADD COLUMN description TEXT` },
|
|
1910
2120
|
{ name: 'role', ddl: `ALTER TABLE ai_profiles ADD COLUMN role TEXT` },
|
|
1911
2121
|
{ name: 'provider', ddl: `ALTER TABLE ai_profiles ADD COLUMN provider TEXT` },
|
|
@@ -1938,6 +2148,8 @@ export class TaskforceCore {
|
|
|
1938
2148
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS color TEXT NOT NULL DEFAULT '#64748b'`,
|
|
1939
2149
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_url TEXT`,
|
|
1940
2150
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_source_url TEXT`,
|
|
2151
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_revision INTEGER NOT NULL DEFAULT 0`,
|
|
2152
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_updated_at TEXT`,
|
|
1941
2153
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS description TEXT`,
|
|
1942
2154
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS role TEXT`,
|
|
1943
2155
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS provider TEXT`,
|
|
@@ -1978,10 +2190,30 @@ export class TaskforceCore {
|
|
|
1978
2190
|
AND roster_state_updated_at < archived_at
|
|
1979
2191
|
)
|
|
1980
2192
|
`);
|
|
2193
|
+
this.db.exec(`
|
|
2194
|
+
UPDATE ai_profiles
|
|
2195
|
+
SET avatar_revision = CASE
|
|
2196
|
+
WHEN (
|
|
2197
|
+
(avatar_url IS NOT NULL AND TRIM(avatar_url) <> '')
|
|
2198
|
+
OR (avatar_source_url IS NOT NULL AND TRIM(avatar_source_url) <> '')
|
|
2199
|
+
) THEN 1
|
|
2200
|
+
ELSE 0
|
|
2201
|
+
END,
|
|
2202
|
+
avatar_updated_at = CASE
|
|
2203
|
+
WHEN (
|
|
2204
|
+
(avatar_url IS NOT NULL AND TRIM(avatar_url) <> '')
|
|
2205
|
+
OR (avatar_source_url IS NOT NULL AND TRIM(avatar_source_url) <> '')
|
|
2206
|
+
) THEN COALESCE(NULLIF(TRIM(avatar_updated_at), ''), NULLIF(TRIM(updated_at), ''), NULLIF(TRIM(created_at), ''))
|
|
2207
|
+
ELSE NULL
|
|
2208
|
+
END
|
|
2209
|
+
WHERE avatar_revision IS NULL
|
|
2210
|
+
OR avatar_revision = 0
|
|
2211
|
+
`);
|
|
1981
2212
|
this.migrateSystemRoleBackfillIfNeeded();
|
|
1982
2213
|
this.migrateUserBetaAccessBackfillIfNeeded();
|
|
1983
2214
|
this.migrateDuplicateAiProfilesIfNeeded();
|
|
1984
2215
|
this.migrateLegacyActorReferencesIfNeeded();
|
|
2216
|
+
this.migrateLegacyCommentActorReferencesIfNeeded();
|
|
1985
2217
|
// Ensure sync watermark index exists on existing databases (added in v0.3.52+).
|
|
1986
2218
|
this.db.exec(`
|
|
1987
2219
|
CREATE INDEX IF NOT EXISTS idx_tasks_sync_watermark ON tasks(tenant_id, workspace_id, updated_at, id);
|
|
@@ -2128,7 +2360,7 @@ export class TaskforceCore {
|
|
|
2128
2360
|
if (existing?.payload)
|
|
2129
2361
|
return;
|
|
2130
2362
|
const rows = this.db.prepare(`
|
|
2131
|
-
SELECT id, workspace_id, name, username, created_at, updated_at, last_active_at
|
|
2363
|
+
SELECT id, workspace_id, name, username, seat_scope, created_at, updated_at, last_active_at
|
|
2132
2364
|
FROM ai_profiles
|
|
2133
2365
|
WHERE tenant_id = ?
|
|
2134
2366
|
`).all(this.tenantId);
|
|
@@ -2137,7 +2369,8 @@ export class TaskforceCore {
|
|
|
2137
2369
|
const workspaceId = this.normalizeWorkspaceId(row.workspace_id);
|
|
2138
2370
|
const normalizedName = String(row.name || '').trim().toLowerCase();
|
|
2139
2371
|
const normalizedUsername = String(row.username || '').trim().toLowerCase();
|
|
2140
|
-
const
|
|
2372
|
+
const seatScope = String(row.seat_scope || 'unknown').trim().toLowerCase() || 'unknown';
|
|
2373
|
+
const dedupeKey = `${workspaceId}::${seatScope}::${normalizedName || normalizedUsername}`;
|
|
2141
2374
|
if (!normalizedName && !normalizedUsername)
|
|
2142
2375
|
continue;
|
|
2143
2376
|
const list = groups.get(dedupeKey) || [];
|
|
@@ -2185,15 +2418,19 @@ export class TaskforceCore {
|
|
|
2185
2418
|
if (existing?.payload)
|
|
2186
2419
|
return;
|
|
2187
2420
|
const rows = this.db.prepare(`
|
|
2188
|
-
SELECT id, workspace_id, name, username
|
|
2421
|
+
SELECT id, workspace_id, name, username, merged_into_profile_id
|
|
2189
2422
|
FROM ai_profiles
|
|
2190
2423
|
WHERE tenant_id = ?
|
|
2191
2424
|
`).all(this.tenantId);
|
|
2425
|
+
const rowById = new Map(rows
|
|
2426
|
+
.map((row) => [String(row.id || '').trim(), row])
|
|
2427
|
+
.filter(([id]) => Boolean(id)));
|
|
2192
2428
|
const aliasesByWorkspace = new Map();
|
|
2193
2429
|
for (const row of rows) {
|
|
2194
2430
|
const workspaceId = this.normalizeWorkspaceId(row.workspace_id);
|
|
2195
2431
|
const profileId = String(row.id || '').trim();
|
|
2196
|
-
|
|
2432
|
+
const canonicalProfileId = this.resolveCanonicalAiProfileId(rowById, profileId);
|
|
2433
|
+
if (!canonicalProfileId)
|
|
2197
2434
|
continue;
|
|
2198
2435
|
const aliases = [
|
|
2199
2436
|
String(row.name || '').trim().toLowerCase(),
|
|
@@ -2208,8 +2445,8 @@ export class TaskforceCore {
|
|
|
2208
2445
|
}
|
|
2209
2446
|
for (const alias of aliases) {
|
|
2210
2447
|
const ids = aliasMap.get(alias) || [];
|
|
2211
|
-
if (!ids.includes(
|
|
2212
|
-
ids.push(
|
|
2448
|
+
if (!ids.includes(canonicalProfileId))
|
|
2449
|
+
ids.push(canonicalProfileId);
|
|
2213
2450
|
aliasMap.set(alias, ids);
|
|
2214
2451
|
}
|
|
2215
2452
|
}
|
|
@@ -2250,6 +2487,135 @@ export class TaskforceCore {
|
|
|
2250
2487
|
updated_at = excluded.updated_at
|
|
2251
2488
|
`).run(this.tenantId, migrationKey, JSON.stringify({ completedAt: now }), now);
|
|
2252
2489
|
}
|
|
2490
|
+
migrateLegacyCommentActorReferencesIfNeeded() {
|
|
2491
|
+
const migrationKey = 'migration.actor_refs.canonicalize_comments.v2';
|
|
2492
|
+
const existing = this.db.prepare(`
|
|
2493
|
+
SELECT payload
|
|
2494
|
+
FROM system_settings
|
|
2495
|
+
WHERE tenant_id = ? AND settings_key = ?
|
|
2496
|
+
LIMIT 1
|
|
2497
|
+
`).get(this.tenantId, migrationKey);
|
|
2498
|
+
if (existing?.payload)
|
|
2499
|
+
return;
|
|
2500
|
+
const commentRows = this.db.prepare(`
|
|
2501
|
+
SELECT id, task_id, workspace_id, author, submitted_author
|
|
2502
|
+
FROM comments
|
|
2503
|
+
WHERE tenant_id = ?
|
|
2504
|
+
`).all(this.tenantId);
|
|
2505
|
+
const updateCommentAuthor = this.db.prepare(`
|
|
2506
|
+
UPDATE comments
|
|
2507
|
+
SET author = ?
|
|
2508
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
2509
|
+
`);
|
|
2510
|
+
const updateTaskSyncWatermark = this.db.prepare(`
|
|
2511
|
+
UPDATE tasks
|
|
2512
|
+
SET updated_at = ?
|
|
2513
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
2514
|
+
`);
|
|
2515
|
+
const selectTaskSyncWatermark = this.db.prepare(`
|
|
2516
|
+
SELECT updated_at, created_at
|
|
2517
|
+
FROM tasks
|
|
2518
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
2519
|
+
LIMIT 1
|
|
2520
|
+
`);
|
|
2521
|
+
let repairedCount = 0;
|
|
2522
|
+
let ambiguousCount = 0;
|
|
2523
|
+
let skippedCount = 0;
|
|
2524
|
+
const touchedTaskIdsByWorkspace = new Map();
|
|
2525
|
+
const now = new Date().toISOString();
|
|
2526
|
+
const nowMs = Date.parse(now);
|
|
2527
|
+
const tx = this.db.transaction(() => {
|
|
2528
|
+
for (const row of commentRows) {
|
|
2529
|
+
const workspaceId = this.normalizeWorkspaceId(row.workspace_id);
|
|
2530
|
+
const commentId = String(row.id || '').trim();
|
|
2531
|
+
const taskId = String(row.task_id || '').trim();
|
|
2532
|
+
const rawAuthor = String(row.author || '').trim();
|
|
2533
|
+
const rawSubmittedAuthor = String(row.submitted_author || '').trim();
|
|
2534
|
+
const normalizedAuthor = rawAuthor.toLowerCase();
|
|
2535
|
+
if (!commentId) {
|
|
2536
|
+
skippedCount += 1;
|
|
2537
|
+
continue;
|
|
2538
|
+
}
|
|
2539
|
+
if (this.isStableActorReference(rawAuthor)
|
|
2540
|
+
|| this.isHumanActorReference(rawAuthor)
|
|
2541
|
+
|| normalizedAuthor === 'system') {
|
|
2542
|
+
skippedCount += 1;
|
|
2543
|
+
continue;
|
|
2544
|
+
}
|
|
2545
|
+
const candidateIds = new Set();
|
|
2546
|
+
let hasAmbiguousEvidence = false;
|
|
2547
|
+
const evidenceValues = [rawAuthor, rawSubmittedAuthor];
|
|
2548
|
+
for (const value of evidenceValues) {
|
|
2549
|
+
const rawValue = String(value || '').trim();
|
|
2550
|
+
const normalizedValue = rawValue.toLowerCase();
|
|
2551
|
+
if (!rawValue)
|
|
2552
|
+
continue;
|
|
2553
|
+
if (this.isHumanActorReference(rawValue)
|
|
2554
|
+
|| this.isGenericAiActorReference(rawValue)
|
|
2555
|
+
|| normalizedValue === 'system'
|
|
2556
|
+
|| normalizedValue.startsWith('user-')) {
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
2559
|
+
if (normalizedValue.startsWith('ai-profile-')) {
|
|
2560
|
+
const actor = this.resolveWorkspaceActor(workspaceId, rawValue);
|
|
2561
|
+
if (actor?.kind === 'ai' && actor.meta?.unresolved !== true) {
|
|
2562
|
+
candidateIds.add(actor.id);
|
|
2563
|
+
}
|
|
2564
|
+
continue;
|
|
2565
|
+
}
|
|
2566
|
+
const matches = this.findCanonicalAiProfileIdsByAlias(workspaceId, rawValue);
|
|
2567
|
+
if (matches.length === 1) {
|
|
2568
|
+
candidateIds.add(matches[0]);
|
|
2569
|
+
}
|
|
2570
|
+
else if (matches.length > 1) {
|
|
2571
|
+
hasAmbiguousEvidence = true;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
if (!hasAmbiguousEvidence && candidateIds.size === 1) {
|
|
2575
|
+
updateCommentAuthor.run([...candidateIds][0], this.tenantId, workspaceId, commentId);
|
|
2576
|
+
if (taskId) {
|
|
2577
|
+
let touchedTaskIds = touchedTaskIdsByWorkspace.get(workspaceId);
|
|
2578
|
+
if (!touchedTaskIds) {
|
|
2579
|
+
touchedTaskIds = new Set();
|
|
2580
|
+
touchedTaskIdsByWorkspace.set(workspaceId, touchedTaskIds);
|
|
2581
|
+
}
|
|
2582
|
+
touchedTaskIds.add(taskId);
|
|
2583
|
+
}
|
|
2584
|
+
repairedCount += 1;
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
if (hasAmbiguousEvidence || candidateIds.size > 1) {
|
|
2588
|
+
ambiguousCount += 1;
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
2591
|
+
skippedCount += 1;
|
|
2592
|
+
}
|
|
2593
|
+
for (const [workspaceId, touchedTaskIds] of touchedTaskIdsByWorkspace.entries()) {
|
|
2594
|
+
for (const taskId of touchedTaskIds) {
|
|
2595
|
+
const taskRow = selectTaskSyncWatermark.get(this.tenantId, workspaceId, taskId);
|
|
2596
|
+
const existingWatermark = String(taskRow?.updated_at || taskRow?.created_at || '').trim();
|
|
2597
|
+
const existingWatermarkMs = Date.parse(existingWatermark);
|
|
2598
|
+
const nextWatermarkMs = Number.isFinite(existingWatermarkMs)
|
|
2599
|
+
? Math.max(existingWatermarkMs, nowMs) + 1
|
|
2600
|
+
: nowMs;
|
|
2601
|
+
updateTaskSyncWatermark.run(new Date(nextWatermarkMs).toISOString(), this.tenantId, workspaceId, taskId);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
});
|
|
2605
|
+
tx();
|
|
2606
|
+
this.db.prepare(`
|
|
2607
|
+
INSERT INTO system_settings (tenant_id, settings_key, payload, updated_at)
|
|
2608
|
+
VALUES (?, ?, ?, ?)
|
|
2609
|
+
ON CONFLICT (tenant_id, settings_key) DO UPDATE SET
|
|
2610
|
+
payload = excluded.payload,
|
|
2611
|
+
updated_at = excluded.updated_at
|
|
2612
|
+
`).run(this.tenantId, migrationKey, JSON.stringify({
|
|
2613
|
+
completedAt: now,
|
|
2614
|
+
repairedCount,
|
|
2615
|
+
ambiguousCount,
|
|
2616
|
+
skippedCount,
|
|
2617
|
+
}), now);
|
|
2618
|
+
}
|
|
2253
2619
|
getWorkspaceOwnerUserId(workspaceId) {
|
|
2254
2620
|
const normalizedWorkspaceId = String(workspaceId || '').trim();
|
|
2255
2621
|
if (!normalizedWorkspaceId)
|
|
@@ -2925,6 +3291,9 @@ export class TaskforceCore {
|
|
|
2925
3291
|
syncCanonicalTaskAttachmentLinks(taskId, workspaceId, attachments) {
|
|
2926
3292
|
syncCanonicalTaskAttachmentLinksService(this.getAttachmentLinkServiceDeps(), taskId, workspaceId, attachments);
|
|
2927
3293
|
}
|
|
3294
|
+
syncCanonicalPlanningAttachmentLinks(targetType, targetId, workspaceId, attachments) {
|
|
3295
|
+
syncCanonicalAttachmentLinksForTargetService(this.getAttachmentLinkServiceDeps(), targetType, targetId, workspaceId, attachments);
|
|
3296
|
+
}
|
|
2928
3297
|
rowToTask(row) {
|
|
2929
3298
|
const workspaceId = this.normalizeWorkspaceId(row?.workspace_id);
|
|
2930
3299
|
const commentsRaw = this.db.prepare('SELECT * FROM comments WHERE tenant_id = ? AND workspace_id = ? AND task_id = ? ORDER BY timestamp ASC').all(this.tenantId, workspaceId, row.id);
|
|
@@ -2983,12 +3352,21 @@ export class TaskforceCore {
|
|
|
2983
3352
|
checklistItems
|
|
2984
3353
|
};
|
|
2985
3354
|
}
|
|
2986
|
-
rowToInitiative(row) {
|
|
3355
|
+
rowToInitiative(row, options) {
|
|
3356
|
+
const id = String(row.id || '').trim();
|
|
3357
|
+
const workspaceId = this.normalizeWorkspaceId(typeof row.workspace_id === 'string' ? row.workspace_id : undefined);
|
|
2987
3358
|
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
2988
3359
|
const comments = this.normalizeIncomingComments(this.parseJsonField(row.comments_json, []));
|
|
3360
|
+
const includeActivity = options?.includeActivity === true;
|
|
3361
|
+
const entityEvents = includeActivity
|
|
3362
|
+
? this.listEntityEventsInternal('initiative', id, workspaceId)
|
|
3363
|
+
: undefined;
|
|
3364
|
+
const activity = includeActivity
|
|
3365
|
+
? this.buildEntityActivity(comments, entityEvents || [])
|
|
3366
|
+
: undefined;
|
|
2989
3367
|
const attachments = this.normalizePlanningAttachments(this.parseJsonField(row.attachments_json, []));
|
|
2990
3368
|
return {
|
|
2991
|
-
id
|
|
3369
|
+
id,
|
|
2992
3370
|
referenceNumber,
|
|
2993
3371
|
referenceLabel: formatInitiativeReference(referenceNumber),
|
|
2994
3372
|
title: String(row.title || '').trim() || 'Untitled Initiative',
|
|
@@ -3000,14 +3378,27 @@ export class TaskforceCore {
|
|
|
3000
3378
|
isArchived: Boolean(row.is_archived),
|
|
3001
3379
|
comments,
|
|
3002
3380
|
attachments,
|
|
3381
|
+
...(includeActivity ? {
|
|
3382
|
+
activity,
|
|
3383
|
+
entityEvents,
|
|
3384
|
+
} : {}),
|
|
3003
3385
|
};
|
|
3004
3386
|
}
|
|
3005
|
-
rowToWorkstream(row) {
|
|
3387
|
+
rowToWorkstream(row, options) {
|
|
3388
|
+
const id = String(row.id || '').trim();
|
|
3389
|
+
const workspaceId = this.normalizeWorkspaceId(typeof row.workspace_id === 'string' ? row.workspace_id : undefined);
|
|
3006
3390
|
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
3007
3391
|
const comments = this.normalizeIncomingComments(this.parseJsonField(row.comments_json, []));
|
|
3392
|
+
const includeActivity = options?.includeActivity === true;
|
|
3393
|
+
const entityEvents = includeActivity
|
|
3394
|
+
? this.listEntityEventsInternal('workstream', id, workspaceId)
|
|
3395
|
+
: undefined;
|
|
3396
|
+
const activity = includeActivity
|
|
3397
|
+
? this.buildEntityActivity(comments, entityEvents || [])
|
|
3398
|
+
: undefined;
|
|
3008
3399
|
const attachments = this.normalizePlanningAttachments(this.parseJsonField(row.attachments_json, []));
|
|
3009
3400
|
return {
|
|
3010
|
-
id
|
|
3401
|
+
id,
|
|
3011
3402
|
referenceNumber,
|
|
3012
3403
|
referenceLabel: formatWorkstreamReference(referenceNumber),
|
|
3013
3404
|
initiativeId: typeof row.initiative_id === 'string' && row.initiative_id.trim().length > 0 ? row.initiative_id.trim() : null,
|
|
@@ -3020,6 +3411,10 @@ export class TaskforceCore {
|
|
|
3020
3411
|
isArchived: Boolean(row.is_archived),
|
|
3021
3412
|
comments,
|
|
3022
3413
|
attachments,
|
|
3414
|
+
...(includeActivity ? {
|
|
3415
|
+
activity,
|
|
3416
|
+
entityEvents,
|
|
3417
|
+
} : {}),
|
|
3023
3418
|
};
|
|
3024
3419
|
}
|
|
3025
3420
|
rowToChecklistItem(row) {
|
|
@@ -3072,7 +3467,7 @@ export class TaskforceCore {
|
|
|
3072
3467
|
`).get(this.tenantId, normalizedWorkspaceId, normalizedId);
|
|
3073
3468
|
if (!row)
|
|
3074
3469
|
return null;
|
|
3075
|
-
return this.rowToInitiative(row);
|
|
3470
|
+
return this.rowToInitiative(row, { includeActivity: true });
|
|
3076
3471
|
}
|
|
3077
3472
|
listInitiatives(workspaceId) {
|
|
3078
3473
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
@@ -3084,7 +3479,45 @@ export class TaskforceCore {
|
|
|
3084
3479
|
`).all(this.tenantId, normalizedWorkspaceId);
|
|
3085
3480
|
return rows.map((row) => this.rowToInitiative(row));
|
|
3086
3481
|
}
|
|
3087
|
-
|
|
3482
|
+
isTransientPlanningHydrationError(error) {
|
|
3483
|
+
const message = error instanceof Error ? String(error.message || '').trim() : String(error || '').trim();
|
|
3484
|
+
if (!message)
|
|
3485
|
+
return false;
|
|
3486
|
+
return /(timed out|timeout|ETIMEDOUT|query canceled|statement timeout|database timeout|postgres worker)/i.test(message);
|
|
3487
|
+
}
|
|
3488
|
+
getInitiativeForWriteResult(id, workspaceId, fallback, context) {
|
|
3489
|
+
try {
|
|
3490
|
+
return this.getInitiative(id, workspaceId) || fallback;
|
|
3491
|
+
}
|
|
3492
|
+
catch (error) {
|
|
3493
|
+
if (!this.isTransientPlanningHydrationError(error))
|
|
3494
|
+
throw error;
|
|
3495
|
+
console.warn('[Taskforce] Falling back to post-write initiative snapshot after hydration failed', {
|
|
3496
|
+
context,
|
|
3497
|
+
initiativeId: id,
|
|
3498
|
+
workspaceId,
|
|
3499
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3500
|
+
});
|
|
3501
|
+
return fallback;
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
getWorkstreamForWriteResult(id, workspaceId, fallback, context) {
|
|
3505
|
+
try {
|
|
3506
|
+
return this.getWorkstream(id, workspaceId) || fallback;
|
|
3507
|
+
}
|
|
3508
|
+
catch (error) {
|
|
3509
|
+
if (!this.isTransientPlanningHydrationError(error))
|
|
3510
|
+
throw error;
|
|
3511
|
+
console.warn('[Taskforce] Falling back to post-write workstream snapshot after hydration failed', {
|
|
3512
|
+
context,
|
|
3513
|
+
workstreamId: id,
|
|
3514
|
+
workspaceId,
|
|
3515
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3516
|
+
});
|
|
3517
|
+
return fallback;
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
createInitiative(input, workspaceId, options) {
|
|
3088
3521
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3089
3522
|
const id = String(input.id || '').trim() || this.generateEntityId('initiative');
|
|
3090
3523
|
const referenceNumber = this.reserveInitiativeReferenceNumber(normalizedWorkspaceId, input.referenceNumber);
|
|
@@ -3123,18 +3556,37 @@ export class TaskforceCore {
|
|
|
3123
3556
|
sortOrder: initiative.order,
|
|
3124
3557
|
isArchived: initiative.isArchived ? 1 : 0,
|
|
3125
3558
|
});
|
|
3126
|
-
|
|
3559
|
+
this.syncCanonicalPlanningAttachmentLinks('initiative', initiative.id, normalizedWorkspaceId, initiative.attachments || []);
|
|
3560
|
+
if (options?.skipEntityEvents !== true) {
|
|
3561
|
+
this.addEntityEvent({
|
|
3562
|
+
entityType: 'initiative',
|
|
3563
|
+
entityId: initiative.id,
|
|
3564
|
+
workspaceId: normalizedWorkspaceId,
|
|
3565
|
+
action: 'initiative-created',
|
|
3566
|
+
actor: options?.actorRef || 'system',
|
|
3567
|
+
createdAt: initiative.updatedAt || initiative.createdAt,
|
|
3568
|
+
details: {
|
|
3569
|
+
title: initiative.title,
|
|
3570
|
+
ownerId: initiative.ownerId || null,
|
|
3571
|
+
}
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
return this.getInitiativeForWriteResult(initiative.id, normalizedWorkspaceId, initiative, 'createInitiative:post-write');
|
|
3127
3575
|
}
|
|
3128
|
-
updateInitiative(id, patch, workspaceId) {
|
|
3576
|
+
updateInitiative(id, patch, workspaceId, options) {
|
|
3129
3577
|
const existing = this.getInitiative(id, workspaceId);
|
|
3130
3578
|
if (!existing) {
|
|
3131
3579
|
throw new TaskforceRuleError(`Initiative not found: ${id}`, 404, 'INITIATIVE_NOT_FOUND');
|
|
3132
3580
|
}
|
|
3133
3581
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3582
|
+
const nextAttachments = Object.prototype.hasOwnProperty.call(patch, 'attachments') && Array.isArray(patch.attachments)
|
|
3583
|
+
? patch.attachments
|
|
3584
|
+
: existing.attachments;
|
|
3134
3585
|
const next = {
|
|
3135
3586
|
...existing,
|
|
3136
3587
|
...patch,
|
|
3137
3588
|
id: existing.id,
|
|
3589
|
+
attachments: nextAttachments,
|
|
3138
3590
|
updatedAt: String(patch.updatedAt || '').trim() || new Date().toISOString(),
|
|
3139
3591
|
referenceLabel: formatInitiativeReference(patch.referenceNumber ?? existing.referenceNumber),
|
|
3140
3592
|
};
|
|
@@ -3164,25 +3616,68 @@ export class TaskforceCore {
|
|
|
3164
3616
|
sortOrder: next.order,
|
|
3165
3617
|
isArchived: next.isArchived ? 1 : 0,
|
|
3166
3618
|
});
|
|
3167
|
-
|
|
3619
|
+
this.syncCanonicalPlanningAttachmentLinks('initiative', existing.id, normalizedWorkspaceId, next.attachments || []);
|
|
3620
|
+
const hydrated = this.getInitiativeForWriteResult(existing.id, normalizedWorkspaceId, next, 'updateInitiative:post-write');
|
|
3621
|
+
if (options?.skipEntityEvents !== true) {
|
|
3622
|
+
const changes = this.buildPlanningEntityEventChanges(existing, next, ['title', 'description', 'ownerId', 'attachments']);
|
|
3623
|
+
if (Object.keys(changes).length > 0) {
|
|
3624
|
+
this.addEntityEvent({
|
|
3625
|
+
entityType: 'initiative',
|
|
3626
|
+
entityId: existing.id,
|
|
3627
|
+
workspaceId: normalizedWorkspaceId,
|
|
3628
|
+
action: this.getPlanningEntityEventAction('initiative', changes),
|
|
3629
|
+
actor: options?.actorRef || 'system',
|
|
3630
|
+
createdAt: next.updatedAt,
|
|
3631
|
+
details: { changes }
|
|
3632
|
+
});
|
|
3633
|
+
return this.getInitiativeForWriteResult(existing.id, normalizedWorkspaceId, hydrated, 'updateInitiative:post-event');
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
return hydrated;
|
|
3168
3637
|
}
|
|
3169
|
-
archiveInitiative(id, workspaceId) {
|
|
3638
|
+
archiveInitiative(id, workspaceId, options) {
|
|
3170
3639
|
const existing = this.getInitiative(id, workspaceId);
|
|
3171
3640
|
if (!existing) {
|
|
3172
3641
|
throw new TaskforceRuleError(`Initiative not found: ${id}`, 404, 'INITIATIVE_NOT_FOUND');
|
|
3173
3642
|
}
|
|
3174
3643
|
if (existing.isArchived)
|
|
3175
3644
|
return existing;
|
|
3176
|
-
|
|
3645
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3646
|
+
const archived = this.updateInitiative(id, { isArchived: true }, workspaceId, { ...options, skipEntityEvents: true });
|
|
3647
|
+
if (options?.skipEntityEvents !== true) {
|
|
3648
|
+
this.addEntityEvent({
|
|
3649
|
+
entityType: 'initiative',
|
|
3650
|
+
entityId: id,
|
|
3651
|
+
workspaceId: normalizedWorkspaceId,
|
|
3652
|
+
action: 'initiative-archived',
|
|
3653
|
+
actor: options?.actorRef || 'system',
|
|
3654
|
+
createdAt: archived.updatedAt || new Date().toISOString(),
|
|
3655
|
+
});
|
|
3656
|
+
return this.getInitiativeForWriteResult(id, normalizedWorkspaceId, archived, 'archiveInitiative:post-event');
|
|
3657
|
+
}
|
|
3658
|
+
return archived;
|
|
3177
3659
|
}
|
|
3178
|
-
unarchiveInitiative(id, workspaceId) {
|
|
3660
|
+
unarchiveInitiative(id, workspaceId, options) {
|
|
3179
3661
|
const existing = this.getInitiative(id, workspaceId);
|
|
3180
3662
|
if (!existing) {
|
|
3181
3663
|
throw new TaskforceRuleError(`Initiative not found: ${id}`, 404, 'INITIATIVE_NOT_FOUND');
|
|
3182
3664
|
}
|
|
3183
3665
|
if (!existing.isArchived)
|
|
3184
3666
|
return existing;
|
|
3185
|
-
|
|
3667
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3668
|
+
const restored = this.updateInitiative(id, { isArchived: false }, workspaceId, { ...options, skipEntityEvents: true });
|
|
3669
|
+
if (options?.skipEntityEvents !== true) {
|
|
3670
|
+
this.addEntityEvent({
|
|
3671
|
+
entityType: 'initiative',
|
|
3672
|
+
entityId: id,
|
|
3673
|
+
workspaceId: normalizedWorkspaceId,
|
|
3674
|
+
action: 'initiative-unarchived',
|
|
3675
|
+
actor: options?.actorRef || 'system',
|
|
3676
|
+
createdAt: restored.updatedAt || new Date().toISOString(),
|
|
3677
|
+
});
|
|
3678
|
+
return this.getInitiativeForWriteResult(id, normalizedWorkspaceId, restored, 'unarchiveInitiative:post-event');
|
|
3679
|
+
}
|
|
3680
|
+
return restored;
|
|
3186
3681
|
}
|
|
3187
3682
|
getWorkstream(id, workspaceId) {
|
|
3188
3683
|
const normalizedId = String(id || '').trim();
|
|
@@ -3197,7 +3692,7 @@ export class TaskforceCore {
|
|
|
3197
3692
|
`).get(this.tenantId, normalizedWorkspaceId, normalizedId);
|
|
3198
3693
|
if (!row)
|
|
3199
3694
|
return null;
|
|
3200
|
-
return this.rowToWorkstream(row);
|
|
3695
|
+
return this.rowToWorkstream(row, { includeActivity: true });
|
|
3201
3696
|
}
|
|
3202
3697
|
listWorkstreams(workspaceId) {
|
|
3203
3698
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
@@ -3266,7 +3761,7 @@ export class TaskforceCore {
|
|
|
3266
3761
|
workstreamTaskSummaries: Array.from(workstreamTaskSummaries.values()),
|
|
3267
3762
|
};
|
|
3268
3763
|
}
|
|
3269
|
-
createWorkstream(input, workspaceId) {
|
|
3764
|
+
createWorkstream(input, workspaceId, options) {
|
|
3270
3765
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3271
3766
|
const id = String(input.id || '').trim() || this.generateEntityId('workstream');
|
|
3272
3767
|
const referenceNumber = this.reserveWorkstreamReferenceNumber(normalizedWorkspaceId, input.referenceNumber);
|
|
@@ -3309,14 +3804,33 @@ export class TaskforceCore {
|
|
|
3309
3804
|
sortOrder: workstream.order,
|
|
3310
3805
|
isArchived: workstream.isArchived ? 1 : 0,
|
|
3311
3806
|
});
|
|
3312
|
-
|
|
3807
|
+
this.syncCanonicalPlanningAttachmentLinks('workstream', workstream.id, normalizedWorkspaceId, workstream.attachments || []);
|
|
3808
|
+
if (options?.skipEntityEvents !== true) {
|
|
3809
|
+
this.addEntityEvent({
|
|
3810
|
+
entityType: 'workstream',
|
|
3811
|
+
entityId: workstream.id,
|
|
3812
|
+
workspaceId: normalizedWorkspaceId,
|
|
3813
|
+
action: 'workstream-created',
|
|
3814
|
+
actor: options?.actorRef || 'system',
|
|
3815
|
+
createdAt: workstream.updatedAt || workstream.createdAt,
|
|
3816
|
+
details: {
|
|
3817
|
+
title: workstream.title,
|
|
3818
|
+
ownerId: workstream.ownerId || null,
|
|
3819
|
+
initiativeId: workstream.initiativeId || null,
|
|
3820
|
+
}
|
|
3821
|
+
});
|
|
3822
|
+
}
|
|
3823
|
+
return this.getWorkstreamForWriteResult(workstream.id, normalizedWorkspaceId, workstream, 'createWorkstream:post-write');
|
|
3313
3824
|
}
|
|
3314
|
-
updateWorkstream(id, patch, workspaceId) {
|
|
3825
|
+
updateWorkstream(id, patch, workspaceId, options) {
|
|
3315
3826
|
const existing = this.getWorkstream(id, workspaceId);
|
|
3316
3827
|
if (!existing) {
|
|
3317
3828
|
throw new TaskforceRuleError(`Workstream not found: ${id}`, 404, 'WORKSTREAM_NOT_FOUND');
|
|
3318
3829
|
}
|
|
3319
3830
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3831
|
+
const nextAttachments = Object.prototype.hasOwnProperty.call(patch, 'attachments') && Array.isArray(patch.attachments)
|
|
3832
|
+
? patch.attachments
|
|
3833
|
+
: existing.attachments;
|
|
3320
3834
|
const next = {
|
|
3321
3835
|
...existing,
|
|
3322
3836
|
...patch,
|
|
@@ -3324,6 +3838,7 @@ export class TaskforceCore {
|
|
|
3324
3838
|
initiativeId: Object.prototype.hasOwnProperty.call(patch, 'initiativeId')
|
|
3325
3839
|
? this.sanitizeInitiativeId(patch.initiativeId)
|
|
3326
3840
|
: existing.initiativeId,
|
|
3841
|
+
attachments: nextAttachments,
|
|
3327
3842
|
updatedAt: String(patch.updatedAt || '').trim() || new Date().toISOString(),
|
|
3328
3843
|
referenceLabel: formatWorkstreamReference(patch.referenceNumber ?? existing.referenceNumber),
|
|
3329
3844
|
};
|
|
@@ -3356,41 +3871,94 @@ export class TaskforceCore {
|
|
|
3356
3871
|
sortOrder: next.order,
|
|
3357
3872
|
isArchived: next.isArchived ? 1 : 0,
|
|
3358
3873
|
});
|
|
3359
|
-
|
|
3874
|
+
this.syncCanonicalPlanningAttachmentLinks('workstream', existing.id, normalizedWorkspaceId, next.attachments || []);
|
|
3875
|
+
const hydrated = this.getWorkstreamForWriteResult(existing.id, normalizedWorkspaceId, next, 'updateWorkstream:post-write');
|
|
3876
|
+
if (options?.skipEntityEvents !== true) {
|
|
3877
|
+
const changes = this.buildPlanningEntityEventChanges(existing, next, ['title', 'description', 'ownerId', 'initiativeId', 'attachments']);
|
|
3878
|
+
if (Object.keys(changes).length > 0) {
|
|
3879
|
+
this.addEntityEvent({
|
|
3880
|
+
entityType: 'workstream',
|
|
3881
|
+
entityId: existing.id,
|
|
3882
|
+
workspaceId: normalizedWorkspaceId,
|
|
3883
|
+
action: this.getPlanningEntityEventAction('workstream', changes),
|
|
3884
|
+
actor: options?.actorRef || 'system',
|
|
3885
|
+
createdAt: next.updatedAt,
|
|
3886
|
+
details: { changes }
|
|
3887
|
+
});
|
|
3888
|
+
return this.getWorkstreamForWriteResult(existing.id, normalizedWorkspaceId, hydrated, 'updateWorkstream:post-event');
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
return hydrated;
|
|
3360
3892
|
}
|
|
3361
|
-
archiveWorkstream(id, workspaceId) {
|
|
3893
|
+
archiveWorkstream(id, workspaceId, options) {
|
|
3362
3894
|
const existing = this.getWorkstream(id, workspaceId);
|
|
3363
3895
|
if (!existing) {
|
|
3364
3896
|
throw new TaskforceRuleError(`Workstream not found: ${id}`, 404, 'WORKSTREAM_NOT_FOUND');
|
|
3365
3897
|
}
|
|
3366
3898
|
if (existing.isArchived)
|
|
3367
3899
|
return existing;
|
|
3368
|
-
|
|
3900
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3901
|
+
const archived = this.updateWorkstream(id, { isArchived: true }, workspaceId, { ...options, skipEntityEvents: true });
|
|
3902
|
+
if (options?.skipEntityEvents !== true) {
|
|
3903
|
+
this.addEntityEvent({
|
|
3904
|
+
entityType: 'workstream',
|
|
3905
|
+
entityId: id,
|
|
3906
|
+
workspaceId: normalizedWorkspaceId,
|
|
3907
|
+
action: 'workstream-archived',
|
|
3908
|
+
actor: options?.actorRef || 'system',
|
|
3909
|
+
createdAt: archived.updatedAt || new Date().toISOString(),
|
|
3910
|
+
});
|
|
3911
|
+
return this.getWorkstreamForWriteResult(id, normalizedWorkspaceId, archived, 'archiveWorkstream:post-event');
|
|
3912
|
+
}
|
|
3913
|
+
return archived;
|
|
3369
3914
|
}
|
|
3370
|
-
unarchiveWorkstream(id, workspaceId) {
|
|
3915
|
+
unarchiveWorkstream(id, workspaceId, options) {
|
|
3371
3916
|
const existing = this.getWorkstream(id, workspaceId);
|
|
3372
3917
|
if (!existing) {
|
|
3373
3918
|
throw new TaskforceRuleError(`Workstream not found: ${id}`, 404, 'WORKSTREAM_NOT_FOUND');
|
|
3374
3919
|
}
|
|
3375
3920
|
if (!existing.isArchived)
|
|
3376
3921
|
return existing;
|
|
3377
|
-
|
|
3922
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3923
|
+
const restored = this.updateWorkstream(id, { isArchived: false }, workspaceId, { ...options, skipEntityEvents: true });
|
|
3924
|
+
if (options?.skipEntityEvents !== true) {
|
|
3925
|
+
this.addEntityEvent({
|
|
3926
|
+
entityType: 'workstream',
|
|
3927
|
+
entityId: id,
|
|
3928
|
+
workspaceId: normalizedWorkspaceId,
|
|
3929
|
+
action: 'workstream-unarchived',
|
|
3930
|
+
actor: options?.actorRef || 'system',
|
|
3931
|
+
createdAt: restored.updatedAt || new Date().toISOString(),
|
|
3932
|
+
});
|
|
3933
|
+
return this.getWorkstreamForWriteResult(id, normalizedWorkspaceId, restored, 'unarchiveWorkstream:post-event');
|
|
3934
|
+
}
|
|
3935
|
+
return restored;
|
|
3378
3936
|
}
|
|
3379
3937
|
upsertInitiativeForSync(initiative, workspaceId) {
|
|
3380
3938
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3381
3939
|
const existing = this.getInitiative(initiative.id, normalizedWorkspaceId);
|
|
3382
3940
|
if (existing) {
|
|
3383
|
-
|
|
3941
|
+
const existingUpdatedAtMs = Date.parse(String(existing.updatedAt || existing.createdAt || '').trim());
|
|
3942
|
+
const incomingUpdatedAtMs = Date.parse(String(initiative.updatedAt || initiative.createdAt || '').trim());
|
|
3943
|
+
if (Number.isFinite(existingUpdatedAtMs) && Number.isFinite(incomingUpdatedAtMs) && incomingUpdatedAtMs < existingUpdatedAtMs) {
|
|
3944
|
+
return existing;
|
|
3945
|
+
}
|
|
3946
|
+
return this.updateInitiative(existing.id, initiative, normalizedWorkspaceId, { skipEntityEvents: true });
|
|
3384
3947
|
}
|
|
3385
|
-
return this.createInitiative(initiative, normalizedWorkspaceId);
|
|
3948
|
+
return this.createInitiative(initiative, normalizedWorkspaceId, { skipEntityEvents: true });
|
|
3386
3949
|
}
|
|
3387
3950
|
upsertWorkstreamForSync(workstream, workspaceId) {
|
|
3388
3951
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3389
3952
|
const existing = this.getWorkstream(workstream.id, normalizedWorkspaceId);
|
|
3390
3953
|
if (existing) {
|
|
3391
|
-
|
|
3954
|
+
const existingUpdatedAtMs = Date.parse(String(existing.updatedAt || existing.createdAt || '').trim());
|
|
3955
|
+
const incomingUpdatedAtMs = Date.parse(String(workstream.updatedAt || workstream.createdAt || '').trim());
|
|
3956
|
+
if (Number.isFinite(existingUpdatedAtMs) && Number.isFinite(incomingUpdatedAtMs) && incomingUpdatedAtMs < existingUpdatedAtMs) {
|
|
3957
|
+
return existing;
|
|
3958
|
+
}
|
|
3959
|
+
return this.updateWorkstream(existing.id, workstream, normalizedWorkspaceId, { skipEntityEvents: true });
|
|
3392
3960
|
}
|
|
3393
|
-
return this.createWorkstream(workstream, normalizedWorkspaceId);
|
|
3961
|
+
return this.createWorkstream(workstream, normalizedWorkspaceId, { skipEntityEvents: true });
|
|
3394
3962
|
}
|
|
3395
3963
|
listChecklistItems(taskId, workspaceId) {
|
|
3396
3964
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
@@ -4542,6 +5110,91 @@ export class TaskforceCore {
|
|
|
4542
5110
|
`).run(now, now, this.tenantId, normalizedUserId, cutoff);
|
|
4543
5111
|
return staleDrafts;
|
|
4544
5112
|
}
|
|
5113
|
+
createTaskAttachmentUploadDraft(input) {
|
|
5114
|
+
const draftId = String(input.draftId || '').trim();
|
|
5115
|
+
const workspaceId = String(input.workspaceId || '').trim();
|
|
5116
|
+
const taskId = String(input.taskId || '').trim();
|
|
5117
|
+
const actorRef = String(input.actorRef || '').trim() || 'system';
|
|
5118
|
+
const storageKey = String(input.storageKey || '').trim();
|
|
5119
|
+
const mimeType = String(input.mimeType || 'application/octet-stream').trim() || 'application/octet-stream';
|
|
5120
|
+
const originalFilename = String(input.originalFilename || '').trim() || 'attachment';
|
|
5121
|
+
const sizeBytes = Number.isFinite(Number(input.sizeBytes)) ? Math.max(0, Math.floor(Number(input.sizeBytes))) : 0;
|
|
5122
|
+
const storageProvider = input.storageProvider === 'r2' || input.storageProvider === 'external' ? input.storageProvider : 'local';
|
|
5123
|
+
if (!draftId)
|
|
5124
|
+
throw new TaskforceRuleError('draftId is required', 400, 'TASK_ATTACHMENT_DRAFT_ID_REQUIRED');
|
|
5125
|
+
if (!workspaceId)
|
|
5126
|
+
throw new TaskforceRuleError('workspaceId is required', 400, 'WORKSPACE_ID_REQUIRED');
|
|
5127
|
+
if (!taskId)
|
|
5128
|
+
throw new TaskforceRuleError('taskId is required', 400, 'TASK_ID_REQUIRED');
|
|
5129
|
+
if (!storageKey)
|
|
5130
|
+
throw new TaskforceRuleError('storageKey is required', 400, 'TASK_ATTACHMENT_STORAGE_KEY_REQUIRED');
|
|
5131
|
+
const now = new Date().toISOString();
|
|
5132
|
+
this.db.prepare(`
|
|
5133
|
+
INSERT INTO task_attachment_upload_drafts (
|
|
5134
|
+
draft_id, tenant_id, workspace_id, task_id, actor_ref, storage_key, mime_type, original_filename,
|
|
5135
|
+
size_bytes, storage_provider, consumed_at, discarded_at, created_at, updated_at
|
|
5136
|
+
)
|
|
5137
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)
|
|
5138
|
+
`).run(draftId, this.tenantId, workspaceId, taskId, actorRef, storageKey, mimeType, originalFilename, sizeBytes, storageProvider, now, now);
|
|
5139
|
+
return this.getTaskAttachmentUploadDraft(draftId);
|
|
5140
|
+
}
|
|
5141
|
+
getTaskAttachmentUploadDraft(draftId) {
|
|
5142
|
+
const normalizedDraftId = String(draftId || '').trim();
|
|
5143
|
+
if (!normalizedDraftId)
|
|
5144
|
+
return null;
|
|
5145
|
+
const row = this.db.prepare(`
|
|
5146
|
+
SELECT draft_id, workspace_id, task_id, actor_ref, storage_key, mime_type, original_filename,
|
|
5147
|
+
size_bytes, storage_provider, consumed_at, discarded_at, created_at, updated_at
|
|
5148
|
+
FROM task_attachment_upload_drafts
|
|
5149
|
+
WHERE tenant_id = ? AND draft_id = ?
|
|
5150
|
+
LIMIT 1
|
|
5151
|
+
`).get(this.tenantId, normalizedDraftId);
|
|
5152
|
+
if (!row?.draft_id)
|
|
5153
|
+
return null;
|
|
5154
|
+
return {
|
|
5155
|
+
draftId: String(row.draft_id),
|
|
5156
|
+
workspaceId: String(row.workspace_id || ''),
|
|
5157
|
+
taskId: String(row.task_id || ''),
|
|
5158
|
+
actorRef: String(row.actor_ref || 'system'),
|
|
5159
|
+
storageKey: String(row.storage_key || ''),
|
|
5160
|
+
mimeType: String(row.mime_type || 'application/octet-stream'),
|
|
5161
|
+
originalFilename: String(row.original_filename || 'attachment'),
|
|
5162
|
+
sizeBytes: Number(row.size_bytes || 0),
|
|
5163
|
+
storageProvider: String(row.storage_provider || 'local') === 'r2'
|
|
5164
|
+
? 'r2'
|
|
5165
|
+
: String(row.storage_provider || 'local') === 'external'
|
|
5166
|
+
? 'external'
|
|
5167
|
+
: 'local',
|
|
5168
|
+
consumedAt: row.consumed_at ? String(row.consumed_at) : null,
|
|
5169
|
+
discardedAt: row.discarded_at ? String(row.discarded_at) : null,
|
|
5170
|
+
createdAt: String(row.created_at || ''),
|
|
5171
|
+
updatedAt: String(row.updated_at || ''),
|
|
5172
|
+
};
|
|
5173
|
+
}
|
|
5174
|
+
markTaskAttachmentUploadDraftConsumed(draftId) {
|
|
5175
|
+
const existing = this.getTaskAttachmentUploadDraft(draftId);
|
|
5176
|
+
if (!existing || existing.discardedAt)
|
|
5177
|
+
return null;
|
|
5178
|
+
const now = new Date().toISOString();
|
|
5179
|
+
this.db.prepare(`
|
|
5180
|
+
UPDATE task_attachment_upload_drafts
|
|
5181
|
+
SET consumed_at = COALESCE(consumed_at, ?), updated_at = ?
|
|
5182
|
+
WHERE tenant_id = ? AND draft_id = ? AND discarded_at IS NULL
|
|
5183
|
+
`).run(now, now, this.tenantId, existing.draftId);
|
|
5184
|
+
return this.getTaskAttachmentUploadDraft(existing.draftId);
|
|
5185
|
+
}
|
|
5186
|
+
discardTaskAttachmentUploadDraft(draftId) {
|
|
5187
|
+
const existing = this.getTaskAttachmentUploadDraft(draftId);
|
|
5188
|
+
if (!existing || existing.discardedAt)
|
|
5189
|
+
return existing;
|
|
5190
|
+
const now = new Date().toISOString();
|
|
5191
|
+
this.db.prepare(`
|
|
5192
|
+
UPDATE task_attachment_upload_drafts
|
|
5193
|
+
SET discarded_at = ?, updated_at = ?
|
|
5194
|
+
WHERE tenant_id = ? AND draft_id = ? AND discarded_at IS NULL
|
|
5195
|
+
`).run(now, now, this.tenantId, existing.draftId);
|
|
5196
|
+
return this.getTaskAttachmentUploadDraft(existing.draftId);
|
|
5197
|
+
}
|
|
4545
5198
|
getSystemRole(userId) {
|
|
4546
5199
|
const normalizedUserId = String(userId || '').trim();
|
|
4547
5200
|
if (!normalizedUserId || normalizedUserId === 'anonymous')
|
|
@@ -5106,6 +5759,9 @@ export class TaskforceCore {
|
|
|
5106
5759
|
updateAiProfileAvatar(input) {
|
|
5107
5760
|
return updateAiProfileAvatarService(this.getAiProfileServiceDeps(), input);
|
|
5108
5761
|
}
|
|
5762
|
+
updateAiProfileSurfaceType(input) {
|
|
5763
|
+
return updateAiProfileSurfaceTypeService(this.getAiProfileServiceDeps(), input);
|
|
5764
|
+
}
|
|
5109
5765
|
resolveAiProfile(input, options) {
|
|
5110
5766
|
return resolveAiProfileService(this.getAiProfileServiceDeps(), input, options);
|
|
5111
5767
|
}
|
|
@@ -5246,6 +5902,9 @@ export class TaskforceCore {
|
|
|
5246
5902
|
upsertTaskEventForSync(event) {
|
|
5247
5903
|
return upsertTaskEventForSyncService(this.getTaskActivityServiceDeps(), event);
|
|
5248
5904
|
}
|
|
5905
|
+
upsertEntityEventForSync(event) {
|
|
5906
|
+
return upsertEntityEventForSyncService(this.getTaskActivityServiceDeps(), event);
|
|
5907
|
+
}
|
|
5249
5908
|
deleteDocumentReviewSessionForSync(input) {
|
|
5250
5909
|
const deps = this.getDocumentReviewServiceDeps();
|
|
5251
5910
|
return deps.sessionStore.softDelete(input.sessionId, deps.normalizeWorkspaceId(input.workspaceId), input.deletedAt || undefined);
|
|
@@ -5313,38 +5972,9 @@ export class TaskforceCore {
|
|
|
5313
5972
|
if (aiProfile) {
|
|
5314
5973
|
return this.aiProfileToActor(this.aiProfileRowToRecord(aiProfile));
|
|
5315
5974
|
}
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
FROM ai_profiles
|
|
5320
|
-
WHERE tenant_id = ?
|
|
5321
|
-
AND workspace_id = ?
|
|
5322
|
-
AND archived_at IS NULL
|
|
5323
|
-
AND (
|
|
5324
|
-
LOWER(name) = LOWER(?)
|
|
5325
|
-
OR LOWER(username) = LOWER(?)
|
|
5326
|
-
)
|
|
5327
|
-
ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
|
|
5328
|
-
LIMIT 1
|
|
5329
|
-
`).get(this.tenantId, normalizedWorkspaceId, raw, raw);
|
|
5330
|
-
if (aiProfileByName) {
|
|
5331
|
-
return this.aiProfileToActor(this.aiProfileRowToRecord(aiProfileByName));
|
|
5332
|
-
}
|
|
5333
|
-
const archivedAiProfileByName = this.db.prepare(`
|
|
5334
|
-
SELECT *
|
|
5335
|
-
FROM ai_profiles
|
|
5336
|
-
WHERE tenant_id = ?
|
|
5337
|
-
AND workspace_id = ?
|
|
5338
|
-
AND archived_at IS NOT NULL
|
|
5339
|
-
AND (
|
|
5340
|
-
LOWER(name) = LOWER(?)
|
|
5341
|
-
OR LOWER(username) = LOWER(?)
|
|
5342
|
-
)
|
|
5343
|
-
ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
|
|
5344
|
-
LIMIT 1
|
|
5345
|
-
`).get(this.tenantId, normalizedWorkspaceId, raw, raw);
|
|
5346
|
-
if (archivedAiProfileByName) {
|
|
5347
|
-
return this.aiProfileToActor(this.aiProfileRowToRecord(archivedAiProfileByName));
|
|
5975
|
+
const uniqueAiProfileByAlias = this.resolveUniqueAiProfileActorByAlias(normalizedWorkspaceId, raw);
|
|
5976
|
+
if (uniqueAiProfileByAlias) {
|
|
5977
|
+
return uniqueAiProfileByAlias;
|
|
5348
5978
|
}
|
|
5349
5979
|
const workspaceUser = this.listWorkspaceUsers(normalizedWorkspaceId).find((user) => user.userId === raw);
|
|
5350
5980
|
if (workspaceUser) {
|
|
@@ -6157,6 +6787,9 @@ export class TaskforceCore {
|
|
|
6157
6787
|
revokeMcpAccessToken(input) {
|
|
6158
6788
|
return this.mcpTokenService.revokeMcpAccessToken(input);
|
|
6159
6789
|
}
|
|
6790
|
+
deleteMcpAccessToken(input) {
|
|
6791
|
+
return this.mcpTokenService.deleteMcpAccessToken(input);
|
|
6792
|
+
}
|
|
6160
6793
|
regenerateMcpAccessToken(input) {
|
|
6161
6794
|
return this.mcpTokenService.regenerateMcpAccessToken(input);
|
|
6162
6795
|
}
|
|
@@ -6190,6 +6823,9 @@ export class TaskforceCore {
|
|
|
6190
6823
|
authenticateMcpOAuthAccessToken(token, workspaceId) {
|
|
6191
6824
|
return this.mcpTokenService.authenticateMcpOAuthAccessToken(token, workspaceId);
|
|
6192
6825
|
}
|
|
6826
|
+
bindMcpOAuthConnectorGrantAiProfile(input) {
|
|
6827
|
+
return this.mcpTokenService.bindMcpOAuthConnectorGrantAiProfile(input);
|
|
6828
|
+
}
|
|
6193
6829
|
recordMcpOAuthConnectorGrantUsage(input) {
|
|
6194
6830
|
return this.mcpTokenService.recordMcpOAuthConnectorGrantUsage(input);
|
|
6195
6831
|
}
|
|
@@ -6600,9 +7236,47 @@ export class TaskforceCore {
|
|
|
6600
7236
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
6601
7237
|
return this.listTaskEventsInternal(taskId, normalizedWorkspaceId);
|
|
6602
7238
|
}
|
|
7239
|
+
taskExistsForSync(id, workspaceId) {
|
|
7240
|
+
return this.taskExists(id, this.normalizeWorkspaceId(workspaceId || undefined));
|
|
7241
|
+
}
|
|
7242
|
+
initiativeExistsForSync(id, workspaceId) {
|
|
7243
|
+
return this.initiativeExists(id, this.normalizeWorkspaceId(workspaceId || undefined));
|
|
7244
|
+
}
|
|
7245
|
+
workstreamExistsForSync(id, workspaceId) {
|
|
7246
|
+
return this.workstreamExists(id, this.normalizeWorkspaceId(workspaceId || undefined));
|
|
7247
|
+
}
|
|
7248
|
+
recordTaskEvent(input) {
|
|
7249
|
+
this.addTaskEvent({
|
|
7250
|
+
taskId: input.taskId,
|
|
7251
|
+
workspaceId: this.normalizeWorkspaceId(input.workspaceId || undefined),
|
|
7252
|
+
action: input.action,
|
|
7253
|
+
actor: input.actor || 'system',
|
|
7254
|
+
createdAt: input.createdAt,
|
|
7255
|
+
details: input.details,
|
|
7256
|
+
});
|
|
7257
|
+
}
|
|
6603
7258
|
listTaskEventsForSync(input) {
|
|
6604
7259
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(input.workspaceId || undefined);
|
|
6605
|
-
return
|
|
7260
|
+
return listWorkspaceTaskEventsForSyncService(this.getTaskActivityServiceDeps(), normalizedWorkspaceId);
|
|
7261
|
+
}
|
|
7262
|
+
getTaskEventForSync(input) {
|
|
7263
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(input.workspaceId || undefined);
|
|
7264
|
+
return getTaskEventForSyncService(this.getTaskActivityServiceDeps(), {
|
|
7265
|
+
workspaceId: normalizedWorkspaceId,
|
|
7266
|
+
taskId: input.taskId,
|
|
7267
|
+
eventId: input.eventId,
|
|
7268
|
+
});
|
|
7269
|
+
}
|
|
7270
|
+
listEntityEventsForSync(input) {
|
|
7271
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(input.workspaceId || undefined);
|
|
7272
|
+
return listWorkspaceEntityEventsForSyncService(this.getTaskActivityServiceDeps(), normalizedWorkspaceId);
|
|
7273
|
+
}
|
|
7274
|
+
getEntityEventForSync(input) {
|
|
7275
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(input.workspaceId || undefined);
|
|
7276
|
+
return getEntityEventForSyncService(this.getTaskActivityServiceDeps(), {
|
|
7277
|
+
workspaceId: normalizedWorkspaceId,
|
|
7278
|
+
eventId: input.eventId,
|
|
7279
|
+
});
|
|
6606
7280
|
}
|
|
6607
7281
|
getScheduleBoard(weekKey, showWeekends = false, workspaceId) {
|
|
6608
7282
|
const parsedWeek = this.parseIsoWeekKeyToDates(weekKey);
|
|
@@ -6866,6 +7540,16 @@ export class TaskforceCore {
|
|
|
6866
7540
|
workstreamId: task.workstreamId || null
|
|
6867
7541
|
}
|
|
6868
7542
|
});
|
|
7543
|
+
if (task.workstreamId) {
|
|
7544
|
+
this.addWorkstreamTaskMembershipEvents({
|
|
7545
|
+
task,
|
|
7546
|
+
workspaceId: normalizedWorkspaceId,
|
|
7547
|
+
actor: options?.actorRef || createdBy || 'system',
|
|
7548
|
+
createdAt: task.updatedAt || task.createdAt,
|
|
7549
|
+
previousWorkstreamId: null,
|
|
7550
|
+
nextWorkstreamId: task.workstreamId,
|
|
7551
|
+
});
|
|
7552
|
+
}
|
|
6869
7553
|
}
|
|
6870
7554
|
if (persistBackup) {
|
|
6871
7555
|
this.persistBackup();
|
|
@@ -7187,6 +7871,16 @@ export class TaskforceCore {
|
|
|
7187
7871
|
createdAt: nextTask.updatedAt,
|
|
7188
7872
|
details: { changes }
|
|
7189
7873
|
});
|
|
7874
|
+
if (changes.workstreamId) {
|
|
7875
|
+
this.addWorkstreamTaskMembershipEvents({
|
|
7876
|
+
task: nextTask,
|
|
7877
|
+
workspaceId: normalizedWorkspaceId,
|
|
7878
|
+
actor: options?.actorRef || 'system',
|
|
7879
|
+
createdAt: nextTask.updatedAt,
|
|
7880
|
+
previousWorkstreamId: existing.workstreamId || null,
|
|
7881
|
+
nextWorkstreamId: nextTask.workstreamId || null,
|
|
7882
|
+
});
|
|
7883
|
+
}
|
|
7190
7884
|
}
|
|
7191
7885
|
}
|
|
7192
7886
|
return this.getTask(taskId, normalizedWorkspaceId);
|
|
@@ -7206,7 +7900,11 @@ export class TaskforceCore {
|
|
|
7206
7900
|
}
|
|
7207
7901
|
addComment(id, text, author, workspaceId, options) {
|
|
7208
7902
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
7209
|
-
const existing = this.db.prepare(
|
|
7903
|
+
const existing = this.db.prepare(`
|
|
7904
|
+
SELECT id, updated_at, created_at
|
|
7905
|
+
FROM tasks
|
|
7906
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
7907
|
+
`).get(this.tenantId, normalizedWorkspaceId, id);
|
|
7210
7908
|
if (!existing)
|
|
7211
7909
|
return null;
|
|
7212
7910
|
const submittedAuthor = String(author || '').trim() || 'user';
|
|
@@ -7231,7 +7929,13 @@ export class TaskforceCore {
|
|
|
7231
7929
|
submitted_author: comment.submittedAuthor,
|
|
7232
7930
|
timestamp: comment.timestamp
|
|
7233
7931
|
});
|
|
7234
|
-
|
|
7932
|
+
const existingWatermark = String(existing.updated_at || existing.created_at || '').trim();
|
|
7933
|
+
const existingWatermarkMs = Date.parse(existingWatermark);
|
|
7934
|
+
const commentTimestampMs = Date.parse(comment.timestamp);
|
|
7935
|
+
const nextTaskUpdatedAt = Number.isFinite(existingWatermarkMs) && existingWatermarkMs >= commentTimestampMs
|
|
7936
|
+
? new Date(existingWatermarkMs + 1).toISOString()
|
|
7937
|
+
: comment.timestamp;
|
|
7938
|
+
this.db.prepare('UPDATE tasks SET updated_at = ? WHERE tenant_id = ? AND workspace_id = ? AND id = ?').run(nextTaskUpdatedAt, this.tenantId, normalizedWorkspaceId, id);
|
|
7235
7939
|
if (options?.skipTaskEvents !== true) {
|
|
7236
7940
|
this.addTaskEvent({
|
|
7237
7941
|
taskId: id,
|