@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
|
@@ -21,6 +21,8 @@ const NON_APPLYING_OPERATION_CLASSES = new Set([
|
|
|
21
21
|
const MAX_DRAIN_INSPECTION_LIMIT = 500;
|
|
22
22
|
export const LOCAL_SYNC_PERMIT_RETENTION_MIN_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
23
23
|
export const LOCAL_SYNC_PERMIT_RETENTION_SOFT_LIMIT = 1_000;
|
|
24
|
+
export const LOCAL_SYNC_PERMIT_RETENTION_HARD_LIMIT = 10_000;
|
|
25
|
+
export const LOCAL_SYNC_OBLIGATION_RETENTION_HARD_LIMIT = 2_000;
|
|
24
26
|
export const LOCAL_SYNC_PERMIT_RETENTION_DELETE_BATCH = 250;
|
|
25
27
|
export const LOCAL_SYNC_PERMIT_RETENTION_SWEEP_LIMIT = 5_000;
|
|
26
28
|
const LOCAL_SYNC_PERMIT_RETENTION_CHECK_INTERVAL_MS = 60 * 60 * 1000;
|
|
@@ -166,11 +168,29 @@ export class LocalSyncExecutionPermitStore {
|
|
|
166
168
|
FROM local_sync_recovery_obligations
|
|
167
169
|
WHERE workspace_id = ? AND resolved_at IS NULL
|
|
168
170
|
`).get(workspaceId);
|
|
171
|
+
const terminalHistory = this.db.prepare(`
|
|
172
|
+
SELECT COUNT(*) AS count, MIN(completed_at) AS oldest_at
|
|
173
|
+
FROM local_sync_execution_permits
|
|
174
|
+
WHERE workspace_id = ?
|
|
175
|
+
AND state IN ('released', 'abandoned-clean', 'abandoned-reconcile-required')
|
|
176
|
+
AND completed_at IS NOT NULL
|
|
177
|
+
`).get(workspaceId);
|
|
178
|
+
const resolvedHistory = this.db.prepare(`
|
|
179
|
+
SELECT COUNT(*) AS count, MIN(resolved_at) AS oldest_at
|
|
180
|
+
FROM local_sync_recovery_obligations
|
|
181
|
+
WHERE workspace_id = ? AND resolved_at IS NOT NULL
|
|
182
|
+
`).get(workspaceId);
|
|
169
183
|
return {
|
|
170
184
|
active,
|
|
171
185
|
byState,
|
|
172
186
|
recoveryObligations: Number(obligation?.count || 0),
|
|
173
187
|
localCommitInvariantViolation: this.invariantBlockedWorkspaces.has(workspaceId),
|
|
188
|
+
history: {
|
|
189
|
+
terminalPermits: Number(terminalHistory?.count || 0),
|
|
190
|
+
oldestTerminalPermitAt: terminalHistory?.oldest_at || null,
|
|
191
|
+
resolvedRecoveryObligations: Number(resolvedHistory?.count || 0),
|
|
192
|
+
oldestResolvedRecoveryObligationAt: resolvedHistory?.oldest_at || null,
|
|
193
|
+
},
|
|
174
194
|
};
|
|
175
195
|
}
|
|
176
196
|
inspectGenerationDrain(input) {
|
|
@@ -417,6 +437,42 @@ export class LocalSyncExecutionPermitStore {
|
|
|
417
437
|
});
|
|
418
438
|
})();
|
|
419
439
|
}
|
|
440
|
+
renewAcceptedApplyCompletion(input) {
|
|
441
|
+
return this.db.transaction(() => {
|
|
442
|
+
const current = this.requirePermit(input.permitId);
|
|
443
|
+
this.assertFence(current, input);
|
|
444
|
+
if (current.ownerMode !== input.ownerMode) {
|
|
445
|
+
throw new LocalSyncExecutionPermitError(`Execution permit ${input.permitId} belongs to ${current.ownerMode}, not ${input.ownerMode}.`, 'SYNC_COORDINATOR_PERMIT_IDENTITY_CONFLICT');
|
|
446
|
+
}
|
|
447
|
+
if (current.state !== 'gateway-complete-awaiting-apply'
|
|
448
|
+
|| current.upstreamDisposition !== 'accepted') {
|
|
449
|
+
throw new LocalSyncExecutionPermitError(`Execution permit ${input.permitId} has no accepted apply completion to renew.`, 'SYNC_COORDINATOR_PERMIT_STATE_CONFLICT');
|
|
450
|
+
}
|
|
451
|
+
const now = this.now();
|
|
452
|
+
const deadlineMs = Date.parse(input.deadlineAt);
|
|
453
|
+
if (!Number.isFinite(deadlineMs)
|
|
454
|
+
|| deadlineMs <= Date.parse(now)
|
|
455
|
+
|| deadlineMs <= Date.parse(current.deadlineAt)) {
|
|
456
|
+
throw new LocalSyncExecutionPermitError(`Execution permit ${input.permitId} renewal deadline is invalid.`, 'SYNC_COORDINATOR_PERMIT_EXPIRED');
|
|
457
|
+
}
|
|
458
|
+
const result = this.db.prepare(`
|
|
459
|
+
UPDATE local_sync_execution_permits
|
|
460
|
+
SET deadline_at = ?,
|
|
461
|
+
last_transition_at = ?
|
|
462
|
+
WHERE permit_id = ?
|
|
463
|
+
AND workspace_id = ?
|
|
464
|
+
AND coordinator_generation = ?
|
|
465
|
+
AND server_instance_id = ?
|
|
466
|
+
AND owner_mode = ?
|
|
467
|
+
AND state = 'gateway-complete-awaiting-apply'
|
|
468
|
+
AND upstream_disposition = 'accepted'
|
|
469
|
+
`).run(input.deadlineAt, now, input.permitId, input.workspaceId, input.coordinatorGeneration, input.serverInstanceId, input.ownerMode);
|
|
470
|
+
if (Number(result?.changes || 0) !== 1) {
|
|
471
|
+
throw new LocalSyncExecutionPermitError(`Execution permit ${input.permitId} changed before its renewal committed.`, 'SYNC_COORDINATOR_PERMIT_STATE_CONFLICT');
|
|
472
|
+
}
|
|
473
|
+
return this.read(input.permitId);
|
|
474
|
+
})();
|
|
475
|
+
}
|
|
420
476
|
completeAcceptedV3Dispatch(input, applyAuthoritativeAndAck) {
|
|
421
477
|
return this.db.transaction(() => {
|
|
422
478
|
const current = this.requirePermit(input.permitId);
|
|
@@ -677,19 +733,14 @@ export class LocalSyncExecutionPermitStore {
|
|
|
677
733
|
WHERE resolved_at IS NOT NULL
|
|
678
734
|
ORDER BY resolved_at DESC, obligation_id DESC
|
|
679
735
|
`).all().map(mapObligation);
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
.map((obligation) => obligation.obligationId));
|
|
683
|
-
for (const obligation of resolvedObligations) {
|
|
684
|
-
if (obligation.resolvedAt && obligation.resolvedAt >= cutoff) {
|
|
685
|
-
protectedObligationIds.add(obligation.obligationId);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
736
|
+
const obligationAgeDeleteCount = Math.max(0, resolvedObligations.length - LOCAL_SYNC_PERMIT_RETENTION_SOFT_LIMIT);
|
|
737
|
+
const obligationHardDeleteCount = Math.max(0, resolvedObligations.length - LOCAL_SYNC_OBLIGATION_RETENTION_HARD_LIMIT);
|
|
688
738
|
const obligationCandidates = resolvedObligations
|
|
689
|
-
.
|
|
690
|
-
&& obligation.resolvedAt < cutoff
|
|
691
|
-
&& !protectedObligationIds.has(obligation.obligationId)))
|
|
739
|
+
.slice()
|
|
692
740
|
.reverse()
|
|
741
|
+
.filter((obligation, index) => (index < obligationHardDeleteCount
|
|
742
|
+
|| (index < obligationAgeDeleteCount
|
|
743
|
+
&& Boolean(obligation.resolvedAt && obligation.resolvedAt < cutoff))))
|
|
693
744
|
.slice(0, boundedDeleteLimit);
|
|
694
745
|
const removeObligation = this.db.prepare(`
|
|
695
746
|
DELETE FROM local_sync_recovery_obligations
|
|
@@ -707,14 +758,7 @@ export class LocalSyncExecutionPermitStore {
|
|
|
707
758
|
AND completed_at IS NOT NULL
|
|
708
759
|
ORDER BY completed_at DESC, permit_id DESC
|
|
709
760
|
`).all().map(mapPermit);
|
|
710
|
-
const protectedPermitIds = new Set(
|
|
711
|
-
.slice(0, LOCAL_SYNC_PERMIT_RETENTION_SOFT_LIMIT)
|
|
712
|
-
.map((permit) => permit.permitId));
|
|
713
|
-
for (const permit of terminalPermits) {
|
|
714
|
-
if (permit.completedAt && permit.completedAt >= cutoff) {
|
|
715
|
-
protectedPermitIds.add(permit.permitId);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
761
|
+
const protectedPermitIds = new Set();
|
|
718
762
|
const referencedPermitRows = this.db.prepare(`
|
|
719
763
|
SELECT permit_id
|
|
720
764
|
FROM local_sync_recovery_obligations
|
|
@@ -723,11 +767,15 @@ export class LocalSyncExecutionPermitStore {
|
|
|
723
767
|
if (row.permit_id)
|
|
724
768
|
protectedPermitIds.add(row.permit_id);
|
|
725
769
|
}
|
|
770
|
+
const permitAgeDeleteCount = Math.max(0, terminalPermits.length - LOCAL_SYNC_PERMIT_RETENTION_SOFT_LIMIT);
|
|
771
|
+
const permitHardDeleteCount = Math.max(0, terminalPermits.length - LOCAL_SYNC_PERMIT_RETENTION_HARD_LIMIT);
|
|
726
772
|
const permitCandidates = terminalPermits
|
|
727
|
-
.
|
|
728
|
-
&& permit.completedAt < cutoff
|
|
729
|
-
&& !protectedPermitIds.has(permit.permitId)))
|
|
773
|
+
.slice()
|
|
730
774
|
.reverse()
|
|
775
|
+
.filter((permit, index) => (!protectedPermitIds.has(permit.permitId)
|
|
776
|
+
&& (index < permitHardDeleteCount
|
|
777
|
+
|| (index < permitAgeDeleteCount
|
|
778
|
+
&& Boolean(permit.completedAt && permit.completedAt < cutoff)))))
|
|
731
779
|
.slice(0, boundedDeleteLimit);
|
|
732
780
|
const removePermit = this.db.prepare(`
|
|
733
781
|
DELETE FROM local_sync_execution_permits
|
|
@@ -500,10 +500,11 @@ export function workspaceSyncGatewayTimeoutMs(operation) {
|
|
|
500
500
|
if (operation.type === 'initiative-repair'
|
|
501
501
|
|| operation.type === 'combined-repair'
|
|
502
502
|
|| operation.type === 'v3-feed-repair'
|
|
503
|
-
|| (operation.type === 'push' && operation.repairMode === true)
|
|
504
|
-
|| (operation.type === 'pull' && operation.repairMode === true)
|
|
505
503
|
|| (operation.type === 'v3-feed'
|
|
506
504
|
&& (operation.forceRepair === true || Boolean(operation.repairCursor))))
|
|
505
|
+
return 75_000;
|
|
506
|
+
if ((operation.type === 'push' && operation.repairMode === true)
|
|
507
|
+
|| (operation.type === 'pull' && operation.repairMode === true))
|
|
507
508
|
return 180_000;
|
|
508
509
|
if (operation.type === 'push'
|
|
509
510
|
|| operation.type === 'bootstrap-push')
|
|
@@ -638,6 +639,7 @@ const CHANGE_FIELDS = {
|
|
|
638
639
|
'task-relationship-delete': { allowed: ['op', 'relationship'], required: ['relationship'], objectField: 'relationship' },
|
|
639
640
|
'initiative-upsert': { allowed: ['op', 'initiative'], required: ['initiative'], objectField: 'initiative' },
|
|
640
641
|
'workstream-upsert': { allowed: ['op', 'workstream'], required: ['workstream'], objectField: 'workstream' },
|
|
642
|
+
'planning-identity-upsert': { allowed: ['op', 'identity'], required: ['identity'], objectField: 'identity' },
|
|
641
643
|
'taxonomy-upsert': { allowed: ['op', 'taxonomies', 'taxonomyState', 'updatedAt', 'durableRevision'], required: ['updatedAt'] },
|
|
642
644
|
'ai-profile-upsert': { allowed: ['op', 'profile'], required: ['profile'], objectField: 'profile' },
|
|
643
645
|
'document-upsert': { allowed: [
|
|
@@ -83,6 +83,11 @@ export interface WorkspaceSyncEngineRemoteServices {
|
|
|
83
83
|
workspaceId: string;
|
|
84
84
|
receipts: readonly WorkspaceSyncEngineRemoteTransferReceipt[];
|
|
85
85
|
}): void | Promise<void>;
|
|
86
|
+
/** Keeps accepted applying receipts alive until a bounded transfer reaches its atomic commit. */
|
|
87
|
+
retainTransferReceipts?(input: {
|
|
88
|
+
workspaceId: string;
|
|
89
|
+
receipts: readonly WorkspaceSyncEngineRemoteTransferReceipt[];
|
|
90
|
+
}): void | Promise<void>;
|
|
86
91
|
buildV2ApplyCompletionGroup(input: {
|
|
87
92
|
workspaceId: string;
|
|
88
93
|
previousCursor: string | null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { LocalSyncExecutionPermitStore } from '../coordinator/localSyncExecutionPermitStore.js';
|
|
2
2
|
import type { WorkspaceSyncTransportGateway } from '../coordinator/workspaceSyncTransportGateway.js';
|
|
3
3
|
import type { WorkspaceSyncEngineRemoteServices } from './workspaceSyncEngineRemoteServices.js';
|
|
4
|
-
type ServerRemotePermitStore = Pick<LocalSyncExecutionPermitStore, 'issueServer' | 'read' | 'acknowledgeNonApplyingCompletion' | 'completeAcceptedPushWithoutEcho' | 'abandonPermit'>;
|
|
4
|
+
type ServerRemotePermitStore = Pick<LocalSyncExecutionPermitStore, 'issueServer' | 'read' | 'acknowledgeNonApplyingCompletion' | 'completeAcceptedPushWithoutEcho' | 'renewAcceptedApplyCompletion' | 'abandonPermit'>;
|
|
5
5
|
type ServerRemoteGateway = Pick<WorkspaceSyncTransportGateway, 'executeServer'>;
|
|
6
6
|
export interface ServerWorkspaceSyncEngineRemoteServicesOptions {
|
|
7
7
|
workspaceId: string;
|
|
@@ -181,6 +181,7 @@ export function createServerWorkspaceSyncEngineRemoteServices(options) {
|
|
|
181
181
|
workspaceId: options.workspaceId,
|
|
182
182
|
previousBoundary: input.previousBoundary,
|
|
183
183
|
operationClass,
|
|
184
|
+
retentionMs: permitDeadlineMs,
|
|
184
185
|
});
|
|
185
186
|
return {
|
|
186
187
|
...result,
|
|
@@ -417,6 +418,20 @@ export function createServerWorkspaceSyncEngineRemoteServices(options) {
|
|
|
417
418
|
}
|
|
418
419
|
}
|
|
419
420
|
},
|
|
421
|
+
retainTransferReceipts: ({ workspaceId, receipts }) => {
|
|
422
|
+
const metadata = readReceipts(workspaceId, receipts);
|
|
423
|
+
const retainedAt = now();
|
|
424
|
+
for (const receipt of metadata) {
|
|
425
|
+
options.permitStore.renewAcceptedApplyCompletion({
|
|
426
|
+
permitId: receipt.permitId,
|
|
427
|
+
workspaceId,
|
|
428
|
+
coordinatorGeneration: options.coordinatorGeneration,
|
|
429
|
+
ownerMode: 'server',
|
|
430
|
+
serverInstanceId: options.serverInstanceId,
|
|
431
|
+
deadlineAt: new Date(retainedAt.getTime() + receipt.retentionMs).toISOString(),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
},
|
|
420
435
|
buildV2ApplyCompletionGroup: ({ workspaceId, previousCursor, nextCursor, receipts, }) => completionGroup(workspaceId, receipts, v2Boundary(previousCursor), v2Boundary(nextCursor)),
|
|
421
436
|
buildV2PushApplyCompletionGroup: ({ workspaceId, pushBoundary, receipts, }) => completionGroup(workspaceId, receipts, parseWorkspaceSyncEngineV2PushBoundary(pushBoundary), parseWorkspaceSyncEngineV2PushBoundary(pushBoundary)),
|
|
422
437
|
buildV3ApplyCompletionGroup: ({ workspaceId, apply, receipts }) => {
|
|
@@ -146,6 +146,20 @@ export function createWorkspaceSyncEngineServerRunnerOperations(options) {
|
|
|
146
146
|
transformCheckpointAtTransferBoundary: (boundary) => (transferBoundaryCheckpointTransform(boundary)),
|
|
147
147
|
},
|
|
148
148
|
});
|
|
149
|
+
const prepareAndDrainRecoveryReplay = async (preparedRecovery) => {
|
|
150
|
+
let recovery = preparedRecovery
|
|
151
|
+
?? await localServices.prepareOutboxRecovery(workspaceId);
|
|
152
|
+
if (recovery.kind === 'recovery' && recovery.state === 'replay_queued') {
|
|
153
|
+
const drainResult = await options.drainV3Outbox?.({
|
|
154
|
+
beforeTransport: () => undefined,
|
|
155
|
+
});
|
|
156
|
+
if (drainResult === 'v2-content-push-required') {
|
|
157
|
+
fail('v2-content-push', 'A referenced attachment must be published before durable repair can continue.', 'SYNC_RUNNER_V2_CONTENT_PUSH_REQUIRED');
|
|
158
|
+
}
|
|
159
|
+
recovery = await localServices.prepareOutboxRecovery(workspaceId);
|
|
160
|
+
}
|
|
161
|
+
return recovery;
|
|
162
|
+
};
|
|
149
163
|
const ensureReady = options.ensureCloudWorkspaceReadyForSync
|
|
150
164
|
|| (async () => {
|
|
151
165
|
const result = await options.remoteServices.handshakeWorkspace(workspaceId);
|
|
@@ -884,8 +898,7 @@ export function createWorkspaceSyncEngineServerRunnerOperations(options) {
|
|
|
884
898
|
}
|
|
885
899
|
if (localRecovery.kind === 'recovery'
|
|
886
900
|
&& localRecovery.state === 'replay_queued') {
|
|
887
|
-
|
|
888
|
-
localRecovery = await localServices.prepareOutboxRecovery(workspaceId);
|
|
901
|
+
localRecovery = await prepareAndDrainRecoveryReplay(localRecovery);
|
|
889
902
|
}
|
|
890
903
|
if (localRecovery.kind === 'recovery'
|
|
891
904
|
&& localRecovery.state === 'replay_queued') {
|
|
@@ -912,7 +925,10 @@ export function createWorkspaceSyncEngineServerRunnerOperations(options) {
|
|
|
912
925
|
const continuationInvalid = continuation !== null && (continuation.coordinatorGeneration !== input.coordinatorGeneration
|
|
913
926
|
|| continuation.ownershipFingerprint !== ownershipFingerprint
|
|
914
927
|
|| (recoveryBatchId !== null
|
|
915
|
-
&& continuation.recoveryBatchId !== recoveryBatchId)
|
|
928
|
+
&& continuation.recoveryBatchId !== recoveryBatchId)
|
|
929
|
+
|| (recoveryBatchId !== null
|
|
930
|
+
&& continuation.kind !== 'v3-combined'
|
|
931
|
+
&& continuation.kind !== 'v2-reconcile'));
|
|
916
932
|
if (continuationInvalid || (checkpoint.repairContinuation !== null && !continuation)) {
|
|
917
933
|
if (checkpoint.pendingV2Push) {
|
|
918
934
|
await push({
|
|
@@ -1030,6 +1046,13 @@ export function createWorkspaceSyncEngineServerRunnerOperations(options) {
|
|
|
1030
1046
|
}
|
|
1031
1047
|
continuation = checkpoint.repairContinuation;
|
|
1032
1048
|
}
|
|
1049
|
+
if (continuation.recoveryBatchId) {
|
|
1050
|
+
const postPullRecovery = await prepareAndDrainRecoveryReplay();
|
|
1051
|
+
if (postPullRecovery.kind === 'recovery'
|
|
1052
|
+
|| Number(options.inspectV3Outbox?.().activeDepth || 0) > 0) {
|
|
1053
|
+
fail('repair', 'Durable recovery replay must settle before authoritative repair continues.', 'SYNC_RUNNER_RECOVERY_REPLAY_PENDING', RETRY_MAX_DELAY_MS);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1033
1056
|
let pushResult = null;
|
|
1034
1057
|
if (continuation.kind === 'final-push') {
|
|
1035
1058
|
pushResult = await push({
|
|
@@ -279,6 +279,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
|
|
|
279
279
|
workspaceId,
|
|
280
280
|
incomingInitiatives: applyModel.planning.incomingInitiatives,
|
|
281
281
|
incomingWorkstreams: applyModel.planning.incomingWorkstreams,
|
|
282
|
+
incomingIdentities: applyModel.planning.incomingIdentities,
|
|
282
283
|
recordSyncDiagnostic,
|
|
283
284
|
...(options.coexistenceDirection ? {
|
|
284
285
|
coexistenceDirection: options.coexistenceDirection,
|
|
@@ -722,6 +723,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
|
|
|
722
723
|
workspaceId,
|
|
723
724
|
incomingInitiatives: applyModel.planning.incomingInitiatives,
|
|
724
725
|
incomingWorkstreams: applyModel.planning.incomingWorkstreams,
|
|
726
|
+
incomingIdentities: applyModel.planning.incomingIdentities,
|
|
725
727
|
recordSyncDiagnostic,
|
|
726
728
|
...(options.coexistenceDirection ? {
|
|
727
729
|
coexistenceDirection: options.coexistenceDirection,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TaskforceCore, TaskforceSyncCapable } from '../core/Taskforce.js';
|
|
2
2
|
import type { DeletedTaskDetails, DeletedTaskSource, TaskRelationshipRecord } from '../core/Taskforce.js';
|
|
3
|
-
import type { WorkspaceSyncAiProfileSnapshot, WorkspaceSyncAgentConversationSnapshot, WorkspaceSyncAgentRoleSnapshot, WorkspaceSyncAnnotatedAttachmentSessionSnapshot, WorkspaceSyncChange, WorkspaceSyncDocumentReviewCommentSnapshot, WorkspaceSyncDocumentReviewSessionSnapshot, WorkspaceSyncDurableTaskRevision, WorkspaceSyncDurableTaxonomyRevision, WorkspaceSyncEntityEventSnapshot, WorkspaceSyncInitiativeSnapshot, WorkspaceSyncTaskforceAgentSnapshot, WorkspaceSyncTaskforceAgentSkillSnapshot, WorkspaceSyncTaskSnapshot, WorkspaceSyncWorkspaceMemberSnapshot, WorkspaceSyncWorkstreamSnapshot } from './workspaceSyncModel.js';
|
|
3
|
+
import type { WorkspaceSyncAiProfileSnapshot, WorkspaceSyncAgentConversationSnapshot, WorkspaceSyncAgentRoleSnapshot, WorkspaceSyncAnnotatedAttachmentSessionSnapshot, WorkspaceSyncChange, WorkspaceSyncDocumentReviewCommentSnapshot, WorkspaceSyncDocumentReviewSessionSnapshot, WorkspaceSyncDurableTaskRevision, WorkspaceSyncDurableTaxonomyRevision, WorkspaceSyncEntityEventSnapshot, WorkspaceSyncInitiativeSnapshot, WorkspaceSyncPlanningIdentitySnapshot, WorkspaceSyncTaskforceAgentSnapshot, WorkspaceSyncTaskforceAgentSkillSnapshot, WorkspaceSyncTaskSnapshot, WorkspaceSyncWorkspaceMemberSnapshot, WorkspaceSyncWorkstreamSnapshot } from './workspaceSyncModel.js';
|
|
4
4
|
import type { BinaryAssetSyncPayload, DocumentSyncPayload } from './contentSyncPayload.js';
|
|
5
5
|
import type { WorkspaceTaxonomyState } from '../shared/taxonomyState.js';
|
|
6
6
|
import { WorkspaceSyncDurabilityStore } from './v3/syncDurabilityStore.js';
|
|
@@ -55,6 +55,7 @@ export interface WorkspaceSyncIncomingApplyModel {
|
|
|
55
55
|
incomingTaxonomyDurableRevision: WorkspaceSyncDurableTaxonomyRevision | null;
|
|
56
56
|
incomingInitiatives: Map<string, WorkspaceSyncInitiativeSnapshot>;
|
|
57
57
|
incomingWorkstreams: Map<string, WorkspaceSyncWorkstreamSnapshot>;
|
|
58
|
+
incomingIdentities: Map<string, WorkspaceSyncPlanningIdentitySnapshot>;
|
|
58
59
|
};
|
|
59
60
|
profiles: {
|
|
60
61
|
incomingAiProfiles: Map<string, WorkspaceSyncAiProfileSnapshot>;
|
|
@@ -163,6 +164,7 @@ export declare function applyPlanningSyncChanges(input: {
|
|
|
163
164
|
workspaceId: string;
|
|
164
165
|
incomingInitiatives: Map<string, WorkspaceSyncInitiativeSnapshot>;
|
|
165
166
|
incomingWorkstreams: Map<string, WorkspaceSyncWorkstreamSnapshot>;
|
|
167
|
+
incomingIdentities?: Map<string, WorkspaceSyncPlanningIdentitySnapshot>;
|
|
166
168
|
recordSyncDiagnostic: RecordSyncDiagnostic;
|
|
167
169
|
coexistenceDirection?: TaskMetadataCoexistenceDirection;
|
|
168
170
|
assertedActiveV3InitiativeIds?: ReadonlySet<string>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { createHash } from 'crypto';
|
|
4
|
+
import { assertPlanningIdentityColor, assertPlanningIdentityIcon } from '../shared/planningIdentity.js';
|
|
4
5
|
import { parseAiProfileColorSource } from '../shared/aiProfileColors.js';
|
|
5
6
|
import { normalizeBinaryAssetDeleteSyncPayload, normalizeBinaryAssetSyncPayload, normalizeDocumentDeleteSyncPayload, normalizeDocumentSyncPayload, } from './contentSyncPayload.js';
|
|
6
7
|
import { normalizeAiProfileSyncPayload, normalizeAgentConversationSyncPayload, normalizeAgentRoleSyncPayload, normalizeAnnotatedAttachmentSessionSyncPayload, normalizeDeleteSyncPayload, normalizeDocumentReviewCommentSyncPayload, normalizeDocumentReviewSessionSyncPayload, normalizeTaskforceAgentSkillSyncPayload, normalizeTaskforceAgentSyncPayload, } from './collaborationSyncPayload.js';
|
|
@@ -32,6 +33,7 @@ function createIncomingApplyModel() {
|
|
|
32
33
|
incomingTaxonomyDurableRevision: null,
|
|
33
34
|
incomingInitiatives: new Map(),
|
|
34
35
|
incomingWorkstreams: new Map(),
|
|
36
|
+
incomingIdentities: new Map(),
|
|
35
37
|
},
|
|
36
38
|
profiles: {
|
|
37
39
|
incomingAiProfiles: new Map(),
|
|
@@ -68,6 +70,7 @@ function shouldSkipTaskClassification(op) {
|
|
|
68
70
|
|| op === 'task-relationship-delete'
|
|
69
71
|
|| op === 'initiative-upsert'
|
|
70
72
|
|| op === 'workstream-upsert'
|
|
73
|
+
|| op === 'planning-identity-upsert'
|
|
71
74
|
|| op === 'document-upsert'
|
|
72
75
|
|| op === 'document-delete'
|
|
73
76
|
|| op === 'asset-upsert'
|
|
@@ -185,6 +188,26 @@ export function classifyWorkspaceSyncChanges(input) {
|
|
|
185
188
|
model.planning.incomingWorkstreams.set(workstream.id, workstream);
|
|
186
189
|
continue;
|
|
187
190
|
}
|
|
191
|
+
if (op === 'planning-identity-upsert') {
|
|
192
|
+
const value = raw.identity;
|
|
193
|
+
const entityType = value?.entityType === 'initiative' || value?.entityType === 'workstream'
|
|
194
|
+
? value.entityType
|
|
195
|
+
: null;
|
|
196
|
+
const entityId = String(value?.entityId || '').trim();
|
|
197
|
+
const updatedAt = String(value?.updatedAt || raw.updatedAt || raw.watermark || '').trim();
|
|
198
|
+
if (!entityType || !entityId || !Number.isFinite(Date.parse(updatedAt))) {
|
|
199
|
+
throw new Error('Planning identity sync payload is invalid.');
|
|
200
|
+
}
|
|
201
|
+
const identity = {
|
|
202
|
+
entityType,
|
|
203
|
+
entityId,
|
|
204
|
+
icon: assertPlanningIdentityIcon(value?.icon),
|
|
205
|
+
color: assertPlanningIdentityColor(value?.color),
|
|
206
|
+
updatedAt,
|
|
207
|
+
};
|
|
208
|
+
model.planning.incomingIdentities.set(`${entityType}:${entityId}`, identity);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
188
211
|
if (op === 'ai-profile-upsert') {
|
|
189
212
|
const profile = normalizeAiProfileSyncPayload(raw.profile, workspaceId, normalizeIncomingAiProfileSurfaceType, normalizeIncomingAiProfileSeatScope, 'apply.aiProfileUpsert');
|
|
190
213
|
const profileId = String(profile?.id || '').trim();
|
|
@@ -690,6 +713,12 @@ export function applyPlanningSyncChanges(input) {
|
|
|
690
713
|
}
|
|
691
714
|
}
|
|
692
715
|
}
|
|
716
|
+
for (const identity of input.incomingIdentities?.values() || []) {
|
|
717
|
+
const applied = core.applyPlanningEntityIdentityForSync(identity, workspaceId);
|
|
718
|
+
if (!applied) {
|
|
719
|
+
throw new Error(`Planning identity parent is missing: ${identity.entityType}:${identity.entityId}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
693
722
|
}
|
|
694
723
|
export function applyTaxonomySyncChanges(input) {
|
|
695
724
|
const hasTaxonomy = Boolean(input.incomingTaxonomyState) || Array.isArray(input.incomingTaxonomies);
|
package/dist/sync/syncService.js
CHANGED
|
@@ -217,6 +217,7 @@ function workspacePushChangeStableIdentity(change) {
|
|
|
217
217
|
case 'task-relationship-delete': return `${change.op}:${change.relationship.id}`;
|
|
218
218
|
case 'initiative-upsert': return `${change.op}:${change.initiative.id}`;
|
|
219
219
|
case 'workstream-upsert': return `${change.op}:${change.workstream.id}`;
|
|
220
|
+
case 'planning-identity-upsert': return `${change.op}:${change.identity.entityType}:${change.identity.entityId}`;
|
|
220
221
|
case 'taxonomy-upsert': return change.op;
|
|
221
222
|
case 'ai-profile-upsert': return `${change.op}:${change.profile.id}`;
|
|
222
223
|
case 'document-upsert':
|
|
@@ -284,6 +285,8 @@ function resolveWorkspacePushPhase(change) {
|
|
|
284
285
|
return 0;
|
|
285
286
|
case 'workstream-upsert':
|
|
286
287
|
return 1;
|
|
288
|
+
case 'planning-identity-upsert':
|
|
289
|
+
return 2;
|
|
287
290
|
case 'upsert':
|
|
288
291
|
return 2;
|
|
289
292
|
case 'document-upsert':
|
|
@@ -689,6 +692,12 @@ export async function runWorkspacePushSyncService(input) {
|
|
|
689
692
|
completedChangeCount += plainBatchChanges.length;
|
|
690
693
|
continue;
|
|
691
694
|
}
|
|
695
|
+
if (pushTransferReceipts.length > 0) {
|
|
696
|
+
await remoteServices.retainTransferReceipts?.({
|
|
697
|
+
workspaceId: input.workspaceId,
|
|
698
|
+
receipts: pushTransferReceipts,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
692
701
|
const sendPush = async () => {
|
|
693
702
|
const requestStartedAtMs = Date.now();
|
|
694
703
|
const result = await remoteServices.pushWorkspaceChanges({
|
|
@@ -25,6 +25,7 @@ export interface TaskSyncPayload {
|
|
|
25
25
|
referenceNumber?: number | null;
|
|
26
26
|
comments?: Comment[];
|
|
27
27
|
attachments?: Array<string | AttachmentItem>;
|
|
28
|
+
cardCoverAssetId?: string | null;
|
|
28
29
|
checklistItems?: ChecklistItem[];
|
|
29
30
|
}
|
|
30
31
|
export declare function serializeTaskForSync(task: Partial<TaskItem> | null | undefined): TaskSyncPayload;
|
|
@@ -64,6 +64,7 @@ function cloneChecklistItems(value) {
|
|
|
64
64
|
}
|
|
65
65
|
export function serializeTaskForSync(task) {
|
|
66
66
|
const source = task && typeof task === 'object' ? task : {};
|
|
67
|
+
const hasCardCoverAssetId = Object.prototype.hasOwnProperty.call(source, 'cardCoverAssetId');
|
|
67
68
|
return {
|
|
68
69
|
id: String(source.id || '').trim(),
|
|
69
70
|
title: String(source.title || '').trim(),
|
|
@@ -89,6 +90,9 @@ export function serializeTaskForSync(task) {
|
|
|
89
90
|
referenceNumber: toOptionalInteger(source.referenceNumber, { min: 1, nullable: true }),
|
|
90
91
|
comments: cloneComments(source.comments),
|
|
91
92
|
attachments: cloneAttachments(source.attachments),
|
|
93
|
+
...(hasCardCoverAssetId
|
|
94
|
+
? { cardCoverAssetId: toNullableString(source.cardCoverAssetId) }
|
|
95
|
+
: {}),
|
|
92
96
|
checklistItems: cloneChecklistItems(source.checklistItems),
|
|
93
97
|
};
|
|
94
98
|
}
|
|
@@ -102,6 +106,7 @@ export function normalizeTaskSyncPayload(raw) {
|
|
|
102
106
|
return serializeTaskForSync(source);
|
|
103
107
|
}
|
|
104
108
|
export function taskSyncPayloadToCoreTask(task) {
|
|
109
|
+
const hasCardCoverAssetId = Object.prototype.hasOwnProperty.call(task, 'cardCoverAssetId');
|
|
105
110
|
return {
|
|
106
111
|
id: task.id,
|
|
107
112
|
title: task.title,
|
|
@@ -127,6 +132,7 @@ export function taskSyncPayloadToCoreTask(task) {
|
|
|
127
132
|
referenceNumber: task.referenceNumber ?? undefined,
|
|
128
133
|
comments: task.comments,
|
|
129
134
|
attachments: task.attachments,
|
|
135
|
+
...(hasCardCoverAssetId ? { cardCoverAssetId: task.cardCoverAssetId } : {}),
|
|
130
136
|
checklistItems: task.checklistItems
|
|
131
137
|
};
|
|
132
138
|
}
|
|
@@ -40,6 +40,7 @@ export declare function createDurableTaskAttachmentLinksMutationRouter(input: {
|
|
|
40
40
|
replace(args: {
|
|
41
41
|
taskId: string;
|
|
42
42
|
links: readonly WorkspaceSyncV3TaskAttachmentLink[];
|
|
43
|
+
cardCoverAssetId?: string | null;
|
|
43
44
|
}): {
|
|
44
45
|
state: "pending" | "committed";
|
|
45
46
|
route: "local-outbox" | "cloud-acceptance";
|
|
@@ -4,6 +4,7 @@ import { enqueueLocalTaskAttachmentLinksMutation, } from './localTaskAttachmentL
|
|
|
4
4
|
import { DurableTaskAttachmentLinksMutationError, runDurableTaskAttachmentLinksReplace, } from './taskAttachmentLinksMutationService.js';
|
|
5
5
|
import { WorkspaceSyncDurabilityStore, WorkspaceSyncEntityBusyError } from './syncDurabilityStore.js';
|
|
6
6
|
import { serializeWorkspaceSyncV3TaskAttachmentLinksPayload, } from './workspaceSyncV3NestedTaskCodecs.js';
|
|
7
|
+
import { resolveTaskCardCoverAssetIdForLinks } from '../../core/TaskAttachmentLinksDurableMutationService.js';
|
|
7
8
|
const OPERATION_LEASE_MS = 30_000;
|
|
8
9
|
function enabled(value) {
|
|
9
10
|
return /^(1|true|on|yes)$/i.test(String(value || '').trim());
|
|
@@ -93,6 +94,22 @@ export function createDurableTaskAttachmentLinksMutationRouter(input) {
|
|
|
93
94
|
catch (error) {
|
|
94
95
|
throw new DurableTaskAttachmentLinksMutationError(error instanceof Error ? error.message : 'Task attachment links are invalid.', 'TASK_ATTACHMENT_LINKS_INVALID', 400);
|
|
95
96
|
}
|
|
97
|
+
const currentTask = input.core.getTask(taskId, workspaceId);
|
|
98
|
+
if (!currentTask)
|
|
99
|
+
throw new DurableTaskAttachmentLinksMutationError('Task not found.', 'TASK_NOT_FOUND', 404);
|
|
100
|
+
let cardCoverAssetId;
|
|
101
|
+
try {
|
|
102
|
+
cardCoverAssetId = resolveTaskCardCoverAssetIdForLinks({
|
|
103
|
+
currentCardCoverAssetId: currentTask.cardCoverAssetId,
|
|
104
|
+
...(Object.prototype.hasOwnProperty.call(args, 'cardCoverAssetId')
|
|
105
|
+
? { requestedCardCoverAssetId: args.cardCoverAssetId }
|
|
106
|
+
: {}),
|
|
107
|
+
links,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw new DurableTaskAttachmentLinksMutationError(error instanceof Error ? error.message : 'Task card cover is invalid.', 'TASK_ATTACHMENT_LINKS_INVALID', Number(error?.statusCode) || 400);
|
|
112
|
+
}
|
|
96
113
|
const acceptedAt = now();
|
|
97
114
|
const acceptedAtIso = acceptedAt.toISOString();
|
|
98
115
|
const operationId = operationIdRequired(input.operationId);
|
|
@@ -123,7 +140,8 @@ export function createDurableTaskAttachmentLinksMutationRouter(input) {
|
|
|
123
140
|
catch {
|
|
124
141
|
throw new DurableTaskAttachmentLinksMutationError('Captured task attachment-link payload is invalid.', 'TASK_ATTACHMENT_LINKS_IDENTITY_CONFLICT', 409);
|
|
125
142
|
}
|
|
126
|
-
if (canonicalWorkspaceSyncJson(frozen) !== canonicalWorkspaceSyncJson(links)
|
|
143
|
+
if (canonicalWorkspaceSyncJson(frozen) !== canonicalWorkspaceSyncJson(links)
|
|
144
|
+
|| String(payload.cardCoverAssetId || '') !== String(cardCoverAssetId || '')) {
|
|
127
145
|
throw new DurableTaskAttachmentLinksMutationError('Operation identity is assigned to different task attachment links.', 'TASK_ATTACHMENT_LINKS_IDENTITY_CONFLICT', 409);
|
|
128
146
|
}
|
|
129
147
|
const task = input.core.getTask(taskId, workspaceId);
|
|
@@ -138,7 +156,7 @@ export function createDurableTaskAttachmentLinksMutationRouter(input) {
|
|
|
138
156
|
let result;
|
|
139
157
|
try {
|
|
140
158
|
result = enqueueLocalTaskAttachmentLinksMutation({
|
|
141
|
-
store, core: serviceCore, identity, taskId, links, now: acceptedAtIso,
|
|
159
|
+
store, core: serviceCore, identity, taskId, links, cardCoverAssetId, now: acceptedAtIso,
|
|
142
160
|
actorRef, onPostCommitError: input.onPostCommitError,
|
|
143
161
|
requireV3CombinedOwnership: persistedOwnershipChecked,
|
|
144
162
|
});
|
|
@@ -182,7 +200,7 @@ export function createDurableTaskAttachmentLinksMutationRouter(input) {
|
|
|
182
200
|
});
|
|
183
201
|
}
|
|
184
202
|
const result = runDurableTaskAttachmentLinksReplace({
|
|
185
|
-
store, core: serviceCore, tenantId, workspaceId, clientId, operationId, taskId, links,
|
|
203
|
+
store, core: serviceCore, tenantId, workspaceId, clientId, operationId, taskId, links, cardCoverAssetId,
|
|
186
204
|
baseEntityRevision: coordinates.entityRevision, baseLifecycleRevision: coordinates.lifecycleRevision,
|
|
187
205
|
now: acceptedAtIso, leaseExpiresAt: new Date(acceptedAt.getTime() + OPERATION_LEASE_MS).toISOString(),
|
|
188
206
|
actorRef, onPostCommitError: input.onPostCommitError,
|
|
@@ -245,15 +245,22 @@ export function createDurableTaskHttpBulkMutationRouter(input) {
|
|
|
245
245
|
if (hasAttachments && !Array.isArray(item.fields.attachments)) {
|
|
246
246
|
throw new TaskforceRuleError('attachments must be an array.', 400, 'TASK_ATTACHMENT_LINKS_INVALID');
|
|
247
247
|
}
|
|
248
|
-
const
|
|
248
|
+
const hasCardCover = Object.prototype.hasOwnProperty.call(item.fields, 'cardCoverAssetId');
|
|
249
|
+
if (hasCardCover && item.fields.cardCoverAssetId !== null
|
|
250
|
+
&& (typeof item.fields.cardCoverAssetId !== 'string' || !item.fields.cardCoverAssetId.trim())) {
|
|
251
|
+
throw new TaskforceRuleError('cardCoverAssetId must be a non-empty string or null.', 400, 'TASK_CARD_COVER_INVALID');
|
|
252
|
+
}
|
|
253
|
+
const attachmentOperationId = hasAttachments || hasCardCover
|
|
249
254
|
? input.deriveChildOperationId('bulk-update-fields-attachments', item.index, item.id)
|
|
250
255
|
: '';
|
|
251
|
-
const attachmentLinks = hasAttachments
|
|
256
|
+
const attachmentLinks = hasAttachments || hasCardCover
|
|
252
257
|
? input.createAttachmentLinksRouter(attachmentOperationId)
|
|
253
258
|
: null;
|
|
254
259
|
const durableAttachmentLinks = attachmentLinks?.enabled === true;
|
|
255
260
|
const normalizedAttachmentLinks = durableAttachmentLinks
|
|
256
|
-
? normalizeAttachmentLinks(
|
|
261
|
+
? normalizeAttachmentLinks(hasAttachments
|
|
262
|
+
? item.fields.attachments
|
|
263
|
+
: input.core.getTask(item.id, input.workspaceId)?.attachments || [])
|
|
257
264
|
: [];
|
|
258
265
|
const checklistOperationId = hasChecklist
|
|
259
266
|
? input.deriveChildOperationId('bulk-update-fields-checklist', item.index, item.id)
|
|
@@ -262,7 +269,8 @@ export function createDurableTaskHttpBulkMutationRouter(input) {
|
|
|
262
269
|
const durableChecklist = checklist?.enabled === true;
|
|
263
270
|
const rootFields = durableChecklist || durableAttachmentLinks
|
|
264
271
|
? Object.fromEntries(Object.entries(item.fields).filter(([key]) => (!(durableChecklist && key === 'checklistItems')
|
|
265
|
-
&& !(durableAttachmentLinks && key === 'attachments')
|
|
272
|
+
&& !(durableAttachmentLinks && key === 'attachments')
|
|
273
|
+
&& !(durableAttachmentLinks && key === 'cardCoverAssetId'))))
|
|
266
274
|
: item.fields;
|
|
267
275
|
let updated = input.core.getTask(item.id, input.workspaceId);
|
|
268
276
|
if (Object.keys(rootFields).length > 0) {
|
|
@@ -299,6 +307,11 @@ export function createDurableTaskHttpBulkMutationRouter(input) {
|
|
|
299
307
|
updated = batch.runChild(() => attachmentLinks.replace({
|
|
300
308
|
taskId: item.id,
|
|
301
309
|
links: normalizedAttachmentLinks,
|
|
310
|
+
...(hasCardCover ? {
|
|
311
|
+
cardCoverAssetId: item.fields.cardCoverAssetId === null
|
|
312
|
+
? null
|
|
313
|
+
: String(item.fields.cardCoverAssetId).trim(),
|
|
314
|
+
} : {}),
|
|
302
315
|
}).task);
|
|
303
316
|
}
|
|
304
317
|
if (updated)
|
|
@@ -294,8 +294,9 @@ function validateActivitySemantics(input) {
|
|
|
294
294
|
const authoritativeAttachmentLinks = validateCanonicalPlanningAttachmentLinks(input.authoritative.attachments);
|
|
295
295
|
const comparableExpectedDetails = normalizePlanningActivityDetailsForComparison(expectedDetails, input.record.mutationKind, authoritativeAttachmentLinks);
|
|
296
296
|
const comparableAuthoritativeDetails = normalizePlanningActivityDetailsForComparison((input.activity.details || {}), input.record.mutationKind, authoritativeAttachmentLinks);
|
|
297
|
-
if (
|
|
298
|
-
|
|
297
|
+
if (!input.boundedAttachmentRebase
|
|
298
|
+
&& fingerprintWorkspaceSyncPayload(comparableExpectedDetails)
|
|
299
|
+
!== fingerprintWorkspaceSyncPayload(comparableAuthoritativeDetails)) {
|
|
299
300
|
throw new LocalAuthoritativeInitiativeError('Authoritative initiative activity details differ from the captured mutation.');
|
|
300
301
|
}
|
|
301
302
|
}
|
|
@@ -437,6 +438,7 @@ export function applyAuthoritativeInitiativeAndAck(input) {
|
|
|
437
438
|
payload: payload,
|
|
438
439
|
authoritative: initiative,
|
|
439
440
|
activity,
|
|
441
|
+
boundedAttachmentRebase,
|
|
440
442
|
});
|
|
441
443
|
const revision = validateInitiativeActivityAuthoritativeRevision({
|
|
442
444
|
response: response,
|
|
@@ -279,7 +279,11 @@ export async function dispatchLocalOutboxBatch(input) {
|
|
|
279
279
|
const details = errorDetails(result.data, `WORKSPACE_SYNC_TRANSPORT_${result.status || 'NETWORK'}`, 'Workspace sync delivery failed.');
|
|
280
280
|
const stableErrorCode = result.status === 401 ? 'WORKSPACE_SYNC_TRANSPORT_401' : details.code;
|
|
281
281
|
const contentPushRequired = result.status === 425
|
|
282
|
-
&&
|
|
282
|
+
&& [
|
|
283
|
+
'TASK_ATTACHMENT_ASSET_NOT_READY',
|
|
284
|
+
'WORKSTREAM_ATTACHMENT_NOT_READY',
|
|
285
|
+
'INITIATIVE_ATTACHMENT_NOT_READY',
|
|
286
|
+
].includes(stableErrorCode);
|
|
283
287
|
const inProgressDelay = operationRetryAt(result.data, now().getTime());
|
|
284
288
|
const cloudV3Disabled = result.status === 404 && isCloudV3Disabled(record, result.data);
|
|
285
289
|
// A first metadata edit may dispatch before legacy reconciliation has
|
|
@@ -17,8 +17,13 @@ export function buildTaskAttachmentLinksEnvelope(record) {
|
|
|
17
17
|
}
|
|
18
18
|
const source = payload;
|
|
19
19
|
if (!Object.prototype.hasOwnProperty.call(source, 'links')
|
|
20
|
-
|| Object.keys(source).some((key) =>
|
|
21
|
-
throw new Error('Local task attachment-link outbox payload must contain links and optional
|
|
20
|
+
|| Object.keys(source).some((key) => !['links', 'cardCoverAssetId', 'originActor'].includes(key))) {
|
|
21
|
+
throw new Error('Local task attachment-link outbox payload must contain links and optional card cover metadata.');
|
|
22
|
+
}
|
|
23
|
+
const hasCardCoverAssetId = Object.prototype.hasOwnProperty.call(source, 'cardCoverAssetId');
|
|
24
|
+
if (hasCardCoverAssetId && source.cardCoverAssetId !== null
|
|
25
|
+
&& (typeof source.cardCoverAssetId !== 'string' || !source.cardCoverAssetId.trim())) {
|
|
26
|
+
throw new Error('Local task attachment-link card cover must be a non-empty string or null.');
|
|
22
27
|
}
|
|
23
28
|
const aggregate = validateWorkspaceSyncV3TaskAttachmentLinksPayload({ taskId: record.entityId, links: source.links });
|
|
24
29
|
return {
|
|
@@ -31,6 +36,11 @@ export function buildTaskAttachmentLinksEnvelope(record) {
|
|
|
31
36
|
baseEntityRevision: record.baseEntityRevision, baseLifecycleRevision: '0',
|
|
32
37
|
payload: {
|
|
33
38
|
links: aggregate.links,
|
|
39
|
+
...(hasCardCoverAssetId ? {
|
|
40
|
+
cardCoverAssetId: source.cardCoverAssetId === null
|
|
41
|
+
? null
|
|
42
|
+
: String(source.cardCoverAssetId).trim(),
|
|
43
|
+
} : {}),
|
|
34
44
|
...(source.originActor !== undefined
|
|
35
45
|
? { originActor: parseWorkspaceSyncV3OriginActor(source.originActor) }
|
|
36
46
|
: {}),
|