@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.
- package/dist/Taskforce.module.css +24 -53
- package/dist/components/activity/EntityActivityTimeline.js +15 -5
- package/dist/components/context/AttachmentPreviewHost.js +1 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
- package/dist/components/context/ContextAttachmentManager.js +116 -20
- package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
- package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
- package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
- package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
- package/dist/components/features/planning/PlanningModule.js +26 -9
- package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
- package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
- package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +2 -2
- package/dist/components/task/TaskCard.js +37 -4
- package/dist/components/task/TaskContextUpload.d.ts +3 -0
- package/dist/components/task/TaskContextUpload.js +2 -2
- package/dist/components/task/TaskForm.d.ts +4 -0
- package/dist/components/task/TaskForm.js +32 -7
- package/dist/components/task/TaskImageReviews.js +16 -16
- package/dist/components/task/TaskStatusActions.js +11 -2
- package/dist/components/task/TaskWorkstreamPicker.js +2 -1
- package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
- package/dist/components/ui/DocumentTypeIcon.js +8 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
- package/dist/components/ui/TaxonomyDropdown.js +30 -3
- package/dist/components/views/StandaloneLayout.js +62 -9
- package/dist/components/views/WidgetView.js +8 -5
- package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
- package/dist/components/views/panels/PlanningDrawer.js +48 -44
- package/dist/config/envSchema.js +1 -0
- package/dist/config/localServiceCloudTarget.d.ts +10 -0
- package/dist/config/localServiceCloudTarget.js +76 -0
- package/dist/core/AiProfileService.d.ts +1 -0
- package/dist/core/AiProfileService.js +2 -1
- package/dist/core/AttachmentLinkService.js +7 -4
- package/dist/core/DeletedTaskLifecycleService.d.ts +4 -0
- package/dist/core/DeletedTaskLifecycleService.js +21 -0
- package/dist/core/McpTokenService.d.ts +4 -0
- package/dist/core/McpTokenService.js +52 -0
- package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +5 -0
- package/dist/core/TaskAttachmentLinksDurableMutationService.js +20 -11
- package/dist/core/Taskforce.d.ts +63 -1
- package/dist/core/Taskforce.js +558 -73
- package/dist/core/types.d.ts +2 -1
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
- package/dist/documentReviews/service.d.ts +4 -0
- package/dist/documentReviews/service.js +141 -46
- package/dist/hooks/tasks/useTaskFormActions.d.ts +3 -0
- package/dist/hooks/tasks/useTaskFormActions.js +33 -7
- package/dist/hooks/useTaskforce.d.ts +59 -50
- package/dist/hooks/useTaskforce.js +103 -1
- package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
- package/dist/mcp/canonicalAssetHelpers.js +39 -5
- package/dist/mcp/collaborationRegistrar.js +15 -13
- package/dist/mcp/documentAssetRegistrar.js +16 -9
- package/dist/mcp/localServiceProxy.js +4 -1
- package/dist/mcp/runtime.d.ts +16 -0
- package/dist/mcp/runtime.js +566 -171
- package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
- package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
- package/dist/mcp/taskPlanningRegistrar.js +42 -34
- package/dist/mcp/toolRegistry.js +2 -2
- package/dist/migrations/taskSchemaMigrations.js +35 -0
- package/dist/runtime/appGateDefinitions.d.ts +2 -2
- package/dist/runtime/appGateDefinitions.js +2 -2
- package/dist/server/appAccess.d.ts +12 -0
- package/dist/server/appAccess.js +21 -0
- package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
- package/dist/server/authorizedWorkspaceAccess.js +40 -0
- package/dist/server/cloudCollectionRead.d.ts +25 -0
- package/dist/server/cloudCollectionRead.js +120 -0
- package/dist/server/cloudCollectionReadPool.d.ts +52 -0
- package/dist/server/cloudCollectionReadPool.js +140 -0
- package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
- package/dist/server/cloudCollectionReadWorker.js +36 -0
- package/dist/server/index.d.ts +3 -11
- package/dist/server/index.js +53 -40
- package/dist/server/localServiceLease.d.ts +3 -0
- package/dist/server/localServiceLease.js +9 -1
- package/dist/server/routes/documents.d.ts +5 -2
- package/dist/server/routes/documents.js +160 -13
- package/dist/server/routes/durableTaskHttpRouters.d.ts +2 -0
- package/dist/server/routes/durableTaskHttpRouters.js +2 -2
- package/dist/server/routes/localService.js +2 -0
- package/dist/server/routes/primitives.d.ts +2 -0
- package/dist/server/routes/shared.d.ts +24 -0
- package/dist/server/routes/shared.js +114 -18
- package/dist/server/routes/sync.js +22 -0
- package/dist/server/routes/syncV3FeedRoutes.js +4 -2
- package/dist/server/routes/syncV3LocalCaptureRoutes.js +20 -2
- package/dist/server/routes/syncV3OperationRoutes.js +17 -2
- package/dist/server/routes/tasks.js +199 -89
- package/dist/server/routes.js +35 -7
- package/dist/service/localServiceCli.js +36 -25
- package/dist/service/localServiceClientLifecycle.js +67 -6
- package/dist/shared/planningIdentity.d.ts +20 -0
- package/dist/shared/planningIdentity.js +79 -0
- package/dist/storage/postgresAdapter.js +4 -0
- package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
- package/dist/storage/postgresRequestDiagnostics.js +24 -0
- package/dist/storage/workspaceAssetStore.d.ts +32 -0
- package/dist/storage/workspaceAssetStore.js +70 -0
- package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +16 -0
- package/dist/sync/coordinator/localSyncExecutionPermitStore.js +70 -22
- package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
- package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +5 -0
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +15 -0
- package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +26 -3
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
- package/dist/sync/syncApplyHandlers.d.ts +3 -1
- package/dist/sync/syncApplyHandlers.js +29 -0
- package/dist/sync/syncService.js +9 -0
- package/dist/sync/taskSyncPayload.d.ts +1 -0
- package/dist/sync/taskSyncPayload.js +6 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +1 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +21 -3
- package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +17 -4
- package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
- package/dist/sync/v3/localOutboxDispatcher.js +5 -1
- package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +1 -0
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +15 -3
- package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
- package/dist/sync/v3/syncDurabilityStore.d.ts +8 -0
- package/dist/sync/v3/syncDurabilityStore.js +171 -33
- package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +1 -0
- package/dist/sync/v3/taskAttachmentLinksMutationService.js +18 -2
- package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
- package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +1 -9
- package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +1 -0
- package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +7 -1
- package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +15 -3
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +4 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +29 -6
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +29 -7
- package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +62 -7
- package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
- package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
- package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
- package/dist/sync/workspacePullFeed.d.ts +6 -1
- package/dist/sync/workspacePullFeed.js +172 -161
- package/dist/sync/workspaceSyncModel.d.ts +11 -0
- package/dist/sync/workspaceSyncModel.js +27 -2
- package/dist/types.d.ts +8 -0
- package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-MZs7lVED.js} +1 -1
- package/dist/ui/assets/{AiProfilesModule-DPxQeF9N.js → AiProfilesModule-C3i0LOxe.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-DHNYaNg6.js → AnnotatedAttachmentWorkspace-DVZk5Tln.js} +1 -1
- package/dist/ui/assets/AssetTraySortControl-Kf1pelSy.js +1 -0
- package/dist/ui/assets/ContextAttachmentManager-B8boOs6Q.js +3 -0
- package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-DXH4XUsB.js} +4 -4
- package/dist/ui/assets/EntityActivityTimeline-BtSTq0ON.js +1 -0
- package/dist/ui/assets/PlanningModule-BPVyEa-f.js +1 -0
- package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
- package/dist/ui/assets/{PlansPage-BVH_NfgT.js → PlansPage-ChqgxALc.js} +1 -1
- package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +1 -0
- package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
- package/dist/ui/assets/{TaskSettings-xVsxSa0g.js → TaskSettings-g4ECKNOz.js} +1 -1
- package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BazdUwxZ.js} +1 -1
- package/dist/ui/assets/WorkflowManagerModule-DWFspC-o.js +1 -0
- package/dist/ui/assets/index-BUplSsv_.js +7 -0
- package/dist/ui/assets/index-gz2EXuoK.css +1 -0
- package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-BkFLXavV.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/constants.d.ts +2 -2
- package/dist/utils/constants.js +1 -1
- package/dist/utils/contextAttachmentUpload.d.ts +2 -0
- package/dist/utils/contextAttachmentUpload.js +7 -0
- package/dist/utils/contextFiles.d.ts +1 -1
- package/dist/utils/contextFiles.js +4 -4
- package/package.json +6 -1
- package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
- package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
- package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
- package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
- package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
- package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
- package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
- package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
- package/dist/ui/assets/index-BzJlVnNS.css +0 -1
- package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
|
@@ -26,6 +26,7 @@ export declare function enqueueLocalTaskAttachmentLinksMutation(input: {
|
|
|
26
26
|
identity: WorkspaceSyncOperationIdentity;
|
|
27
27
|
taskId: string;
|
|
28
28
|
links: readonly WorkspaceSyncV3TaskAttachmentLink[];
|
|
29
|
+
cardCoverAssetId?: string | null;
|
|
29
30
|
now: string;
|
|
30
31
|
actorRef: string;
|
|
31
32
|
requireV3CombinedOwnership?: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { projectResolvedTaskAttachmentLinksForDurableSync } from '../../core/TaskAttachmentLinksDurableMutationService.js';
|
|
1
|
+
import { projectResolvedTaskAttachmentLinksForDurableSync, resolveTaskCardCoverAssetIdForLinks, } from '../../core/TaskAttachmentLinksDurableMutationService.js';
|
|
2
2
|
import { selectDurableTaskMetadataPatch } from '../../core/TaskDurableSyncMutationService.js';
|
|
3
3
|
import { canonicalWorkspaceSyncJson } from './canonicalWorkspaceSyncJson.js';
|
|
4
4
|
import { buildTaskAttachmentLinksActivityEventId } from './taskAttachmentLinksActivityIdentity.js';
|
|
@@ -111,7 +111,12 @@ export function enqueueLocalTaskAttachmentLinksMutation(input) {
|
|
|
111
111
|
const result = input.store.enqueueLocalOperation({
|
|
112
112
|
identity: input.identity,
|
|
113
113
|
domain: 'task-attachment-links', entityId: input.taskId, mutationKind: 'replace',
|
|
114
|
-
revisionSource: 'current', payload: withOriginActorPayload({
|
|
114
|
+
revisionSource: 'current', payload: withOriginActorPayload({
|
|
115
|
+
links,
|
|
116
|
+
...(Object.prototype.hasOwnProperty.call(input, 'cardCoverAssetId')
|
|
117
|
+
? { cardCoverAssetId: input.cardCoverAssetId ?? null }
|
|
118
|
+
: {}),
|
|
119
|
+
}, input.actorRef),
|
|
115
120
|
additionalTargets: [{
|
|
116
121
|
domain: 'task', entityId: input.taskId, mutationKind: 'metadata-update',
|
|
117
122
|
baseEntityRevision: rootRevision.entityRevision, baseLifecycleRevision: rootRevision.lifecycleRevision,
|
|
@@ -127,11 +132,18 @@ export function enqueueLocalTaskAttachmentLinksMutation(input) {
|
|
|
127
132
|
if (existing.isArchived)
|
|
128
133
|
throw new LocalAuthoritativeTaskAttachmentLinksError('Archived tasks are read-only.');
|
|
129
134
|
const previousLinks = input.core.listTaskAttachmentLinksForDurableSync(input.taskId, input.identity.workspaceId);
|
|
135
|
+
const cardCoverAssetId = resolveTaskCardCoverAssetIdForLinks({
|
|
136
|
+
currentCardCoverAssetId: existing.cardCoverAssetId,
|
|
137
|
+
...(Object.prototype.hasOwnProperty.call(input, 'cardCoverAssetId')
|
|
138
|
+
? { requestedCardCoverAssetId: input.cardCoverAssetId }
|
|
139
|
+
: {}),
|
|
140
|
+
links,
|
|
141
|
+
});
|
|
130
142
|
const currentMs = Date.parse(String(existing.updatedAt || existing.createdAt));
|
|
131
143
|
const requestedMs = Date.parse(input.now);
|
|
132
144
|
const updatedAt = Number.isFinite(currentMs) && currentMs >= requestedMs
|
|
133
145
|
? new Date(currentMs + 1).toISOString() : input.now;
|
|
134
|
-
const task = input.core.updateTask(input.taskId, { attachments }, input.identity.workspaceId, {
|
|
146
|
+
const task = input.core.updateTask(input.taskId, { attachments, cardCoverAssetId }, input.identity.workspaceId, {
|
|
135
147
|
persistBackup: false, emitRealtime: false, syncUpdatedAt: updatedAt,
|
|
136
148
|
actorRef: input.actorRef, taskEventId: activityEventId, taskEventAction: 'task-attachments-changed',
|
|
137
149
|
taskEventDetails: canonicalizeEntityActivityDetailsForSync({
|
|
@@ -300,8 +300,9 @@ function validateActivitySemantics(input) {
|
|
|
300
300
|
const authoritativeAttachmentLinks = validateCanonicalPlanningAttachmentLinks(input.authoritative.attachments);
|
|
301
301
|
const comparableExpectedDetails = normalizePlanningActivityDetailsForComparison(expectedDetails, input.record.mutationKind, authoritativeAttachmentLinks);
|
|
302
302
|
const comparableAuthoritativeDetails = normalizePlanningActivityDetailsForComparison((input.activity.details || {}), input.record.mutationKind, authoritativeAttachmentLinks);
|
|
303
|
-
if (
|
|
304
|
-
|
|
303
|
+
if (!input.boundedAttachmentRebase
|
|
304
|
+
&& fingerprintWorkspaceSyncPayload(comparableExpectedDetails)
|
|
305
|
+
!== fingerprintWorkspaceSyncPayload(comparableAuthoritativeDetails)) {
|
|
305
306
|
throw new LocalAuthoritativeWorkstreamError('Authoritative workstream activity details differ from the captured mutation.');
|
|
306
307
|
}
|
|
307
308
|
}
|
|
@@ -500,6 +501,7 @@ export function applyAuthoritativeWorkstreamAndAck(input) {
|
|
|
500
501
|
payload: payload,
|
|
501
502
|
authoritative: workstream,
|
|
502
503
|
activity,
|
|
504
|
+
boundedAttachmentRebase,
|
|
503
505
|
});
|
|
504
506
|
const revision = validateWorkstreamActivityAuthoritativeRevision({
|
|
505
507
|
response: response,
|
|
@@ -152,6 +152,9 @@ export type WorkspaceSyncDomainCoexistenceTarget = {
|
|
|
152
152
|
domain: string;
|
|
153
153
|
entityId: string;
|
|
154
154
|
};
|
|
155
|
+
export type WorkspaceSyncRecoveryBaselineTarget = WorkspaceSyncDomainCoexistenceTarget & {
|
|
156
|
+
required: boolean;
|
|
157
|
+
};
|
|
155
158
|
export type WorkspaceSyncPlanningRevisionProjection = {
|
|
156
159
|
entityId: string;
|
|
157
160
|
coexistenceOwned: boolean;
|
|
@@ -374,6 +377,11 @@ export declare class WorkspaceSyncDurabilityStore {
|
|
|
374
377
|
workspaceId: string;
|
|
375
378
|
recoveryBatchId: string;
|
|
376
379
|
}): WorkspaceSyncDomainCoexistenceTarget[];
|
|
380
|
+
listOutboxRecoveryBaselineTargets(input: {
|
|
381
|
+
tenantId: string;
|
|
382
|
+
workspaceId: string;
|
|
383
|
+
recoveryBatchId: string;
|
|
384
|
+
}): WorkspaceSyncRecoveryBaselineTarget[];
|
|
377
385
|
prepareTaskRootOutboxRecoveryReplay(input: {
|
|
378
386
|
tenantId: string;
|
|
379
387
|
workspaceId: string;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'crypto';
|
|
2
2
|
import { normalizePlanningComments, } from '../planningCanonicalFields.js';
|
|
3
3
|
import { canonicalWorkspaceSyncJson } from './canonicalWorkspaceSyncJson.js';
|
|
4
|
-
import {
|
|
4
|
+
import { buildTaskAttachmentLinksActivityEventId } from './taskAttachmentLinksActivityIdentity.js';
|
|
5
5
|
import { buildTaskCommentActivityEventId } from './taskCommentActivityIdentity.js';
|
|
6
|
+
import { buildTaskCreateActivityEventId } from './taskCreateActivityIdentity.js';
|
|
6
7
|
import { buildTaskLifecycleActivityEventId, } from './taskLifecycleActivityIdentity.js';
|
|
7
8
|
import { buildTaskMetadataActivityEventId } from './taskMetadataActivityIdentity.js';
|
|
8
9
|
import { parseWorkspaceSyncV3OriginActor } from './workspaceSyncV3OriginActor.js';
|
|
@@ -251,7 +252,8 @@ export function hasCompletedRecoveryForSupersededEntityEvent(db, input) {
|
|
|
251
252
|
if (entityType !== 'task')
|
|
252
253
|
return false;
|
|
253
254
|
const sources = db.prepare(`
|
|
254
|
-
SELECT source.client_id, source.operation_id, source.mutation_kind
|
|
255
|
+
SELECT source.client_id, source.operation_id, source.domain, source.mutation_kind,
|
|
256
|
+
source.last_error_code
|
|
255
257
|
FROM workspace_sync_outbox AS source
|
|
256
258
|
INNER JOIN workspace_sync_outbox_recoveries AS recovery
|
|
257
259
|
ON recovery.tenant_id = source.tenant_id
|
|
@@ -259,7 +261,7 @@ export function hasCompletedRecoveryForSupersededEntityEvent(db, input) {
|
|
|
259
261
|
AND recovery.recovery_batch_id = source.recovery_batch_id
|
|
260
262
|
WHERE source.tenant_id = ?
|
|
261
263
|
AND source.workspace_id = ?
|
|
262
|
-
AND source.domain
|
|
264
|
+
AND source.domain IN ('task', 'task-attachment-links')
|
|
263
265
|
AND source.entity_id = ?
|
|
264
266
|
AND source.status = 'superseded'
|
|
265
267
|
AND recovery.state = 'completed'
|
|
@@ -268,9 +270,19 @@ export function hasCompletedRecoveryForSupersededEntityEvent(db, input) {
|
|
|
268
270
|
return sources.some((source) => {
|
|
269
271
|
const clientId = String(source.client_id || '').trim();
|
|
270
272
|
const operationId = String(source.operation_id || '').trim();
|
|
273
|
+
const domain = String(source.domain || '').trim();
|
|
271
274
|
const mutationKind = String(source.mutation_kind || '').trim();
|
|
272
275
|
if (!clientId || !operationId)
|
|
273
276
|
return false;
|
|
277
|
+
if (domain === 'task-attachment-links'
|
|
278
|
+
&& mutationKind === 'replace'
|
|
279
|
+
&& String(source.last_error_code || '') === 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID') {
|
|
280
|
+
return buildTaskAttachmentLinksActivityEventId({
|
|
281
|
+
workspaceId, clientId, operationId, taskId: entityId,
|
|
282
|
+
}) === eventId;
|
|
283
|
+
}
|
|
284
|
+
if (domain !== 'task')
|
|
285
|
+
return false;
|
|
274
286
|
if (mutationKind === 'create') {
|
|
275
287
|
return buildTaskCreateActivityEventId({
|
|
276
288
|
workspaceId, clientId, operationId, taskId: entityId,
|
|
@@ -1283,11 +1295,16 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
1283
1295
|
})();
|
|
1284
1296
|
}
|
|
1285
1297
|
listOutboxRecoveryTargets(input) {
|
|
1298
|
+
return this.listOutboxRecoveryBaselineTargets(input).map(({ domain, entityId }) => ({ domain, entityId }));
|
|
1299
|
+
}
|
|
1300
|
+
listOutboxRecoveryBaselineTargets(input) {
|
|
1286
1301
|
const tenantId = requireText(input.tenantId, 'Tenant id');
|
|
1287
1302
|
const workspaceId = requireText(input.workspaceId, 'Workspace id');
|
|
1288
1303
|
const recoveryBatchId = requireText(input.recoveryBatchId, 'Recovery batch id');
|
|
1289
1304
|
const rows = this.db.prepare(`
|
|
1290
|
-
SELECT
|
|
1305
|
+
SELECT target.domain, target.entity_id,
|
|
1306
|
+
source.dispatch_class, source.domain AS source_domain,
|
|
1307
|
+
source.mutation_kind AS source_mutation_kind
|
|
1291
1308
|
FROM workspace_sync_outbox AS source
|
|
1292
1309
|
INNER JOIN workspace_sync_outbox_targets AS target
|
|
1293
1310
|
ON target.tenant_id = source.tenant_id
|
|
@@ -1298,14 +1315,52 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
1298
1315
|
AND source.recovery_batch_id = ?
|
|
1299
1316
|
AND source.status = 'superseded'
|
|
1300
1317
|
AND source.replay_of_operation_id IS NULL
|
|
1301
|
-
ORDER BY target.domain, target.entity_id
|
|
1318
|
+
ORDER BY target.domain, target.entity_id, source.enqueue_sequence, source.operation_id
|
|
1302
1319
|
`).all(tenantId, workspaceId, recoveryBatchId);
|
|
1303
|
-
|
|
1320
|
+
const targets = new Map();
|
|
1321
|
+
for (const row of rows) {
|
|
1322
|
+
const domain = requireText(row.domain, 'Recovery target domain');
|
|
1323
|
+
const entityId = requireText(row.entity_id, 'Recovery target entity id');
|
|
1324
|
+
const key = `${domain}\0${entityId}`;
|
|
1325
|
+
const legacyAdoptionEvent = domain === 'entity-event'
|
|
1326
|
+
&& row.dispatch_class === 'legacy-adoption'
|
|
1327
|
+
&& row.source_domain === 'entity-event'
|
|
1328
|
+
&& row.source_mutation_kind === 'adopt-legacy-batch';
|
|
1329
|
+
const existing = targets.get(key);
|
|
1330
|
+
targets.set(key, {
|
|
1331
|
+
domain,
|
|
1332
|
+
entityId,
|
|
1333
|
+
required: Boolean(existing?.required || !legacyAdoptionEvent),
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
const attachmentSources = this.db.prepare(`
|
|
1337
|
+
SELECT source.client_id, source.operation_id, source.entity_id
|
|
1338
|
+
FROM workspace_sync_outbox AS source
|
|
1339
|
+
WHERE source.tenant_id = ? AND source.workspace_id = ?
|
|
1340
|
+
AND source.recovery_batch_id = ?
|
|
1341
|
+
AND source.status = 'superseded'
|
|
1342
|
+
AND source.replay_of_operation_id IS NULL
|
|
1343
|
+
AND source.domain = 'task-attachment-links'
|
|
1344
|
+
AND source.mutation_kind = 'replace'
|
|
1345
|
+
AND source.last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
|
|
1346
|
+
`).all(tenantId, workspaceId, recoveryBatchId);
|
|
1347
|
+
for (const source of attachmentSources) {
|
|
1348
|
+
const target = {
|
|
1349
|
+
domain: 'entity-event',
|
|
1350
|
+
entityId: buildTaskAttachmentLinksActivityEventId({
|
|
1351
|
+
workspaceId,
|
|
1352
|
+
clientId: requireText(source.client_id, 'Recovery client id'),
|
|
1353
|
+
operationId: requireText(source.operation_id, 'Recovery operation id'),
|
|
1354
|
+
taskId: requireText(source.entity_id, 'Recovery task id'),
|
|
1355
|
+
}),
|
|
1356
|
+
required: true,
|
|
1357
|
+
};
|
|
1358
|
+
targets.set(`${target.domain}\0${target.entityId}`, target);
|
|
1359
|
+
}
|
|
1360
|
+
const deduplicated = [...targets.values()].sort((left, right) => (compareCodeUnits(left.domain, right.domain) || compareCodeUnits(left.entityId, right.entityId)));
|
|
1361
|
+
if (deduplicated.length === 0)
|
|
1304
1362
|
throw new Error('Workspace sync recovery snapshot has no targets.');
|
|
1305
|
-
return
|
|
1306
|
-
domain: requireText(row.domain, 'Recovery target domain'),
|
|
1307
|
-
entityId: requireText(row.entity_id, 'Recovery target entity id'),
|
|
1308
|
-
}));
|
|
1363
|
+
return deduplicated;
|
|
1309
1364
|
}
|
|
1310
1365
|
prepareTaskRootOutboxRecoveryReplay(input) {
|
|
1311
1366
|
const tenantId = requireText(input.tenantId, 'Tenant id');
|
|
@@ -1395,6 +1450,34 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
1395
1450
|
this.readEntityEventLegacyAdoptionRecoveryIntent(workspaceId, source);
|
|
1396
1451
|
}
|
|
1397
1452
|
}
|
|
1453
|
+
const recoverySourceKey = (source) => (`${requireText(source.client_id, 'Source client id')}\0${requireText(source.operation_id, 'Source operation id')}`);
|
|
1454
|
+
const legacyAdoptionEventsBySource = new Map();
|
|
1455
|
+
const latestLegacyAdoptionSourceByEvent = new Map();
|
|
1456
|
+
for (const source of recoverySources) {
|
|
1457
|
+
if (source.dispatch_class !== 'legacy-adoption'
|
|
1458
|
+
|| source.domain !== 'entity-event'
|
|
1459
|
+
|| source.mutation_kind !== 'adopt-legacy-batch')
|
|
1460
|
+
continue;
|
|
1461
|
+
const sourceKey = recoverySourceKey(source);
|
|
1462
|
+
const events = this.readEntityEventLegacyAdoptionRecoveryIntent(workspaceId, source).events;
|
|
1463
|
+
legacyAdoptionEventsBySource.set(sourceKey, events);
|
|
1464
|
+
for (const event of events)
|
|
1465
|
+
latestLegacyAdoptionSourceByEvent.set(event.id, sourceKey);
|
|
1466
|
+
}
|
|
1467
|
+
const legacyAdoptionReplayEventsBySource = new Map();
|
|
1468
|
+
for (const [sourceKey, events] of legacyAdoptionEventsBySource) {
|
|
1469
|
+
legacyAdoptionReplayEventsBySource.set(sourceKey, events.filter((event) => {
|
|
1470
|
+
if (latestLegacyAdoptionSourceByEvent.get(event.id) !== sourceKey)
|
|
1471
|
+
return false;
|
|
1472
|
+
const revision = this.readEntityRevisionCoordinates({
|
|
1473
|
+
tenantId,
|
|
1474
|
+
workspaceId,
|
|
1475
|
+
domain: 'entity-event',
|
|
1476
|
+
entityId: event.id,
|
|
1477
|
+
});
|
|
1478
|
+
return revision.entityRevision === '0';
|
|
1479
|
+
}));
|
|
1480
|
+
}
|
|
1398
1481
|
const latestAttachmentSourceByEntity = new Map();
|
|
1399
1482
|
for (const source of recoverySources) {
|
|
1400
1483
|
if (source.domain === 'task-attachment-links' && source.mutation_kind === 'replace') {
|
|
@@ -1419,14 +1502,11 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
1419
1502
|
if (source.dispatch_class === 'legacy-adoption'
|
|
1420
1503
|
&& source.domain === 'entity-event'
|
|
1421
1504
|
&& source.mutation_kind === 'adopt-legacy-batch') {
|
|
1422
|
-
//
|
|
1423
|
-
//
|
|
1424
|
-
//
|
|
1425
|
-
//
|
|
1426
|
-
|
|
1427
|
-
// identity/revision conflict forever, so the baseline itself
|
|
1428
|
-
// satisfies this migration intent.
|
|
1429
|
-
return false;
|
|
1505
|
+
// The authoritative baseline satisfies events already present in
|
|
1506
|
+
// cloud. A mixed legacy batch may also contain new immutable
|
|
1507
|
+
// events; replay only those still lacking authoritative ownership,
|
|
1508
|
+
// and let the latest overlapping batch own each event once.
|
|
1509
|
+
return (legacyAdoptionReplayEventsBySource.get(recoverySourceKey(source)) || []).length > 0;
|
|
1430
1510
|
}
|
|
1431
1511
|
if (Number(source.baseline_satisfaction_candidate || 0) !== 1) {
|
|
1432
1512
|
return true;
|
|
@@ -1492,7 +1572,22 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
1492
1572
|
throw new Error('Workspace sync recovery replay currently supports task-root, task-comment append, '
|
|
1493
1573
|
+ 'task attachment-link, and legacy entity-event adoption intents only.');
|
|
1494
1574
|
}
|
|
1495
|
-
|
|
1575
|
+
const sourceKey = recoverySourceKey(source);
|
|
1576
|
+
if (supportedEntityEventAdoption) {
|
|
1577
|
+
const events = legacyAdoptionReplayEventsBySource.get(sourceKey) || [];
|
|
1578
|
+
if (events.length === 0)
|
|
1579
|
+
throw new Error('Legacy entity-event adoption recovery replay is empty.');
|
|
1580
|
+
const payloadJson = canonicalWorkspaceSyncJson({ events });
|
|
1581
|
+
replayPayloads.set(sourceKey, {
|
|
1582
|
+
payloadJson,
|
|
1583
|
+
payloadFingerprint: fingerprintWorkspaceSyncPayload({ events }),
|
|
1584
|
+
replayMutationKind: 'adopt-legacy-batch',
|
|
1585
|
+
workflowDependencyCoalesced: false,
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
else {
|
|
1589
|
+
replayPayloads.set(sourceKey, this.prepareRecoveryPayload(tenantId, workspaceId, source));
|
|
1590
|
+
}
|
|
1496
1591
|
}
|
|
1497
1592
|
for (const source of sources) {
|
|
1498
1593
|
const sourceClientId = requireText(source.client_id, 'Source client id');
|
|
@@ -1512,11 +1607,22 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
1512
1607
|
WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
|
|
1513
1608
|
ORDER BY domain, entity_id
|
|
1514
1609
|
`).all(tenantId, workspaceId, sourceClientId, sourceOperationId);
|
|
1610
|
+
const legacyReplayEvents = source.dispatch_class === 'legacy-adoption'
|
|
1611
|
+
&& source.domain === 'entity-event'
|
|
1612
|
+
&& source.mutation_kind === 'adopt-legacy-batch'
|
|
1613
|
+
? legacyAdoptionReplayEventsBySource.get(recoverySourceKey(source)) || []
|
|
1614
|
+
: null;
|
|
1615
|
+
const replayEventIds = legacyReplayEvents
|
|
1616
|
+
? new Set(legacyReplayEvents.map((event) => event.id))
|
|
1617
|
+
: null;
|
|
1515
1618
|
const replayTargets = source.domain === 'task-comments'
|
|
1516
1619
|
&& source.mutation_kind === 'append'
|
|
1517
1620
|
? sourceTargets.filter((target) => (target.domain === 'task-comments'
|
|
1518
1621
|
&& target.entity_id === source.entity_id))
|
|
1519
|
-
:
|
|
1622
|
+
: replayEventIds
|
|
1623
|
+
? sourceTargets.filter((target) => (target.domain === 'entity-event'
|
|
1624
|
+
&& replayEventIds.has(String(target.entity_id || ''))))
|
|
1625
|
+
: sourceTargets;
|
|
1520
1626
|
if (replayTargets.length === 0)
|
|
1521
1627
|
throw new Error('Workspace sync recovery source has no targets.');
|
|
1522
1628
|
const targetRevisions = replayTargets.map((target) => ({
|
|
@@ -1528,7 +1634,9 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
1528
1634
|
entityId: requireText(target.entity_id, 'Target entity id'),
|
|
1529
1635
|
}),
|
|
1530
1636
|
}));
|
|
1531
|
-
const
|
|
1637
|
+
const replayEntityId = legacyReplayEvents?.[0]?.id
|
|
1638
|
+
|| requireText(source.entity_id, 'Source entity id');
|
|
1639
|
+
const primary = targetRevisions.find(({ target }) => (target.domain === source.domain && target.entity_id === replayEntityId));
|
|
1532
1640
|
if (!primary)
|
|
1533
1641
|
throw new Error('Workspace sync recovery source primary target is missing.');
|
|
1534
1642
|
const enqueueSequence = this.allocateOutboxEnqueueSequence(tenantId, workspaceId, now);
|
|
@@ -1545,7 +1653,7 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
1545
1653
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, 0, ?, 1,
|
|
1546
1654
|
?, ?, ?, ?, ?, ?)
|
|
1547
1655
|
ON CONFLICT (tenant_id, workspace_id, client_id, operation_id) DO NOTHING
|
|
1548
|
-
`).run(tenantId, workspaceId, operationId, WORKSPACE_SYNC_CONTRACT_VERSION, sourceClientId, requireOutboxDispatchClass(source.dispatch_class), requireText(source.domain, 'Source domain'),
|
|
1656
|
+
`).run(tenantId, workspaceId, operationId, WORKSPACE_SYNC_CONTRACT_VERSION, sourceClientId, requireOutboxDispatchClass(source.dispatch_class), requireText(source.domain, 'Source domain'), replayEntityId, replayPayload.replayMutationKind, Number(source.owns_task_metadata) === 1 ? 1 : 0, Number(source.owns_task_lifecycle) === 1 ? 1 : 0, Number(source.owns_task_assignee) === 1 ? 1 : 0, primary.revision.entityRevision, primary.revision.lifecycleRevision, replayPayload.payloadJson, replayPayload.payloadFingerprint, enqueueSequence, now, recoveryBatchId, sourceClientId, sourceOperationId, now, now);
|
|
1549
1657
|
this.db.prepare(`
|
|
1550
1658
|
UPDATE workspace_sync_outbox
|
|
1551
1659
|
SET base_entity_revision = ?, base_lifecycle_revision = ?,
|
|
@@ -1577,7 +1685,7 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
1577
1685
|
`).get(tenantId, workspaceId, sourceClientId, operationId);
|
|
1578
1686
|
if (!replayRow
|
|
1579
1687
|
|| replayRow.domain !== source.domain
|
|
1580
|
-
|| replayRow.entity_id !==
|
|
1688
|
+
|| replayRow.entity_id !== replayEntityId
|
|
1581
1689
|
|| replayRow.mutation_kind !== replayPayload.replayMutationKind
|
|
1582
1690
|
|| replayRow.payload_fingerprint !== replayPayload.payloadFingerprint
|
|
1583
1691
|
|| replayRow.recovery_batch_id !== recoveryBatchId
|
|
@@ -1936,9 +2044,15 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
1936
2044
|
AND (
|
|
1937
2045
|
(domain = 'task-attachment-links' AND mutation_kind = 'replace')
|
|
1938
2046
|
OR (domain = 'task' AND mutation_kind = 'create')
|
|
2047
|
+
OR (domain = 'workstream' AND mutation_kind IN ('create', 'metadata-update'))
|
|
2048
|
+
OR (domain = 'initiative' AND mutation_kind IN ('create', 'metadata-update'))
|
|
1939
2049
|
)
|
|
1940
2050
|
AND status IN ('pending', 'in_flight')
|
|
1941
|
-
AND last_error_code
|
|
2051
|
+
AND last_error_code IN (
|
|
2052
|
+
'TASK_ATTACHMENT_ASSET_NOT_READY',
|
|
2053
|
+
'WORKSTREAM_ATTACHMENT_NOT_READY',
|
|
2054
|
+
'INITIATIVE_ATTACHMENT_NOT_READY'
|
|
2055
|
+
)
|
|
1942
2056
|
ORDER BY enqueue_sequence
|
|
1943
2057
|
`).all(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION);
|
|
1944
2058
|
const assetIds = new Set();
|
|
@@ -1952,9 +2066,13 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
1952
2066
|
}
|
|
1953
2067
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
1954
2068
|
continue;
|
|
1955
|
-
const
|
|
2069
|
+
const domain = String(row.domain || '');
|
|
2070
|
+
const links = domain === 'task'
|
|
1956
2071
|
? payload.attachmentLinks
|
|
1957
|
-
:
|
|
2072
|
+
: domain === 'task-attachment-links'
|
|
2073
|
+
? payload.links
|
|
2074
|
+
: payload.attachments
|
|
2075
|
+
?? payload.patch?.attachments;
|
|
1958
2076
|
if (!Array.isArray(links))
|
|
1959
2077
|
continue;
|
|
1960
2078
|
for (const link of links) {
|
|
@@ -3268,9 +3386,9 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
3268
3386
|
// Workstream/initiative attachment-only metadata patches are Partial<>
|
|
3269
3387
|
// merges, not full snapshots. The cloud accepts those patches against its
|
|
3270
3388
|
// current revision, so this bounded retry can preserve unrelated concurrent
|
|
3271
|
-
// metadata.
|
|
3272
|
-
//
|
|
3273
|
-
//
|
|
3389
|
+
// metadata. It also covers an idempotent acknowledgement whose revision was
|
|
3390
|
+
// advanced by the prerequisite V2 content push before local installation.
|
|
3391
|
+
// Other metadata patches remain exact-revision operations and are excluded.
|
|
3274
3392
|
requeueBoundedWorkstreamMetadataRevisionConflicts(input) {
|
|
3275
3393
|
const tenantId = requireText(input.tenantId, 'Tenant id');
|
|
3276
3394
|
const workspaceId = requireText(input.workspaceId, 'Workspace id');
|
|
@@ -3285,9 +3403,19 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
3285
3403
|
updated_at = ?
|
|
3286
3404
|
WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
|
|
3287
3405
|
AND domain = 'workstream' AND mutation_kind = 'metadata-update'
|
|
3288
|
-
AND attempt_count = 1
|
|
3289
3406
|
AND workstream_metadata_revision_rebase_count < 1
|
|
3290
|
-
AND
|
|
3407
|
+
AND (
|
|
3408
|
+
(attempt_count = 1 AND last_error_code = 'WORKSPACE_SYNC_REVISION_CONFLICT')
|
|
3409
|
+
OR (
|
|
3410
|
+
last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
|
|
3411
|
+
AND last_error_message = 'Authoritative workstream revisions do not advance the captured bases exactly once.'
|
|
3412
|
+
AND json_type(payload_json, '$.patch.attachments') = 'array'
|
|
3413
|
+
AND NOT EXISTS (
|
|
3414
|
+
SELECT 1 FROM json_each(payload_json, '$.patch')
|
|
3415
|
+
WHERE key <> 'attachments'
|
|
3416
|
+
)
|
|
3417
|
+
)
|
|
3418
|
+
)
|
|
3291
3419
|
`).run(now, now, tenantId, workspaceId);
|
|
3292
3420
|
return Number(result.changes || 0);
|
|
3293
3421
|
}
|
|
@@ -3305,9 +3433,19 @@ export class WorkspaceSyncDurabilityStore {
|
|
|
3305
3433
|
updated_at = ?
|
|
3306
3434
|
WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
|
|
3307
3435
|
AND domain = 'initiative' AND mutation_kind = 'metadata-update'
|
|
3308
|
-
AND attempt_count = 1
|
|
3309
3436
|
AND initiative_metadata_revision_rebase_count < 1
|
|
3310
|
-
AND
|
|
3437
|
+
AND (
|
|
3438
|
+
(attempt_count = 1 AND last_error_code = 'WORKSPACE_SYNC_REVISION_CONFLICT')
|
|
3439
|
+
OR (
|
|
3440
|
+
last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
|
|
3441
|
+
AND last_error_message = 'Authoritative initiative revisions do not advance the captured bases exactly once.'
|
|
3442
|
+
AND json_type(payload_json, '$.patch.attachments') = 'array'
|
|
3443
|
+
AND NOT EXISTS (
|
|
3444
|
+
SELECT 1 FROM json_each(payload_json, '$.patch')
|
|
3445
|
+
WHERE key <> 'attachments'
|
|
3446
|
+
)
|
|
3447
|
+
)
|
|
3448
|
+
)
|
|
3311
3449
|
`).run(now, now, tenantId, workspaceId);
|
|
3312
3450
|
return Number(result.changes || 0);
|
|
3313
3451
|
}
|
|
@@ -99,6 +99,7 @@ export declare function runDurableTaskAttachmentLinksReplace(input: {
|
|
|
99
99
|
operationId: string;
|
|
100
100
|
taskId: string;
|
|
101
101
|
links: readonly WorkspaceSyncV3TaskAttachmentLink[];
|
|
102
|
+
cardCoverAssetId?: string | null;
|
|
102
103
|
baseEntityRevision: string | number;
|
|
103
104
|
baseLifecycleRevision: string | number;
|
|
104
105
|
now: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { projectResolvedTaskAttachmentLinksForDurableSync } from '../../core/TaskAttachmentLinksDurableMutationService.js';
|
|
1
|
+
import { projectResolvedTaskAttachmentLinksForDurableSync, resolveTaskCardCoverAssetIdForLinks, } from '../../core/TaskAttachmentLinksDurableMutationService.js';
|
|
2
2
|
import { serializeTaskRootForSync } from './workspaceSyncV3TaskRootProjection.js';
|
|
3
3
|
import { buildTaskAttachmentLinksActivityEventId } from './taskAttachmentLinksActivityIdentity.js';
|
|
4
4
|
import { asWorkspaceSyncActivityRevision, buildTaskActivityJournalEntry, canonicalizeEntityActivityDetailsForSync, serializeTaskActivityForSync, } from './taskActivityJournal.js';
|
|
@@ -57,6 +57,9 @@ export function runDurableTaskAttachmentLinksReplace(input) {
|
|
|
57
57
|
domain: 'task-attachment-links', entityId: input.taskId, mutationKind: 'replace',
|
|
58
58
|
baseEntityRevision: String(input.baseEntityRevision),
|
|
59
59
|
baseLifecycleRevision: String(input.baseLifecycleRevision), links: requestedLinks,
|
|
60
|
+
...(Object.prototype.hasOwnProperty.call(input, 'cardCoverAssetId')
|
|
61
|
+
? { cardCoverAssetId: input.cardCoverAssetId }
|
|
62
|
+
: {}),
|
|
60
63
|
...(input.originActorRef ? { originActorRef: input.originActorRef } : {}),
|
|
61
64
|
},
|
|
62
65
|
});
|
|
@@ -113,6 +116,19 @@ export function runDurableTaskAttachmentLinksReplace(input) {
|
|
|
113
116
|
throw new DurableTaskAttachmentLinksMutationError('Task not found.', 'TASK_NOT_FOUND', 404);
|
|
114
117
|
if (existing.isArchived)
|
|
115
118
|
throw new DurableTaskAttachmentLinksMutationError('Archived tasks are read-only. Unarchive first to make changes.', 'TASK_ARCHIVED_READ_ONLY', 409);
|
|
119
|
+
let cardCoverAssetId;
|
|
120
|
+
try {
|
|
121
|
+
cardCoverAssetId = resolveTaskCardCoverAssetIdForLinks({
|
|
122
|
+
currentCardCoverAssetId: existing.cardCoverAssetId,
|
|
123
|
+
...(Object.prototype.hasOwnProperty.call(input, 'cardCoverAssetId')
|
|
124
|
+
? { requestedCardCoverAssetId: input.cardCoverAssetId }
|
|
125
|
+
: {}),
|
|
126
|
+
links,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
throw new DurableTaskAttachmentLinksMutationError(error instanceof Error ? error.message : 'Task card cover is invalid.', 'TASK_ATTACHMENT_LINKS_INVALID', Number(error?.statusCode) || 400);
|
|
131
|
+
}
|
|
116
132
|
const previousLinks = input.core.listTaskAttachmentLinksForDurableSync(input.taskId, input.workspaceId);
|
|
117
133
|
let attachments;
|
|
118
134
|
try {
|
|
@@ -126,7 +142,7 @@ export function runDurableTaskAttachmentLinksReplace(input) {
|
|
|
126
142
|
}
|
|
127
143
|
const updatedAt = nextUpdatedAt(String(existing.updatedAt || existing.createdAt), input.now);
|
|
128
144
|
const actorRef = resolveDeferredMutationActorRef(input);
|
|
129
|
-
const task = input.core.updateTask(existing.id, { attachments }, input.workspaceId, {
|
|
145
|
+
const task = input.core.updateTask(existing.id, { attachments, cardCoverAssetId }, input.workspaceId, {
|
|
130
146
|
persistBackup: false, emitRealtime: false, syncUpdatedAt: updatedAt,
|
|
131
147
|
actorRef, skipTaskEvents: true,
|
|
132
148
|
preferExplicitAttachmentRole: true,
|
|
@@ -57,8 +57,12 @@ export function preserveV3OwnedTaskMetadata(incoming, existing) {
|
|
|
57
57
|
return merged;
|
|
58
58
|
}
|
|
59
59
|
export function preserveV3OwnedTaskRoot(incoming, existing) {
|
|
60
|
+
const hasCardCoverAssetId = Object.prototype.hasOwnProperty.call(incoming, 'cardCoverAssetId');
|
|
60
61
|
return {
|
|
61
62
|
...serializeTaskRootForSync(existing),
|
|
63
|
+
cardCoverAssetId: hasCardCoverAssetId
|
|
64
|
+
? incoming.cardCoverAssetId ?? null
|
|
65
|
+
: existing.cardCoverAssetId ?? null,
|
|
62
66
|
comments: incoming.comments ?? existing.comments,
|
|
63
67
|
attachments: incoming.attachments ?? existing.attachments,
|
|
64
68
|
checklistItems: incoming.checklistItems ?? existing.checklistItems,
|
|
@@ -50,9 +50,6 @@ export function prepareWorkspaceSyncV2TaskCarriers(taskMutations, excludedProjec
|
|
|
50
50
|
const commentsOwned = excludedProjections.has('task-comments/full-v1');
|
|
51
51
|
const checklistOwned = excludedProjections.has('task-checklist/full-v1');
|
|
52
52
|
const attachmentsOwned = excludedProjections.has('task-attachment-links/full-v1');
|
|
53
|
-
if (commentsOwned && checklistOwned && attachmentsOwned) {
|
|
54
|
-
return { upserts: [], deletes: taskMutations.deletes };
|
|
55
|
-
}
|
|
56
53
|
return {
|
|
57
54
|
upserts: taskMutations.upserts.map(({ archived, task }) => {
|
|
58
55
|
const existing = readExistingTask?.(String(task.id || '').trim());
|
|
@@ -104,13 +101,8 @@ export function partitionWorkspaceSyncV2LegacyChanges(changes, excludedProjectio
|
|
|
104
101
|
const commentsOwned = excludedProjections.has('task-comments/full-v1');
|
|
105
102
|
const checklistOwned = excludedProjections.has('task-checklist/full-v1');
|
|
106
103
|
const attachmentsOwned = excludedProjections.has('task-attachment-links/full-v1');
|
|
107
|
-
if (excludedProjections.has('task/root-v1')
|
|
104
|
+
if (excludedProjections.has('task/root-v1'))
|
|
108
105
|
suppress('task/root-v1');
|
|
109
|
-
suppress('task-comments/full-v1');
|
|
110
|
-
suppress('task-checklist/full-v1');
|
|
111
|
-
suppress('task-attachment-links/full-v1');
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
106
|
if (commentsOwned || checklistOwned || attachmentsOwned) {
|
|
115
107
|
const task = { ...change.task };
|
|
116
108
|
if (commentsOwned) {
|
|
@@ -261,6 +261,7 @@ export function createWorkspaceSyncV3CloudOperationHandlers() {
|
|
|
261
261
|
...withDeferredOriginActor(context, operation.payload.originActor),
|
|
262
262
|
taskId: operation.entityId,
|
|
263
263
|
links: operation.payload.links,
|
|
264
|
+
cardCoverAssetId: operation.payload.cardCoverAssetId,
|
|
264
265
|
baseEntityRevision: operation.baseEntityRevision,
|
|
265
266
|
baseLifecycleRevision: operation.baseLifecycleRevision,
|
|
266
267
|
});
|
|
@@ -221,6 +221,12 @@ function peerReadinessError(response) {
|
|
|
221
221
|
? `Cloud combined workspace sync prerequisite is not ready (${firstMissing}).`
|
|
222
222
|
: 'Cloud combined workspace sync capability attestation is missing or incompatible.';
|
|
223
223
|
}
|
|
224
|
+
function repairResponseError(response, workspaceId, expected) {
|
|
225
|
+
const code = String(response.data?.code || '').trim();
|
|
226
|
+
return code && !['CURSOR_EXPIRED', 'COVERAGE_CHANGED', 'EXPLICIT_REPAIR'].includes(code)
|
|
227
|
+
? invalidRepairResponseError(response, workspaceId, expected)
|
|
228
|
+
: peerReadinessError(response);
|
|
229
|
+
}
|
|
224
230
|
function isUnavailablePersistedRepairCursor(response) {
|
|
225
231
|
if (response.data?.success !== false)
|
|
226
232
|
return false;
|
|
@@ -431,7 +437,7 @@ async function runWorkspaceSyncV3CombinedProjectionCycleInternal(input, staleRep
|
|
|
431
437
|
}
|
|
432
438
|
const repair = repairFromFeed(feed, input.workspaceId, coverage);
|
|
433
439
|
if (!repair)
|
|
434
|
-
return localFailure(projection,
|
|
440
|
+
return localFailure(projection, repairResponseError(feed, input.workspaceId, coverage));
|
|
435
441
|
const applied = await applySelectedProjection(dependencies, input.workspaceId, {
|
|
436
442
|
kind: 'repair', epoch: projection.epoch, previousCursor, page: repair,
|
|
437
443
|
...(input.recoveryBatchId ? { recoveryBatchId: input.recoveryBatchId } : {}),
|
|
@@ -172,6 +172,15 @@ function isExactNestedProjectionReplay(input) {
|
|
|
172
172
|
&& input.current.lastSequence === input.entry.sequence
|
|
173
173
|
&& input.current.canonicalFingerprint === (input.entry.canonicalFingerprint || input.entry.payloadFingerprint);
|
|
174
174
|
}
|
|
175
|
+
function isExactPlanningRootProjectionReplay(input) {
|
|
176
|
+
return (input.entry.projection === 'initiative/full-v1'
|
|
177
|
+
|| input.entry.projection === 'workstream/full-v1')
|
|
178
|
+
&& input.current.owned
|
|
179
|
+
&& input.current.entityRevision === input.entry.entityRevision
|
|
180
|
+
&& input.current.lifecycleRevision === input.entry.lifecycleRevision
|
|
181
|
+
&& input.current.lastSequence === input.entry.sequence
|
|
182
|
+
&& input.current.canonicalFingerprint === (input.entry.canonicalFingerprint || input.entry.payloadFingerprint);
|
|
183
|
+
}
|
|
175
184
|
function applyEntries(input) {
|
|
176
185
|
const store = new WorkspaceSyncDurabilityStore(input.core.getDatabaseAdapter());
|
|
177
186
|
const workflowApply = prepareWorkspaceSyncV3WorkflowRuntimeApply({
|
|
@@ -198,6 +207,7 @@ function applyEntries(input) {
|
|
|
198
207
|
workspaceId: input.workspaceId, domain: entry.domain, entityId: entry.entityId });
|
|
199
208
|
const exactTaskRootReplay = isExactTaskRootProjectionReplay({ entry, current });
|
|
200
209
|
const exactNestedReplay = isExactNestedProjectionReplay({ entry, current });
|
|
210
|
+
const exactPlanningRootReplay = isExactPlanningRootProjectionReplay({ entry, current });
|
|
201
211
|
const applyEntry = () => {
|
|
202
212
|
if (entry.projection === 'task/root-v1') {
|
|
203
213
|
taskChange = applyTaskEntry({ core: input.core, workspaceId: input.workspaceId, entry });
|
|
@@ -259,7 +269,7 @@ function applyEntries(input) {
|
|
|
259
269
|
}
|
|
260
270
|
};
|
|
261
271
|
let applied;
|
|
262
|
-
if (exactTaskRootReplay || exactNestedReplay) {
|
|
272
|
+
if (exactTaskRootReplay || exactNestedReplay || exactPlanningRootReplay) {
|
|
263
273
|
const result = applyEntry();
|
|
264
274
|
applied = result?.state === 'deferred'
|
|
265
275
|
? { state: 'deferred' }
|
|
@@ -957,13 +967,15 @@ export function applyWorkspaceSyncV3CombinedRepairPage(input) {
|
|
|
957
967
|
})();
|
|
958
968
|
}
|
|
959
969
|
const recoveryTargets = recoveryBatchId
|
|
960
|
-
? recoveryStore.
|
|
970
|
+
? recoveryStore.listOutboxRecoveryBaselineTargets({ tenantId, workspaceId, recoveryBatchId })
|
|
961
971
|
: [];
|
|
962
972
|
const selectRecoveryEntries = (entries) => {
|
|
963
973
|
const byTarget = new Map(entries.map((entry) => [`${entry.domain}\0${entry.entityId}`, entry]));
|
|
964
974
|
return recoveryTargets.map((target) => {
|
|
965
975
|
const entry = byTarget.get(`${target.domain}\0${target.entityId}`);
|
|
966
976
|
if (!entry) {
|
|
977
|
+
if (!target.required)
|
|
978
|
+
return null;
|
|
967
979
|
throw staleRepairSnapshotError(target.domain, target.entityId);
|
|
968
980
|
}
|
|
969
981
|
const supportedTarget = ((target.domain === 'task' && entry.projection === 'task/root-v1')
|
|
@@ -975,7 +987,7 @@ export function applyWorkspaceSyncV3CombinedRepairPage(input) {
|
|
|
975
987
|
throw new Error('Targeted workspace sync recovery does not support this projection.');
|
|
976
988
|
}
|
|
977
989
|
return entry;
|
|
978
|
-
});
|
|
990
|
+
}).filter((entry) => entry !== null);
|
|
979
991
|
};
|
|
980
992
|
const projections = coveredProjections(input.page);
|
|
981
993
|
let notices = notifications();
|